From 94ff632b39cbb7c5ba94beaa4025511b7b1a5178 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 13:09:22 +0100 Subject: [PATCH 1/5] fixed string to int error --- Cargo.lock | 2 +- Cargo.toml | 16 ++++----- README.md | 2 ++ config.example.toml | 2 ++ src/app.rs | 71 ++++++++++++++++++++++++++++++++++++++ src/config.rs | 4 +++ src/format/env.rs | 5 ++- src/format/hierarchical.rs | 47 +++++++++++++++++++------ src/format/mod.rs | 9 +++++ src/runner.rs | 4 +++ 10 files changed, 141 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d55012e..fe29849 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "mould" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index b7bd626..0530bc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,3 @@ -[package] -edition = "2024" -name = "mould" -version = "0.4.0" -authors = ["Nils Pukropp "] - [[bin]] name = "mould" path = "src/main.rs" @@ -22,12 +16,18 @@ toml = "1.0.6" tui-input = "0.15.0" [dependencies.clap] -version = "4.6.0" features = ["derive"] +version = "4.6.0" [dependencies.serde] -version = "1.0.228" features = ["derive"] +version = "1.0.228" [dev-dependencies] tempfile = "3.27.0" + +[package] +authors = ["Nils Pukropp "] +edition = "2024" +name = "mould" +version = "0.4.1" diff --git a/README.md b/README.md index 345cb0c..81bf6cb 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ mould config.template.json -o config.json - `gg`: Jump to the top - `G`: Jump to the bottom - `i`: Edit the value of the currently selected key (Enter Insert Mode) + - `o`: Append a new item to the current array + - `O`: Prepend a new item to the current array - `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 ae07924..94c1d5e 100644 --- a/config.example.toml +++ b/config.example.toml @@ -39,3 +39,5 @@ next_match = "n" previous_match = "N" jump_top = "gg" jump_bottom = "G" +append_item = "o" +prepend_item = "O" diff --git a/src/app.rs b/src/app.rs index 6e10a6a..3ef8b4d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -164,4 +164,75 @@ impl App { self.commit_input(); self.mode = Mode::Normal; } + + /// Adds a new item to an array if the selected item is part of one. + pub fn add_array_item(&mut self, after: bool) { + if self.vars.is_empty() { + return; + } + + let (base, idx, depth) = { + let selected_item = &self.vars[self.selected]; + if selected_item.is_group { + return; + } + let path = &selected_item.path; + if let Some((base, idx)) = parse_index(path) { + (base.to_string(), idx, selected_item.depth) + } else { + return; + } + }; + + let new_idx = if after { idx + 1 } else { idx }; + let insert_pos = if after { + self.selected + 1 + } else { + self.selected + }; + + // 1. Shift all items in this array that have index >= new_idx + for var in self.vars.iter_mut() { + if var.path.starts_with(&base) { + if let Some((b, i)) = parse_index(&var.path) { + if b == base && i >= new_idx { + var.path = format!("{}[{}]", base, i + 1); + // Also update key if it was just the index + if var.key == format!("[{}]", i) { + var.key = format!("[{}]", i + 1); + } + } + } + } + } + + // 2. Insert new item + let new_item = ConfigItem { + key: format!("[{}]", new_idx), + path: format!("{}[{}]", base, new_idx), + value: Some("".to_string()), + template_value: None, + default_value: None, + depth, + is_group: false, + status: crate::format::ItemStatus::Modified, + value_type: crate::format::ValueType::String, + }; + self.vars.insert(insert_pos, new_item); + self.selected = insert_pos; + self.sync_input_with_selected(); + self.mode = Mode::Insert; + self.status_message = None; + } +} + +fn parse_index(path: &str) -> Option<(&str, usize)> { + if path.ends_with(']') { + if let Some(start) = path.rfind('[') { + if let Ok(idx) = path[start + 1..path.len() - 1].parse::() { + return Some((&path[..start], idx)); + } + } + } + None } diff --git a/src/config.rs b/src/config.rs index d05f87c..963bbfd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -114,6 +114,8 @@ pub struct KeybindsConfig { pub previous_match: String, pub jump_top: String, pub jump_bottom: String, + pub append_item: String, + pub prepend_item: String, } impl Default for KeybindsConfig { @@ -130,6 +132,8 @@ impl Default for KeybindsConfig { previous_match: "N".to_string(), jump_top: "gg".to_string(), jump_bottom: "G".to_string(), + append_item: "o".to_string(), + prepend_item: "O".to_string(), } } } diff --git a/src/format/env.rs b/src/format/env.rs index 7852ea3..5277283 100644 --- a/src/format/env.rs +++ b/src/format/env.rs @@ -1,4 +1,4 @@ -use super::{ConfigItem, FormatHandler, ItemStatus}; +use super::{ConfigItem, FormatHandler, ItemStatus, ValueType}; use std::fs; use std::io::{self, Write}; use std::path::Path; @@ -27,6 +27,7 @@ impl FormatHandler for EnvHandler { depth: 0, is_group: false, status: ItemStatus::Present, + value_type: ValueType::String, }); } } @@ -65,6 +66,7 @@ impl FormatHandler for EnvHandler { depth: 0, is_group: false, status: ItemStatus::MissingFromTemplate, + value_type: ValueType::String, }); } } @@ -159,6 +161,7 @@ mod tests { depth: 0, is_group: false, status: ItemStatus::Present, + value_type: ValueType::String, }]; let handler = EnvHandler; diff --git a/src/format/hierarchical.rs b/src/format/hierarchical.rs index bab1066..6970f91 100644 --- a/src/format/hierarchical.rs +++ b/src/format/hierarchical.rs @@ -1,4 +1,4 @@ -use super::{ConfigItem, FormatHandler, FormatType, ItemStatus}; +use super::{ConfigItem, FormatHandler, FormatType, ItemStatus, ValueType}; use serde_json::{Map, Value}; use std::fs; use std::io; @@ -58,6 +58,8 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut key_name.to_string() } else if key_name.is_empty() { prefix.to_string() + } else if key_name.starts_with('[') { + format!("{}{}", prefix, key_name) } else { format!("{}.{}", prefix, key_name) }; @@ -74,6 +76,7 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut depth, is_group: true, status: ItemStatus::Present, + value_type: ValueType::Null, }); } let next_depth = if path.is_empty() { depth } else { depth + 1 }; @@ -92,6 +95,7 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut depth, is_group: true, status: ItemStatus::Present, + value_type: ValueType::Null, }); } let next_depth = if path.is_empty() { depth } else { depth + 1 }; @@ -110,6 +114,7 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut depth, is_group: false, status: ItemStatus::Present, + value_type: ValueType::String, }); } Value::Number(n) => { @@ -123,6 +128,7 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut depth, is_group: false, status: ItemStatus::Present, + value_type: ValueType::Number, }); } Value::Bool(b) => { @@ -136,6 +142,7 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut depth, is_group: false, status: ItemStatus::Present, + value_type: ValueType::Bool, }); } Value::Null => { @@ -148,6 +155,7 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut depth, is_group: false, status: ItemStatus::Present, + value_type: ValueType::Null, }); } } @@ -205,14 +213,14 @@ impl FormatHandler for HierarchicalHandler { let val = var.value.as_deref() .or(var.template_value.as_deref()) .unwrap_or(""); - insert_into_value(&mut root, &var.path, val); + insert_into_value(&mut root, &var.path, val, var.value_type); } } self.write_value(path, &root) } } -fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str) { +fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str, value_type: ValueType) { let mut parts = path.split('.'); let last_part = match parts.next_back() { Some(p) => p, @@ -255,13 +263,30 @@ fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str) { } let map = current.as_object_mut().unwrap(); - // Attempt basic type inference - let final_val = if let Ok(n) = new_val_str.parse::() { - Value::Number(n.into()) - } else if let Ok(b) = new_val_str.parse::() { - Value::Bool(b) - } else { - Value::String(new_val_str.to_string()) + // Use the preserved ValueType instead of aggressive inference + let final_val = match value_type { + ValueType::Number => { + if let Ok(n) = new_val_str.parse::() { + Value::Number(n.into()) + } else if let Ok(f) = new_val_str.parse::() { + if let Some(n) = serde_json::Number::from_f64(f) { + Value::Number(n) + } else { + Value::String(new_val_str.to_string()) + } + } else { + Value::String(new_val_str.to_string()) + } + } + ValueType::Bool => { + if let Ok(b) = new_val_str.parse::() { + Value::Bool(b) + } else { + Value::String(new_val_str.to_string()) + } + } + ValueType::Null if new_val_str.is_empty() => Value::Null, + _ => Value::String(new_val_str.to_string()), }; if let Some(i) = final_idx { @@ -316,7 +341,7 @@ mod tests { let mut root = Value::Object(Map::new()); for var in vars { if !var.is_group { - insert_into_value(&mut root, &var.path, var.value.as_deref().unwrap_or("")); + insert_into_value(&mut root, &var.path, var.value.as_deref().unwrap_or(""), var.value_type); } } diff --git a/src/format/mod.rs b/src/format/mod.rs index b444db6..30afb02 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -12,6 +12,14 @@ pub enum ItemStatus { Modified, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueType { + String, + Number, + Bool, + Null, +} + #[derive(Debug, Clone)] pub struct ConfigItem { pub key: String, @@ -22,6 +30,7 @@ pub struct ConfigItem { pub depth: usize, pub is_group: bool, pub status: ItemStatus, + pub value_type: ValueType, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/runner.rs b/src/runner.rs index a91df22..332130a 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -122,6 +122,8 @@ where (&self.config.keybinds.previous_match, "previous_match"), (&self.config.keybinds.jump_top, "jump_top"), (&self.config.keybinds.jump_bottom, "jump_bottom"), + (&self.config.keybinds.append_item, "append_item"), + (&self.config.keybinds.prepend_item, "prepend_item"), (&"a".to_string(), "add_missing"), (&":".to_string(), "command"), (&"q".to_string(), "quit"), @@ -154,6 +156,8 @@ where "previous_match" => self.app.jump_previous_match(), "jump_top" => self.app.jump_top(), "jump_bottom" => self.app.jump_bottom(), + "append_item" => self.app.add_array_item(true), + "prepend_item" => self.app.add_array_item(false), "add_missing" => self.add_missing_item(), "command" => { self.command_buffer.push(':'); -- 2.49.1 From f123f2d6df481d2c7cff76430d6b4b7deaec1a63 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 13:12:22 +0100 Subject: [PATCH 2/5] added dd remove --- README.md | 1 + config.example.toml | 1 + src/app.rs | 82 +++++++++++++++++++++++++++++++++++++++++++-- src/config.rs | 2 ++ src/runner.rs | 2 ++ 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81bf6cb..6677834 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ mould config.template.json -o config.json - `i`: Edit the value of the currently selected key (Enter Insert Mode) - `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 - `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 94c1d5e..2b70a63 100644 --- a/config.example.toml +++ b/config.example.toml @@ -41,3 +41,4 @@ jump_top = "gg" jump_bottom = "G" append_item = "o" prepend_item = "O" +delete_item = "dd" diff --git a/src/app.rs b/src/app.rs index 3ef8b4d..c8e7195 100644 --- a/src/app.rs +++ b/src/app.rs @@ -165,6 +165,64 @@ impl App { self.mode = Mode::Normal; } + /// Deletes the currently selected item. If it's a group, deletes all children. + pub fn delete_selected(&mut self) { + if self.vars.is_empty() { + return; + } + + let selected_path = self.vars[self.selected].path.clone(); + let is_group = self.vars[self.selected].is_group; + + // Identify if the item being removed is an array item + let array_info = parse_index(&selected_path); + + // 1. Identify all items to remove + let mut to_remove = Vec::new(); + to_remove.push(self.selected); + + if is_group { + let prefix = format!("{}.", selected_path); + for (i, var) in self.vars.iter().enumerate() { + if var.path.starts_with(&prefix) { + to_remove.push(i); + } + } + } + + // 2. Perform removal (reverse order to preserve indices) + to_remove.sort_unstable_by(|a, b| b.cmp(a)); + for i in to_remove { + self.vars.remove(i); + } + + // 3. Re-index subsequent array items if applicable + if let Some((base, removed_idx)) = array_info { + let base = base.to_string(); + for var in self.vars.iter_mut() { + if var.path.starts_with(&base) { + // We need to find the index segment that matches this array + if let Some((b, i, suffix)) = find_array_segment(&var.path, &base) { + if b == base && i > removed_idx { + let new_idx = i - 1; + var.path = format!("{}[{}]{}", base, new_idx, suffix); + // Also update key if it matches the old index exactly + if var.key == format!("[{}]", i) { + var.key = format!("[{}]", new_idx); + } + } + } + } + } + } + + // 4. Adjust selection + if self.selected >= self.vars.len() && !self.vars.is_empty() { + self.selected = self.vars.len() - 1; + } + self.sync_input_with_selected(); + } + /// Adds a new item to an array if the selected item is part of one. pub fn add_array_item(&mut self, after: bool) { if self.vars.is_empty() { @@ -227,12 +285,30 @@ impl App { } fn parse_index(path: &str) -> Option<(&str, usize)> { - if path.ends_with(']') { - if let Some(start) = path.rfind('[') { - if let Ok(idx) = path[start + 1..path.len() - 1].parse::() { + if let Some(end) = path.rfind(']') { + let segment = &path[..=end]; + if let Some(start) = segment.rfind('[') { + if let Ok(idx) = segment[start + 1..end].parse::() { + // Return the base and index return Some((&path[..start], idx)); } } } None } + +/// Helper to find an array segment in a path given a base prefix. +fn find_array_segment<'a>(path: &'a str, base: &str) -> Option<(&'a str, usize, &'a str)> { + if !path.starts_with(base) { + return None; + } + let remaining = &path[base.len()..]; + if remaining.starts_with('[') { + if let Some(end) = remaining.find(']') { + if let Ok(idx) = remaining[1..end].parse::() { + return Some((&path[..base.len()], idx, &remaining[end + 1..])); + } + } + } + None +} diff --git a/src/config.rs b/src/config.rs index 963bbfd..ab3084d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -116,6 +116,7 @@ pub struct KeybindsConfig { pub jump_bottom: String, pub append_item: String, pub prepend_item: String, + pub delete_item: String, } impl Default for KeybindsConfig { @@ -134,6 +135,7 @@ impl Default for KeybindsConfig { jump_bottom: "G".to_string(), append_item: "o".to_string(), prepend_item: "O".to_string(), + delete_item: "dd".to_string(), } } } diff --git a/src/runner.rs b/src/runner.rs index 332130a..ec738d1 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -124,6 +124,7 @@ where (&self.config.keybinds.jump_bottom, "jump_bottom"), (&self.config.keybinds.append_item, "append_item"), (&self.config.keybinds.prepend_item, "prepend_item"), + (&self.config.keybinds.delete_item, "delete_item"), (&"a".to_string(), "add_missing"), (&":".to_string(), "command"), (&"q".to_string(), "quit"), @@ -158,6 +159,7 @@ where "jump_bottom" => self.app.jump_bottom(), "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(), "command" => { self.command_buffer.push(':'); -- 2.49.1 From 01a7bd44b741aca90dd914c095b60d6c8ddbed0e Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 13:40:01 +0100 Subject: [PATCH 3/5] updated nvim readme desc --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6677834..92f6ba3 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,13 @@ cargo build --release ``` ### Neovim Plugin -If you use a plugin manager like `lazy.nvim`, you can add the local repository (or remote once published) directly: +If you use a plugin manager like `mini.deps`, you can add the repository directly: ```lua -{ - "username/mould", -- replace with actual repo path or github url - config = function() - -- Provides the :Mould command - end -} +add({ + source = 'https://git.narl.io/nvrl/mould-rs', + checkout = 'main', +}) ``` ## Usage @@ -123,4 +121,4 @@ tree_depth_4 = "#fab387" ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. -- 2.49.1 From f6a84416e66f4f858733df27826d45a7e959051c Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 14:29:51 +0100 Subject: [PATCH 4/5] updated insert modes + fixed status bar help --- Cargo.toml | 2 +- README.md | 4 +++- config.example.toml | 2 ++ src/app.rs | 50 ++++++++++++++++++++++++++++++++++++++++----- src/config.rs | 4 ++++ src/runner.rs | 8 ++++++-- src/ui.rs | 32 +++++++++++++++++++++-------- 7 files changed, 85 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0530bc0..97e0a8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,4 @@ tempfile = "3.27.0" authors = ["Nils Pukropp "] edition = "2024" name = "mould" -version = "0.4.1" +version = "0.4.2" diff --git a/README.md b/README.md index 92f6ba3..a30551c 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,9 @@ mould config.template.json -o config.json - `k` / `Up`: Move selection up - `gg`: Jump to the top - `G`: Jump to the bottom - - `i`: Edit the value of the currently selected key (Enter Insert Mode) + - `i`: Edit value (cursor at start) + - `A`: Edit value (cursor at end) + - `S`: Substitute value (clear and edit) - `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 diff --git a/config.example.toml b/config.example.toml index 2b70a63..7e3310b 100644 --- a/config.example.toml +++ b/config.example.toml @@ -31,6 +31,8 @@ tree_depth_4 = "#fab387" down = "j" up = "k" edit = "i" +edit_append = "A" +edit_substitute = "S" save = ":w" quit = ":q" normal_mode = "Esc" diff --git a/src/app.rs b/src/app.rs index c8e7195..fc4f797 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,6 +11,12 @@ pub enum Mode { Search, } +pub enum InsertVariant { + Start, + End, + Substitute, +} + /// The core application state, holding all configuration variables and UI status. pub struct App { /// The list of configuration variables being edited. @@ -149,12 +155,25 @@ impl App { } } - /// Transitions the application into Insert Mode. - pub fn enter_insert(&mut self) { + /// Transitions the application into Insert Mode with a specific variant. + pub fn enter_insert(&mut self, variant: InsertVariant) { if let Some(var) = self.vars.get(self.selected) { if !var.is_group { self.mode = Mode::Insert; self.status_message = None; + match variant { + InsertVariant::Start => { + use tui_input::InputRequest; + self.input.handle(InputRequest::GoToStart); + } + InsertVariant::End => { + use tui_input::InputRequest; + self.input.handle(InputRequest::GoToEnd); + } + InsertVariant::Substitute => { + self.input = Input::new(String::new()); + } + } } } } @@ -182,9 +201,13 @@ impl App { to_remove.push(self.selected); if is_group { - let prefix = format!("{}.", selected_path); + let prefix_dot = format!("{}.", selected_path); + let prefix_bracket = format!("{}[", selected_path); for (i, var) in self.vars.iter().enumerate() { - if var.path.starts_with(&prefix) { + if i == self.selected { + continue; + } + if var.path.starts_with(&prefix_dot) || var.path.starts_with(&prefix_bracket) { to_remove.push(i); } } @@ -279,9 +302,26 @@ impl App { self.vars.insert(insert_pos, new_item); self.selected = insert_pos; self.sync_input_with_selected(); - self.mode = Mode::Insert; + self.enter_insert(InsertVariant::Start); self.status_message = None; } + + /// Status bar helpers + pub fn selected_is_group(&self) -> bool { + self.vars.get(self.selected).map(|v| v.is_group).unwrap_or(false) + } + + pub fn selected_is_array(&self) -> bool { + self.vars.get(self.selected) + .map(|v| !v.is_group && v.path.contains('[')) + .unwrap_or(false) + } + + pub fn selected_is_missing(&self) -> bool { + self.vars.get(self.selected) + .map(|v| v.status == crate::format::ItemStatus::MissingFromActive) + .unwrap_or(false) + } } fn parse_index(path: &str) -> Option<(&str, usize)> { diff --git a/src/config.rs b/src/config.rs index ab3084d..1072de3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -106,6 +106,8 @@ pub struct KeybindsConfig { pub down: String, pub up: String, pub edit: String, + pub edit_append: String, + pub edit_substitute: String, pub save: String, pub quit: String, pub normal_mode: String, @@ -125,6 +127,8 @@ impl Default for KeybindsConfig { down: "j".to_string(), up: "k".to_string(), edit: "i".to_string(), + edit_append: "A".to_string(), + edit_substitute: "S".to_string(), save: ":w".to_string(), quit: ":q".to_string(), normal_mode: "Esc".to_string(), diff --git a/src/runner.rs b/src/runner.rs index ec738d1..7112e68 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,4 +1,4 @@ -use crate::app::{App, Mode}; +use crate::app::{App, InsertVariant, Mode}; use crate::config::Config; use crate::format::FormatHandler; use crossterm::event::{self, Event, KeyCode, KeyEvent}; @@ -117,6 +117,8 @@ where (&self.config.keybinds.down, "down"), (&self.config.keybinds.up, "up"), (&self.config.keybinds.edit, "edit"), + (&self.config.keybinds.edit_append, "edit_append"), + (&self.config.keybinds.edit_substitute, "edit_substitute"), (&self.config.keybinds.search, "search"), (&self.config.keybinds.next_match, "next_match"), (&self.config.keybinds.previous_match, "previous_match"), @@ -147,7 +149,9 @@ where match action { "down" => self.app.next(), "up" => self.app.previous(), - "edit" => self.app.enter_insert(), + "edit" => self.app.enter_insert(InsertVariant::Start), + "edit_append" => self.app.enter_insert(InsertVariant::End), + "edit_substitute" => self.app.enter_insert(InsertVariant::Substitute), "search" => { self.app.mode = Mode::Search; self.app.search_query.clear(); diff --git a/src/ui.rs b/src/ui.rs index ef584a4..3f1f591 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -269,14 +269,30 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) { ), }; - let status_msg = app - .status_message - .as_deref() - .unwrap_or_else(|| match app.mode { - Mode::Normal => " navigation | i: edit | /: search | :w: save | :q: quit ", - Mode::Insert => " Esc: back to normal | Enter: commit ", - Mode::Search => " Esc: back to normal | type to filter ", - }); + let status_msg = if let Some(msg) = &app.status_message { + msg.clone() + } else { + match app.mode { + Mode::Normal => { + let mut parts = vec!["j/k move", "gg/G jump", "/ search"]; + if !app.selected_is_group() { + parts.push("i/A/S edit"); + } + if app.selected_is_missing() { + parts.push("a add"); + } + if app.selected_is_array() { + parts.push("o/O array"); + } + parts.push("dd del"); + parts.push(":w save"); + parts.push(":q quit"); + parts.join(" · ") + } + Mode::Insert => "Esc normal · Enter commit".to_string(), + Mode::Search => "Esc normal · type to filter".to_string(), + } + }; let status_line = Line::from(vec![ Span::styled(mode_str, mode_style), -- 2.49.1 From 1d9342186a7da9d8274b70e8f33dc594b7843964 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 17 Mar 2026 14:33:26 +0100 Subject: [PATCH 5/5] 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(), -- 2.49.1