diff --git a/src/book/mod.rs b/src/book/mod.rs index bb6c1cb..a770edd 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,35 +1,51 @@ -use std::path::PathBuf; +use std::cmp::max; use anyhow::{anyhow, Result}; use ironcalc::{ base::{ - locale, types::{SheetData, Worksheet}, Model + types::{SheetData, Worksheet}, + worksheet::WorksheetDimension, + Model, }, export::save_to_xlsx, import::load_from_xlsx, }; +use crate::ui::Address; + +#[cfg(test)] +mod test; + /// A spreadsheet book with some internal state tracking. pub struct Book { - model: Model, - current_sheet: u32, - current_location: (i32, i32), + pub(crate) model: Model, + pub current_sheet: u32, + pub location: crate::ui::Address, + // TODO(zaphar): Because the ironcalc model is sparse we need to track our render size + // separately } impl Book { - /// Construct a new book from a Model pub fn new(model: Model) -> Self { Self { model, current_sheet: 0, - current_location: (0, 0), + location: Address::default(), } } + pub fn new_from_xlsx(path: &str) -> Result { + Ok(Self::new(load_from_xlsx(path, "en", "America/New_York")?)) + } + + pub fn evaluate(&mut self) { + self.model.evaluate(); + } + // TODO(zaphar): Should I support ICalc? /// Construct a new book from a path. - pub fn new_from_xlsx(path: &str, locale: &str, tz: &str) -> Result { + pub fn new_from_xlsx_with_locale(path: &str, locale: &str, tz: &str) -> Result { Ok(Self::new(load_from_xlsx(path, locale, tz)?)) } @@ -39,6 +55,15 @@ impl Book { Ok(()) } + pub fn get_all_sheets_identifiers(&self) -> Vec<(String, u32)> { + self.model + .workbook + .worksheets + .iter() + .map(|sheet| (sheet.get_name(), sheet.get_sheet_id())) + .collect() + } + /// Get the currently set sheets name. pub fn get_sheet_name(&self) -> Result<&str> { Ok(&self.get_sheet()?.name) @@ -49,49 +74,90 @@ impl Book { Ok(&self.get_sheet()?.sheet_data) } + pub fn move_to(&mut self, loc: Address) -> Result<()> { + // FIXME(zaphar): Check that this is safe first. + self.location.row = loc.row; + self.location.col = loc.col; + Ok(()) + } + /// Get a cells formatted content. - pub fn get_cell_rendered(&self) -> Result { + pub fn get_current_cell_rendered(&self) -> Result { + Ok(self.get_cell_addr_rendered(self.location.row, self.location.col)?) + } + + // TODO(zaphar): Use Address here too + pub fn get_cell_addr_rendered(&self, row: usize, col: usize) -> Result { Ok(self .model - .get_formatted_cell_value( - self.current_sheet, - self.current_location.0, - self.current_location.1, - ) + .get_formatted_cell_value(self.current_sheet, row as i32, col as i32) .map_err(|s| anyhow!("Unable to format cell {}", s))?) } /// Get a cells actual content as a string. - pub fn get_cell_contents(&self) -> Result { + pub fn get_current_cell_contents(&self) -> Result { Ok(self .model .get_cell_content( self.current_sheet, - self.current_location.0, - self.current_location.1, + self.location.row as i32, + self.location.col as i32, ) .map_err(|s| anyhow!("Unable to format cell {}", s))?) } - pub fn edit_cell(&mut self, value: String) -> Result<()> { + /// Update the current cell in a book. + /// This update won't be reflected until you call `Book::evaluate`. + pub fn edit_current_cell>(&mut self, value: S) -> Result<()> { + self.update_entry(&self.location.clone(), value)?; + Ok(()) + } + + /// Update an entry in the current sheet for a book. + /// This update won't be reflected until you call `Book::evaluate`. + pub fn update_entry>(&mut self, location: &Address, value: S) -> Result<()> { self.model .set_user_input( self.current_sheet, - self.current_location.0, - self.current_location.1, - value, + location.row as i32, + location.col as i32, + value.into(), ) .map_err(|e| anyhow!("Invalid cell contents: {}", e))?; Ok(()) } + pub fn insert_rows(&mut self, row_idx: usize, count: usize) -> Result<()> { + Ok(self + .model + .insert_rows(self.current_sheet, row_idx as i32, count as i32) + .map_err(|e| anyhow!("Unable to insert row(s): {}", e))?) + } + + pub fn insert_columns(&mut self, col_idx: usize, count: usize) -> Result<()> { + Ok(self + .model + .insert_columns(self.current_sheet, col_idx as i32, count as i32) + .map_err(|e| anyhow!("Unable to insert column(s): {}", e))?) + } + /// Get the current sheets dimensions. This is a somewhat expensive calculation. - pub fn get_dimensions(&self) -> Result<(usize, usize)> { - let dimensions = self.get_sheet()?.dimension(); - Ok(( - dimensions.max_row.try_into()?, - dimensions.max_column.try_into()?, - )) + pub fn get_dimensions(&self) -> Result { + Ok(self.get_sheet()?.dimension()) + } + + // Get the size of the current sheet as a `(row_count, column_count)` + pub fn get_size(&self) -> Result<(usize, usize)> { + let sheet = &self.get_sheet()?.sheet_data; + let mut row_count = 0 as i32; + let mut col_count = 0 as i32; + for (ri, cols) in sheet.iter() { + row_count = max(*ri, row_count); + for (ci, _) in cols.iter() { + col_count = max(*ci, col_count); + } + } + Ok((row_count as usize, col_count as usize)) } /// Select a sheet by name. @@ -129,19 +195,17 @@ impl Book { false } - fn get_sheet(&self) -> Result<&Worksheet> { + pub(crate) fn get_sheet(&self) -> Result<&Worksheet> { Ok(self .model .workbook .worksheet(self.current_sheet) .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?) } +} - fn get_sheet_mut(&mut self) -> Result<&mut Worksheet> { - Ok(self - .model - .workbook - .worksheet_mut(self.current_sheet) - .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?) +impl Default for Book { + fn default() -> Self { + Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap()) } } diff --git a/src/book/test.rs b/src/book/test.rs new file mode 100644 index 0000000..9ac86ad --- /dev/null +++ b/src/book/test.rs @@ -0,0 +1,73 @@ +use ironcalc::base::worksheet::WorksheetDimension; + +use crate::ui::Address; + +use super::Book; + +#[test] +fn test_book_default() { + let mut book = Book::default(); + let WorksheetDimension { + min_row, + max_row, + min_column, + max_column, + } = book.get_dimensions().expect("couldn't get dimensions"); + assert_eq!((1, 1, 1, 1), (min_row, max_row, min_column, max_column)); + assert_eq!((0, 0), book.get_size().expect("Failed to get size")); + let cell = book + .get_current_cell_contents() + .expect("couldn't get contents"); + assert_eq!("", cell); + book.edit_current_cell("1").expect("failed to edit cell"); + book.evaluate(); + let cell = book + .get_current_cell_contents() + .expect("couldn't get contents"); + assert_eq!("1", cell); + let cell = book + .get_current_cell_rendered() + .expect("couldn't get contents"); + assert_eq!("1", cell); + let sheets = book.get_all_sheets_identifiers(); + dbg!(&sheets); + assert_eq!(1, sheets.len()); +} + +#[test] +fn test_insert_rows() { + let mut book = Book::default(); +} + +#[test] +fn test_book_insert_cell_new_row() { + let mut book = Book::default(); + book.update_entry(&Address { row: 2, col: 1 }, "1") + .expect("failed to edit cell"); + book.evaluate(); + dbg!(book.get_sheet().expect("Failed to get sheet")); + let WorksheetDimension { + min_row, + max_row, + min_column, + max_column, + } = book.get_dimensions().expect("couldn't get dimensions"); + assert_eq!((2, 2, 1, 1), (min_row, max_row, min_column, max_column)); + assert_eq!((2, 1), book.get_size().expect("Failed to get size")); +} + +#[test] +fn test_book_insert_cell_new_column() { + let mut book = Book::default(); + book.insert_columns(1, 1).expect("couldn't insert rows"); + book.update_entry(&Address { row: 1, col: 2 }, "1") + .expect("failed to edit cell"); + let WorksheetDimension { + min_row, + max_row, + min_column, + max_column, + } = book.get_dimensions().expect("couldn't get dimensions"); + assert_eq!((1, 1, 2, 2), (min_row, max_row, min_column, max_column)); + assert_eq!((1, 2), book.get_size().expect("Failed to get size")); +} diff --git a/src/main.rs b/src/main.rs index d82a847..1fe5b98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ use clap::Parser; use ratatui; use ui::Workspace; -mod sheet; mod ui; mod book; @@ -13,10 +12,15 @@ mod book; pub struct Args { #[arg()] workbook: PathBuf, + #[arg(default_value_t=String::from("en"), short, long)] + locale_name: String, + #[arg(default_value_t=String::from("America/New_York"), short, long)] + timezone_name: String, } -fn run(terminal: &mut ratatui::DefaultTerminal, name: PathBuf) -> anyhow::Result { - let mut ws = Workspace::load(&name)?; +fn run(terminal: &mut ratatui::DefaultTerminal, args: Args) -> anyhow::Result { + let mut ws = Workspace::load(&args.workbook, &args.locale_name, &args.timezone_name)?; + loop { terminal.draw(|frame| ui::draw(frame, &mut ws))?; if let Some(code) = ws.handle_input()? { @@ -30,7 +34,7 @@ fn main() -> anyhow::Result { let mut terminal = ratatui::init(); terminal.clear()?; - let app_result = run(&mut terminal, args.workbook); + let app_result = run(&mut terminal, args); ratatui::restore(); app_result } diff --git a/src/sheet/mod.rs b/src/sheet/mod.rs deleted file mode 100644 index 553631a..0000000 --- a/src/sheet/mod.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! DataModel for a SpreadSheet -//! -//! # Overview -//! -//! Sheets can contain a [Tbl]. Tbl's contain a collection of [Address] to [Computable] -//! associations. From this we can compute the dimensions of a Tbl as well as render -//! them into a [Table] Widget. - -use anyhow::{anyhow, Result}; -use csvx; - -use std::borrow::Borrow; - -/// The Address in a [Tbl]. -#[derive(Default, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] -pub struct Address { - pub row: usize, - pub col: usize, -} - -impl Address { - pub fn new(row: usize, col: usize) -> Self { - Self { row, col } - } -} - -/// A single table of addressable computable values. -pub struct Tbl { - pub csv: csvx::Table, - pub location: Address, -} - -impl Tbl { - pub fn new() -> Self { - Self::from_str("").unwrap() - } - - pub fn dimensions(&self) -> (usize, usize) { - let table = self.csv.get_raw_table(); - let row_count = table.len(); - if row_count > 0 { - let col_count = table.first().unwrap().len(); - return (row_count, col_count); - } - return (0, 0); - } - - pub fn from_str>(input: S) -> Result { - Ok(Self { - csv: csvx::Table::new(input) - .map_err(|e| anyhow!("Error parsing table from csv text: {}", e))?, - location: Address::default(), - }) - } - - pub fn get_raw_value(&self, Address {row, col}: &Address) -> String { - self.csv.get_raw_table()[*row][*col].clone() - } - - pub fn move_to(&mut self, addr: Address) -> Result<()> { - let (row, col) = self.dimensions(); - if addr.row >= row || addr.col >= col { - return Err(anyhow!("Invalid address to move to: {:?}", addr)); - } - self.location = addr; - Ok(()) - } - - pub fn update_entry(&mut self, address: &Address, value: String) -> Result<()> { - let (row, col) = self.dimensions(); - if address.row >= row { - // then we need to add rows. - for r in row..=address.row { - self.csv.insert_y(r); - } - } - if address.col >= col { - for c in col..=address.col { - self.csv.insert_x(c); - } - } - Ok(self - .csv - .update(address.col, address.row, value.trim())?) - } -} - -#[cfg(test)] -mod tests; diff --git a/src/sheet/tests.rs b/src/sheet/tests.rs deleted file mode 100644 index f430d53..0000000 --- a/src/sheet/tests.rs +++ /dev/null @@ -1,13 +0,0 @@ -use super::*; - -#[test] -fn test_dimensions_calculation() { - let mut tbl = Tbl::new(); - tbl.update_entry(&Address::new(0, 0), String::new()).unwrap(); - assert_eq!((1, 1), tbl.dimensions()); - tbl.update_entry(&Address::new(0, 10), String::new()).unwrap(); - assert_eq!((1, 11), tbl.dimensions()); - tbl.update_entry(&Address::new(20, 5), String::new()).unwrap(); - assert_eq!((21, 11), tbl.dimensions()); -} - diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c9bfe8d..37bad6a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,16 +1,12 @@ //! Ui rendering logic -use std::{ - fs::File, - io::Read, - path::PathBuf, - process::ExitCode, -}; +use std::{path::PathBuf, process::ExitCode}; -use super::sheet::{Address, Tbl}; +use crate::book::Book; -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use ironcalc::base::worksheet::WorksheetDimension; use ratatui::{ self, layout::{Constraint, Flex, Layout}, @@ -35,21 +31,41 @@ pub struct AppState { pub table_state: TableState, } +// TODO(jwall): This should probably move to a different module. +/// The Address in a Table. +#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] +pub struct Address { + pub row: usize, + pub col: usize, +} + +impl Address { + pub fn new(row: usize, col: usize) -> Self { + Self { row, col } + } +} + +impl Default for Address { + fn default() -> Self { + Address::new(1, 1) + } +} + // Interaction Modalities // * Navigate // * Edit pub struct Workspace<'ws> { name: PathBuf, - tbl: Tbl, + book: Book, state: AppState, text_area: TextArea<'ws>, dirty: bool, } impl<'ws> Workspace<'ws> { - pub fn new(tbl: Tbl, name: PathBuf) -> Self { + pub fn new(book: Book, name: PathBuf) -> Self { let mut ws = Self { - tbl, + book, name, state: AppState::default(), text_area: reset_text_area("".to_owned()), @@ -59,61 +75,52 @@ impl<'ws> Workspace<'ws> { ws } - pub fn load(path: &PathBuf) -> Result { - let input = if path.exists() { - if path.is_file() { - let mut f = File::open(path)?; - let mut buf = Vec::new(); - let _ = f.read_to_end(&mut buf)?; - String::from_utf8(buf).context(format!("Error reading file: {:?}", path))? - } else { - return Err(anyhow!("Not a valid path: {}", path.to_string_lossy().to_string())); - } + pub fn load(path: &PathBuf, locale: &str, tz: &str) -> Result { + let book = if path.exists() { + Book::new_from_xlsx_with_locale(&path.to_string_lossy().to_string(), locale, tz)? } else { - String::from(",,,\n,,,\n") + Book::default() }; - let mut tbl = Tbl::from_str(input)?; - tbl.move_to(Address { row: 0, col: 0 })?; - Ok(Workspace::new( - tbl, - path.clone(), - )) + //book.move_to(Address { row: 0, col: 0 })?; + Ok(Workspace::new(book, path.clone())) } pub fn move_down(&mut self) -> Result<()> { - let mut loc = self.tbl.location.clone(); - let (row, _) = self.tbl.dimensions(); - if loc.row < row - 1 { + let mut loc = self.book.location.clone(); + let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?; + if loc.row <= max_row as usize { loc.row += 1; - self.tbl.move_to(loc)?; + self.book.move_to(loc)?; } Ok(()) } pub fn move_up(&mut self) -> Result<()> { - let mut loc = self.tbl.location.clone(); - if loc.row > 0 { + let mut loc = self.book.location.clone(); + let WorksheetDimension { min_row, max_row: _, min_column: _, max_column: _ } = self.book.get_dimensions()?; + if loc.row > min_row as usize { loc.row -= 1; - self.tbl.move_to(loc)?; + self.book.move_to(loc)?; } Ok(()) } pub fn move_left(&mut self) -> Result<()> { - let mut loc = self.tbl.location.clone(); - if loc.col > 0 { + let mut loc = self.book.location.clone(); + let WorksheetDimension { min_row: _, max_row: _, min_column, max_column: _ } = self.book.get_dimensions()?; + if loc.col > min_column as usize { loc.col -= 1; - self.tbl.move_to(loc)?; + self.book.move_to(loc)?; } Ok(()) } pub fn move_right(&mut self) -> Result<()> { - let mut loc = self.tbl.location.clone(); - let (_, col) = self.tbl.dimensions(); - if loc.col < col - 1 { + let mut loc = self.book.location.clone(); + let WorksheetDimension { min_row: _, max_row: _, min_column: _, max_column} = self.book.get_dimensions()?; + if loc.col < max_column as usize { loc.col += 1; - self.tbl.move_to(loc)?; + self.book.move_to(loc)?; } Ok(()) } @@ -146,7 +153,8 @@ impl<'ws> Workspace<'ws> { "* ESC: Exit edit mode".into(), "Otherwise edit as normal".into(), ]), - }).block(info_block) + }) + .block(info_block) } fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result> { @@ -157,8 +165,7 @@ impl<'ws> Workspace<'ws> { self.text_area.set_cursor_style(Style::default()); let contents = self.text_area.lines().join("\n"); if self.dirty { - let loc = self.tbl.location.clone(); - self.tbl.update_entry(&loc, contents)?; + self.book.edit_current_cell(contents)?; } return Ok(None); } @@ -189,19 +196,19 @@ impl<'ws> Workspace<'ws> { self.save_file()?; } KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { - let (row, _) = self.tbl.dimensions(); - self.tbl.csv.insert_y(row); - let (row, _) = self.tbl.dimensions(); - let mut loc = self.tbl.location.clone(); - if loc.row < row - 1 { - loc.row = row - 1; - self.tbl.move_to(loc)?; + let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?; + self.book.insert_rows(max_row as usize, 1)?; + let WorksheetDimension { min_row: _, max_row, min_column: _, max_column: _ } = self.book.get_dimensions()?; + let mut loc = self.book.location.clone(); + if loc.row < max_row as usize { + loc.row = (max_row - 1) as usize; + self.book.move_to(loc)?; } self.handle_movement_change(); } KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { - let (_, col) = self.tbl.dimensions(); - self.tbl.csv.insert_x(col); + let WorksheetDimension { min_row: _, max_row: _, min_column: _, max_column} = self.book.get_dimensions()?; + self.book.insert_columns(max_column as usize, 1)?; } KeyCode::Char('q') => { return Ok(Some(ExitCode::SUCCESS)); @@ -236,13 +243,15 @@ impl<'ws> Workspace<'ws> { } fn handle_movement_change(&mut self) { - let contents = self.tbl.get_raw_value(&self.tbl.location); + let contents = self + .book + .get_current_cell_contents() + .expect("Unexpected failure getting current cell contents"); self.text_area = reset_text_area(contents); } fn save_file(&self) -> Result<()> { - let contents = self.tbl.csv.export_raw_table().map_err(|e| anyhow::anyhow!("Error serializing to csv: {:?}", e))?; - std::fs::write(&self.name, contents)?; + self.book.save_to_xlsx(&self.name.to_string_lossy().to_string())?; Ok(()) } } @@ -260,9 +269,12 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { Self: Sized, { let outer_block = Block::bordered() - .title(Line::from(self.name - .file_name().map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| String::from("Unknown")))) + .title(Line::from( + self.name + .file_name() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| String::from("Unknown")), + )) .title_bottom(match &self.state.modality { Modality::Navigate => "navigate", Modality::CellEdit => "edit", @@ -270,22 +282,26 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { .title_bottom( Line::from(format!( "{},{}", - self.tbl.location.row, self.tbl.location.col + self.book.location.row, self.book.location.col )) .right_aligned(), ); - let [edit_rect, table_rect, info_rect] = - Layout::vertical(&[Constraint::Fill(1), Constraint::Fill(30), Constraint::Fill(9)]) - .vertical_margin(2) - .horizontal_margin(2) - .flex(Flex::Legacy) - .areas(area.clone()); + let [edit_rect, table_rect, info_rect] = Layout::vertical(&[ + Constraint::Fill(1), + Constraint::Fill(30), + Constraint::Fill(9), + ]) + .vertical_margin(2) + .horizontal_margin(2) + .flex(Flex::Legacy) + .areas(area.clone()); outer_block.render(area, buf); self.text_area.render(edit_rect, buf); let table_block = Block::bordered(); - let table = Table::from(&self.tbl).block(table_block); + let table_inner: Table = TryFrom::try_from(&self.book).expect(""); + let table = table_inner.block(table_block); // https://docs.rs/ratatui/latest/ratatui/widgets/struct.TableState.html - let Address { row, col } = self.tbl.location; + let Address { row, col } = self.book.location; // TODO(zaphar): Apparently scrolling by columns doesn't work? self.state.table_state.select_cell(Some((row, col))); self.state.table_state.select_column(Some(col)); @@ -298,62 +314,65 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { } const COLNAMES: [&'static str; 26] = [ - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", - "S", "T", "U", "V", "W", "X", "Y", "Z", + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", + "T", "U", "V", "W", "X", "Y", "Z", ]; -impl<'t> From<&Tbl> for Table<'t> { - fn from(value: &Tbl) -> Self { - let (_, cols) = value.dimensions(); - let rows: Vec = value - .csv - .get_calculated_table() - .iter() - .enumerate() - .map(|(ri, r)| { - let cells = - vec![Cell::new(format!("{}", ri))] - .into_iter() - .chain(r.iter().enumerate().map(|(ci, v)| { - let content = format!("{}", v); - let cell = Cell::new(Text::raw(content)); - match (value.location.row == ri, value.location.col == ci) { - (true, true) => cell.fg(Color::White).underlined(), - _ => cell - .bg(if ri % 2 == 0 { - Color::Rgb(57, 61, 71) - } else { - Color::Rgb(165, 169, 160) - }) - .fg(if ri % 2 == 0 { - Color::White - } else { - Color::Rgb(31, 32, 34) - }), - } - .bold() - })); +// TODO(jwall): Maybe this should be TryFrom? +impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> { + fn try_from(value: &'book Book) -> std::result::Result { + // TODO(zaphar): This is apparently expensive. Maybe we can cache it somehow? + // We should do the correct thing here if this fails + let WorksheetDimension { min_row, max_row, min_column, max_column } = value.get_dimensions()?; + let (row_count, col_count) = ((max_row - min_row) as usize, (max_column - min_column) as usize); + let rows: Vec = (1..=row_count) + .into_iter() + .map(|ri| { + let cells: Vec = (1..=col_count) + .into_iter() + .map(|ci| { + // TODO(zaphar): Is this safe? + let content = value.get_cell_addr_rendered(ri, ci).unwrap(); + let cell = Cell::new(Text::raw(content)); + match (value.location.row == ri, value.location.col == ci) { + (true, true) => cell.fg(Color::White).underlined(), + _ => cell + .bg(if ri % 2 == 0 { + Color::Rgb(57, 61, 71) + } else { + Color::Rgb(165, 169, 160) + }) + .fg(if ri % 2 == 0 { + Color::White + } else { + Color::Rgb(31, 32, 34) + }), + } + .bold() + }) + .collect(); Row::new(cells) }) .collect(); - // TODO(zaphar): Handle the double letter column names - let mut header = Vec::with_capacity(cols+1); + let mut constraints: Vec = Vec::new(); + constraints.push(Constraint::Max(5)); + for _ in 0..col_count { + constraints.push(Constraint::Min(5)); + } + let mut header = Vec::with_capacity(col_count as usize); header.push(Cell::new("")); - header.extend((0..cols).map(|i| { + header.extend((0..(col_count as usize)).map(|i| { let count = (i / 26) + 1; Cell::new(COLNAMES[i % 26].repeat(count)) })); - let mut constraints: Vec = Vec::new(); - constraints.push(Constraint::Max(5)); - for _ in 0..cols { - constraints.push(Constraint::Min(5)); - } - Table::new(rows, constraints) + Ok(Table::new(rows, constraints) .block(Block::bordered()) .header(Row::new(header).underlined()) .column_spacing(1) - .flex(Flex::SpaceAround) + .flex(Flex::SpaceAround)) } + + type Error = anyhow::Error; } pub fn draw(frame: &mut Frame, ws: &mut Workspace) {