better-cursive-table 0.3.0

TableView and ArrayView for Cursive TUIs
Documentation
use std::sync::{Arc, Mutex};

use better_cursive_table::{SelectMode, TableBuilder, TableDataRow, TableView};
use cursive::traits::*;
use cursive::views::{Button, Dialog, DummyView, EditView, LinearLayout};
use rand::Rng;

fn random_cell(rng: &mut impl Rng) -> String {
    rng.gen_range(0..1000).to_string()
}

struct TableState {
    column_headers: Vec<String>,
    data: Vec<Vec<String>>,
}

impl TableState {
    fn new() -> Self {
        let mut state = Self {
            column_headers: Vec::new(),
            data: Vec::new(),
        };
        for _ in 0..3 {
            state.add_column();
        }
        for _ in 0..3 {
            state.add_row();
        }
        state
    }

    fn add_row(&mut self) {
        let mut rng = rand::thread_rng();
        let row = (0..self.column_headers.len())
            .map(|_| random_cell(&mut rng))
            .collect();
        self.data.push(row);
    }

    fn remove_row(&mut self) {
        self.data.pop();
    }

    fn add_column(&mut self) {
        let label = format!("C{}", self.column_headers.len() + 1);
        self.column_headers.push(label);
        let mut rng = rand::thread_rng();
        for row in &mut self.data {
            row.push(random_cell(&mut rng));
        }
    }

    fn remove_column(&mut self) {
        if self.column_headers.pop().is_some() {
            for row in &mut self.data {
                row.pop();
            }
        }
    }
}

fn build_table(
    state: &TableState,
    shared: &Arc<Mutex<TableState>>,
) -> TableView<TableDataRow<String>, usize> {
    let mut table = TableBuilder::new()
        .column_header(state.column_headers.clone())
        .data(state.data.clone())
        .sortable(false)
        .build();

    let shared = Arc::clone(shared);
    table.set_on_submit_cell(move |s, row, col| {
        open_cell_editor(s, Arc::clone(&shared), row, col);
    });
    table.set_selection_mode(SelectMode::Cell);

    table
}

fn refresh_table(siv: &mut cursive::Cursive, state: &Arc<Mutex<TableState>>) {
    let table = {
        let state_guard = state.lock().expect("table state lock");
        build_table(&state_guard, state)
    };
    siv.call_on_name("table", |view: &mut TableView<TableDataRow<String>, usize>| {
        *view = table;
    });
}

fn open_cell_editor(
    siv: &mut cursive::Cursive,
    state: Arc<Mutex<TableState>>,
    row: usize,
    col: usize,
) {
    let current = {
        let state = state.lock().expect("table state lock");
        state
            .data
            .get(row)
            .and_then(|r| r.get(col))
            .cloned()
            .unwrap_or_default()
    };

    let state_submit = Arc::clone(&state);
    let edit = EditView::new().content(current).on_submit(move |s, text| {
        if let Ok(mut state) = state_submit.lock() {
            if row < state.data.len() {
                let cols = state.column_headers.len();
                if cols > 0 && state.data[row].len() < cols {
                    state.data[row].resize(cols, String::new());
                }
                if col < state.data[row].len() {
                    state.data[row][col] = text.to_string();
                }
            }
        }
        refresh_table(s, &state_submit);
        s.pop_layer();
    });

    let title = format!("Edit R{} C{}", row + 1, col + 1);
    siv.add_layer(Dialog::around(edit).title(title).button("Cancel", |s| {
        s.pop_layer();
    }));
}

fn main() {
    let state = Arc::new(Mutex::new(TableState::new()));

    let table = {
        let state_guard = state.lock().expect("table state lock");
        build_table(&state_guard, &state).with_name("table")
    };

    let state_add_row = Arc::clone(&state);
    let add_row = Button::new("Add Row", move |s| {
        if let Ok(mut state) = state_add_row.lock() {
            state.add_row();
        }
        refresh_table(s, &state_add_row);
    });

    let state_remove_row = Arc::clone(&state);
    let remove_row = Button::new("Remove Row", move |s| {
        if let Ok(mut state) = state_remove_row.lock() {
            state.remove_row();
        }
        refresh_table(s, &state_remove_row);
    });

    let state_add_col = Arc::clone(&state);
    let add_col = Button::new("Add Col", move |s| {
        if let Ok(mut state) = state_add_col.lock() {
            state.add_column();
        }
        refresh_table(s, &state_add_col);
    });

    let state_remove_col = Arc::clone(&state);
    let remove_col = Button::new("Remove Col", move |s| {
        if let Ok(mut state) = state_remove_col.lock() {
            state.remove_column();
        }
        refresh_table(s, &state_remove_col);
    });

    let buttons = LinearLayout::horizontal()
        .child(add_row)
        .child(DummyView::new().fixed_width(2))
        .child(remove_row)
        .child(DummyView::new().fixed_width(2))
        .child(add_col)
        .child(DummyView::new().fixed_width(2))
        .child(remove_col);

    let layout = LinearLayout::vertical()
        .child(table)
        .child(DummyView::new().fixed_height(1))
        .child(buttons);

    let mut siv = cursive::default();
    siv.add_layer(Dialog::around(layout).title("TableBuilder"));
    siv.run();
}