4 Commits

Author SHA1 Message Date
c773fdf472 Merge pull request 'release/0.5.0' (#15) from release/0.5.0 into main
All checks were successful
Release / Build and Release (push) Successful in 1m2s
Reviewed-on: #15
2026-03-18 22:50:11 +01:00
029b4f9da8 refactored group renaming
All checks were successful
Version Check / check-version (pull_request) Successful in 2s
2026-03-18 22:49:55 +01:00
ee85be4e6b fixed lag when S on empty
All checks were successful
Version Check / check-version (pull_request) Successful in 2s
2026-03-18 21:12:06 +01:00
31dc062ce5 fixed undo tree for new vars/groups + unknown states for lists with o/O
All checks were successful
Version Check / check-version (pull_request) Successful in 3s
2026-03-18 18:56:14 +01:00
3 changed files with 101 additions and 34 deletions

View File

@@ -242,8 +242,13 @@ impl App {
/// Transitions the application into Insert Mode with a specific variant. /// Transitions the application into Insert Mode with a specific variant.
pub fn enter_insert(&mut self, variant: InsertVariant) { pub fn enter_insert(&mut self, variant: InsertVariant) {
if let Some(var) = self.vars.get(self.selected) if let Some(var) = self.vars.get(self.selected) {
&& !var.is_group { if var.is_group {
self.enter_insert_key();
} else {
if !matches!(variant, InsertVariant::Substitute) {
self.sync_input_with_selected();
}
self.mode = Mode::Insert; self.mode = Mode::Insert;
match variant { match variant {
InsertVariant::Start => { InsertVariant::Start => {
@@ -259,6 +264,7 @@ impl App {
} }
} }
} }
}
} }
/// Commits the current input and transitions the application into Normal Mode. /// Commits the current input and transitions the application into Normal Mode.
@@ -337,9 +343,9 @@ impl App {
} }
/// Adds a new item relative to the selected item. /// Adds a new item relative to the selected item.
pub fn add_item(&mut self, after: bool, is_group: bool) { pub fn add_item(&mut self, after: bool, is_group: bool, as_child: bool) {
if self.vars.is_empty() { if self.vars.is_empty() {
let new_key = "NEW_VAR".to_string(); let new_key = if is_group { "NEW_GROUP".to_string() } else { "NEW_VAR".to_string() };
self.vars.push(ConfigItem { self.vars.push(ConfigItem {
key: new_key.clone(), key: new_key.clone(),
path: vec![PathSegment::Key(new_key)], path: vec![PathSegment::Key(new_key)],
@@ -370,8 +376,8 @@ impl App {
let insert_pos; let insert_pos;
let mut is_array_item = false; let mut is_array_item = false;
if let Some(PathSegment::Index(idx)) = selected_item.path.last() { if !as_child && let Some(PathSegment::Index(idx)) = selected_item.path.last() {
// ARRAY ITEM LOGIC // ARRAY ITEM LOGIC (Adding sibling to an existing index)
is_array_item = true; is_array_item = true;
let base_path = selected_item.path[..selected_item.path.len() - 1].to_vec(); let base_path = selected_item.path[..selected_item.path.len() - 1].to_vec();
let new_idx = if after { idx + 1 } else { *idx }; let new_idx = if after { idx + 1 } else { *idx };
@@ -392,11 +398,29 @@ impl App {
new_path = base_path; new_path = base_path;
new_path.push(PathSegment::Index(new_idx)); new_path.push(PathSegment::Index(new_idx));
new_depth = selected_item.depth; new_depth = selected_item.depth;
} else if after && selected_item.is_group { } else if as_child && selected_item.is_group {
// ADD AS CHILD OF GROUP // ADD AS CHILD OF GROUP
insert_pos = self.selected + 1; insert_pos = self.selected + 1;
new_path = selected_item.path.clone(); new_path = selected_item.path.clone();
new_depth = selected_item.depth + 1; new_depth = selected_item.depth + 1;
// Check if this group already contains array items
if self.is_array_group(&selected_item.path) {
is_array_item = true;
let new_idx = 0; // Prepend to array
new_path.push(PathSegment::Index(new_idx));
// Shift existing children
for var in self.vars.iter_mut() {
if var.path.starts_with(&selected_item.path) && var.path.len() > selected_item.path.len()
&& let PathSegment::Index(i) = var.path[selected_item.path.len()] {
var.path[selected_item.path.len()] = PathSegment::Index(i + 1);
if var.path.len() == selected_item.path.len() + 1 {
var.key = format!("[{}]", i + 1);
}
}
}
}
} else { } else {
// ADD AS SIBLING // ADD AS SIBLING
let parent_path = if selected_item.path.len() > 1 { let parent_path = if selected_item.path.len() > 1 {
@@ -417,6 +441,17 @@ impl App {
new_path = parent_path; new_path = parent_path;
new_depth = selected_item.depth; new_depth = selected_item.depth;
// If the parent is an array group, this is also an array item
if !new_path.is_empty() && self.is_array_group(&new_path) {
is_array_item = true;
if let Some(PathSegment::Index(idx)) = selected_item.path.last() {
let new_idx = if after { idx + 1 } else { *idx };
new_path.push(PathSegment::Index(new_idx));
} else {
new_path.push(PathSegment::Index(0));
}
}
} }
// 2. Generate a unique key for non-array items // 2. Generate a unique key for non-array items
@@ -458,9 +493,10 @@ impl App {
self.vars.insert(insert_pos, new_item); self.vars.insert(insert_pos, new_item);
self.selected = insert_pos; self.selected = insert_pos;
self.save_undo_state();
if is_array_item { if is_array_item {
self.sync_input_with_selected(); self.sync_input_with_selected();
self.save_undo_state();
self.enter_insert(InsertVariant::Start); self.enter_insert(InsertVariant::Start);
} else { } else {
self.enter_insert_key(); self.enter_insert_key();
@@ -495,6 +531,14 @@ impl App {
self.vars.get(self.selected).map(|v| v.is_group).unwrap_or(false) self.vars.get(self.selected).map(|v| v.is_group).unwrap_or(false)
} }
pub fn is_array_group(&self, group_path: &[PathSegment]) -> bool {
self.vars.iter().any(|v|
v.path.starts_with(group_path)
&& v.path.len() == group_path.len() + 1
&& matches!(v.path.last(), Some(PathSegment::Index(_)))
)
}
pub fn selected_is_array(&self) -> bool { pub fn selected_is_array(&self) -> bool {
self.vars.get(self.selected) self.vars.get(self.selected)
.map(|v| !v.is_group && matches!(v.path.last(), Some(PathSegment::Index(_)))) .map(|v| !v.is_group && matches!(v.path.last(), Some(PathSegment::Index(_))))

View File

@@ -124,6 +124,9 @@ where
if !key_str.is_empty() { if !key_str.is_empty() {
self.key_sequence.push_str(&key_str); self.key_sequence.push_str(&key_str);
let mut exact_match = None;
let mut prefix_match = false;
// Collect all configured keybinds // Collect all configured keybinds
let binds = [ let binds = [
(&self.config.keybinds.down, "down"), (&self.config.keybinds.down, "down"),
@@ -131,6 +134,7 @@ where
(&self.config.keybinds.edit, "edit"), (&self.config.keybinds.edit, "edit"),
(&self.config.keybinds.edit_append, "edit_append"), (&self.config.keybinds.edit_append, "edit_append"),
(&self.config.keybinds.edit_substitute, "edit_substitute"), (&self.config.keybinds.edit_substitute, "edit_substitute"),
(&"S".to_string(), "edit_substitute"),
(&self.config.keybinds.search, "search"), (&self.config.keybinds.search, "search"),
(&self.config.keybinds.next_match, "next_match"), (&self.config.keybinds.next_match, "next_match"),
(&self.config.keybinds.previous_match, "previous_match"), (&self.config.keybinds.previous_match, "previous_match"),
@@ -150,9 +154,6 @@ where
(&"q".to_string(), "quit"), (&"q".to_string(), "quit"),
]; ];
let mut exact_match = None;
let mut prefix_match = false;
for (bind, action) in binds.iter() { for (bind, action) in binds.iter() {
if bind == &&self.key_sequence { if bind == &&self.key_sequence {
exact_match = Some(*action); exact_match = Some(*action);
@@ -162,6 +163,21 @@ where
} }
} }
if exact_match.is_none() && !prefix_match {
// Not a match and not a prefix, restart with current key
self.key_sequence.clear();
self.key_sequence.push_str(&key_str);
for (bind, action) in binds.iter() {
if bind == &&self.key_sequence {
exact_match = Some(*action);
break;
} else if bind.starts_with(&self.key_sequence) {
prefix_match = true;
}
}
}
if let Some(action) = exact_match { if let Some(action) = exact_match {
self.key_sequence.clear(); self.key_sequence.clear();
match action { match action {
@@ -179,14 +195,14 @@ where
"previous_match" => self.app.jump_previous_match(), "previous_match" => self.app.jump_previous_match(),
"jump_top" => self.app.jump_top(), "jump_top" => self.app.jump_top(),
"jump_bottom" => self.app.jump_bottom(), "jump_bottom" => self.app.jump_bottom(),
"append_item" => self.app.add_item(true, false), "append_item" => self.app.add_item(true, false, false),
"prepend_item" => self.app.add_item(false, false), "prepend_item" => self.app.add_item(false, false, false),
"delete_item" => self.app.delete_selected(), "delete_item" => self.app.delete_selected(),
"undo" => self.app.undo(), "undo" => self.app.undo(),
"redo" => self.app.redo(), "redo" => self.app.redo(),
"rename" => self.app.enter_insert_key(), "rename" => self.app.enter_insert_key(),
"append_group" => self.app.add_item(true, true), "append_group" => self.app.add_item(true, true, true),
"prepend_group" => self.app.add_item(false, true), "prepend_group" => self.app.add_item(false, true, true),
"toggle_group" => { "toggle_group" => {
self.app.toggle_group_selected(); self.app.toggle_group_selected();
self.app.save_undo_state(); self.app.save_undo_state();
@@ -203,7 +219,6 @@ where
} }
} else if !prefix_match { } else if !prefix_match {
self.key_sequence.clear(); self.key_sequence.clear();
self.key_sequence.push_str(&key_str);
} }
} else { } else {
// Non-character keys reset the sequence buffer // Non-character keys reset the sequence buffer

View File

@@ -103,24 +103,30 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) {
Span::styled(&var.key, key_style), Span::styled(&var.key, key_style),
]; ];
// Add status indicator if not present (only for leaf variables) // Add status indicator if not present
if !var.is_group { match var.status {
match var.status { crate::format::ItemStatus::MissingFromActive if !var.is_group => {
crate::format::ItemStatus::MissingFromActive => { let missing_style = if is_selected {
let missing_style = if is_selected { Style::default().fg(theme.fg_highlight()).add_modifier(Modifier::BOLD)
Style::default().fg(theme.fg_highlight()).add_modifier(Modifier::BOLD) } else {
} else { Style::default().fg(theme.fg_warning()).add_modifier(Modifier::BOLD)
Style::default().fg(theme.fg_warning()).add_modifier(Modifier::BOLD) };
}; key_spans.push(Span::styled(" (missing)", missing_style));
key_spans.push(Span::styled(" (missing)", missing_style));
}
crate::format::ItemStatus::Modified => {
if !is_selected {
key_spans.push(Span::styled(" (*)", Style::default().fg(theme.fg_modified())));
}
}
_ => {}
} }
crate::format::ItemStatus::MissingFromActive if var.is_group => {
let missing_style = if is_selected {
Style::default().fg(theme.fg_highlight()).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.fg_warning()).add_modifier(Modifier::BOLD)
};
key_spans.push(Span::styled(" (missing group)", missing_style));
}
crate::format::ItemStatus::Modified => {
if !is_selected {
key_spans.push(Span::styled(" (*)", Style::default().fg(theme.fg_modified())));
}
}
_ => {}
} }
let item_style = if is_selected { let item_style = if is_selected {
@@ -210,7 +216,9 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) {
// Show template value in normal mode if it differs // Show template value in normal mode if it differs
let display_text = if let Some(var) = current_var { let display_text = if let Some(var) = current_var {
if var.is_group { if matches!(app.mode, Mode::InsertKey) {
input_text.to_string()
} else if var.is_group {
"<group>".to_string() "<group>".to_string()
} else if matches!(app.mode, Mode::Normal) { } else if matches!(app.mode, Mode::Normal) {
format!("{}{}", input_text, extra_info) format!("{}{}", input_text, extra_info)