From 9e9e46fe26fa34c9aa11f40aaf770d33f4b44b6e Mon Sep 17 00:00:00 2001 From: Rushmore75 Date: Wed, 12 Nov 2025 09:45:22 -0700 Subject: [PATCH] save --- src/app/app.rs | 30 ++++++++++++++++++++--- src/app/logic/calc.rs | 57 ++++++++++++++++++++++++++++++++++++++++--- src/app/mode.rs | 41 +++++++++++++++++++++++++------ src/app/screen.rs | 4 +-- src/main.rs | 17 ++++++++++++- 5 files changed, 132 insertions(+), 17 deletions(-) diff --git a/src/app/app.rs b/src/app/app.rs index e0b8cda..83db28e 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io, path::PathBuf}; +use std::{cmp::{max, min}, collections::HashMap, io, path::PathBuf}; use ratatui::{ DefaultTerminal, Frame, @@ -164,6 +164,13 @@ impl App { } } + pub fn new_with_file(file: impl Into + Clone) -> std::io::Result { + let mut app = Self::new(); + app.file = Some(file.clone().into()); + app.grid = Grid::new_from_file(file.into())?; + Ok(app) + } + pub fn run(&mut self, mut term: DefaultTerminal) -> Result<(), std::io::Error> { while !self.exit { term.draw(|frame| self.draw(frame))?; @@ -181,13 +188,28 @@ impl App { let cmd_line = layout[0]; let body = layout[1]; + let len = match &self.mode { + Mode::Insert(edit) | + Mode::Command(edit) | + Mode::Chord(edit) => edit.len(), + Mode::Normal => { + let (x, y) = self.grid.selected_cell; + let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string().len()).unwrap_or_default(); + cell + }, + Mode::Visual(_) => 0, + }; + // min 20 chars, expand if needed + let len = max(len as u16 +1, 20); + let bottom_split = Layout::default() .direction(layout::Direction::Horizontal) - .constraints([Constraint::Min(30), Constraint::Min(100)]) + .constraints([Constraint::Length(len), Constraint::Percentage(50), Constraint::Percentage(50)]) .split(cmd_line); let cmd_line_left = bottom_split[0]; let cmd_line_right = bottom_split[1]; + let cmd_line_debug = bottom_split[2]; match &self.mode { Mode::Insert(editor) => { @@ -211,7 +233,7 @@ impl App { frame.render_widget(self, body); frame.render_widget(&self.error_msg, cmd_line_right); #[cfg(debug_assertions)] - frame.render_widget(Paragraph::new(format!("x/w y/h: cursor{:?} scroll({}, {}) cell({}, {}) screen({}, {})", + frame.render_widget(Paragraph::new(format!("x/w y/h: cursor{:?} scroll({}, {}) cell({}, {}) screen({}, {}) len{len}", self.grid.selected_cell, self.screen.scroll_x(), self.screen.scroll_y(), @@ -219,7 +241,7 @@ impl App { self.screen.get_cell_height(&self.vars), body.width, body.height, - )), cmd_line_right); + )), cmd_line_debug); } fn handle_events(&mut self) -> io::Result<()> { diff --git a/src/app/logic/calc.rs b/src/app/logic/calc.rs index fb1fed1..80aa5cd 100644 --- a/src/app/logic/calc.rs +++ b/src/app/logic/calc.rs @@ -1,7 +1,6 @@ -use std::fmt::Display; +use std::{fmt::Display, fs, io::{Read, Write}, path::PathBuf}; use evalexpr::*; -use ratatui::buffer::Cell; use crate::app::logic::ctx; @@ -17,6 +16,8 @@ pub struct Grid { cells: Vec>>, /// (X, Y) pub selected_cell: (usize, usize), + /// Have unsaved modifications been made? + dirty: bool, } impl std::fmt::Debug for Grid { @@ -28,11 +29,43 @@ impl std::fmt::Debug for Grid { } impl Grid { + pub fn needs_to_be_saved(&self) -> bool { + self.dirty + } + + /// Save file to `path` as a csv. Path with have `csv` appended to it if it + /// does not already have the extension. + pub fn save_to(&mut self, path: impl Into) -> std::io::Result<()> { + let mut path = path.into(); + match path.extension() { + Some(ext) => { + if ext != "csv" { + path.add_extension("csv"); + } + }, + None => {path.add_extension("csv");}, + } + + let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?; + let (mx, my) = self.max(); + for y in 0..=my { + for x in 0..=mx { + let cell = &self.cells[x][y]; + let display = cell.as_ref().map(|f| f.to_string()).unwrap_or(String::new()); + write!(f, "{display},")?; + } + write!(f, "\n")?; + } + f.flush()?; + + self.dirty = false; + Ok(()) + } /// Iterate over the entire grid and see where /// the farthest modified cell is. #[must_use] - pub fn max(&self) -> (usize, usize) { + fn max(&self) -> (usize, usize) { let mut max_x = 0; let mut max_y = 0; @@ -52,6 +85,22 @@ impl Grid { (max_x, max_y) } + pub fn new_from_file(path: impl Into) -> std::io::Result { + let mut grid = Self::new(); + + let mut file = fs::OpenOptions::new().read(true).open(path.into())?; + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + for (yi, line) in buf.lines().enumerate() { + for (xi, cell) in line.split(',').enumerate() { + // This gets automatically duck-typed + grid.set_cell_raw((xi, yi), Some(cell.to_string())); + } + } + + Ok(grid) + } + pub fn new() -> Self { let mut a = Vec::with_capacity(LEN); for _ in 0..LEN { @@ -65,6 +114,7 @@ impl Grid { Self { cells: a, selected_cell: (0, 0), + dirty: false, } } @@ -146,6 +196,7 @@ impl Grid { pub fn set_cell_raw>(&mut self, (x,y): (usize, usize), val: Option) { // TODO check oob self.cells[x][y] = val.map(|v| v.into()); + self.dirty = true; } /// Get cells via text like: diff --git a/src/app/mode.rs b/src/app/mode.rs index 612d31f..593f8e0 100644 --- a/src/app/mode.rs +++ b/src/app/mode.rs @@ -1,6 +1,6 @@ use std::{ cmp::min, - fmt::Display, + fmt::Display, fs, }; use ratatui::{ @@ -54,16 +54,40 @@ impl Mode { match args[0] { "w" => { - if let Some(file) = &app.file { - unimplemented!("Figure out how we want to save Grid to a csv or something") - } else { - if let Some(arg) = args.get(1) { - unimplemented!("Saving a file") + // first try the passed argument as file + if let Some(arg) = args.get(1) { + if let Err(e) = app.grid.save_to(arg) { + app.error_msg = ErrorMessage::new(format!("{e}")); + } else { + // file saving was a success, adopt the provided file + // if we don't already have one (this is how vim works) + if let None = app.file { + app.file = Some(arg.into()) + } } + // then try the file that we opened the program with + } else if let Some(file) = &app.file { + if let Err(e) = app.grid.save_to(file) { + app.error_msg = ErrorMessage::new(format!("{e}")); + } + // you need to provide a file from *somewhere* + } else { app.error_msg = ErrorMessage::new("No file selected"); } } - "q" => app.exit = true, + // quit + "q" => { + if app.grid.needs_to_be_saved() { + app.exit = false; + app.error_msg = ErrorMessage::new("File not saved"); + } else { + app.exit = true + } + }, + // force quit + "q!" => { + app.exit = true; + } "set" => { if let Some(arg) = args.get(1) { let parts: Vec<&str> = arg.split('=').collect(); @@ -238,6 +262,9 @@ impl Chord { pub fn as_string(&self) -> String { self.buf.iter().collect() } + pub fn len(&self) -> usize { + self.buf.len() + } } impl Widget for &Chord { diff --git a/src/app/screen.rs b/src/app/screen.rs index e1199bd..d2ec126 100644 --- a/src/app/screen.rs +++ b/src/app/screen.rs @@ -1,8 +1,8 @@ -use std::{collections::HashMap, env::VarError, sync::RwLock}; +use std::{collections::HashMap, sync::RwLock}; use ratatui::prelude; -use crate::app::{app::App, logic::calc::LEN}; +use crate::app::logic::calc::LEN; pub struct ScreenSpace { /// This is measured in cells diff --git a/src/main.rs b/src/main.rs index 94b6ea5..201a029 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,25 @@ mod app; +use std::env::args; + use crate::app::{app::App}; fn main() -> Result<(), std::io::Error> { + + let args = args().collect::>(); + let mut app = if args.len() > 1 { + let file = &args[1]; + match App::new_with_file(file) { + Ok(o) => o, + Err(e) => { + return Err(e); + }, + } + } else { + App::new() + }; + let term = ratatui::init(); - let mut app = App::new(); let res = app.run(term); ratatui::restore(); return res;