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(':');