diff --git a/examples/test.csv b/examples/test.csv new file mode 100644 index 0000000..ca1b615 --- /dev/null +++ b/examples/test.csv @@ -0,0 +1,3 @@ +pi,3^5,"ref(0,0)",-(1/0) +12%5,"pow(3,5)",0/NaN,"""Apollo""" +A1+A2,"if(true , sqrt(25),round(if(false,1.1,2.5)))",D2+1969, diff --git a/src/main.rs b/src/main.rs index 9c55473..675c249 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,11 @@ use std::path::PathBuf; -use anyhow::Context; use clap::Parser; use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::{ - self, - layout::{Constraint, Layout}, - widgets::{Table, Tabs}, - Frame, -}; -use sheet::{Address, CellValue, Tbl}; +use ratatui; mod sheet; +mod ui; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -20,9 +14,9 @@ pub struct Args { workbook: PathBuf, } -fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> { +fn run(terminal: &mut ratatui::DefaultTerminal, name: PathBuf) -> std::io::Result<()> { loop { - terminal.draw(|frame| draw(frame))?; + terminal.draw(|frame| ui::draw(frame, &name))?; if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { return Ok(()); @@ -31,36 +25,12 @@ fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> { } } -fn generate_default_table<'a>() -> Table<'a> { - let mut tbl = Tbl::new(); - tbl.update_entry(Address::new(5, 5), CellValue::text("5,5")) - .context("Failed updating entry at 5,5") - .unwrap(); - tbl.update_entry(Address::new(10, 10), CellValue::float(10.10)) - .context("Failed updating entry at 10,10") - .unwrap(); - tbl.update_entry(Address::new(0, 0), CellValue::other("0.0")) - .context("Failed updating entry at 0,0") - .unwrap(); - tbl.into() -} - -fn draw(frame: &mut Frame) { - use Constraint::{Min, Percentage}; - let table = generate_default_table(); - let tabs = Tabs::new(vec!["sheet1"]).select(0); - let rects = Layout::vertical([Min(1), Percentage(90)]).split(frame.area()); - - frame.render_widget(tabs, rects[0]); - frame.render_widget(table, rects[1]); -} - fn main() -> std::io::Result<()> { - let _ = Args::parse(); + let args = Args::parse(); let mut terminal = ratatui::init(); terminal.clear()?; - let app_result = run(&mut terminal); + let app_result = run(&mut terminal, args.workbook); ratatui::restore(); app_result } diff --git a/src/mod.rs b/src/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/sheet/mod.rs b/src/sheet/mod.rs index f68ce23..d9b37fe 100644 --- a/src/sheet/mod.rs +++ b/src/sheet/mod.rs @@ -8,7 +8,6 @@ use anyhow::{anyhow, Result}; use csvx; -use ratatui::widgets::{Cell, Row, Table}; use std::borrow::Borrow; @@ -16,7 +15,7 @@ pub enum CellValue { Text(String), Float(f64), Integer(i64), - Other(String), + Formula(String), } impl CellValue { @@ -25,7 +24,7 @@ impl CellValue { CellValue::Text(v) => format!("\"{}\"", v), CellValue::Float(v) => format!("{}", v), CellValue::Integer(v) => format!("{}", v), - CellValue::Other(v) => format!("{}", v), + CellValue::Formula(v) => format!("{}", v), } } @@ -33,8 +32,8 @@ impl CellValue { CellValue::Text(Into::::into(value)) } - pub fn other>(value: S) -> CellValue { - CellValue::Other(Into::::into(value)) + pub fn formula>(value: S) -> CellValue { + CellValue::Formula(Into::::into(value)) } pub fn float(value: f64) -> CellValue { @@ -61,14 +60,12 @@ impl Address { /// A single table of addressable computable values. pub struct Tbl { - csv: csvx::Table, + pub csv: csvx::Table, } impl Tbl { pub fn new() -> Self { - Self { - csv: csvx::Table::new("").unwrap(), - } + Self::from_str("").unwrap() } pub fn dimensions(&self) -> (usize, usize) { @@ -108,20 +105,5 @@ impl Tbl { } } -impl<'t> From for Table<'t> { - fn from(value: Tbl) -> Self { - let rows: Vec = value - .csv - .get_calculated_table() - .iter() - .map(|r| { - let cells = r.iter().map(|v| Cell::new(format!("{}", v))); - Row::new(cells) - }) - .collect(); - Table::default().rows(rows) - } -} - #[cfg(test)] mod tests; diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..0916eb5 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,131 @@ +//! Ui rendering logic + +use std::{fs::File, io::Read, path::PathBuf}; + +use super::sheet::{Address, CellValue, Tbl}; + +use anyhow::{Context, Result}; +use ratatui::{ + self, + layout::{Constraint, Flex, Layout}, + style::{Color, Stylize}, + text::Text, + widgets::{Block, Cell, Row, Table, Tabs, Widget}, + Frame, +}; + +pub struct Workspace { + name: String, + tbl: Tbl, +} + +impl Workspace { + pub fn new>(tbl: Tbl, name: S) -> Self { + Self { + tbl, + name: name.into(), + } + } + + pub fn load(path: &PathBuf) -> Result { + let mut f = File::open(path)?; + let mut buf = Vec::new(); + let _ = f.read_to_end(&mut buf)?; + let input = String::from_utf8(buf) + .context(format!("Error reading file: {:?}", path))?; + let tbl = Tbl::from_str(input)?; + Ok(Workspace::new(tbl, path.file_name().map(|p| p.to_string_lossy().to_string()).unwrap_or_else(|| "Unknown".to_string()))) + } +} + +impl Widget for Workspace { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + use Constraint::{Min, Percentage}; + let rects = Layout::vertical([Min(1), Percentage(90)]).split(area); + let table = Table::from(&self.tbl); + let tabs = Tabs::new(vec![self.name.clone()]).select(0); + + tabs.render(rects[0], buf); + table.render(rects[1], buf); + } +} + +fn generate_default_table<'a>() -> Tbl { + let mut tbl = Tbl::new(); + tbl.update_entry(Address::new(3, 3), CellValue::text("3,3")) + .context("Failed updating entry at 5,5") + .expect("Unexpected fail to update entry"); + tbl.update_entry(Address::new(6, 6), CellValue::float(6.6)) + .context("Failed updating entry at 10,10") + .expect("Unexpected fail to update entry"); + tbl.update_entry(Address::new(0, 0), CellValue::formula("0.0")) + .context("Failed updating entry at 0,0") + .expect("Unexpected fail to update entry"); + tbl.update_entry(Address::new(1, 0), CellValue::formula("1.0")) + .context("Failed updating entry at 0,0") + .expect("Unexpected fail to update entry"); + tbl.update_entry(Address::new(2, 0), CellValue::formula("2.0")) + .context("Failed updating entry at 0,0") + .expect("Unexpected fail to update entry"); + tbl +} + +const COLNAMES: [&'static str; 27] = [ + "", "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(|(i, r)| { + let cells = vec![Cell::new(format!("{}", i))] + .into_iter() + .chain(r.iter().map(|v| { + let content = format!("{}", v); + Cell::new(Text::raw(content)) + .bg(if i % 2 == 0 { + Color::Rgb(57, 61, 71) + } else { + Color::Rgb(165, 169, 160) + }) + .fg(if i % 2 == 0 { + Color::White + } else { + Color::Rgb(31,32,34) + }) + .underlined() + .bold() + })); + Row::new(cells) + }) + .collect(); + // TODO(zaphar): Handle the double letter column names + let header: Vec = (0..=cols).map(|i| Cell::new(COLNAMES[i % 26])).collect(); + let mut constraints: Vec = Vec::new(); + constraints.push(Constraint::Max(5)); + for _ in 0..cols { + constraints.push(Constraint::Min(5)); + } + Table::new(rows, constraints) + .block(Block::bordered()) + .header(Row::new(header).underlined()) + .column_spacing(1) + .flex(Flex::SpaceAround) + } +} + +pub fn draw(frame: &mut Frame, name: &PathBuf) { + let table = generate_default_table(); + let ws = Workspace::load(name).unwrap(); + + frame.render_widget(ws, frame.area()); +}