From 649d80cb3519206b65a5096bb3133c642cf82bb3 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Sat, 4 Apr 2026 11:02:00 -0700 Subject: [PATCH] refactor(command): decouple keymap bindings from command implementations Refactor the keymap system to use string-based command names instead of concrete command struct instantiations. This introduces a Binding enum that can represent either a command lookup (name + args) or a prefix sub-keymap. Key changes: - Keymap now stores Binding enum instead of Arc - dispatch() accepts CmdRegistry to resolve commands at runtime - Added bind_args() for commands with arguments - KeymapSet now owns the command registry - Removed PrefixKey struct, inlined its logic - Updated all default keymap bindings to use string names This enables more flexible command configuration and easier testing. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M) --- src/command/cmd.rs | 4 + src/command/keymap.rs | 618 ++++++++++++++++++------------------------ src/ui/app.rs | 3 +- 3 files changed, 267 insertions(+), 358 deletions(-) diff --git a/src/command/cmd.rs b/src/command/cmd.rs index be1de89..2bb04c9 100644 --- a/src/command/cmd.rs +++ b/src/command/cmd.rs @@ -1909,6 +1909,10 @@ pub fn default_registry() -> CmdRegistry { "view-panel" => AppMode::ViewPanel, "tile-select" => AppMode::TileSelect, "command" => AppMode::CommandMode { buffer: String::new() }, + "category-add" => AppMode::CategoryAdd { buffer: String::new() }, + "editing" => AppMode::Editing { buffer: String::new() }, + "formula-edit" => AppMode::FormulaEdit { buffer: String::new() }, + "export-prompt" => AppMode::ExportPrompt { buffer: String::new() }, other => return Err(format!("Unknown mode: {other}")), }; Ok(Box::new(EnterMode(mode))) diff --git a/src/command/keymap.rs b/src/command/keymap.rs index 14a3090..9e652d9 100644 --- a/src/command/keymap.rs +++ b/src/command/keymap.rs @@ -6,24 +6,22 @@ use crossterm::event::{KeyCode, KeyModifiers}; use crate::ui::app::AppMode; use crate::ui::effect::Effect; -use crate::view::Axis; -use super::cmd::{self, Cmd, CmdContext}; +use super::cmd::{self, CmdContext, CmdRegistry}; +// `cmd` module imported for `default_registry()` in default_keymaps() /// A key pattern that can be matched against a KeyEvent. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum KeyPattern { /// Single key with modifiers Key(KeyCode, KeyModifiers), - /// Matches any Char key (for text-entry modes). The actual char - /// is available in CmdContext::key_code. + /// Matches any Char key (for text-entry modes). AnyChar, /// Matches any key at all (lowest priority fallback). Any, } /// Identifies which mode a binding applies to. -/// Uses discriminant matching — mode data (buffers, etc.) is ignored. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ModeKey { Normal, @@ -64,14 +62,21 @@ impl ModeKey { } } -/// A keymap maps key patterns to commands. -/// -/// Keymaps are stored as `Arc` so they can be cheaply shared. -/// A prefix key binding stores a sub-keymap as a `PrefixKey` command, -/// which when executed sets the sub-keymap as the transient keymap -/// (Emacs-style prefix dispatch). +/// What a key binding resolves to. +#[derive(Debug, Clone)] +pub enum Binding { + /// A command name + arguments, looked up in the registry at dispatch time. + Cmd { + name: &'static str, + args: Vec, + }, + /// A prefix sub-keymap (Emacs-style). + Prefix(Arc), +} + +/// A keymap maps key patterns to bindings (command names or prefix sub-keymaps). pub struct Keymap { - bindings: HashMap>, + bindings: HashMap, } impl fmt::Debug for Keymap { @@ -89,21 +94,54 @@ impl Keymap { } } - pub fn bind(&mut self, key: KeyCode, mods: KeyModifiers, cmd: Arc) { - self.bindings.insert(KeyPattern::Key(key, mods), cmd); + /// Bind a key to a command name (no args). + pub fn bind(&mut self, key: KeyCode, mods: KeyModifiers, name: &'static str) { + self.bindings.insert( + KeyPattern::Key(key, mods), + Binding::Cmd { + name, + args: vec![], + }, + ); } - /// Convenience: bind a concrete Cmd value (wraps in Arc). - pub fn bind_cmd(&mut self, key: KeyCode, mods: KeyModifiers, cmd: impl Cmd + 'static) { - self.bind(key, mods, Arc::new(cmd)); + /// Bind a key to a command name with arguments. + pub fn bind_args( + &mut self, + key: KeyCode, + mods: KeyModifiers, + name: &'static str, + args: Vec, + ) { + self.bindings + .insert(KeyPattern::Key(key, mods), Binding::Cmd { name, args }); } /// Bind a prefix key that activates a sub-keymap. pub fn bind_prefix(&mut self, key: KeyCode, mods: KeyModifiers, sub: Arc) { - self.bind(key, mods, Arc::new(PrefixKey(sub))); + self.bindings + .insert(KeyPattern::Key(key, mods), Binding::Prefix(sub)); } - pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&dyn Cmd> { + /// Bind a catch-all for any Char key. + pub fn bind_any_char(&mut self, name: &'static str, args: Vec) { + self.bindings + .insert(KeyPattern::AnyChar, Binding::Cmd { name, args }); + } + + /// Bind a catch-all for any key at all. + pub fn bind_any(&mut self, name: &'static str) { + self.bindings.insert( + KeyPattern::Any, + Binding::Cmd { + name, + args: vec![], + }, + ); + } + + /// Look up the binding for a key. + pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> { self.bindings .get(&KeyPattern::Key(key, mods)) .or_else(|| { @@ -114,37 +152,24 @@ impl Keymap { } }) .or_else(|| self.bindings.get(&KeyPattern::Any)) - .map(|c| c.as_ref()) } - /// Bind a catch-all for any Char key (used for text-entry modes). - pub fn bind_any_char(&mut self, cmd: impl Cmd + 'static) { - self.bindings.insert(KeyPattern::AnyChar, Arc::new(cmd)); - } - - /// Execute a keymap lookup and return effects, or None if no binding. + /// Dispatch a key: look up binding, resolve through registry, return effects. pub fn dispatch( &self, + registry: &CmdRegistry, ctx: &CmdContext, key: KeyCode, mods: KeyModifiers, ) -> Option>> { - let cmd = self.lookup(key, mods)?; - Some(cmd.execute(ctx)) - } -} - -/// A prefix key command that activates a sub-keymap as transient. -#[derive(Debug, Clone)] -pub struct PrefixKey(pub Arc); - -impl Cmd for PrefixKey { - fn name(&self) -> &str { - "prefix-key" - } - - fn execute(&self, _ctx: &CmdContext) -> Vec> { - vec![Box::new(SetTransientKeymap(self.0.clone()))] + let binding = self.lookup(key, mods)?; + match binding { + Binding::Cmd { name, args } => { + let cmd = registry.parse(name, args).ok()?; + Some(cmd.execute(ctx)) + } + Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]), + } } } @@ -158,15 +183,17 @@ impl Effect for SetTransientKeymap { } } -/// Maps modes to their root keymaps. +/// Maps modes to their root keymaps + owns the command registry. pub struct KeymapSet { mode_maps: HashMap>, + registry: CmdRegistry, } impl KeymapSet { - pub fn new() -> Self { + pub fn new(registry: CmdRegistry) -> Self { Self { mode_maps: HashMap::new(), + registry, } } @@ -174,10 +201,8 @@ impl KeymapSet { self.mode_maps.insert(mode, keymap); } - /// Look up the root keymap for a given app mode. - pub fn get(&self, mode: &AppMode, search_mode: bool) -> Option<&Arc> { - let mode_key = ModeKey::from_app_mode(mode, search_mode)?; - self.mode_maps.get(&mode_key) + pub fn registry(&self) -> &CmdRegistry { + &self.registry } /// Dispatch a key event: returns effects if a binding matched. @@ -187,421 +212,300 @@ impl KeymapSet { key: KeyCode, mods: KeyModifiers, ) -> Option>> { - let keymap = self.get(ctx.mode, ctx.search_mode)?; - keymap.dispatch(ctx, key, mods) + let mode_key = ModeKey::from_app_mode(ctx.mode, ctx.search_mode)?; + let keymap = self.mode_maps.get(&mode_key)?; + keymap.dispatch(&self.registry, ctx, key, mods) } - /// Build the default keymap set with all vim-style bindings. - pub fn default_keymaps() -> Self { - let mut set = Self::new(); + /// Dispatch against a specific keymap (for transient/prefix keymaps). + pub fn dispatch_transient( + &self, + keymap: &Keymap, + ctx: &CmdContext, + key: KeyCode, + mods: KeyModifiers, + ) -> Option>> { + keymap.dispatch(&self.registry, ctx, key, mods) + } - // ── Normal mode ────────────────────────────────────────────────── - let mut normal = Keymap::new(); + /// Build the default keymap set with all bindings. + pub fn default_keymaps() -> Self { + let registry = cmd::default_registry(); + let mut set = Self::new(registry); let none = KeyModifiers::NONE; let ctrl = KeyModifiers::CONTROL; let shift = KeyModifiers::SHIFT; + // ── Normal mode ────────────────────────────────────────────────── + let mut normal = Keymap::new(); + // Navigation - normal.bind_cmd(KeyCode::Up, none, cmd::MoveSelection { dr: -1, dc: 0 }); - normal.bind_cmd(KeyCode::Down, none, cmd::MoveSelection { dr: 1, dc: 0 }); - normal.bind_cmd(KeyCode::Left, none, cmd::MoveSelection { dr: 0, dc: -1 }); - normal.bind_cmd(KeyCode::Right, none, cmd::MoveSelection { dr: 0, dc: 1 }); - normal.bind_cmd( - KeyCode::Char('k'), - none, - cmd::MoveSelection { dr: -1, dc: 0 }, - ); - normal.bind_cmd( - KeyCode::Char('j'), - none, - cmd::MoveSelection { dr: 1, dc: 0 }, - ); - normal.bind_cmd( - KeyCode::Char('h'), - none, - cmd::MoveSelection { dr: 0, dc: -1 }, - ); - normal.bind_cmd( - KeyCode::Char('l'), - none, - cmd::MoveSelection { dr: 0, dc: 1 }, - ); + for (key, dr, dc) in [ + (KeyCode::Up, -1, 0), + (KeyCode::Down, 1, 0), + (KeyCode::Left, 0, -1), + (KeyCode::Right, 0, 1), + ] { + normal.bind_args(key, none, "move-selection", vec![dr.to_string(), dc.to_string()]); + } + for (ch, dr, dc) in [('k', -1, 0), ('j', 1, 0), ('h', 0, -1), ('l', 0, 1)] { + normal.bind_args( + KeyCode::Char(ch), + none, + "move-selection", + vec![dr.to_string(), dc.to_string()], + ); + } // Jump to boundaries - normal.bind_cmd(KeyCode::Char('G'), shift, cmd::JumpToLastRow); - normal.bind_cmd(KeyCode::Char('0'), none, cmd::JumpToFirstCol); - normal.bind_cmd(KeyCode::Char('$'), shift, cmd::JumpToLastCol); + normal.bind(KeyCode::Char('G'), shift, "jump-last-row"); + normal.bind(KeyCode::Char('0'), none, "jump-first-col"); + normal.bind(KeyCode::Char('$'), shift, "jump-last-col"); // Scroll - normal.bind_cmd(KeyCode::Char('d'), ctrl, cmd::ScrollRows(5)); - normal.bind_cmd(KeyCode::Char('u'), ctrl, cmd::ScrollRows(-5)); + normal.bind_args(KeyCode::Char('d'), ctrl, "scroll-rows", vec!["5".into()]); + normal.bind_args(KeyCode::Char('u'), ctrl, "scroll-rows", vec!["-5".into()]); // Cell operations - normal.bind_cmd(KeyCode::Char('x'), none, cmd::ClearSelectedCell); - normal.bind_cmd(KeyCode::Char('p'), none, cmd::PasteCell); + normal.bind(KeyCode::Char('x'), none, "clear-selected-cell"); + normal.bind(KeyCode::Char('p'), none, "paste"); // View - normal.bind_cmd(KeyCode::Char('t'), none, cmd::TransposeAxes); + normal.bind(KeyCode::Char('t'), none, "transpose"); // Mode changes - normal.bind_cmd(KeyCode::Char('q'), ctrl, cmd::ForceQuit); - normal.bind_cmd( + normal.bind(KeyCode::Char('q'), ctrl, "force-quit"); + normal.bind_args( KeyCode::Char(':'), shift, - cmd::EnterMode(AppMode::CommandMode { - buffer: String::new(), - }), + "enter-mode", + vec!["command".into()], ); - normal.bind_cmd(KeyCode::Char('/'), none, cmd::EnterSearchMode); - normal.bind_cmd(KeyCode::Char('s'), ctrl, cmd::SaveCmd); - normal.bind_cmd(KeyCode::F(1), none, cmd::EnterMode(AppMode::Help)); - normal.bind_cmd(KeyCode::Char('?'), shift, cmd::EnterMode(AppMode::Help)); + normal.bind(KeyCode::Char('/'), none, "search"); + normal.bind(KeyCode::Char('s'), ctrl, "save"); + normal.bind(KeyCode::F(1), none, "enter-mode"); + normal.bind_args(KeyCode::F(1), none, "enter-mode", vec!["help".into()]); + normal.bind_args(KeyCode::Char('?'), shift, "enter-mode", vec!["help".into()]); - // Panel toggles (uppercase = toggle + focus) - normal.bind_cmd( + // Panel toggles + normal.bind_args( KeyCode::Char('F'), shift, - cmd::TogglePanelAndFocus(crate::ui::effect::Panel::Formula), + "toggle-panel-and-focus", + vec!["formula".into()], ); - normal.bind_cmd( + normal.bind_args( KeyCode::Char('C'), shift, - cmd::TogglePanelAndFocus(crate::ui::effect::Panel::Category), + "toggle-panel-and-focus", + vec!["category".into()], ); - normal.bind_cmd( + normal.bind_args( KeyCode::Char('V'), shift, - cmd::TogglePanelAndFocus(crate::ui::effect::Panel::View), + "toggle-panel-and-focus", + vec!["view".into()], ); - - // Legacy Ctrl+ panel toggles (visibility only) - normal.bind_cmd( + normal.bind_args( KeyCode::Char('f'), ctrl, - cmd::TogglePanelVisibility(crate::ui::effect::Panel::Formula), + "toggle-panel-visibility", + vec!["formula".into()], ); - normal.bind_cmd( + normal.bind_args( KeyCode::Char('c'), ctrl, - cmd::TogglePanelVisibility(crate::ui::effect::Panel::Category), + "toggle-panel-visibility", + vec!["category".into()], ); - normal.bind_cmd( + normal.bind_args( KeyCode::Char('v'), ctrl, - cmd::TogglePanelVisibility(crate::ui::effect::Panel::View), + "toggle-panel-visibility", + vec!["view".into()], ); - - // Tab cycles open panels - normal.bind_cmd(KeyCode::Tab, none, cmd::CyclePanelFocus); + normal.bind(KeyCode::Tab, none, "cycle-panel-focus"); // Editing entry - normal.bind_cmd(KeyCode::Char('i'), none, cmd::EnterEditMode); - normal.bind_cmd(KeyCode::Char('a'), none, cmd::EnterEditMode); - normal.bind_cmd(KeyCode::Enter, none, cmd::EnterAdvance); - normal.bind_cmd(KeyCode::Char('e'), ctrl, cmd::EnterExportPrompt); + normal.bind(KeyCode::Char('i'), none, "enter-edit-mode"); + normal.bind(KeyCode::Char('a'), none, "enter-edit-mode"); + normal.bind(KeyCode::Enter, none, "enter-advance"); + normal.bind(KeyCode::Char('e'), ctrl, "enter-export-prompt"); // Search / category add - normal.bind_cmd(KeyCode::Char('n'), none, cmd::SearchNavigate(true)); - normal.bind_cmd(KeyCode::Char('N'), shift, cmd::SearchOrCategoryAdd); + normal.bind_args(KeyCode::Char('n'), none, "search-navigate", vec!["forward".into()]); + normal.bind(KeyCode::Char('N'), shift, "search-or-category-add"); // Page navigation - normal.bind_cmd(KeyCode::Char(']'), none, cmd::PageNext); - normal.bind_cmd(KeyCode::Char('['), none, cmd::PagePrev); + normal.bind(KeyCode::Char(']'), none, "page-next"); + normal.bind(KeyCode::Char('['), none, "page-prev"); // Group / hide - normal.bind_cmd(KeyCode::Char('z'), none, cmd::ToggleGroupUnderCursor); - normal.bind_cmd(KeyCode::Char('H'), shift, cmd::HideSelectedRowItem); + normal.bind(KeyCode::Char('z'), none, "toggle-group-under-cursor"); + normal.bind(KeyCode::Char('H'), shift, "hide-selected-row-item"); // Tile select - normal.bind_cmd(KeyCode::Char('T'), shift, cmd::EnterTileSelect); - normal.bind_cmd(KeyCode::Left, ctrl, cmd::EnterTileSelect); - normal.bind_cmd(KeyCode::Right, ctrl, cmd::EnterTileSelect); - normal.bind_cmd(KeyCode::Up, ctrl, cmd::EnterTileSelect); - normal.bind_cmd(KeyCode::Down, ctrl, cmd::EnterTileSelect); + normal.bind(KeyCode::Char('T'), shift, "enter-tile-select"); + normal.bind(KeyCode::Left, ctrl, "enter-tile-select"); + normal.bind(KeyCode::Right, ctrl, "enter-tile-select"); + normal.bind(KeyCode::Up, ctrl, "enter-tile-select"); + normal.bind(KeyCode::Down, ctrl, "enter-tile-select"); - // ── Prefix keys (Emacs-style sub-keymaps) ──────────────────────── - - // g-prefix + // Prefix keys let mut g_map = Keymap::new(); - g_map.bind_cmd(KeyCode::Char('g'), none, cmd::JumpToFirstRow); - g_map.bind_cmd(KeyCode::Char('z'), none, cmd::ToggleColGroupUnderCursor); + g_map.bind(KeyCode::Char('g'), none, "jump-first-row"); + g_map.bind(KeyCode::Char('z'), none, "toggle-col-group-under-cursor"); normal.bind_prefix(KeyCode::Char('g'), none, Arc::new(g_map)); - // y-prefix let mut y_map = Keymap::new(); - y_map.bind_cmd(KeyCode::Char('y'), none, cmd::YankCell); + y_map.bind(KeyCode::Char('y'), none, "yank"); normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map)); - // Z-prefix let mut z_map = Keymap::new(); - z_map.bind_cmd(KeyCode::Char('Z'), shift, cmd::SaveAndQuit); + z_map.bind(KeyCode::Char('Z'), shift, "save-and-quit"); normal.bind_prefix(KeyCode::Char('Z'), shift, Arc::new(z_map)); set.insert(ModeKey::Normal, Arc::new(normal)); // ── Help mode ──────────────────────────────────────────────────── let mut help = Keymap::new(); - help.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - help.bind_cmd(KeyCode::Char('q'), none, cmd::EnterMode(AppMode::Normal)); + help.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + help.bind_args(KeyCode::Char('q'), none, "enter-mode", vec!["normal".into()]); set.insert(ModeKey::Help, Arc::new(help)); - // ── Formula panel mode ─────────────────────────────────────────── + // ── Formula panel ──────────────────────────────────────────────── let mut fp = Keymap::new(); - fp.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - fp.bind_cmd(KeyCode::Tab, none, cmd::EnterMode(AppMode::Normal)); - fp.bind_cmd( - KeyCode::Up, - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Formula, - delta: -1, - }, - ); - fp.bind_cmd( - KeyCode::Char('k'), - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Formula, - delta: -1, - }, - ); - fp.bind_cmd( - KeyCode::Down, - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Formula, - delta: 1, - }, - ); - fp.bind_cmd( - KeyCode::Char('j'), - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Formula, - delta: 1, - }, - ); - fp.bind_cmd(KeyCode::Char('a'), none, cmd::EnterFormulaEdit); - fp.bind_cmd(KeyCode::Char('n'), none, cmd::EnterFormulaEdit); - fp.bind_cmd(KeyCode::Char('o'), none, cmd::EnterFormulaEdit); - fp.bind_cmd(KeyCode::Char('d'), none, cmd::DeleteFormulaAtCursor); - fp.bind_cmd(KeyCode::Delete, none, cmd::DeleteFormulaAtCursor); + fp.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + fp.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]); + for key in [KeyCode::Up, KeyCode::Char('k')] { + fp.bind_args(key, none, "move-panel-cursor", vec!["formula".into(), "-1".into()]); + } + for key in [KeyCode::Down, KeyCode::Char('j')] { + fp.bind_args(key, none, "move-panel-cursor", vec!["formula".into(), "1".into()]); + } + fp.bind(KeyCode::Char('a'), none, "enter-formula-edit"); + fp.bind(KeyCode::Char('n'), none, "enter-formula-edit"); + fp.bind(KeyCode::Char('o'), none, "enter-formula-edit"); + fp.bind(KeyCode::Char('d'), none, "delete-formula-at-cursor"); + fp.bind(KeyCode::Delete, none, "delete-formula-at-cursor"); set.insert(ModeKey::FormulaPanel, Arc::new(fp)); - // ── Category panel mode ────────────────────────────────────────── + // ── Category panel ─────────────────────────────────────────────── let mut cp = Keymap::new(); - cp.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - cp.bind_cmd(KeyCode::Tab, none, cmd::EnterMode(AppMode::Normal)); - cp.bind_cmd( - KeyCode::Up, - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Category, - delta: -1, - }, - ); - cp.bind_cmd( - KeyCode::Char('k'), - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Category, - delta: -1, - }, - ); - cp.bind_cmd( - KeyCode::Down, - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Category, - delta: 1, - }, - ); - cp.bind_cmd( - KeyCode::Char('j'), - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::Category, - delta: 1, - }, - ); - cp.bind_cmd(KeyCode::Enter, none, cmd::CycleAxisAtCursor); - cp.bind_cmd(KeyCode::Char(' '), none, cmd::CycleAxisAtCursor); - cp.bind_cmd( + cp.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + cp.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]); + for key in [KeyCode::Up, KeyCode::Char('k')] { + cp.bind_args(key, none, "move-panel-cursor", vec!["category".into(), "-1".into()]); + } + for key in [KeyCode::Down, KeyCode::Char('j')] { + cp.bind_args(key, none, "move-panel-cursor", vec!["category".into(), "1".into()]); + } + cp.bind(KeyCode::Enter, none, "cycle-axis-at-cursor"); + cp.bind(KeyCode::Char(' '), none, "cycle-axis-at-cursor"); + cp.bind_args( KeyCode::Char('n'), none, - cmd::EnterMode(AppMode::CategoryAdd { - buffer: String::new(), - }), + "enter-mode", + vec!["category-add".into()], ); - cp.bind_cmd(KeyCode::Char('a'), none, cmd::OpenItemAddAtCursor); - cp.bind_cmd(KeyCode::Char('o'), none, cmd::OpenItemAddAtCursor); + cp.bind(KeyCode::Char('a'), none, "open-item-add-at-cursor"); + cp.bind(KeyCode::Char('o'), none, "open-item-add-at-cursor"); set.insert(ModeKey::CategoryPanel, Arc::new(cp)); - // ── View panel mode ────────────────────────────────────────────── + // ── View panel ─────────────────────────────────────────────────── let mut vp = Keymap::new(); - vp.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - vp.bind_cmd(KeyCode::Tab, none, cmd::EnterMode(AppMode::Normal)); - vp.bind_cmd( - KeyCode::Up, - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::View, - delta: -1, - }, - ); - vp.bind_cmd( - KeyCode::Char('k'), - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::View, - delta: -1, - }, - ); - vp.bind_cmd( - KeyCode::Down, - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::View, - delta: 1, - }, - ); - vp.bind_cmd( - KeyCode::Char('j'), - none, - cmd::MovePanelCursor { - panel: crate::ui::effect::Panel::View, - delta: 1, - }, - ); - vp.bind_cmd(KeyCode::Enter, none, cmd::SwitchViewAtCursor); - vp.bind_cmd(KeyCode::Char('n'), none, cmd::CreateAndSwitchView); - vp.bind_cmd(KeyCode::Char('o'), none, cmd::CreateAndSwitchView); - vp.bind_cmd(KeyCode::Char('d'), none, cmd::DeleteViewAtCursor); - vp.bind_cmd(KeyCode::Delete, none, cmd::DeleteViewAtCursor); + vp.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + vp.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]); + for key in [KeyCode::Up, KeyCode::Char('k')] { + vp.bind_args(key, none, "move-panel-cursor", vec!["view".into(), "-1".into()]); + } + for key in [KeyCode::Down, KeyCode::Char('j')] { + vp.bind_args(key, none, "move-panel-cursor", vec!["view".into(), "1".into()]); + } + vp.bind(KeyCode::Enter, none, "switch-view-at-cursor"); + vp.bind(KeyCode::Char('n'), none, "create-and-switch-view"); + vp.bind(KeyCode::Char('o'), none, "create-and-switch-view"); + vp.bind(KeyCode::Char('d'), none, "delete-view-at-cursor"); + vp.bind(KeyCode::Delete, none, "delete-view-at-cursor"); set.insert(ModeKey::ViewPanel, Arc::new(vp)); - // ── Tile select mode ───────────────────────────────────────────── + // ── Tile select ────────────────────────────────────────────────── let mut ts = Keymap::new(); - ts.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - ts.bind_cmd(KeyCode::Tab, none, cmd::EnterMode(AppMode::Normal)); - ts.bind_cmd(KeyCode::Left, none, cmd::MoveTileCursor(-1)); - ts.bind_cmd(KeyCode::Char('h'), none, cmd::MoveTileCursor(-1)); - ts.bind_cmd(KeyCode::Right, none, cmd::MoveTileCursor(1)); - ts.bind_cmd(KeyCode::Char('l'), none, cmd::MoveTileCursor(1)); - ts.bind_cmd(KeyCode::Enter, none, cmd::CycleAxisForTile); - ts.bind_cmd(KeyCode::Char(' '), none, cmd::CycleAxisForTile); - ts.bind_cmd(KeyCode::Char('r'), none, cmd::SetAxisForTile(Axis::Row)); - ts.bind_cmd(KeyCode::Char('c'), none, cmd::SetAxisForTile(Axis::Column)); - ts.bind_cmd(KeyCode::Char('p'), none, cmd::SetAxisForTile(Axis::Page)); - ts.bind_cmd(KeyCode::Char('n'), none, cmd::SetAxisForTile(Axis::None)); + ts.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + ts.bind_args(KeyCode::Tab, none, "enter-mode", vec!["normal".into()]); + ts.bind_args(KeyCode::Left, none, "move-tile-cursor", vec!["-1".into()]); + ts.bind_args(KeyCode::Char('h'), none, "move-tile-cursor", vec!["-1".into()]); + ts.bind_args(KeyCode::Right, none, "move-tile-cursor", vec!["1".into()]); + ts.bind_args(KeyCode::Char('l'), none, "move-tile-cursor", vec!["1".into()]); + ts.bind(KeyCode::Enter, none, "cycle-axis-for-tile"); + ts.bind(KeyCode::Char(' '), none, "cycle-axis-for-tile"); + ts.bind_args(KeyCode::Char('r'), none, "set-axis-for-tile", vec!["row".into()]); + ts.bind_args(KeyCode::Char('c'), none, "set-axis-for-tile", vec!["column".into()]); + ts.bind_args(KeyCode::Char('p'), none, "set-axis-for-tile", vec!["page".into()]); + ts.bind_args(KeyCode::Char('n'), none, "set-axis-for-tile", vec!["none".into()]); set.insert(ModeKey::TileSelect, Arc::new(ts)); // ── Editing mode ───────────────────────────────────────────────── let mut ed = Keymap::new(); - ed.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - ed.bind_cmd(KeyCode::Enter, none, cmd::CommitCellEdit); - ed.bind_cmd( - KeyCode::Backspace, - none, - cmd::PopChar { - buffer: "edit".to_string(), - }, - ); - ed.bind_any_char(cmd::AppendChar { - buffer: "edit".to_string(), - }); + ed.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + ed.bind(KeyCode::Enter, none, "commit-cell-edit"); + ed.bind_args(KeyCode::Backspace, none, "pop-char", vec!["edit".into()]); + ed.bind_any_char("append-char", vec!["edit".into()]); set.insert(ModeKey::Editing, Arc::new(ed)); - // ── Formula edit mode ──────────────────────────────────────────── + // ── Formula edit ───────────────────────────────────────────────── let mut fe = Keymap::new(); - fe.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::FormulaPanel)); - fe.bind_cmd(KeyCode::Enter, none, cmd::CommitFormula); - fe.bind_cmd( - KeyCode::Backspace, - none, - cmd::PopChar { - buffer: "formula".to_string(), - }, - ); - fe.bind_any_char(cmd::AppendChar { - buffer: "formula".to_string(), - }); + fe.bind_args(KeyCode::Esc, none, "enter-mode", vec!["formula-panel".into()]); + fe.bind(KeyCode::Enter, none, "commit-formula"); + fe.bind_args(KeyCode::Backspace, none, "pop-char", vec!["formula".into()]); + fe.bind_any_char("append-char", vec!["formula".into()]); set.insert(ModeKey::FormulaEdit, Arc::new(fe)); - // ── Category add mode ──────────────────────────────────────────── + // ── Category add ───────────────────────────────────────────────── let mut ca = Keymap::new(); - ca.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::CategoryPanel)); - ca.bind_cmd(KeyCode::Enter, none, cmd::CommitCategoryAdd); - ca.bind_cmd(KeyCode::Tab, none, cmd::CommitCategoryAdd); - ca.bind_cmd( - KeyCode::Backspace, - none, - cmd::PopChar { - buffer: "category".to_string(), - }, - ); - ca.bind_any_char(cmd::AppendChar { - buffer: "category".to_string(), - }); + ca.bind_args(KeyCode::Esc, none, "enter-mode", vec!["category-panel".into()]); + ca.bind(KeyCode::Enter, none, "commit-category-add"); + ca.bind(KeyCode::Tab, none, "commit-category-add"); + ca.bind_args(KeyCode::Backspace, none, "pop-char", vec!["category".into()]); + ca.bind_any_char("append-char", vec!["category".into()]); set.insert(ModeKey::CategoryAdd, Arc::new(ca)); - // ── Item add mode ──────────────────────────────────────────────── + // ── Item add ───────────────────────────────────────────────────── let mut ia = Keymap::new(); - ia.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::CategoryPanel)); - ia.bind_cmd(KeyCode::Enter, none, cmd::CommitItemAdd); - ia.bind_cmd(KeyCode::Tab, none, cmd::CommitItemAdd); - ia.bind_cmd( - KeyCode::Backspace, - none, - cmd::PopChar { - buffer: "item".to_string(), - }, - ); - ia.bind_any_char(cmd::AppendChar { - buffer: "item".to_string(), - }); + ia.bind_args(KeyCode::Esc, none, "enter-mode", vec!["category-panel".into()]); + ia.bind(KeyCode::Enter, none, "commit-item-add"); + ia.bind(KeyCode::Tab, none, "commit-item-add"); + ia.bind_args(KeyCode::Backspace, none, "pop-char", vec!["item".into()]); + ia.bind_any_char("append-char", vec!["item".into()]); set.insert(ModeKey::ItemAdd, Arc::new(ia)); - // ── Export prompt mode ─────────────────────────────────────────── + // ── Export prompt ──────────────────────────────────────────────── let mut ep = Keymap::new(); - ep.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - ep.bind_cmd(KeyCode::Enter, none, cmd::CommitExport); - ep.bind_cmd( - KeyCode::Backspace, - none, - cmd::PopChar { - buffer: "export".to_string(), - }, - ); - ep.bind_any_char(cmd::AppendChar { - buffer: "export".to_string(), - }); + ep.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + ep.bind(KeyCode::Enter, none, "commit-export"); + ep.bind_args(KeyCode::Backspace, none, "pop-char", vec!["export".into()]); + ep.bind_any_char("append-char", vec!["export".into()]); set.insert(ModeKey::ExportPrompt, Arc::new(ep)); // ── Command mode ───────────────────────────────────────────────── let mut cm = Keymap::new(); - cm.bind_cmd(KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal)); - cm.bind_cmd(KeyCode::Enter, none, cmd::ExecuteCommand); - cm.bind_cmd(KeyCode::Backspace, none, cmd::CommandModeBackspace); - cm.bind_any_char(cmd::AppendChar { - buffer: "command".to_string(), - }); + cm.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); + cm.bind(KeyCode::Enter, none, "execute-command"); + cm.bind(KeyCode::Backspace, none, "command-mode-backspace"); + cm.bind_any_char("append-char", vec!["command".into()]); set.insert(ModeKey::CommandMode, Arc::new(cm)); // ── Search mode ────────────────────────────────────────────────── let mut sm = Keymap::new(); - sm.bind_cmd(KeyCode::Esc, none, cmd::ExitSearchMode); - sm.bind_cmd(KeyCode::Enter, none, cmd::ExitSearchMode); - sm.bind_cmd(KeyCode::Backspace, none, cmd::SearchPopChar); - sm.bind_any_char(cmd::SearchAppendChar); + sm.bind(KeyCode::Esc, none, "exit-search-mode"); + sm.bind(KeyCode::Enter, none, "exit-search-mode"); + sm.bind(KeyCode::Backspace, none, "search-pop-char"); + sm.bind_any_char("search-append-char", vec![]); set.insert(ModeKey::SearchMode, Arc::new(sm)); - // ── Import wizard mode ──────────────────────────────────────────── + // ── Import wizard ──────────────────────────────────────────────── let mut wiz = Keymap::new(); - // All keys are dispatched to the wizard effect, which handles - // step-specific behavior internally. - wiz.bindings - .insert(KeyPattern::Any, Arc::new(cmd::HandleWizardKey)); + wiz.bind_any("handle-wizard-key"); set.insert(ModeKey::ImportWizard, Arc::new(wiz)); set diff --git a/src/ui/app.rs b/src/ui/app.rs index 81df986..fb2ae5d 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -141,7 +141,8 @@ impl App { if let Some(transient) = self.transient_keymap.take() { let effects = { let ctx = self.cmd_context(key.code, key.modifiers); - transient.dispatch(&ctx, key.code, key.modifiers) + self.keymap_set + .dispatch_transient(&transient, &ctx, key.code, key.modifiers) }; if let Some(effects) = effects { self.apply_effects(effects);