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)
This commit is contained in:
Edward Langley
2026-04-04 18:37:43 -07:00
parent e2ff9cf98e
commit 89fdb27d6c

View File

@ -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<dyn Cmd>) {
/// The name is derived by calling `f()` once.
pub fn register_nullary(&mut self, f: fn() -> Box<dyn Cmd>) {
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<Box<dyn Effect>> {
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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
vec![effect::change_mode(self.0.clone())]
let mut effects: Vec<Box<dyn Effect>> = 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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -840,7 +884,7 @@ fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec<String>, 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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -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<Box<dyn Effect>> {
@ -1674,7 +1718,7 @@ macro_rules! effect_cmd {
#[derive(Debug)]
pub struct $name(pub Vec<String>);
impl Cmd for $name {
fn name(&self) -> &str {
fn name(&self) -> &'static str {
$cmd_name
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
@ -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::<i32>().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::<i32>().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::<i32>().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 <value> <Cat/Item>...
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
}