From 1d9342186a7da9d8274b70e8f33dc594b7843964 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 14:33:26 +0100 Subject: [PATCH] added undo + dynamic status bar --- Cargo.lock | 2 +- README.md | 1 + config.example.toml | 1 + src/app.rs | 29 ++++++++++++++++++++++++++++- src/config.rs | 2 ++ src/runner.rs | 7 ++++++- src/ui.rs | 20 +++++++++++++------- 7 files changed, 52 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe29849..1968e7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "mould" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "clap", diff --git a/README.md b/README.md index a30551c..78a01d9 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ mould config.template.json -o config.json - `o`: Append a new item to the current array - `O`: Prepend a new item to the current array - `dd`: Delete the currently selected variable or group + - `u`: Undo the last change - `a`: Add missing value from template to active config - `/`: Search for configuration keys (Jump to matches) - `n`: Jump to the next search match diff --git a/config.example.toml b/config.example.toml index 7e3310b..c230ca6 100644 --- a/config.example.toml +++ b/config.example.toml @@ -44,3 +44,4 @@ jump_bottom = "G" append_item = "o" prepend_item = "O" delete_item = "dd" +undo = "u" diff --git a/src/app.rs b/src/app.rs index fc4f797..c09f05f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,6 +33,8 @@ pub struct App { pub input: Input, /// The current search query for filtering keys. pub search_query: String, + /// Stack of previous variable states for undo functionality. + pub undo_stack: Vec>, } impl App { @@ -47,6 +49,7 @@ impl App { status_message: None, input: Input::new(initial_input), search_query: String::new(), + undo_stack: Vec::new(), } } @@ -159,8 +162,8 @@ impl App { pub fn enter_insert(&mut self, variant: InsertVariant) { if let Some(var) = self.vars.get(self.selected) { if !var.is_group { + self.save_undo_state(); self.mode = Mode::Insert; - self.status_message = None; match variant { InsertVariant::Start => { use tui_input::InputRequest; @@ -190,6 +193,7 @@ impl App { return; } + self.save_undo_state(); let selected_path = self.vars[self.selected].path.clone(); let is_group = self.vars[self.selected].is_group; @@ -252,6 +256,7 @@ impl App { return; } + self.save_undo_state(); let (base, idx, depth) = { let selected_item = &self.vars[self.selected]; if selected_item.is_group { @@ -322,6 +327,28 @@ impl App { .map(|v| v.status == crate::format::ItemStatus::MissingFromActive) .unwrap_or(false) } + + /// Saves the current state of variables to the undo stack. + pub fn save_undo_state(&mut self) { + self.undo_stack.push(self.vars.clone()); + if self.undo_stack.len() > 50 { + self.undo_stack.remove(0); + } + } + + /// Reverts to the last saved state of variables. + pub fn undo(&mut self) { + if let Some(previous_vars) = self.undo_stack.pop() { + self.vars = previous_vars; + if self.selected >= self.vars.len() && !self.vars.is_empty() { + self.selected = self.vars.len() - 1; + } + self.sync_input_with_selected(); + self.status_message = Some("Undo applied".to_string()); + } else { + self.status_message = Some("Nothing to undo".to_string()); + } + } } fn parse_index(path: &str) -> Option<(&str, usize)> { diff --git a/src/config.rs b/src/config.rs index 1072de3..bed2196 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,6 +119,7 @@ pub struct KeybindsConfig { pub append_item: String, pub prepend_item: String, pub delete_item: String, + pub undo: String, } impl Default for KeybindsConfig { @@ -140,6 +141,7 @@ impl Default for KeybindsConfig { append_item: "o".to_string(), prepend_item: "O".to_string(), delete_item: "dd".to_string(), + undo: "u".to_string(), } } } diff --git a/src/runner.rs b/src/runner.rs index 7112e68..1fecf95 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -127,6 +127,7 @@ where (&self.config.keybinds.append_item, "append_item"), (&self.config.keybinds.prepend_item, "prepend_item"), (&self.config.keybinds.delete_item, "delete_item"), + (&self.config.keybinds.undo, "undo"), (&"a".to_string(), "add_missing"), (&":".to_string(), "command"), (&"q".to_string(), "quit"), @@ -164,7 +165,11 @@ where "append_item" => self.app.add_array_item(true), "prepend_item" => self.app.add_array_item(false), "delete_item" => self.app.delete_selected(), - "add_missing" => self.add_missing_item(), + "undo" => self.app.undo(), + "add_missing" => { + self.app.save_undo_state(); + self.add_missing_item(); + } "command" => { self.command_buffer.push(':'); self.sync_command_status(); diff --git a/src/ui.rs b/src/ui.rs index 3f1f591..f53ac9f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -272,21 +272,27 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { let status_msg = if let Some(msg) = &app.status_message { msg.clone() } else { + let kb = &config.keybinds; match app.mode { Mode::Normal => { - let mut parts = vec!["j/k move", "gg/G jump", "/ search"]; + let mut parts = vec![ + format!("{}/{} move", kb.down, kb.up), + format!("{}/{} jump", kb.jump_top, kb.jump_bottom), + format!("{} search", kb.search), + ]; if !app.selected_is_group() { - parts.push("i/A/S edit"); + parts.push(format!("{}/{}/{} edit", kb.edit, kb.edit_append, kb.edit_substitute)); } if app.selected_is_missing() { - parts.push("a add"); + parts.push(format!("{} add", "a")); // 'a' is currently hardcoded in runner } if app.selected_is_array() { - parts.push("o/O array"); + parts.push(format!("{}/{} array", kb.append_item, kb.prepend_item)); } - parts.push("dd del"); - parts.push(":w save"); - parts.push(":q quit"); + parts.push(format!("{} del", kb.delete_item)); + parts.push(format!("{} undo", kb.undo)); + parts.push(format!("{} save", kb.save)); + parts.push(format!("{} quit", kb.quit)); parts.join(" · ") } Mode::Insert => "Esc normal · Enter commit".to_string(),