From 6ed6cbfd624126ed31d04ba6701877bfcd5dc36d Mon Sep 17 00:00:00 2001 From: Rushmore75 Date: Mon, 10 Nov 2025 15:34:25 -0700 Subject: [PATCH] organized --- src/app/app.rs | 287 ++++++++++++++++++++++++++++++++++++++ src/{ => app}/calc.rs | 103 +------------- src/app/mod.rs | 3 + src/app/mode.rs | 139 +++++++++++++++++++ src/ctx.rs | 2 +- src/main.rs | 314 +----------------------------------------- 6 files changed, 441 insertions(+), 407 deletions(-) create mode 100644 src/app/app.rs rename src/{ => app}/calc.rs (63%) create mode 100644 src/app/mod.rs create mode 100644 src/app/mode.rs diff --git a/src/app/app.rs b/src/app/app.rs new file mode 100644 index 0000000..bbbda5e --- /dev/null +++ b/src/app/app.rs @@ -0,0 +1,287 @@ +use std::{io, path::PathBuf}; + +use ratatui::{DefaultTerminal, Frame, crossterm::event, layout::{self, Constraint, Layout, Rect}, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget}}; + +use crate::app::{calc::{Grid, LEN}, mode::Mode}; + +pub struct App { + exit: bool, + pub grid: Grid, + pub mode: Mode, + file: Option, +} + +impl Widget for &App { + fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { + let len = LEN as u16; + + let cell_height = 1; + let cell_length = 5; + + + let x_max = if area.width / cell_length > len { + len - 1 + } else { + area.width / cell_length + }; + let y_max = if area.height / cell_height > len { + len - 1 + } else { + area.height / cell_height + }; + + for x in 0..x_max { + for y in 0..y_max { + let mut display = String::new(); + let mut style = Style::new().fg(Color::White); + + const ORANGE1: Color = Color::Rgb(200, 160, 0); + const ORANGE2: Color = Color::Rgb(180, 130, 0); + + match (x == 0, y == 0) { + (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); + }, + (true, false) => { + // row names + display = y.to_string(); + + let bg = if y%2==0 { + ORANGE1 + } else { + ORANGE2 + }; + style = Style::new().fg(Color::White).bg(bg); + + }, + (false, true) => { + // column names + display = Grid::num_to_char(x as usize -1); + + let bg = if x%2==0 { + ORANGE1 + } else { + ORANGE2 + }; + + style = Style::new().fg(Color::White).bg(bg) + }, + (false, false) => { + // minus 1 because of header cells + let x_idx = x as usize -1; + let y_idx = y as usize -1; + + 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() { + style = Style::new().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED) + } + } + } + } + if (x_idx, y_idx) == self.grid.selected_cell { + style = Style::new() + .fg(Color::Black) + .bg(Color::White); + // modify the style of the cell you are editing + if let Mode::Insert(_) = self.mode { + style = style.add_modifier(Modifier::ITALIC).add_modifier(Modifier::BOLD); + } + } + } + } + + let area = Rect::new( + area.x + (x * cell_length), + area.y + (y * cell_height), + cell_length, + cell_height, + ); + + Paragraph::new(display).style(style).render(area, buf); + } + } + } +} + +impl App { + pub fn new() -> Self { + Self { + exit: false, + grid: Grid::new(), + mode: Mode::Normal, + file: None, + } + } + + pub fn run(&mut self, mut term: DefaultTerminal) -> Result<(), std::io::Error> { + while !self.exit { + term.draw(|frame| self.draw(frame))?; + self.handle_events()?; + } + Ok(()) + } + + fn draw(&self, frame: &mut Frame) { + let layout = Layout::default() + .direction(layout::Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(1), Constraint::Length(1)]) + .split(frame.area()); + + let bottom_split = Layout::default() + .direction(layout::Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layout[2]); + + match &self.mode { + Mode::Insert(editor) => { + frame.render_widget(editor, layout[0]); + } + Mode::Command(editor) => { + frame.render_widget(editor, bottom_split[0]); + } + Mode::Chord(chord) => frame.render_widget(chord, bottom_split[0]), + 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(); + cell + }), + layout[0], + ), + Mode::Visual(start_pos) => {} + } + + frame.render_widget(self, layout[1]); + frame.render_widget(&self.mode, bottom_split[1]); + } + + fn handle_events(&mut self) -> io::Result<()> { + match &mut self.mode { + Mode::Chord(chord) => match event::read()? { + event::Event::Key(key) => match key.code { + event::KeyCode::Esc => self.mode = Mode::Normal, + event::KeyCode::Char(c) => { + chord.add_char(c); + match chord.as_string()[0..chord.as_string().len() - 1].parse::() { + Ok(num) => match c { + 'G' => { + let sel = self.grid.selected_cell; + self.grid.selected_cell = (sel.0, num); + self.mode = Mode::Normal; + } + _ => { + if c.is_alphabetic() { + self.mode = Mode::Normal; + for _ in 0..num { + Mode::process_key(self, c); + } + } + } + }, + Err(_) => match chord.as_string().as_str() { + "d " | "dw" => { + let loc = self.grid.selected_cell; + self.grid.set_cell_raw(loc, String::new()); + self.mode = Mode::Normal; + } + _ => {} + }, + } + } + event::KeyCode::Backspace => { + chord.backspace(); + } + _ => {} + }, + _ => {} + }, + Mode::Insert(editor) => match event::read()? { + event::Event::Key(key) => match key.code { + event::KeyCode::Esc => { + // just cancel the operation + self.mode = Mode::Normal; + } + event::KeyCode::Enter => { + let v = editor.buf.trim().to_string(); + + if let Ok(v) = v.parse::() { + self.grid.set_cell_raw(editor.location, v); + } else { + self.grid.set_cell_raw(editor.location, v); + } + + self.mode = Mode::Normal; + } + event::KeyCode::Backspace => { + editor.buf.pop(); + } + event::KeyCode::Char(c) => { + editor.buf += &c.to_string(); + } + _ => {} + }, + _ => {} + }, + Mode::Normal => match event::read()? { + event::Event::Key(key_event) => match key_event.code { + event::KeyCode::F(_) => todo!(), + event::KeyCode::Char(c) => { + Mode::process_key(self, c); + } + _ => todo!(), + }, + _ => todo!(), + }, + Mode::Visual(start_pos) => { + if let event::Event::Key(key) = event::read()? { + match key.code { + event::KeyCode::Char(c) => { + Mode::process_key(self, c); + todo!(); + } + event::KeyCode::Esc => self.mode = Mode::Normal, + _ => {} + } + } + } + Mode::Command(editor) => match event::read()? { + event::Event::Key(key) => match key.code { + event::KeyCode::Esc => { + // just cancel the operation + self.mode = Mode::Normal; + } + event::KeyCode::Enter => { + // [':', 'q'] + match editor.as_string().as_bytes()[1] as char { + 'w' => {} + 'q' => self.exit = true, + _ => {} + } + self.mode = Mode::Normal; + } + event::KeyCode::Backspace => { + editor.backspace(); + } + event::KeyCode::Char(c) => { + editor.add_char(c); + } + _ => {} + }, + _ => {} + }, + } + + Ok(()) + } +} diff --git a/src/calc.rs b/src/app/calc.rs similarity index 63% rename from src/calc.rs rename to src/app/calc.rs index f2d3e3c..070bc34 100644 --- a/src/calc.rs +++ b/src/app/calc.rs @@ -1,14 +1,11 @@ -use std::{fmt::Display, thread::yield_now}; +use std::fmt::Display; use evalexpr::*; -use ratatui::{ - layout::{Constraint, Layout, Rect}, style::{Style, palette::material::WHITE, *}, widgets::{Paragraph, Widget} -}; use crate::ctx; // if this is very large at all it will overflow the stack -const LEN: usize = 100; +pub const LEN: usize = 100; pub struct Grid { // a b c ... @@ -58,14 +55,13 @@ impl Grid { return Some(val); } Err(e) => match e { - EvalexprError::VariableIdentifierNotFound(e) => { + EvalexprError::VariableIdentifierNotFound(_e) => { // panic!("Will not be able to parse this equation, cell {e} not found") return None } _ => panic!("{}", e), }, } - None } fn parse_to_idx(i: &str) -> (usize, usize) { @@ -123,7 +119,7 @@ impl Grid { (c.to_ascii_lowercase() as usize - 97) + 26 * idx } - fn num_to_char(idx: usize) -> String { + pub fn num_to_char(idx: usize) -> String { /* A = 0 AA = 26 @@ -250,94 +246,3 @@ fn i_to_c() { assert_eq!(Grid::num_to_char(51), "AZ"); assert_eq!(Grid::num_to_char(701), "ZZ"); } - -impl Widget for &Grid { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let len = LEN as u16; - - let cell_height = 1; - let cell_length = 5; - - - let x_max = if area.width / cell_length > len { - len - 1 - } else { - area.width / cell_length - }; - let y_max = if area.height / cell_height > len { - len - 1 - } else { - area.height / cell_height - }; - - for x in 0..x_max { - for y in 0..y_max { - let mut display = String::new(); - let mut style = Style::new().white(); - - const ORANGE1: Color = Color::Rgb(200, 160, 0); - const ORANGE2: Color = Color::Rgb(180, 130, 0); - - match (x == 0, y == 0) { - (true, true) => {}, - (true, false) => { - // row names - display = y.to_string(); - - let bg = if y%2==0 { - ORANGE1 - } else { - ORANGE2 - }; - style = Style::new().fg(Color::White).bg(bg); - - }, - (false, true) => { - // column names - display = Grid::num_to_char(x as usize -1); - - let bg = if x%2==0 { - ORANGE1 - } else { - ORANGE2 - }; - - style = Style::new().fg(Color::White).bg(bg) - }, - (false, false) => { - // minus 1 because of header cells - let x_idx = x as usize -1; - let y_idx = y as usize -1; - - if let Some(cell) = self.get_cell_raw(x_idx, y_idx) { - display = cell.as_raw_string(); - - if cell.can_be_number() { - if let Some(val) = self.evaluate(&cell.as_raw_string()) { - display = val.to_string(); - } else { - // broken formulas - if cell.is_equation() { - style = Style::new().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED) - } - } - } - } - if (x_idx, y_idx) == self.selected_cell { - style = Style::new().fg(Color::Black).bg(Color::White); - } - } - } - - let area = Rect::new( - area.x + (x * cell_length), - area.y + (y * cell_height), - cell_length, - cell_height, - ); - - Paragraph::new(display).style(style).render(area, buf); - } - } - } -} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..c4ce48b --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod calc; +pub mod mode; \ No newline at end of file diff --git a/src/app/mode.rs b/src/app/mode.rs new file mode 100644 index 0000000..14ef942 --- /dev/null +++ b/src/app/mode.rs @@ -0,0 +1,139 @@ +use std::fmt::Display; + +use ratatui::{ + layout::Rect, + prelude, + widgets::{Paragraph, Widget}, +}; + +use crate::app::app::App; + +pub enum Mode { + Insert(Editor), + Chord(Chord), + Normal, + Command(Chord), + Visual((usize, usize)), +} + +impl Widget for &Mode { + fn render(self, area: Rect, buf: &mut prelude::Buffer) { + Paragraph::new(self.to_string()).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 --"), + } + } +} + +impl Mode { + pub fn process_key(app: &mut App, key: char) { + match key { + // < + 'h' => { + app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_sub(1); + return; + } + // v + 'j' => { + app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_add(1); + return; + } + // ^ + 'k' => { + app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_sub(1); + return; + } + // > + 'l' => { + app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_add(1); + return; + } + _ => {} + } + + if let Mode::Normal = app.mode { + match key { + // edit cell + 'i' | 'a' => { + 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.mode = Mode::Insert(Editor::new(val, (x, y))); + } + 'I' => { /* insert col before */ } + 'A' => { /* insert col after */ } + 'o' => { /* insert row below */ } + 'O' => { /* insert row above */ } + 'v' => app.mode = Mode::Visual(app.grid.selected_cell), + ':' => app.mode = Mode::Command(Chord::new(':')), + // loose chars will put you into chord mode + c => app.mode = Mode::Chord(Chord::new(c)), + } + } + } +} + +pub struct Editor { + pub buf: String, + cursor: usize, + pub location: (usize, usize), +} +impl Editor { + fn new(value: String, loc: (usize, usize)) -> Self { + Self { + buf: value.to_string(), + cursor: value.len(), + location: loc, + } + } +} + +impl Widget for &Editor { + fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { + // TODO add visual cursor + Paragraph::new(self.buf.clone()).render(area, buf); + } +} + +pub struct Chord { + buf: Vec, +} + +impl Chord { + pub fn new(inital: char) -> Self { + let mut buf = Vec::new(); + buf.push(inital); + + Self { + buf, + } + } + + pub fn backspace(&mut self) { + self.buf.pop(); + } + + pub fn add_char(&mut self, c: char) { + self.buf.push(c) + } + + pub fn as_string(&self) -> String { + self.buf.iter().collect() + } +} + +impl Widget for &Chord { + fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { + Paragraph::new(self.buf.iter().collect::()).render(area, buf); + } +} diff --git a/src/ctx.rs b/src/ctx.rs index 3d3023f..736b84c 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, rc::Rc}; +use std::collections::HashMap; use evalexpr::{error::EvalexprResultValue, *}; diff --git a/src/main.rs b/src/main.rs index d62a49b..9ea9e8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,7 @@ -mod calc; +mod app; mod ctx; -use std::{fmt::Display, io, path::PathBuf}; - -use ratatui::{ - crossterm::event, - layout::{Constraint, Layout}, - widgets::{Paragraph, Widget}, - *, -}; - -use crate::calc::Grid; +use crate::app::{app::App, calc::{Grid}}; #[test] fn test_math() { @@ -38,305 +29,14 @@ fn test_math() { fn main() -> Result<(), std::io::Error> { let term = ratatui::init(); let mut app = App::new(); - app.grid.set_cell("A0", 10.); + app.grid.set_cell("A0", "Apples".to_string()); + app.grid.set_cell("A1", 10.); + app.grid.set_cell("B0", "Bananas".to_string()); app.grid.set_cell("B1", 10.); - app.grid.set_cell("C2", "=A0+B1".to_string()); + app.grid.set_cell("C0", "Fruit".to_string()); + app.grid.set_cell("C1", "=A1+B1".to_string()); let res = app.run(term); ratatui::restore(); return res; } - -enum Mode { - Insert(Editor), - Chord(Chord), - Normal, - Command(Chord), - Visual((usize, usize)), -} - -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 --"), - } - } -} - -impl Mode { - fn process_key(app: &mut App, key: char) { - match key { - // < - 'h' => app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_sub(1), - // v - 'j' => app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_add(1), - // ^ - 'k' => app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_sub(1), - // > - 'l' => app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_add(1), - _ => {} - } - - if let Mode::Normal = app.mode { - match key { - // edit cell - 'i' | 'a' => { - 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.mode = Mode::Insert(Editor::new(val, (x, y))); - } - 'I' => { /* insert col before */ } - 'A' => { /* insert col after */ } - 'o' => { /* insert row below */ } - 'O' => { /* insert row above */ } - 'v' => app.mode = Mode::Visual(app.grid.selected_cell), - ':' => app.mode = Mode::Command(Chord::new(':')), - // loose chars will put you into chord mode - c => app.mode = Mode::Chord(Chord::new(c)), - } - } - } -} - -struct App { - exit: bool, - grid: Grid, - mode: Mode, - file: Option, -} - -impl Widget for &App { - fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { - Paragraph::new(self.mode.to_string()).render(area, buf); - } -} - -impl App { - fn new() -> Self { - Self { - exit: false, - grid: Grid::new(), - mode: Mode::Normal, - file: None, - } - } - - fn run(&mut self, mut term: DefaultTerminal) -> Result<(), std::io::Error> { - while !self.exit { - term.draw(|frame| self.draw(frame))?; - self.handle_events()?; - } - Ok(()) - } - - fn draw(&self, frame: &mut Frame) { - let layout = Layout::default() - .direction(layout::Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Min(1), Constraint::Length(1)]) - .split(frame.area()); - - let bottom_split = Layout::default() - .direction(layout::Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(layout[2]); - - match &self.mode { - Mode::Insert(editor) => { - frame.render_widget(editor, layout[0]); - } - Mode::Command(editor) => { - frame.render_widget(editor, bottom_split[0]); - } - Mode::Chord(chord) => frame.render_widget(chord, bottom_split[0]), - 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(); - cell - }), - layout[0], - ), - Mode::Visual(start_pos) => {} - } - - frame.render_widget(&self.grid, layout[1]); - frame.render_widget(self, bottom_split[1]); - } - - fn handle_events(&mut self) -> io::Result<()> { - match &mut self.mode { - Mode::Chord(chord) => match event::read()? { - event::Event::Key(key) => match key.code { - event::KeyCode::Esc => self.mode = Mode::Normal, - event::KeyCode::Char(c) => { - chord.buf.push(c); - match chord.as_string()[0..chord.buf.len() - 1].parse::() { - Ok(num) => match c { - 'G' => { - let sel = self.grid.selected_cell; - self.grid.selected_cell = (sel.0, num); - self.mode = Mode::Normal; - } - _ => { - if c.is_alphabetic() { - self.mode = Mode::Normal; - for _ in 0..num { - Mode::process_key(self, c); - } - } - } - }, - Err(_) => match chord.as_string().as_str() { - "d " | "dw" => { - let loc = self.grid.selected_cell; - self.grid.set_cell_raw(loc, String::new()); - self.mode = Mode::Normal; - } - _ => {} - }, - } - } - _ => {} - }, - _ => {} - }, - Mode::Insert(editor) => match event::read()? { - event::Event::Key(key) => match key.code { - event::KeyCode::Esc => { - // just cancel the operation - self.mode = Mode::Normal; - } - event::KeyCode::Enter => { - let v = editor.buf.trim().to_string(); - - if let Ok(v) = v.parse::() { - self.grid.set_cell_raw(editor.location, v); - } else { - self.grid.set_cell_raw(editor.location, v); - } - - self.mode = Mode::Normal; - } - event::KeyCode::Backspace => { - editor.buf.pop(); - } - event::KeyCode::Char(c) => { - editor.buf += &c.to_string(); - } - _ => {} - }, - _ => {} - }, - Mode::Normal => match event::read()? { - event::Event::Key(key_event) => match key_event.code { - event::KeyCode::F(_) => todo!(), - event::KeyCode::Char(c) => { - Mode::process_key(self, c); - } - _ => todo!(), - }, - _ => todo!(), - }, - Mode::Visual(start_pos) => { - if let event::Event::Key(key) = event::read()? { - match key.code { - event::KeyCode::Char(c) => { - Mode::process_key(self, c); - todo!(); - } - event::KeyCode::Esc => self.mode = Mode::Normal, - _ => {} - } - } - } - Mode::Command(editor) => match event::read()? { - event::Event::Key(key) => match key.code { - event::KeyCode::Esc => { - // just cancel the operation - self.mode = Mode::Normal; - } - event::KeyCode::Enter => { - // [':', 'q'] - match editor.buf[1] { - 'w' => {} - 'q' => self.exit = true, - _ => {} - } - self.mode = Mode::Normal; - } - event::KeyCode::Backspace => { - editor.buf.pop(); - } - event::KeyCode::Char(c) => { - editor.add_char(c); - } - _ => {} - }, - _ => {} - }, - } - - Ok(()) - } -} - -struct Editor { - buf: String, - cursor: usize, - location: (usize, usize), -} -impl Editor { - fn new(value: String, loc: (usize, usize)) -> Self { - Self { - buf: value.to_string(), - cursor: value.len(), - location: loc, - } - } -} - -impl Widget for &Editor { - fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { - // TODO add visual cursor - Paragraph::new(self.buf.clone()).render(area, buf); - } -} - -struct Chord { - buf: Vec, -} - -impl Chord { - fn new(inital: char) -> Self { - let mut buf = Vec::new(); - buf.push(inital); - - Self { - buf, - } - } - - fn backspace(&mut self) { - self.buf.pop(); - } - - fn add_char(&mut self, c: char) { - self.buf.push(c) - } - - fn as_string(&self) -> String { - self.buf.iter().collect() - } -} - -impl Widget for &Chord { - fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { - Paragraph::new(self.buf.iter().collect::()).render(area, buf); - } -}