From 0a98df90001df1f9167989a68232f07340adea9c Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Wed, 18 Mar 2026 17:30:43 -0500 Subject: [PATCH] saving --- src/action.rs | 31 +++++++++++++++++++++++++------ src/app/mod.rs | 26 ++++++++++++++++++++++++-- src/app/widget.rs | 6 +++--- src/config.rs | 16 ++++++++++++---- src/edit_action.rs | 6 +++++- src/main.rs | 8 +++++--- 6 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/action.rs b/src/action.rs index 6afca38..f984389 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,4 +1,4 @@ -use std::{cmp::min, mem::{replace, swap}}; +use std::{cmp::min, fs::File, io::Write, mem::{replace, swap}}; use crate::{BYTES_PER_LINE, app::{App, Mode, PartialAction}, edit_action::EditAction}; @@ -10,8 +10,9 @@ pub enum Action { SelectMode, Goto, - Zview, + View, Replace, + Space, MoveByteUp, MoveByteDown, @@ -54,6 +55,8 @@ pub enum Action { Undo, Redo, + + Save, } impl App { @@ -65,8 +68,9 @@ impl App { Action::SelectMode => self.select_mode(), Action::Goto => self.goto(), - Action::Zview => self.zview(), + Action::View => self.view(), Action::Replace => self.replace(), + Action::Space => self.space(), Action::MoveByteUp => self.move_byte_up(), Action::MoveByteDown => self.move_byte_down(), @@ -109,6 +113,8 @@ impl App { Action::Undo => self.undo(), Action::Redo => self.redo(), + + Action::Save => self.save(), } } @@ -128,14 +134,18 @@ impl App { self.partial_action = Some(PartialAction::Goto); } - const fn zview(&mut self) { - self.partial_action = Some(PartialAction::Zview); + const fn view(&mut self) { + self.partial_action = Some(PartialAction::View); } const fn replace(&mut self) { self.partial_action = Some(PartialAction::Replace); } + const fn space(&mut self) { + self.partial_action = Some(PartialAction::Space); + } + const fn move_byte_up(&mut self) { if self.cursor.head >= BYTES_PER_LINE { self.cursor.head -= BYTES_PER_LINE; @@ -359,7 +369,7 @@ impl App { } fn undo(&mut self) { - if self.time_traveling == Some(0) { return } + if self.time_traveling == Some(0) || self.edit_history.is_empty() { return } let current_date = self.time_traveling .map_or(self.edit_history.len() - 1, |date| date - 1); @@ -396,6 +406,15 @@ impl App { self.edit_history[previous_date] = edit_action; } + + fn save(&mut self) { + let mut file = File::create(&self.file_path).unwrap(); + file.write_all(&self.contents).unwrap(); + + self.last_saved_at = Some( + self.time_traveling.unwrap_or(self.edit_history.len()) + ); + } } // helpers diff --git a/src/app/mod.rs b/src/app/mod.rs index 26216fb..28e4384 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -8,6 +8,7 @@ mod widget; pub struct App { pub config: Config, pub file_name: String, + pub file_path: PathBuf, pub contents: Vec, @@ -25,6 +26,8 @@ pub struct App { pub edit_history: Vec, // the index *after* the latest edit action pub time_traveling: Option, + // the index *after* the last saved edit action + pub last_saved_at: Option, pub logs: Vec, } @@ -36,7 +39,7 @@ pub enum Mode { #[derive(Clone, Copy, Hash, PartialEq, Eq)] pub enum PartialAction { - Goto, Zview, Replace + Goto, View, Replace, Space } impl Mode { @@ -61,8 +64,9 @@ impl PartialAction { pub const fn label(self) -> &'static str { match self { Self::Goto => "g", - Self::Zview => "z", + Self::View => "z", Self::Replace => "r", + Self::Space => "␠", } } } @@ -87,6 +91,7 @@ impl App { Self { config: Config::default(), file_name: file_path.file_name().unwrap().to_str().unwrap().to_owned(), + file_path, contents, @@ -104,6 +109,7 @@ impl App { edit_history: Vec::new(), time_traveling: None, + last_saved_at: Some(0), logs: Vec::new(), } @@ -161,6 +167,22 @@ impl App { } } } + + const fn has_unsaved_changes(&self) -> bool { + !self.all_changes_saved() + } + + const fn all_changes_saved(&self) -> bool { + if let Some(last_saved_at) = self.last_saved_at { + if let Some(time_traveling) = self.time_traveling { + last_saved_at == time_traveling + } else { + last_saved_at == self.edit_history.len() + } + } else { + false + } + } } fn nybble_from_hex(hex: char) -> Option { diff --git a/src/app/widget.rs b/src/app/widget.rs index 1c3a8e5..90451eb 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -401,10 +401,10 @@ mod status_line { } fn modified_indicator(&self) -> Span<'static> { - if self.edit_history.is_empty() { - "".into() - } else { + if self.has_unsaved_changes() { " [+]".into() + } else { + "".into() } } } diff --git a/src/config.rs b/src/config.rs index b56f08b..a7098fa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,8 +104,9 @@ impl Default for Config { ("v".try_into().unwrap(), Action::SelectMode), ("g".try_into().unwrap(), Action::Goto), - ("z".try_into().unwrap(), Action::Zview), + ("z".try_into().unwrap(), Action::View), ("r".try_into().unwrap(), Action::Replace), + (" ".try_into().unwrap(), Action::Space), ("i".try_into().unwrap(), Action::MoveByteUp), ("k".try_into().unwrap(), Action::MoveByteDown), @@ -142,7 +143,10 @@ impl Default for Config { ("l".try_into().unwrap(), Action::GotoLineEnd), ("g".try_into().unwrap(), Action::GotoFileStart), - ].into()) + ].into()), + (Some(PartialAction::Space), [ + ("w".try_into().unwrap(), Action::Save), + ].into()), ].into()), (Mode::Select, [ (None, [ @@ -151,8 +155,9 @@ impl Default for Config { ("v".try_into().unwrap(), Action::NormalMode), ("g".try_into().unwrap(), Action::Goto), - ("z".try_into().unwrap(), Action::Zview), + ("z".try_into().unwrap(), Action::View), ("r".try_into().unwrap(), Action::Replace), + (" ".try_into().unwrap(), Action::Space), ("i".try_into().unwrap(), Action::ExtendByteUp), ("k".try_into().unwrap(), Action::ExtendByteDown), @@ -181,7 +186,10 @@ impl Default for Config { ("u".try_into().unwrap(), Action::Undo), ("U".try_into().unwrap(), Action::Redo), - ].into()) + ].into()), + (Some(PartialAction::Space), [ + ("w".try_into().unwrap(), Action::Save), + ].into()), ].into()) ].into() } diff --git a/src/edit_action.rs b/src/edit_action.rs index b5dc9fb..bf32149 100644 --- a/src/edit_action.rs +++ b/src/edit_action.rs @@ -34,6 +34,10 @@ impl App { if let Some(date) = self.time_traveling { self.edit_history.truncate(date); self.time_traveling = None; + + if self.last_saved_at.is_some_and(|it| it > date) { + self.last_saved_at = None; + } } self.edit_history.push(edit_action); @@ -62,7 +66,7 @@ impl App { fn delete_at(&mut self, cursor: Cursor) { self.contents.drain(cursor.range()); - self.cursor.head = min(cursor.head, cursor.tail); + self.cursor.head = min(min(cursor.head, cursor.tail), self.contents.len() - 1); self.cursor.collapse(); } diff --git a/src/main.rs b/src/main.rs index 96ee97b..6e5878b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,8 @@ const BYTES_PER_CHUNK: usize = 4; const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: -// - undo/redo +// - multiple buffers (tabs) +// - search // - modifications // - insert/append // - mode @@ -26,8 +27,6 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // - replace-and-keep-going // - mode // - change -// - saving -// - search // - edit character panel // - modifier on existing keys like teehee? or jump to panel? // - if jump to panel, space? @@ -51,6 +50,9 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // - utf8? // - diffing +// TODO: opening empty file crashes (or deleting entire file) +// - cursor is NOT guaranteed to be in-bounds..? + // when AsciiChar is stabilized, use it instead of char everywhere fn main() {