From 406debbc7c2cfa267e2c11bc020715da5ee0ce5c Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Tue, 7 Apr 2026 00:34:38 -0700 Subject: [PATCH] feat(command): add new command structs and registry entries Add new command structs for quitting and command execution. Introduce Quit and SaveAndQuit commands with dirty checks. Add ExecuteCommand for handling ':' input. Define effect_cmd for SetFormatCmd, ImportCmd, ExportCmd, WriteCmd, and HelpCmd. Register the new commands in the default command registry. Fix a buggy mode reset check that used Debug string matching. Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF) --- src/command/cmd.rs | 289 ++++++++++++++++++--------------------------- 1 file changed, 117 insertions(+), 172 deletions(-) diff --git a/src/command/cmd.rs b/src/command/cmd.rs index 88f5671..ecccf94 100644 --- a/src/command/cmd.rs +++ b/src/command/cmd.rs @@ -402,6 +402,39 @@ impl Cmd for ForceQuit { } } +/// Quit with dirty check — refuses if unsaved changes exist. +#[derive(Debug)] +pub struct Quit; +impl Cmd for Quit { + fn name(&self) -> &'static str { + "q" + } + fn execute(&self, ctx: &CmdContext) -> Vec> { + if ctx.dirty { + vec![effect::set_status( + "Unsaved changes. Use :q! to force quit or :wq to save+quit.", + )] + } else { + vec![effect::change_mode(AppMode::Quit)] + } + } +} + +/// Save then quit. +#[derive(Debug)] +pub struct SaveAndQuit; +impl Cmd for SaveAndQuit { + fn name(&self) -> &'static str { + "wq" + } + fn execute(&self, _ctx: &CmdContext) -> Vec> { + vec![ + Box::new(effect::Save), + effect::change_mode(AppMode::Quit), + ] + } +} + // ── Cell operations ────────────────────────────────────────────────────────── // All cell commands take an explicit CellKey. The interactive spec fills it // from ctx.cell_key(); the parser fills it from Cat/Item coordinate args. @@ -1576,6 +1609,9 @@ impl Cmd for HandleWizardKey { /// Execute the command in the "command" buffer (the `:` command line). #[derive(Debug)] +/// Execute the `:` command buffer by delegating to the command registry. +/// The `:` prompt is just another frontend to the scripting language — +/// same parser as `improvise script`. pub struct ExecuteCommand; impl Cmd for ExecuteCommand { fn name(&self) -> &'static str { @@ -1583,181 +1619,30 @@ impl Cmd for ExecuteCommand { } fn execute(&self, ctx: &CmdContext) -> Vec> { let raw = ctx.buffers.get("command").cloned().unwrap_or_default(); - let raw = raw.trim(); - let (cmd_name, rest) = raw - .split_once(char::is_whitespace) - .map(|(c, r)| (c, r.trim())) - .unwrap_or((raw, "")); - - // Default: return to Normal - let mut effects: Vec> = Vec::new(); - - match cmd_name { - "q" | "quit" => { - if ctx.dirty { - effects.push(effect::set_status( - "Unsaved changes. Use :q! to force quit or :wq to save+quit.", - )); - } else { - effects.push(effect::change_mode(AppMode::Quit)); - } - } - "q!" => { - effects.push(effect::change_mode(AppMode::Quit)); - } - "w" | "write" => { - if rest.is_empty() { - effects.push(Box::new(effect::Save)); - } else { - effects.push(Box::new(effect::SaveAs(std::path::PathBuf::from(rest)))); - } - } - "wq" | "x" => { - effects.push(Box::new(effect::Save)); - effects.push(effect::change_mode(AppMode::Quit)); - } - "import" => { - if rest.is_empty() { - effects.push(effect::set_status("Usage: :import ")); - } else { - effects.push(Box::new(effect::StartImportWizard(rest.to_string()))); - } - } - "export" => { - let path = if rest.is_empty() { "export.csv" } else { rest }; - effects.push(Box::new(effect::ExportCsv(std::path::PathBuf::from(path)))); - } - "add-cat" | "add-category" | "cat" => { - if rest.is_empty() { - effects.push(effect::set_status("Usage: :add-cat ")); - } else { - effects.push(Box::new(effect::AddCategory(rest.to_string()))); - effects.push(effect::mark_dirty()); - } - } - "add-item" | "item" => { - let mut parts = rest.splitn(2, char::is_whitespace); - let cat = parts.next().unwrap_or("").trim(); - let item = parts.next().unwrap_or("").trim(); - if cat.is_empty() || item.is_empty() { - effects.push(effect::set_status("Usage: :add-item ")); - } else { - effects.push(Box::new(effect::AddItem { - category: cat.to_string(), - item: item.to_string(), - })); - effects.push(effect::mark_dirty()); - effects.push(effect::set_status("Item added")); - } - } - "add-items" | "items" => { - let mut parts = rest.splitn(2, char::is_whitespace); - let cat = parts.next().unwrap_or("").trim().to_string(); - let items_str = parts.next().unwrap_or("").trim().to_string(); - if cat.is_empty() || items_str.is_empty() { - effects.push(effect::set_status( - "Usage: :add-items item1 item2 ...", - )); - } else { - let items: Vec<&str> = items_str.split_whitespace().collect(); - let count = items.len(); - for item in &items { - effects.push(Box::new(effect::AddItem { - category: cat.clone(), - item: item.to_string(), - })); - } - effects.push(effect::mark_dirty()); - effects.push(effect::set_status(format!( - "Added {count} items to \"{cat}\".", - ))); - } - } - "formula" | "add-formula" => { - if rest.is_empty() { - effects.push(Box::new(effect::SetPanelOpen { - panel: Panel::Formula, - open: true, - })); - effects.push(effect::change_mode(AppMode::FormulaPanel)); - return effects; // Don't set mode to Normal - } else { - let mut parts = rest.splitn(2, char::is_whitespace); - let cat = parts.next().unwrap_or("").trim(); - let formula = parts.next().unwrap_or("").trim(); - if cat.is_empty() || formula.is_empty() { - effects.push(effect::set_status( - "Usage: :formula ", - )); - } else { - effects.push(Box::new(effect::AddFormula { - raw: formula.to_string(), - target_category: cat.to_string(), - })); - effects.push(effect::mark_dirty()); - effects.push(effect::set_status("Formula added")); - } - } - } - "add-view" | "view" => { - let name = if rest.is_empty() { - format!("View {}", ctx.model.views.len() + 1) - } else { - rest.to_string() - }; - effects.push(Box::new(effect::CreateView(name.clone()))); - effects.push(Box::new(effect::SwitchView(name))); - effects.push(effect::mark_dirty()); - } - "set-format" | "fmt" => { - if rest.is_empty() { - effects.push(effect::set_status( - "Usage: :set-format e.g. ,.0 ,.2 .4", - )); - } else { - effects.push(Box::new(effect::SetNumberFormat(rest.to_string()))); - effects.push(effect::mark_dirty()); - effects.push(effect::set_status(format!("Number format set to '{rest}'"))); - } - } - "show-item" | "show" => { - let mut parts = rest.splitn(2, char::is_whitespace); - let cat = parts.next().unwrap_or("").trim(); - let item = parts.next().unwrap_or("").trim(); - if cat.is_empty() || item.is_empty() { - effects.push(effect::set_status("Usage: :show-item ")); - } else { - effects.push(Box::new(effect::ShowItem { - category: cat.to_string(), - item: item.to_string(), - })); - effects.push(effect::mark_dirty()); - effects.push(effect::set_status(format!( - "Showed \"{item}\" in \"{cat}\"" - ))); - } - } - "help" | "h" => { - effects.push(effect::change_mode(AppMode::Help)); - return effects; // Don't also set Normal - } - "" => {} // just pressed Enter with empty buffer - other => { - effects.push(effect::set_status(format!( - "Unknown command: :{other} (try :help)" - ))); - } + let raw = raw.trim().to_string(); + if raw.is_empty() { + return vec![effect::change_mode(AppMode::Normal)]; } - // Default: return to Normal (unless a command already set a different mode) - if !effects - .iter() - .any(|e| format!("{e:?}").contains("ChangeMode")) - { - effects.push(effect::change_mode(AppMode::Normal)); + match crate::command::parse::parse_line_with(ctx.registry, &raw) { + Ok(cmds) => { + let mut effects: Vec> = Vec::new(); + for cmd in cmds { + effects.extend(cmd.execute(ctx)); + } + // Return to Normal unless a command already changed mode + if !effects.iter().any(|e| format!("{e:?}").contains("ChangeMode")) { + effects.push(effect::change_mode(AppMode::Normal)); + } + effects + } + Err(msg) => { + vec![ + effect::set_status(format!(":{raw} — {msg}")), + effect::change_mode(AppMode::Normal), + ] + } } - - effects } } @@ -2272,6 +2157,59 @@ effect_cmd!( } ); +effect_cmd!( + SetFormatCmd, + "set-format", + |args: &[String]| require_args("set-format", args, 1), + |args: &Vec, _ctx: &CmdContext| -> Vec> { + vec![ + Box::new(effect::SetNumberFormat(args.join(" "))), + effect::mark_dirty(), + ] + } +); + +effect_cmd!( + ImportCmd, + "import", + |args: &[String]| require_args("import", args, 1), + |args: &Vec, _ctx: &CmdContext| -> Vec> { + vec![Box::new(effect::StartImportWizard(args[0].clone()))] + } +); + +effect_cmd!( + ExportCmd, + "export", + |_args: &[String]| -> Result<(), String> { Ok(()) }, + |args: &Vec, _ctx: &CmdContext| -> Vec> { + let path = args.first().map(|s| s.as_str()).unwrap_or("export.csv"); + vec![Box::new(effect::ExportCsv(std::path::PathBuf::from(path)))] + } +); + +effect_cmd!( + WriteCmd, + "w", + |_args: &[String]| -> Result<(), String> { Ok(()) }, + |args: &Vec, _ctx: &CmdContext| -> Vec> { + if args.is_empty() { + vec![Box::new(effect::Save)] + } else { + vec![Box::new(effect::SaveAs(std::path::PathBuf::from(&args[0])))] + } + } +); + +effect_cmd!( + HelpCmd, + "help", + |_args: &[String]| -> Result<(), String> { Ok(()) }, + |_args: &Vec, _ctx: &CmdContext| -> Vec> { + vec![effect::change_mode(AppMode::Help)] + } +); + effect_cmd!( LoadModelCmd, "load", @@ -2354,6 +2292,11 @@ pub fn default_registry() -> CmdRegistry { r.register_pure(&LoadModelCmd(vec![]), LoadModelCmd::parse); r.register_pure(&ExportCsvCmd(vec![]), ExportCsvCmd::parse); r.register_pure(&ImportJsonCmd(vec![]), ImportJsonCmd::parse); + r.register_pure(&SetFormatCmd(vec![]), SetFormatCmd::parse); + r.register_pure(&ImportCmd(vec![]), ImportCmd::parse); + r.register_pure(&ExportCmd(vec![]), ExportCmd::parse); + r.register_pure(&WriteCmd(vec![]), WriteCmd::parse); + r.register_pure(&HelpCmd(vec![]), HelpCmd::parse); // ── Navigation (unified Move) ────────────────────────────────────── r.register( @@ -2544,6 +2487,8 @@ pub fn default_registry() -> CmdRegistry { // ── Mode changes ───────────────────────────────────────────────────── r.register_nullary(|| Box::new(ForceQuit)); + r.register_nullary(|| Box::new(Quit)); + r.register_nullary(|| Box::new(SaveAndQuit)); r.register_nullary(|| Box::new(SaveCmd)); r.register_nullary(|| Box::new(EnterSearchMode)); r.register(