diff --git a/src/app/app.rs b/src/app/app.rs index f921722..926f4a4 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -11,6 +11,7 @@ use ratatui::{ use crate::app::{ calc::{Grid, LEN}, + error_msg::ErrorMessage, mode::Mode, }; @@ -19,6 +20,7 @@ pub struct App { pub grid: Grid, pub mode: Mode, pub file: Option, + pub error_msg: ErrorMessage, } impl Widget for &App { @@ -75,14 +77,13 @@ impl Widget for &App { const ORANGE2: Color = Color::Rgb(180, 130, 0); match (x == 0, y == 0) { + // 0,0 dead space (true, true) => { - let (x, y) = self.grid.selected_cell; - let c = Grid::num_to_char(x); - display = format!("{y}{c}",); - style = Style::new().fg(Color::Green).bg(Color::Black); + display = self.mode.to_string(); + style = self.mode.get_style(); } + // row names (true, false) => { - // row names display = y_idx.to_string(); let bg = if y_idx == self.grid.selected_cell.1 { @@ -94,8 +95,8 @@ impl Widget for &App { }; style = Style::new().fg(Color::White).bg(bg); } + // column names (false, true) => { - // column names display = Grid::num_to_char(x_idx); let bg = if x_idx == self.grid.selected_cell.0 { @@ -108,19 +109,19 @@ impl Widget for &App { style = Style::new().fg(Color::White).bg(bg) } + // grid squares (false, false) => { if let Some(cell) = self.grid.get_cell_raw(x_idx, y_idx) { - display = cell.as_raw_string(); - - if cell.can_be_number() { - if let Some(val) = self.grid.evaluate(&cell.as_raw_string()) { - display = val.to_string(); - style = Style::new() - .underline_color(Color::DarkGray) - .add_modifier(Modifier::UNDERLINED); - } else { - // broken formulas - if cell.is_equation() { + match cell { + crate::app::calc::CellType::Number(c) => display = c.to_string(), + crate::app::calc::CellType::String(s) => display = s.to_owned(), + crate::app::calc::CellType::Equation(e) => { + if let Some(val) = self.grid.evaluate(e) { + display = val.to_string(); + style = Style::new() + .underline_color(Color::DarkGray) + .add_modifier(Modifier::UNDERLINED); + } else { style = Style::new().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED) } @@ -152,6 +153,7 @@ impl App { grid: Grid::new(), mode: Mode::Normal, file: None, + error_msg: ErrorMessage::none(), } } @@ -191,7 +193,7 @@ impl App { Mode::Normal => frame.render_widget( Paragraph::new({ let (x, y) = self.grid.selected_cell; - let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.as_raw_string()).unwrap_or_default(); + let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default(); cell }), cmd_line_left, @@ -200,7 +202,7 @@ impl App { } frame.render_widget(self, body); - frame.render_widget(&self.mode, cmd_line_right); + frame.render_widget(&self.error_msg, cmd_line_right); } fn handle_events(&mut self) -> io::Result<()> { @@ -251,9 +253,9 @@ impl App { event::KeyCode::Char(c) => { Mode::process_key(self, c); } - _ => todo!(), + _ => {} }, - _ => todo!(), + _ => {} }, Mode::Visual(_start_pos) => { if let event::Event::Key(key) = event::read()? { diff --git a/src/app/calc.rs b/src/app/calc.rs index 070bc34..b9c6733 100644 --- a/src/app/calc.rs +++ b/src/app/calc.rs @@ -2,10 +2,10 @@ use std::fmt::Display; use evalexpr::*; -use crate::ctx; +use crate::app::ctx; // if this is very large at all it will overflow the stack -pub const LEN: usize = 100; +pub const LEN: usize = 10; pub struct Grid { // a b c ... @@ -13,7 +13,7 @@ pub struct Grid { // 1 // 2 // ... - cells: [[Option>; LEN]; LEN], + cells: [[Option; LEN]; LEN], /// (X, Y) pub selected_cell: (usize, usize), } @@ -29,7 +29,7 @@ impl std::fmt::Debug for Grid { impl Grid { pub fn new() -> Self { // TODO this needs to be moved to the heap - let b: [[Option>; LEN]; LEN] = + let b: [[Option; LEN]; LEN] = core::array::from_fn(|_| core::array::from_fn(|_| None)); Self { @@ -90,12 +90,12 @@ impl Grid { (x_idx, y_idx) } - pub fn set_cell>>(&mut self, cell_id: &str, val: T) { + pub fn set_cell>(&mut self, cell_id: &str, val: T) { let loc = Self::parse_to_idx(cell_id); self.set_cell_raw(loc, val); } - pub fn set_cell_raw>>(&mut self, (x,y): (usize, usize), val: T) { + pub fn set_cell_raw>(&mut self, (x,y): (usize, usize), val: T) { // TODO check oob self.cells[x][y] = Some(val.into()); } @@ -104,13 +104,15 @@ impl Grid { /// A6, /// F0, /// etc - pub fn get_cell(&self, cell_id: &str) -> &Option> { + pub fn get_cell(&self, cell_id: &str) -> &Option { let (x, y) = Self::parse_to_idx(cell_id); self.get_cell_raw(x, y) } - pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option> { - // TODO check oob + pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option { + if x >= LEN || y >= LEN { + return &None + } &self.cells[x][y] } @@ -144,68 +146,50 @@ impl Default for Grid { } } -pub trait Cell { - /// Important! This is IS NOT the return value of an equation. - /// This is the raw equation it's self. - fn as_raw_string(&self) -> String; - fn can_be_number(&self) -> bool; - fn as_num(&self) -> f64; - fn is_equation(&self) -> bool { - self.as_raw_string().starts_with('=') +pub enum CellType { + Number(f64), + String(String), + Equation(String), +} + +impl Into for f64 { + fn into(self) -> CellType { + CellType::duck_type(self.to_string()) } } -impl Display for dyn Cell { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Into for String { + fn into(self) -> CellType { + CellType::duck_type(self) + } +} - let disp = if self.can_be_number() { - self.as_num().to_string() +impl CellType { + fn duck_type<'a>(value: impl Into) -> Self { + let value = value.into(); + + if let Ok(parse) = value.parse::() { + Self::Number(parse) } else { - self.as_raw_string() + if value.starts_with('=') { + Self::Equation(value) + } else { + Self::String(value) + } + } + } +} + +impl Display for CellType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let d = match self { + CellType::Number(n) => n.to_string(), + CellType::String(n) => n.to_owned(), + CellType::Equation(r) => { + r.to_owned() + }, }; - - write!(f, "{disp}") - } -} - -impl Cell for f64 { - fn as_raw_string(&self) -> String { - ToString::to_string(self) - } - - fn can_be_number(&self) -> bool { - true - } - - fn as_num(&self) -> f64 { - *self - } -} - -impl Into> for f64 { - fn into(self) -> Box { - Box::new(self) - } -} - -impl Into> for String { - fn into(self) -> Box { - Box::new(self) - } -} - -impl Cell for String { - fn as_raw_string(&self) -> String { - ToString::to_string(self) - } - - fn can_be_number(&self) -> bool { - // checking if the string is an equation - self.starts_with('=') - } - - fn as_num(&self) -> f64 { - unimplemented!("&str cannot be used in a numeric context") + write!(f, "{d}") } } @@ -218,7 +202,7 @@ fn test_cells() { assert!(grid.get_cell("A0").is_some()); assert_eq!( - grid.get_cell("A0").as_ref().unwrap().as_raw_string(), + grid.get_cell("A0").as_ref().unwrap().to_string(), String::from("Hello") ); } @@ -246,3 +230,75 @@ fn i_to_c() { assert_eq!(Grid::num_to_char(51), "AZ"); assert_eq!(Grid::num_to_char(701), "ZZ"); } + +#[test] +fn test_math() { + use evalexpr::*; + + let mut grid = Grid::new(); + grid.set_cell("A0", 2.); + grid.set_cell("B0", 1.); + grid.set_cell("C0", "=A0+B0".to_string()); + + assert_eq!(eval("1+2").unwrap(), Value::Int(3)); + if let Some(cell) = grid.get_cell("C0") { + match cell { + CellType::Number(_) => todo!(), + CellType::String(_) => todo!(), + CellType::Equation(a) => { + let res = grid.evaluate(&a); + assert!(res.is_some()); + assert_eq!(res.unwrap(), 3.); + return; + }, + } + } + + panic!("Should've found the value and returned"); +} + +#[test] +fn fn_of_fn() { + let mut grid = Grid::new(); + grid.set_cell("A0", 2.); + grid.set_cell("B0", 1.); + grid.set_cell("C0", "=A0+B0".to_string()); + grid.set_cell("D0", "=C0*2".to_string()); + + if let Some(cell) = grid.get_cell("D0") { + let res = grid.evaluate(&cell.to_string()); + + assert!(res.is_some()); + assert_eq!(res.unwrap(), 6.); + } +} + +#[test] +fn circular_reference_cells() { + let mut grid = Grid::new(); + grid.set_cell("A0", "=B0".to_string()); + grid.set_cell("B0", "=A0".to_string()); + + if let Some(cell) = grid.get_cell("A0") { + let res = grid.evaluate(&cell.to_string()); + assert!(res.is_none()); + } +} + +#[test] +fn fn_of_fn_one_shot() { + let mut grid = Grid::new(); + grid.set_cell("A0", 2.); + grid.set_cell("B0", 1.); + grid.set_cell("C0", "=A0+B0".to_string()); + grid.set_cell("D0", "=C0*2".to_string()); + + grid.set_cell("E0", "=D0+C0".to_string()); + + if let Some(cell) = grid.get_cell("E0") { + let res = grid.evaluate(&cell.to_string()); + + assert!(res.is_some()); + assert_eq!(res.unwrap(), 9.); + } +} \ No newline at end of file diff --git a/src/ctx.rs b/src/app/ctx.rs similarity index 76% rename from src/ctx.rs rename to src/app/ctx.rs index 736b84c..ecd41a0 100644 --- a/src/ctx.rs +++ b/src/app/ctx.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use evalexpr::{error::EvalexprResultValue, *}; -use crate::Grid; +use crate::app::calc::Grid; pub struct CallbackContext<'a, T: EvalexprNumericTypes = DefaultNumericTypes> { variables: &'a Grid, @@ -40,8 +40,16 @@ impl<'a> Context for CallbackContext<'a, DefaultNumericTypes> { fn get_value(&self, identifier: &str) -> Option> { if let Some(v) = self.variables.get_cell(identifier) { - if v.can_be_number() { - return Some(Value::Float(v.as_num())); + + match v { + super::calc::CellType::Number(n) => return Some(Value::Float(n.to_owned())), + super::calc::CellType::String(s) => unimplemented!("{s}"), + super::calc::CellType::Equation(eq) => { + match eval_with_context(&eq[1..], self) { + Ok(e) => return Some(e), + Err(e) => panic!("{e} \"{eq}\""), + } + }, } } return None; diff --git a/src/app/error_msg.rs b/src/app/error_msg.rs new file mode 100644 index 0000000..f10b56f --- /dev/null +++ b/src/app/error_msg.rs @@ -0,0 +1,39 @@ +use std::time::Instant; + +use ratatui::{layout::Rect, prelude, style::{Color, Style}, widgets::{Paragraph, Widget}}; + +pub struct ErrorMessage { + start: Instant, + error_msg: Option, +} + +impl ErrorMessage { + pub fn new(msg: impl Into) -> Self { + Self { + error_msg: Some(msg.into()), + start: Instant::now(), + } + } + + pub fn none() -> Self { + Self { + start: Instant::now(), + error_msg: None, + } + } +} + +impl Widget for &ErrorMessage { + fn render(self, area: Rect, buf: &mut prelude::Buffer) { + // The screen doesn't refresh at a fixed fps like a normal GUI, + // so if the user isn't moving around the timeout will *happen* but + // won't be visualized until they move + let msg = if self.start.elapsed().as_secs() > 3 { + String::new() + } else { + self.error_msg.clone().unwrap_or(String::new()) + }; + + Paragraph::new(msg).style(Style::new().fg(Color::Red)).render(area, buf); + } +} \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs index c4ce48b..5c735a6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,5 @@ pub mod app; -pub mod calc; -pub mod mode; \ No newline at end of file +mod calc; +mod mode; +mod error_msg; +mod ctx; \ No newline at end of file diff --git a/src/app/mode.rs b/src/app/mode.rs index 171f978..311e097 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -1,10 +1,10 @@ -use std::fmt::Display; +use std::{cmp::{max, min}, fmt::Display}; use ratatui::{ - layout::Rect, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget} + prelude, style::{Color, Style}, widgets::{Paragraph, Widget} }; -use crate::app::{app::App}; +use crate::app::{app::App, calc::LEN, error_msg::ErrorMessage}; pub enum Mode { Insert(Editor), @@ -14,37 +14,33 @@ pub enum Mode { Visual((usize, usize)), } -impl Widget for &Mode { - fn render(self, area: Rect, buf: &mut prelude::Buffer) { - let style = match self { - // Where you are typing - italic - Mode::Insert(_) => Style::new().fg(Color::Blue).add_modifier(Modifier::ITALIC), - Mode::Command(_) => Style::new().fg(Color::LightGreen).add_modifier(Modifier::ITALIC), - Mode::Chord(_) => Style::new().fg(Color::Blue).add_modifier(Modifier::ITALIC), - // Movement-based modes - Mode::Visual(_) => Style::new().fg(Color::Yellow), - Mode::Normal => Style::new().fg(Color::Green), - }; - - Paragraph::new(self.to_string()) - .style(style) - .render(area, buf); - } -} - impl Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Mode::Normal => write!(f, "-- NORMAL --"), - Mode::Insert(_) => write!(f, "-- INSERT --"), - Mode::Chord(_) => write!(f, "-- CHORD --"), - Mode::Command(_) => write!(f, "-- COMMAND --"), - Mode::Visual(_) => write!(f, "-- VISUAL --"), + Mode::Normal => write!(f, "NORMAL"), + Mode::Insert(_) => write!(f, "INSERT"), + Mode::Chord(_) => write!(f, "CHORD"), + Mode::Command(_) => write!(f, "COMMAND"), + Mode::Visual(_) => write!(f, "VISUAL"), } } } impl Mode { + + pub fn get_style(&self) -> Style { + match self { + // Where you are typing + Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue), + Mode::Command(_) => Style::new().fg(Color::Black).bg(Color::Magenta), + Mode::Chord(_) => Style::new().fg(Color::Black).bg(Color::LightBlue), + // Movement-based modes + Mode::Visual(_) => Style::new().fg(Color::Yellow), + Mode::Normal => Style::new().fg(Color::Green), + } + } + + pub fn process_cmd(app: &mut App) { if let Mode::Command(editor) = &mut app.mode { // [':', 'q'] @@ -53,7 +49,7 @@ impl Mode { if let Some(file) = &app.file { unimplemented!("Figure out how we want to save Grid to a csv or something") } else { - // TODO figure out how to get an error message to the user + app.error_msg = ErrorMessage::new("No file selected"); } } 'q' => app.exit = true, @@ -73,7 +69,7 @@ impl Mode { } // v 'j' => { - app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_add(1); + app.grid.selected_cell.1 = min(app.grid.selected_cell.1.saturating_add(1), LEN); return; } // ^ @@ -83,7 +79,7 @@ impl Mode { } // > 'l' => { - app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_add(1); + app.grid.selected_cell.0 = min(app.grid.selected_cell.0.saturating_add(1), LEN); return; } '0' => { @@ -95,7 +91,7 @@ impl Mode { let (x, y) = app.grid.selected_cell; let val = - app.grid.get_cell_raw(x, y).as_ref().map(|f| f.as_raw_string()).unwrap_or(String::new()); + app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new()); app.mode = Mode::Insert(Editor::new(val, (x, y))); } diff --git a/src/main.rs b/src/main.rs index 9ea9e8e..29dcf1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,6 @@ mod app; -mod ctx; -use crate::app::{app::App, calc::{Grid}}; - -#[test] -fn test_math() { - use evalexpr::*; - - let mut grid = Grid::new(); - grid.set_cell("A0", 2.); - grid.set_cell("B0", 1.); - grid.set_cell("C0", "=A0+B0".to_string()); - - assert_eq!(eval("1+2").unwrap(), Value::Int(3)); - - let cell_text = &grid.get_cell("C0"); - if let Some(text) = cell_text { - if text.is_equation() { - println!("{}", text.as_raw_string()); - let display = grid.evaluate(&text.as_raw_string()); - assert_eq!(display, Some(3.)); - return; - } - } - panic!("Should've found the value and returned"); -} +use crate::app::{app::App}; fn main() -> Result<(), std::io::Error> { let term = ratatui::init(); @@ -36,6 +12,9 @@ fn main() -> Result<(), std::io::Error> { app.grid.set_cell("C0", "Fruit".to_string()); app.grid.set_cell("C1", "=A1+B1".to_string()); + app.grid.set_cell("D0", "x2".to_string()); + app.grid.set_cell("D1", "=C1*2".to_string()); + let res = app.run(term); ratatui::restore(); return res;