This commit is contained in:
2025-11-11 08:56:56 -07:00
parent 40f5c3d535
commit d22c053ce0
7 changed files with 229 additions and 147 deletions

View File

@@ -11,6 +11,7 @@ use ratatui::{
use crate::app::{ use crate::app::{
calc::{Grid, LEN}, calc::{Grid, LEN},
error_msg::ErrorMessage,
mode::Mode, mode::Mode,
}; };
@@ -19,6 +20,7 @@ pub struct App {
pub grid: Grid, pub grid: Grid,
pub mode: Mode, pub mode: Mode,
pub file: Option<PathBuf>, pub file: Option<PathBuf>,
pub error_msg: ErrorMessage,
} }
impl Widget for &App { impl Widget for &App {
@@ -75,14 +77,13 @@ impl Widget for &App {
const ORANGE2: Color = Color::Rgb(180, 130, 0); const ORANGE2: Color = Color::Rgb(180, 130, 0);
match (x == 0, y == 0) { match (x == 0, y == 0) {
// 0,0 dead space
(true, true) => { (true, true) => {
let (x, y) = self.grid.selected_cell; display = self.mode.to_string();
let c = Grid::num_to_char(x); style = self.mode.get_style();
display = format!("{y}{c}",);
style = Style::new().fg(Color::Green).bg(Color::Black);
} }
// row names
(true, false) => { (true, false) => {
// row names
display = y_idx.to_string(); display = y_idx.to_string();
let bg = if y_idx == self.grid.selected_cell.1 { 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); style = Style::new().fg(Color::White).bg(bg);
} }
// column names
(false, true) => { (false, true) => {
// column names
display = Grid::num_to_char(x_idx); display = Grid::num_to_char(x_idx);
let bg = if x_idx == self.grid.selected_cell.0 { 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) style = Style::new().fg(Color::White).bg(bg)
} }
// grid squares
(false, false) => { (false, false) => {
if let Some(cell) = self.grid.get_cell_raw(x_idx, y_idx) { if let Some(cell) = self.grid.get_cell_raw(x_idx, y_idx) {
display = cell.as_raw_string(); match cell {
crate::app::calc::CellType::Number(c) => display = c.to_string(),
if cell.can_be_number() { crate::app::calc::CellType::String(s) => display = s.to_owned(),
if let Some(val) = self.grid.evaluate(&cell.as_raw_string()) { crate::app::calc::CellType::Equation(e) => {
display = val.to_string(); if let Some(val) = self.grid.evaluate(e) {
style = Style::new() display = val.to_string();
.underline_color(Color::DarkGray) style = Style::new()
.add_modifier(Modifier::UNDERLINED); .underline_color(Color::DarkGray)
} else { .add_modifier(Modifier::UNDERLINED);
// broken formulas } else {
if cell.is_equation() {
style = style =
Style::new().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED) Style::new().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED)
} }
@@ -152,6 +153,7 @@ impl App {
grid: Grid::new(), grid: Grid::new(),
mode: Mode::Normal, mode: Mode::Normal,
file: None, file: None,
error_msg: ErrorMessage::none(),
} }
} }
@@ -191,7 +193,7 @@ impl App {
Mode::Normal => frame.render_widget( Mode::Normal => frame.render_widget(
Paragraph::new({ Paragraph::new({
let (x, y) = self.grid.selected_cell; 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 cell
}), }),
cmd_line_left, cmd_line_left,
@@ -200,7 +202,7 @@ impl App {
} }
frame.render_widget(self, body); 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<()> { fn handle_events(&mut self) -> io::Result<()> {
@@ -251,9 +253,9 @@ impl App {
event::KeyCode::Char(c) => { event::KeyCode::Char(c) => {
Mode::process_key(self, c); Mode::process_key(self, c);
} }
_ => todo!(), _ => {}
}, },
_ => todo!(), _ => {}
}, },
Mode::Visual(_start_pos) => { Mode::Visual(_start_pos) => {
if let event::Event::Key(key) = event::read()? { if let event::Event::Key(key) = event::read()? {

View File

@@ -2,10 +2,10 @@ use std::fmt::Display;
use evalexpr::*; use evalexpr::*;
use crate::ctx; use crate::app::ctx;
// if this is very large at all it will overflow the stack // 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 { pub struct Grid {
// a b c ... // a b c ...
@@ -13,7 +13,7 @@ pub struct Grid {
// 1 // 1
// 2 // 2
// ... // ...
cells: [[Option<Box<dyn Cell>>; LEN]; LEN], cells: [[Option<CellType>; LEN]; LEN],
/// (X, Y) /// (X, Y)
pub selected_cell: (usize, usize), pub selected_cell: (usize, usize),
} }
@@ -29,7 +29,7 @@ impl std::fmt::Debug for Grid {
impl Grid { impl Grid {
pub fn new() -> Self { pub fn new() -> Self {
// TODO this needs to be moved to the heap // TODO this needs to be moved to the heap
let b: [[Option<Box<dyn Cell>>; LEN]; LEN] = let b: [[Option<CellType>; LEN]; LEN] =
core::array::from_fn(|_| core::array::from_fn(|_| None)); core::array::from_fn(|_| core::array::from_fn(|_| None));
Self { Self {
@@ -90,12 +90,12 @@ impl Grid {
(x_idx, y_idx) (x_idx, y_idx)
} }
pub fn set_cell<T: Into<Box<dyn Cell>>>(&mut self, cell_id: &str, val: T) { pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
let loc = Self::parse_to_idx(cell_id); let loc = Self::parse_to_idx(cell_id);
self.set_cell_raw(loc, val); self.set_cell_raw(loc, val);
} }
pub fn set_cell_raw<T: Into<Box<dyn Cell>>>(&mut self, (x,y): (usize, usize), val: T) { pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x,y): (usize, usize), val: T) {
// TODO check oob // TODO check oob
self.cells[x][y] = Some(val.into()); self.cells[x][y] = Some(val.into());
} }
@@ -104,13 +104,15 @@ impl Grid {
/// A6, /// A6,
/// F0, /// F0,
/// etc /// etc
pub fn get_cell(&self, cell_id: &str) -> &Option<Box<dyn Cell>> { pub fn get_cell(&self, cell_id: &str) -> &Option<CellType> {
let (x, y) = Self::parse_to_idx(cell_id); let (x, y) = Self::parse_to_idx(cell_id);
self.get_cell_raw(x, y) self.get_cell_raw(x, y)
} }
pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option<Box<dyn Cell>> { pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option<CellType> {
// TODO check oob if x >= LEN || y >= LEN {
return &None
}
&self.cells[x][y] &self.cells[x][y]
} }
@@ -144,68 +146,50 @@ impl Default for Grid {
} }
} }
pub trait Cell { pub enum CellType {
/// Important! This is IS NOT the return value of an equation. Number(f64),
/// This is the raw equation it's self. String(String),
fn as_raw_string(&self) -> String; Equation(String),
fn can_be_number(&self) -> bool; }
fn as_num(&self) -> f64;
fn is_equation(&self) -> bool { impl Into<CellType> for f64 {
self.as_raw_string().starts_with('=') fn into(self) -> CellType {
CellType::duck_type(self.to_string())
} }
} }
impl Display for dyn Cell { impl Into<CellType> for String {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn into(self) -> CellType {
CellType::duck_type(self)
}
}
let disp = if self.can_be_number() { impl CellType {
self.as_num().to_string() fn duck_type<'a>(value: impl Into<String>) -> Self {
let value = value.into();
if let Ok(parse) = value.parse::<f64>() {
Self::Number(parse)
} else { } 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, "{d}")
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<Box<dyn Cell>> for f64 {
fn into(self) -> Box<dyn Cell> {
Box::new(self)
}
}
impl Into<Box<dyn Cell>> for String {
fn into(self) -> Box<dyn Cell> {
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")
} }
} }
@@ -218,7 +202,7 @@ fn test_cells() {
assert!(grid.get_cell("A0").is_some()); assert!(grid.get_cell("A0").is_some());
assert_eq!( assert_eq!(
grid.get_cell("A0").as_ref().unwrap().as_raw_string(), grid.get_cell("A0").as_ref().unwrap().to_string(),
String::from("Hello") 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(51), "AZ");
assert_eq!(Grid::num_to_char(701), "ZZ"); 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.);
}
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use evalexpr::{error::EvalexprResultValue, *}; use evalexpr::{error::EvalexprResultValue, *};
use crate::Grid; use crate::app::calc::Grid;
pub struct CallbackContext<'a, T: EvalexprNumericTypes = DefaultNumericTypes> { pub struct CallbackContext<'a, T: EvalexprNumericTypes = DefaultNumericTypes> {
variables: &'a Grid, variables: &'a Grid,
@@ -40,8 +40,16 @@ impl<'a> Context for CallbackContext<'a, DefaultNumericTypes> {
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> { fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
if let Some(v) = self.variables.get_cell(identifier) { 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; return None;

39
src/app/error_msg.rs Normal file
View File

@@ -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<String>,
}
impl ErrorMessage {
pub fn new(msg: impl Into<String>) -> 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);
}
}

View File

@@ -1,3 +1,5 @@
pub mod app; pub mod app;
pub mod calc; mod calc;
pub mod mode; mod mode;
mod error_msg;
mod ctx;

View File

@@ -1,10 +1,10 @@
use std::fmt::Display; use std::{cmp::{max, min}, fmt::Display};
use ratatui::{ 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 { pub enum Mode {
Insert(Editor), Insert(Editor),
@@ -14,37 +14,33 @@ pub enum Mode {
Visual((usize, usize)), 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 { impl Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Mode::Normal => write!(f, "-- NORMAL --"), Mode::Normal => write!(f, "NORMAL"),
Mode::Insert(_) => write!(f, "-- INSERT --"), Mode::Insert(_) => write!(f, "INSERT"),
Mode::Chord(_) => write!(f, "-- CHORD --"), Mode::Chord(_) => write!(f, "CHORD"),
Mode::Command(_) => write!(f, "-- COMMAND --"), Mode::Command(_) => write!(f, "COMMAND"),
Mode::Visual(_) => write!(f, "-- VISUAL --"), Mode::Visual(_) => write!(f, "VISUAL"),
} }
} }
} }
impl Mode { 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) { pub fn process_cmd(app: &mut App) {
if let Mode::Command(editor) = &mut app.mode { if let Mode::Command(editor) = &mut app.mode {
// [':', 'q'] // [':', 'q']
@@ -53,7 +49,7 @@ impl Mode {
if let Some(file) = &app.file { if let Some(file) = &app.file {
unimplemented!("Figure out how we want to save Grid to a csv or something") unimplemented!("Figure out how we want to save Grid to a csv or something")
} else { } 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, 'q' => app.exit = true,
@@ -73,7 +69,7 @@ impl Mode {
} }
// v // v
'j' => { '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; return;
} }
// ^ // ^
@@ -83,7 +79,7 @@ impl Mode {
} }
// > // >
'l' => { '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; return;
} }
'0' => { '0' => {
@@ -95,7 +91,7 @@ impl Mode {
let (x, y) = app.grid.selected_cell; let (x, y) = app.grid.selected_cell;
let val = 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))); app.mode = Mode::Insert(Editor::new(val, (x, y)));
} }

View File

@@ -1,30 +1,6 @@
mod app; mod app;
mod ctx;
use crate::app::{app::App, calc::{Grid}}; use crate::app::{app::App};
#[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");
}
fn main() -> Result<(), std::io::Error> { fn main() -> Result<(), std::io::Error> {
let term = ratatui::init(); 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("C0", "Fruit".to_string());
app.grid.set_cell("C1", "=A1+B1".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); let res = app.run(term);
ratatui::restore(); ratatui::restore();
return res; return res;