From 89fdb27d6c25ef5a89fa32adf269dd25f0c950cf Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Sat, 4 Apr 2026 18:37:43 -0700 Subject: [PATCH] refactor(command): switch to prototype-based command registration Refactor command registry to use prototype-based registration instead of string-based names. This makes the API more consistent and type-safe. Changes: - Changed Cmd::name() to return &'static str instead of &str - Updated CmdRegistry::register, register_pure, and register_nullary to accept prototype command instances instead of string names - Added NamedCmd helper struct for cases where command is built by closure - Updated all command implementations to return static string literals - Modified EnterMode::execute to clear buffers when entering text-entry modes Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M) --- src/command/cmd.rs | 318 ++++++++++++++++++++++++++------------------- 1 file changed, 181 insertions(+), 137 deletions(-) diff --git a/src/command/cmd.rs b/src/command/cmd.rs index fcfe11e..3c9a25c 100644 --- a/src/command/cmd.rs +++ b/src/command/cmd.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use crossterm::event::KeyCode; -use crate::model::cell::CellValue; +use crate::model::cell::{CellKey, CellValue}; use crate::model::Model; use crate::ui::app::AppMode; use crate::ui::effect::{self, Effect, Panel}; @@ -46,7 +46,7 @@ pub trait Cmd: Debug + Send + Sync { /// The canonical name of this command (matches its registry key). /// Used by the parser tests and for introspection. #[allow(dead_code)] - fn name(&self) -> &str; + fn name(&self) -> &'static str; } /// Factory that constructs a Cmd from text arguments (headless/script). @@ -81,27 +81,41 @@ impl CmdRegistry { } /// Register a command with both a text parser and an interactive constructor. - pub fn register(&mut self, name: &'static str, parse: ParseFn, interactive: InteractiveFn) { + /// The name is derived from a prototype command instance. + pub fn register( + &mut self, + prototype: &dyn Cmd, + parse: ParseFn, + interactive: InteractiveFn, + ) { self.entries.push(CmdEntry { - name, + name: prototype.name(), parse: Box::new(parse), interactive: Box::new(interactive), }); } - /// Register a command that takes no context — same constructor for both paths. - pub fn register_pure(&mut self, name: &'static str, parse: ParseFn) { + /// Register a command that doesn't need interactive context. + /// When called interactively with args, delegates to parse. + /// When called interactively without args, returns an error. + pub fn register_pure(&mut self, prototype: &dyn Cmd, parse: ParseFn) { self.entries.push(CmdEntry { - name, + name: prototype.name(), parse: Box::new(parse), - interactive: Box::new(|_args, _ctx| { - Err("not available interactively without args".into()) + interactive: Box::new(move |args, _ctx| { + if args.is_empty() { + Err("this command requires arguments".into()) + } else { + parse(args) + } }), }); } /// Register a zero-arg command (same instance for parse and interactive). - pub fn register_nullary(&mut self, name: &'static str, f: fn() -> Box) { + /// The name is derived by calling `f()` once. + pub fn register_nullary(&mut self, f: fn() -> Box) { + let name = f().name(); self.entries.push(CmdEntry { name, parse: Box::new(move |_| Ok(f())), @@ -142,6 +156,19 @@ impl CmdRegistry { } } +/// Dummy prototype used only for name extraction in registry calls +/// where the real command struct is built by a closure. +#[derive(Debug)] +struct NamedCmd(&'static str); +impl Cmd for NamedCmd { + fn name(&self) -> &'static str { + self.0 + } + fn execute(&self, _: &CmdContext) -> Vec> { + vec![] + } +} + fn require_args(word: &str, args: &[String], n: usize) -> Result<(), String> { if args.len() < n { Err(format!( @@ -170,7 +197,7 @@ fn parse_cell_key_from_args(args: &[String]) -> crate::model::cell::CellKey { // fills position/bounds from context; the parser accepts them as args. /// Shared viewport state for navigation commands. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct CursorState { pub row: usize, pub col: usize, @@ -232,7 +259,7 @@ pub struct MoveSelection { } impl Cmd for MoveSelection { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "move-selection" } @@ -250,7 +277,7 @@ pub struct JumpToFirstRow { pub col: usize, } impl Cmd for JumpToFirstRow { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "jump-first-row" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -268,7 +295,7 @@ pub struct JumpToLastRow { pub row_offset: usize, } impl Cmd for JumpToLastRow { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "jump-last-row" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -286,7 +313,7 @@ pub struct JumpToFirstCol { pub row: usize, } impl Cmd for JumpToFirstCol { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "jump-first-col" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -304,7 +331,7 @@ pub struct JumpToLastCol { pub col_offset: usize, } impl Cmd for JumpToLastCol { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "jump-last-col" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -323,7 +350,7 @@ pub struct ScrollRows { pub cursor: CursorState, } impl Cmd for ScrollRows { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "scroll-rows" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -350,18 +377,35 @@ impl Cmd for ScrollRows { #[derive(Debug)] pub struct EnterMode(pub AppMode); impl Cmd for EnterMode { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "enter-mode" } fn execute(&self, _ctx: &CmdContext) -> Vec> { - vec![effect::change_mode(self.0.clone())] + let mut effects: Vec> = Vec::new(); + // Clear the corresponding buffer when entering a text-entry mode + let buffer_name = match &self.0 { + AppMode::CommandMode { .. } => Some("command"), + AppMode::Editing { .. } => Some("edit"), + AppMode::FormulaEdit { .. } => Some("formula"), + AppMode::CategoryAdd { .. } => Some("category"), + AppMode::ExportPrompt { .. } => Some("export"), + _ => None, + }; + if let Some(name) = buffer_name { + effects.push(Box::new(effect::SetBuffer { + name: name.to_string(), + value: String::new(), + })); + } + effects.push(effect::change_mode(self.0.clone())); + effects } } #[derive(Debug)] pub struct ForceQuit; impl Cmd for ForceQuit { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "force-quit" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -372,7 +416,7 @@ impl Cmd for ForceQuit { #[derive(Debug)] pub struct SaveAndQuit; impl Cmd for SaveAndQuit { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "save-and-quit" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -390,7 +434,7 @@ pub struct ClearCellCommand { pub key: crate::model::cell::CellKey, } impl Cmd for ClearCellCommand { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "clear-cell" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -407,7 +451,7 @@ pub struct YankCell { pub key: crate::model::cell::CellKey, } impl Cmd for YankCell { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "yank" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -426,7 +470,7 @@ pub struct PasteCell { pub key: crate::model::cell::CellKey, } impl Cmd for PasteCell { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "paste" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -446,7 +490,7 @@ impl Cmd for PasteCell { #[derive(Debug)] pub struct TransposeAxes; impl Cmd for TransposeAxes { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "transpose" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -457,7 +501,7 @@ impl Cmd for TransposeAxes { #[derive(Debug)] pub struct SaveCmd; impl Cmd for SaveCmd { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "save" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -470,7 +514,7 @@ impl Cmd for SaveCmd { #[derive(Debug)] pub struct EnterSearchMode; impl Cmd for EnterSearchMode { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "search" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -490,7 +534,7 @@ pub struct TogglePanelAndFocus { pub currently_open: bool, } impl Cmd for TogglePanelAndFocus { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "toggle-panel-and-focus" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -518,7 +562,7 @@ pub struct TogglePanelVisibility { pub currently_open: bool, } impl Cmd for TogglePanelVisibility { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "toggle-panel-visibility" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -537,7 +581,7 @@ pub struct CyclePanelFocus { pub view_open: bool, } impl Cmd for CyclePanelFocus { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "cycle-panel-focus" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -561,7 +605,7 @@ pub struct EnterEditMode { pub initial_value: String, } impl Cmd for EnterEditMode { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "enter-edit-mode" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -583,7 +627,7 @@ pub struct EnterAdvance { pub cursor: CursorState, } impl Cmd for EnterAdvance { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "enter-advance" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -605,7 +649,7 @@ impl Cmd for EnterAdvance { #[derive(Debug)] pub struct EnterExportPrompt; impl Cmd for EnterExportPrompt { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "enter-export-prompt" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -621,7 +665,7 @@ impl Cmd for EnterExportPrompt { #[derive(Debug)] pub struct SearchNavigate(pub bool); impl Cmd for SearchNavigate { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "search-navigate" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -703,7 +747,7 @@ impl Cmd for SearchNavigate { #[derive(Debug)] pub struct SearchOrCategoryAdd; impl Cmd for SearchOrCategoryAdd { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "search-or-category-add" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -729,7 +773,7 @@ impl Cmd for SearchOrCategoryAdd { #[derive(Debug)] pub struct PageNext; impl Cmd for PageNext { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "page-next" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -766,7 +810,7 @@ impl Cmd for PageNext { #[derive(Debug)] pub struct PagePrev; impl Cmd for PagePrev { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "page-prev" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -840,7 +884,7 @@ fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec, usize)> { #[derive(Debug)] pub struct ToggleGroupUnderCursor; impl Cmd for ToggleGroupUnderCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "toggle-group-under-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -863,7 +907,7 @@ impl Cmd for ToggleGroupUnderCursor { #[derive(Debug)] pub struct ToggleColGroupUnderCursor; impl Cmd for ToggleColGroupUnderCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "toggle-col-group-under-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -889,7 +933,7 @@ impl Cmd for ToggleColGroupUnderCursor { #[derive(Debug)] pub struct HideSelectedRowItem; impl Cmd for HideSelectedRowItem { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "hide-selected-row-item" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -927,7 +971,7 @@ impl Cmd for HideSelectedRowItem { #[derive(Debug)] pub struct EnterTileSelect; impl Cmd for EnterTileSelect { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "enter-tile-select" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -954,7 +998,7 @@ pub struct MovePanelCursor { pub max: usize, } impl Cmd for MovePanelCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "move-panel-cursor" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -982,7 +1026,7 @@ impl Cmd for MovePanelCursor { #[derive(Debug)] pub struct EnterFormulaEdit; impl Cmd for EnterFormulaEdit { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "enter-formula-edit" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -996,7 +1040,7 @@ impl Cmd for EnterFormulaEdit { #[derive(Debug)] pub struct DeleteFormulaAtCursor; impl Cmd for DeleteFormulaAtCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "delete-formula-at-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1030,7 +1074,7 @@ impl Cmd for DeleteFormulaAtCursor { #[derive(Debug)] pub struct CycleAxisAtCursor; impl Cmd for CycleAxisAtCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "cycle-axis-at-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1047,7 +1091,7 @@ impl Cmd for CycleAxisAtCursor { #[derive(Debug)] pub struct OpenItemAddAtCursor; impl Cmd for OpenItemAddAtCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "open-item-add-at-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1071,7 +1115,7 @@ impl Cmd for OpenItemAddAtCursor { #[derive(Debug)] pub struct SwitchViewAtCursor; impl Cmd for SwitchViewAtCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "switch-view-at-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1091,7 +1135,7 @@ impl Cmd for SwitchViewAtCursor { #[derive(Debug)] pub struct CreateAndSwitchView; impl Cmd for CreateAndSwitchView { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "create-and-switch-view" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1109,7 +1153,7 @@ impl Cmd for CreateAndSwitchView { #[derive(Debug)] pub struct DeleteViewAtCursor; impl Cmd for DeleteViewAtCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "delete-view-at-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1138,7 +1182,7 @@ impl Cmd for DeleteViewAtCursor { #[derive(Debug)] pub struct MoveTileCursor(pub i32); impl Cmd for MoveTileCursor { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "move-tile-cursor" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1155,7 +1199,7 @@ impl Cmd for MoveTileCursor { #[derive(Debug)] pub struct CycleAxisForTile; impl Cmd for CycleAxisForTile { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "cycle-axis-for-tile" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1176,7 +1220,7 @@ impl Cmd for CycleAxisForTile { #[derive(Debug)] pub struct SetAxisForTile(pub Axis); impl Cmd for SetAxisForTile { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "set-axis-for-tile" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1202,7 +1246,7 @@ impl Cmd for SetAxisForTile { #[derive(Debug)] pub struct HandleWizardKey; impl Cmd for HandleWizardKey { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "handle-wizard-key" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1218,7 +1262,7 @@ impl Cmd for HandleWizardKey { #[derive(Debug)] pub struct ExecuteCommand; impl Cmd for ExecuteCommand { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "execute-command" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1418,7 +1462,7 @@ pub struct AppendChar { pub buffer: String, } impl Cmd for AppendChar { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "append-char" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1441,7 +1485,7 @@ pub struct PopChar { pub buffer: String, } impl Cmd for PopChar { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "pop-char" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1463,7 +1507,7 @@ pub struct CommitCellEdit { pub value: String, } impl Cmd for CommitCellEdit { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "commit-cell-edit" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1497,7 +1541,7 @@ impl Cmd for CommitCellEdit { #[derive(Debug)] pub struct CommitFormula; impl Cmd for CommitFormula { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "commit-formula" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1528,7 +1572,7 @@ impl Cmd for CommitFormula { #[derive(Debug)] pub struct CommitCategoryAdd; impl Cmd for CommitCategoryAdd { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "commit-category-add" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1553,7 +1597,7 @@ impl Cmd for CommitCategoryAdd { #[derive(Debug)] pub struct CommitItemAdd; impl Cmd for CommitItemAdd { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "commit-item-add" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1587,7 +1631,7 @@ impl Cmd for CommitItemAdd { #[derive(Debug)] pub struct CommitExport; impl Cmd for CommitExport { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "commit-export" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1603,7 +1647,7 @@ impl Cmd for CommitExport { #[derive(Debug)] pub struct ExitSearchMode; impl Cmd for ExitSearchMode { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "exit-search-mode" } fn execute(&self, _ctx: &CmdContext) -> Vec> { @@ -1615,7 +1659,7 @@ impl Cmd for ExitSearchMode { #[derive(Debug)] pub struct SearchAppendChar; impl Cmd for SearchAppendChar { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "search-append-char" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1633,7 +1677,7 @@ impl Cmd for SearchAppendChar { #[derive(Debug)] pub struct SearchPopChar; impl Cmd for SearchPopChar { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "search-pop-char" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1647,7 +1691,7 @@ impl Cmd for SearchPopChar { #[derive(Debug)] pub struct CommandModeBackspace; impl Cmd for CommandModeBackspace { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "command-mode-backspace" } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1674,7 +1718,7 @@ macro_rules! effect_cmd { #[derive(Debug)] pub struct $name(pub Vec); impl Cmd for $name { - fn name(&self) -> &str { + fn name(&self) -> &'static str { $cmd_name } fn execute(&self, ctx: &CmdContext) -> Vec> { @@ -1929,12 +1973,12 @@ pub fn default_registry() -> CmdRegistry { let mut r = CmdRegistry::new(); // ── Model mutations (effect_cmd! wrappers) ─────────────────────────── - r.register_pure("add-category", AddCategoryCmd::parse); - r.register_pure("add-item", AddItemCmd::parse); - r.register_pure("add-item-in-group", AddItemInGroupCmd::parse); - r.register_pure("set-cell", SetCellCmd::parse); + r.register_pure(&AddCategoryCmd(vec![]), AddCategoryCmd::parse); + r.register_pure(&AddItemCmd(vec![]), AddItemCmd::parse); + r.register_pure(&AddItemInGroupCmd(vec![]), AddItemInGroupCmd::parse); + r.register_pure(&SetCellCmd(vec![]), SetCellCmd::parse); r.register( - "clear-cell", + &ClearCellCommand { key: CellKey::new(vec![]) }, |args| { if args.is_empty() { return Err("clear-cell requires at least one Cat/Item coordinate".into()); @@ -1948,24 +1992,24 @@ pub fn default_registry() -> CmdRegistry { Ok(Box::new(ClearCellCommand { key })) }, ); - r.register_pure("add-formula", AddFormulaCmd::parse); - r.register_pure("remove-formula", RemoveFormulaCmd::parse); - r.register_pure("create-view", CreateViewCmd::parse); - r.register_pure("delete-view", DeleteViewCmd::parse); - r.register_pure("switch-view", SwitchViewCmd::parse); - r.register_pure("set-axis", SetAxisCmd::parse); - r.register_pure("set-page", SetPageCmd::parse); - r.register_pure("toggle-group", ToggleGroupCmd::parse); - r.register_pure("hide-item", HideItemCmd::parse); - r.register_pure("show-item", ShowItemCmd::parse); - r.register_pure("save-as", SaveAsCmd::parse); - r.register_pure("load", LoadModelCmd::parse); - r.register_pure("export-csv", ExportCsvCmd::parse); - r.register_pure("import-json", ImportJsonCmd::parse); + r.register_pure(&AddFormulaCmd(vec![]), AddFormulaCmd::parse); + r.register_pure(&RemoveFormulaCmd(vec![]), RemoveFormulaCmd::parse); + r.register_pure(&CreateViewCmd(vec![]), CreateViewCmd::parse); + r.register_pure(&DeleteViewCmd(vec![]), DeleteViewCmd::parse); + r.register_pure(&SwitchViewCmd(vec![]), SwitchViewCmd::parse); + r.register_pure(&SetAxisCmd(vec![]), SetAxisCmd::parse); + r.register_pure(&SetPageCmd(vec![]), SetPageCmd::parse); + r.register_pure(&ToggleGroupCmd(vec![]), ToggleGroupCmd::parse); + r.register_pure(&HideItemCmd(vec![]), HideItemCmd::parse); + r.register_pure(&ShowItemCmd(vec![]), ShowItemCmd::parse); + r.register_pure(&SaveAsCmd(vec![]), SaveAsCmd::parse); + r.register_pure(&LoadModelCmd(vec![]), LoadModelCmd::parse); + r.register_pure(&ExportCsvCmd(vec![]), ExportCsvCmd::parse); + r.register_pure(&ImportJsonCmd(vec![]), ImportJsonCmd::parse); // ── Navigation ─────────────────────────────────────────────────────── r.register( - "move-selection", + &MoveSelection { dr: 0, dc: 0, cursor: CursorState::default() }, |args| { require_args("move-selection", args, 2)?; let dr = args[0].parse::().map_err(|e| e.to_string())?; @@ -1995,7 +2039,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "jump-first-row", + &JumpToFirstRow { col: 0 }, |_| Ok(Box::new(JumpToFirstRow { col: 0 })), |_, ctx| { Ok(Box::new(JumpToFirstRow { @@ -2004,7 +2048,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "jump-last-row", + &JumpToLastRow { col: 0, row_count: 0, row_offset: 0 }, |_| { Ok(Box::new(JumpToLastRow { col: 0, @@ -2021,7 +2065,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "jump-first-col", + &JumpToFirstCol { row: 0 }, |_| Ok(Box::new(JumpToFirstCol { row: 0 })), |_, ctx| { Ok(Box::new(JumpToFirstCol { @@ -2030,7 +2074,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "jump-last-col", + &JumpToLastCol { row: 0, col_count: 0, col_offset: 0 }, |_| { Ok(Box::new(JumpToLastCol { row: 0, @@ -2047,7 +2091,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "scroll-rows", + &ScrollRows { delta: 0, cursor: CursorState::default() }, |args| { require_args("scroll-rows", args, 1)?; let n = args[0].parse::().map_err(|e| e.to_string())?; @@ -2073,7 +2117,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "enter-advance", + &EnterAdvance { cursor: CursorState::default() }, |_| { Ok(Box::new(EnterAdvance { cursor: CursorState { @@ -2095,7 +2139,7 @@ pub fn default_registry() -> CmdRegistry { // ── Cell operations ────────────────────────────────────────────────── r.register( - "yank", + &YankCell { key: CellKey::new(vec![]) }, |args| { if args.is_empty() { return Err("yank requires at least one Cat/Item coordinate".into()); @@ -2110,7 +2154,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "paste", + &PasteCell { key: CellKey::new(vec![]) }, |args| { if args.is_empty() { return Err("paste requires at least one Cat/Item coordinate".into()); @@ -2127,17 +2171,17 @@ pub fn default_registry() -> CmdRegistry { // clear-cell is registered above (unified: ctx.cell_key or explicit coords) // ── View / page ────────────────────────────────────────────────────── - r.register_nullary("transpose", || Box::new(TransposeAxes)); - r.register_nullary("page-next", || Box::new(PageNext)); - r.register_nullary("page-prev", || Box::new(PagePrev)); + r.register_nullary(|| Box::new(TransposeAxes)); + r.register_nullary(|| Box::new(PageNext)); + r.register_nullary(|| Box::new(PagePrev)); // ── Mode changes ───────────────────────────────────────────────────── - r.register_nullary("force-quit", || Box::new(ForceQuit)); - r.register_nullary("save-and-quit", || Box::new(SaveAndQuit)); - r.register_nullary("save", || Box::new(SaveCmd)); - r.register_nullary("search", || Box::new(EnterSearchMode)); + r.register_nullary(|| Box::new(ForceQuit)); + r.register_nullary(|| Box::new(SaveAndQuit)); + r.register_nullary(|| Box::new(SaveCmd)); + r.register_nullary(|| Box::new(EnterSearchMode)); r.register( - "enter-edit-mode", + &EnterEditMode { initial_value: String::new() }, |args| { let val = args.first().cloned().unwrap_or_default(); Ok(Box::new(EnterEditMode { initial_value: val })) @@ -2154,10 +2198,10 @@ pub fn default_registry() -> CmdRegistry { })) }, ); - r.register_nullary("enter-export-prompt", || Box::new(EnterExportPrompt)); - r.register_nullary("enter-formula-edit", || Box::new(EnterFormulaEdit)); - r.register_nullary("enter-tile-select", || Box::new(EnterTileSelect)); - r.register_pure("enter-mode", |args| { + r.register_nullary(|| Box::new(EnterExportPrompt)); + r.register_nullary(|| Box::new(EnterFormulaEdit)); + r.register_nullary(|| Box::new(EnterTileSelect)); + r.register_pure(&NamedCmd("enter-mode"), |args| { require_args("enter-mode", args, 1)?; let mode = match args[0].as_str() { "normal" => AppMode::Normal, @@ -2187,18 +2231,18 @@ pub fn default_registry() -> CmdRegistry { }); // ── Search ─────────────────────────────────────────────────────────── - r.register_pure("search-navigate", |args| { + r.register_pure(&NamedCmd("search-navigate"), |args| { let forward = args.first().map(|s| s != "backward").unwrap_or(true); Ok(Box::new(SearchNavigate(forward))) }); - r.register_nullary("search-or-category-add", || Box::new(SearchOrCategoryAdd)); - r.register_nullary("exit-search-mode", || Box::new(ExitSearchMode)); - r.register_nullary("search-append-char", || Box::new(SearchAppendChar)); - r.register_nullary("search-pop-char", || Box::new(SearchPopChar)); + r.register_nullary(|| Box::new(SearchOrCategoryAdd)); + r.register_nullary(|| Box::new(ExitSearchMode)); + r.register_nullary(|| Box::new(SearchAppendChar)); + r.register_nullary(|| Box::new(SearchPopChar)); // ── Panel operations ───────────────────────────────────────────────── r.register( - "toggle-panel-and-focus", + &TogglePanelAndFocus { panel: Panel::Formula, currently_open: false }, |args| { require_args("toggle-panel-and-focus", args, 1)?; let panel = parse_panel(&args[0])?; @@ -2222,7 +2266,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "toggle-panel-visibility", + &TogglePanelVisibility { panel: Panel::Formula, currently_open: false }, |args| { require_args("toggle-panel-visibility", args, 1)?; let panel = parse_panel(&args[0])?; @@ -2246,7 +2290,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "cycle-panel-focus", + &CyclePanelFocus { formula_open: false, category_open: false, view_open: false }, |_| { Ok(Box::new(CyclePanelFocus { formula_open: false, @@ -2263,7 +2307,7 @@ pub fn default_registry() -> CmdRegistry { }, ); r.register( - "move-panel-cursor", + &MovePanelCursor { panel: Panel::Formula, delta: 0, current: 0, max: 0 }, |args| { require_args("move-panel-cursor", args, 2)?; let panel = parse_panel(&args[0])?; @@ -2292,55 +2336,55 @@ pub fn default_registry() -> CmdRegistry { })) }, ); - r.register_nullary("delete-formula-at-cursor", || { + r.register_nullary(|| { Box::new(DeleteFormulaAtCursor) }); - r.register_nullary("cycle-axis-at-cursor", || Box::new(CycleAxisAtCursor)); - r.register_nullary("open-item-add-at-cursor", || Box::new(OpenItemAddAtCursor)); - r.register_nullary("switch-view-at-cursor", || Box::new(SwitchViewAtCursor)); - r.register_nullary("create-and-switch-view", || Box::new(CreateAndSwitchView)); - r.register_nullary("delete-view-at-cursor", || Box::new(DeleteViewAtCursor)); + r.register_nullary(|| Box::new(CycleAxisAtCursor)); + r.register_nullary(|| Box::new(OpenItemAddAtCursor)); + r.register_nullary(|| Box::new(SwitchViewAtCursor)); + r.register_nullary(|| Box::new(CreateAndSwitchView)); + r.register_nullary(|| Box::new(DeleteViewAtCursor)); // ── Tile select ────────────────────────────────────────────────────── - r.register_pure("move-tile-cursor", |args| { + r.register_pure(&NamedCmd("move-tile-cursor"), |args| { require_args("move-tile-cursor", args, 1)?; let delta = args[0].parse::().map_err(|e| e.to_string())?; Ok(Box::new(MoveTileCursor(delta))) }); - r.register_nullary("cycle-axis-for-tile", || Box::new(CycleAxisForTile)); - r.register_pure("set-axis-for-tile", |args| { + r.register_nullary(|| Box::new(CycleAxisForTile)); + r.register_pure(&NamedCmd("set-axis-for-tile"), |args| { require_args("set-axis-for-tile", args, 1)?; let axis = parse_axis(&args[0])?; Ok(Box::new(SetAxisForTile(axis))) }); // ── Grid operations ────────────────────────────────────────────────── - r.register_nullary("toggle-group-under-cursor", || { + r.register_nullary(|| { Box::new(ToggleGroupUnderCursor) }); - r.register_nullary("toggle-col-group-under-cursor", || { + r.register_nullary(|| { Box::new(ToggleColGroupUnderCursor) }); - r.register_nullary("hide-selected-row-item", || Box::new(HideSelectedRowItem)); + r.register_nullary(|| Box::new(HideSelectedRowItem)); // ── Text buffer ────────────────────────────────────────────────────── - r.register_pure("append-char", |args| { + r.register_pure(&NamedCmd("append-char"), |args| { require_args("append-char", args, 1)?; Ok(Box::new(AppendChar { buffer: args[0].clone(), })) }); - r.register_pure("pop-char", |args| { + r.register_pure(&NamedCmd("pop-char"), |args| { require_args("pop-char", args, 1)?; Ok(Box::new(PopChar { buffer: args[0].clone(), })) }); - r.register_nullary("command-mode-backspace", || Box::new(CommandModeBackspace)); + r.register_nullary(|| Box::new(CommandModeBackspace)); // ── Commit ─────────────────────────────────────────────────────────── r.register( - "commit-cell-edit", + &CommitCellEdit { key: CellKey::new(vec![]), value: String::new() }, |args| { // parse: commit-cell-edit ... if args.len() < 2 { @@ -2357,14 +2401,14 @@ pub fn default_registry() -> CmdRegistry { Ok(Box::new(CommitCellEdit { key, value })) }, ); - r.register_nullary("commit-formula", || Box::new(CommitFormula)); - r.register_nullary("commit-category-add", || Box::new(CommitCategoryAdd)); - r.register_nullary("commit-item-add", || Box::new(CommitItemAdd)); - r.register_nullary("commit-export", || Box::new(CommitExport)); - r.register_nullary("execute-command", || Box::new(ExecuteCommand)); + r.register_nullary(|| Box::new(CommitFormula)); + r.register_nullary(|| Box::new(CommitCategoryAdd)); + r.register_nullary(|| Box::new(CommitItemAdd)); + r.register_nullary(|| Box::new(CommitExport)); + r.register_nullary(|| Box::new(ExecuteCommand)); // ── Wizard ─────────────────────────────────────────────────────────── - r.register_nullary("handle-wizard-key", || Box::new(HandleWizardKey)); + r.register_nullary(|| Box::new(HandleWizardKey)); r }