beginner – Simple terminal Game of Life in Rust

I’ve been learning Rust for a few weeks in my free time, and as a first project I decided to make a simple terminal Game of Life program (without UI, that might come later). It just starts with a glider and loops indefinitely (Torus topology). I decided to keep it simple so I can learn from early mistakes before I start to make it more complex. Here are the 3 files of the project (The only dependency I’m using is termion for colored terminal output):

grid.rs

use std::fmt;

pub struct Grid {
    rows: usize,
    cols: usize,
    data: Vec<bool>,
    aux_data: Vec<bool>,
}

impl Grid {
    pub fn new(rows: usize, cols: usize) -> Self {
        Grid {
            rows: rows,
            cols: cols,
            data: vec!(false; rows * cols),
            aux_data: vec!(false; rows * cols),
        }
    }

    pub fn get_rows(&self) -> usize {
        self.rows
    }

    pub fn get_cols(&self) -> usize {
        self.cols
    }

    pub fn get_cell(&self, row: usize, col: usize) -> bool {
        self.data(row*self.get_cols() + col)
    }

    pub fn set_cell(&mut self, value: bool, row: usize, col: usize) {
        let ncols = self.get_cols();
        self.data(row*ncols + col) = value;
    }

    fn count_alive_neighbours(&self, row: usize, col: usize) -> i32 {
        let offsets: ((usize, usize); 8) = (
            (self.get_rows()-1, self.get_cols()-1), (self.get_rows()-1,  0), (self.get_rows()-1,  1),
            (                0, self.get_cols()-1),                          (                0,  1),
            (                1, self.get_cols()-1), (                1,  0), (                1,  1)
        );

        let mut count = 0;
        
        for offset in offsets.iter() {
            let is_alive = self.get_cell(
                (row + offset.0)%self.get_rows(),
                (col + offset.1)%self.get_cols()
            );
            if is_alive {
                count += 1
            }
        }

        count
    }

    pub fn update(&mut self) {
        use std::mem::swap;

        self.aux_data.resize(self.data.len(), false);
        let ncols = self.get_cols();
        
        for row in 0..self.get_rows() {
            for col in 0..self.get_cols() {
                let is_alive = self.get_cell(row, col);
                let alive_neighbours = self.count_alive_neighbours(row, col);
                self.aux_data(row*ncols + col) = match (is_alive, alive_neighbours) {
                    (true, n) if n < 2 || 3 < n => false,
                    (false, 3) => true,
                    (cell, _) => cell,
                };
            }
        }

        swap(&mut self.data, &mut self.aux_data);
    }
}

impl fmt::Display for Grid {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use termion::color;
        for index_row in 0..self.get_rows() {
            for index_col in 0..self.get_cols() {
                match self.get_cell(index_row, index_col) {
                    false => write!(f, "{}u{25FC} ", color::Fg(color::White))?,
                    true => write!(f, "{}u{25FC} ", color::Fg(color::Red))?,
                }
            }
            write!(f, "n")?
        }

        Ok(())
    }
}

lib.rs

pub mod grid;

main.rs

extern crate game_of_life;

use game_of_life::grid::Grid;
use std::{thread, time};

fn main() {
    let mut grid = Grid::new(5, 5);
    grid.set_cell(true, 1, 2);
    grid.set_cell(true, 2, 3);
    grid.set_cell(true, 3, 1);
    grid.set_cell(true, 3, 2);
    grid.set_cell(true, 3, 3);
    
    loop {
        print!("{}n", grid);
        grid.update();
        thread::sleep(time::Duration::from_secs(1));
    }
}

I come from a very strong C++ background, so some C++ practices might leak in my Rust code, but I promise I want to learn about them and try to think in the Rust way.

Thank you everyone and have a nice day!