overhaul keymap API and add Debug
- Replaced ModeKey with direct KeyPattern keys. - Stored bindings as Arc<dyn Cmd> for cheap sharing. - Added Debug implementation for Keymap. - Updated bind, bind_cmd, bind_prefix, lookup, and dispatch signatures. - Introduced PrefixKey command and SetTransientKeymap effect. - Added KeymapSet for mode-specific keymaps. Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
This commit is contained in:
@ -1,4 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
@ -33,8 +35,22 @@ impl ModeKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// A keymap maps key patterns to commands.
|
||||
///
|
||||
/// Keymaps are stored as `Arc<Keymap>` 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).
|
||||
pub struct Keymap {
|
||||
bindings: HashMap<(ModeKey, KeyPattern), Box<dyn Cmd>>,
|
||||
bindings: HashMap<KeyPattern, Arc<dyn Cmd>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Keymap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Keymap")
|
||||
.field("binding_count", &self.bindings.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
@ -44,20 +60,23 @@ impl Keymap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind(&mut self, mode: ModeKey, key: KeyCode, mods: KeyModifiers, cmd: impl Cmd + 'static) {
|
||||
self.bindings
|
||||
.insert((mode, KeyPattern::Key(key, mods)), Box::new(cmd));
|
||||
pub fn bind(&mut self, key: KeyCode, mods: KeyModifiers, cmd: Arc<dyn Cmd>) {
|
||||
self.bindings.insert(KeyPattern::Key(key, mods), cmd);
|
||||
}
|
||||
|
||||
pub fn lookup(
|
||||
&self,
|
||||
mode: &AppMode,
|
||||
key: KeyCode,
|
||||
mods: KeyModifiers,
|
||||
) -> Option<&dyn Cmd> {
|
||||
let mode_key = ModeKey::from_app_mode(mode)?;
|
||||
/// 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 prefix key that activates a sub-keymap.
|
||||
pub fn bind_prefix(&mut self, key: KeyCode, mods: KeyModifiers, sub: Arc<Keymap>) {
|
||||
self.bind(key, mods, Arc::new(PrefixKey(sub)));
|
||||
}
|
||||
|
||||
pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&dyn Cmd> {
|
||||
self.bindings
|
||||
.get(&(mode_key, KeyPattern::Key(key, mods)))
|
||||
.get(&KeyPattern::Key(key, mods))
|
||||
.map(|c| c.as_ref())
|
||||
}
|
||||
|
||||
@ -68,59 +87,222 @@ impl Keymap {
|
||||
key: KeyCode,
|
||||
mods: KeyModifiers,
|
||||
) -> Option<Vec<Box<dyn Effect>>> {
|
||||
let cmd = self.lookup(ctx.mode, key, mods)?;
|
||||
let cmd = self.lookup(key, mods)?;
|
||||
Some(cmd.execute(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the default keymap with all vim-style bindings.
|
||||
pub fn default_keymap() -> Self {
|
||||
let mut km = Self::new();
|
||||
let none = KeyModifiers::NONE;
|
||||
let ctrl = KeyModifiers::CONTROL;
|
||||
/// A prefix key command that activates a sub-keymap as transient.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrefixKey(pub Arc<Keymap>);
|
||||
|
||||
// ── Normal mode: Navigation ──────────────────────────────────────
|
||||
km.bind(ModeKey::Normal, KeyCode::Up, none, cmd::MoveSelection { dr: -1, dc: 0 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Down, none, cmd::MoveSelection { dr: 1, dc: 0 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Left, none, cmd::MoveSelection { dr: 0, dc: -1 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Right, none, cmd::MoveSelection { dr: 0, dc: 1 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('k'), none, cmd::MoveSelection { dr: -1, dc: 0 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('j'), none, cmd::MoveSelection { dr: 1, dc: 0 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('h'), none, cmd::MoveSelection { dr: 0, dc: -1 });
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('l'), none, cmd::MoveSelection { dr: 0, dc: 1 });
|
||||
impl Cmd for PrefixKey {
|
||||
fn name(&self) -> &str {
|
||||
"prefix-key"
|
||||
}
|
||||
|
||||
// Jump to boundaries
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('G'), KeyModifiers::SHIFT, cmd::JumpToLastRow);
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('0'), none, cmd::JumpToFirstCol);
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('$'), KeyModifiers::SHIFT, cmd::JumpToLastCol);
|
||||
|
||||
// Scroll
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('d'), ctrl, cmd::ScrollRows(5));
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('u'), ctrl, cmd::ScrollRows(-5));
|
||||
|
||||
// ── Normal mode: Cell operations ─────────────────────────────────
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('x'), none, cmd::ClearSelectedCell);
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('p'), none, cmd::PasteCell);
|
||||
|
||||
// ── Normal mode: View ────────────────────────────────────────────
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('t'), none, cmd::TransposeAxes);
|
||||
|
||||
// ── Normal mode: Mode changes ────────────────────────────────────
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('q'), ctrl, cmd::ForceQuit);
|
||||
km.bind(ModeKey::Normal, KeyCode::Char(':'), KeyModifiers::SHIFT, cmd::EnterMode(AppMode::CommandMode { buffer: String::new() }));
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('/'), none, cmd::EnterSearchMode);
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('s'), ctrl, cmd::SaveCmd);
|
||||
km.bind(ModeKey::Normal, KeyCode::F(1), none, cmd::EnterMode(AppMode::Help));
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('?'), KeyModifiers::SHIFT, cmd::EnterMode(AppMode::Help));
|
||||
|
||||
// Two-key sequence starters
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('g'), none, cmd::SetPendingKey('g'));
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('y'), none, cmd::SetPendingKey('y'));
|
||||
km.bind(ModeKey::Normal, KeyCode::Char('Z'), KeyModifiers::SHIFT, cmd::SetPendingKey('Z'));
|
||||
|
||||
// ── Help mode ────────────────────────────────────────────────────
|
||||
km.bind(ModeKey::Help, KeyCode::Esc, none, cmd::EnterMode(AppMode::Normal));
|
||||
km.bind(ModeKey::Help, KeyCode::Char('q'), none, cmd::EnterMode(AppMode::Normal));
|
||||
|
||||
km
|
||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
vec![Box::new(SetTransientKeymap(self.0.clone()))]
|
||||
}
|
||||
}
|
||||
|
||||
/// Effect that sets the transient keymap on the App.
|
||||
#[derive(Debug)]
|
||||
pub struct SetTransientKeymap(pub Arc<Keymap>);
|
||||
|
||||
impl Effect for SetTransientKeymap {
|
||||
fn apply(&self, app: &mut crate::ui::app::App) {
|
||||
app.transient_keymap = Some(self.0.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps modes to their root keymaps.
|
||||
pub struct KeymapSet {
|
||||
mode_maps: HashMap<ModeKey, Arc<Keymap>>,
|
||||
}
|
||||
|
||||
impl KeymapSet {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
mode_maps: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, mode: ModeKey, keymap: Arc<Keymap>) {
|
||||
self.mode_maps.insert(mode, keymap);
|
||||
}
|
||||
|
||||
/// Look up the root keymap for a given app mode.
|
||||
pub fn get(&self, mode: &AppMode) -> Option<&Arc<Keymap>> {
|
||||
let mode_key = ModeKey::from_app_mode(mode)?;
|
||||
self.mode_maps.get(&mode_key)
|
||||
}
|
||||
|
||||
/// Dispatch a key event: returns effects if a binding matched.
|
||||
pub fn dispatch(
|
||||
&self,
|
||||
ctx: &CmdContext,
|
||||
key: KeyCode,
|
||||
mods: KeyModifiers,
|
||||
) -> Option<Vec<Box<dyn Effect>>> {
|
||||
let keymap = self.get(ctx.mode)?;
|
||||
keymap.dispatch(ctx, key, mods)
|
||||
}
|
||||
|
||||
/// Build the default keymap set with all vim-style bindings.
|
||||
pub fn default_keymaps() -> Self {
|
||||
let mut set = Self::new();
|
||||
|
||||
// ── Normal mode ──────────────────────────────────────────────────
|
||||
let mut normal = Keymap::new();
|
||||
let none = KeyModifiers::NONE;
|
||||
let ctrl = KeyModifiers::CONTROL;
|
||||
let shift = KeyModifiers::SHIFT;
|
||||
|
||||
// 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 },
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// Scroll
|
||||
normal.bind_cmd(KeyCode::Char('d'), ctrl, cmd::ScrollRows(5));
|
||||
normal.bind_cmd(KeyCode::Char('u'), ctrl, cmd::ScrollRows(-5));
|
||||
|
||||
// Cell operations
|
||||
normal.bind_cmd(KeyCode::Char('x'), none, cmd::ClearSelectedCell);
|
||||
normal.bind_cmd(KeyCode::Char('p'), none, cmd::PasteCell);
|
||||
|
||||
// View
|
||||
normal.bind_cmd(KeyCode::Char('t'), none, cmd::TransposeAxes);
|
||||
|
||||
// Mode changes
|
||||
normal.bind_cmd(KeyCode::Char('q'), ctrl, cmd::ForceQuit);
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char(':'),
|
||||
shift,
|
||||
cmd::EnterMode(AppMode::CommandMode {
|
||||
buffer: String::new(),
|
||||
}),
|
||||
);
|
||||
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));
|
||||
|
||||
// Panel toggles (uppercase = toggle + focus)
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char('F'),
|
||||
shift,
|
||||
cmd::TogglePanelAndFocus(crate::ui::effect::Panel::Formula),
|
||||
);
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char('C'),
|
||||
shift,
|
||||
cmd::TogglePanelAndFocus(crate::ui::effect::Panel::Category),
|
||||
);
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char('V'),
|
||||
shift,
|
||||
cmd::TogglePanelAndFocus(crate::ui::effect::Panel::View),
|
||||
);
|
||||
|
||||
// Legacy Ctrl+ panel toggles (visibility only)
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char('f'),
|
||||
ctrl,
|
||||
cmd::TogglePanelVisibility(crate::ui::effect::Panel::Formula),
|
||||
);
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char('c'),
|
||||
ctrl,
|
||||
cmd::TogglePanelVisibility(crate::ui::effect::Panel::Category),
|
||||
);
|
||||
normal.bind_cmd(
|
||||
KeyCode::Char('v'),
|
||||
ctrl,
|
||||
cmd::TogglePanelVisibility(crate::ui::effect::Panel::View),
|
||||
);
|
||||
|
||||
// Tab cycles open panels
|
||||
normal.bind_cmd(KeyCode::Tab, none, cmd::CyclePanelFocus);
|
||||
|
||||
// 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);
|
||||
|
||||
// Search / category add
|
||||
normal.bind_cmd(KeyCode::Char('n'), none, cmd::SearchNavigate(true));
|
||||
normal.bind_cmd(KeyCode::Char('N'), shift, cmd::SearchOrCategoryAdd);
|
||||
|
||||
// Page navigation
|
||||
normal.bind_cmd(KeyCode::Char(']'), none, cmd::PageNext);
|
||||
normal.bind_cmd(KeyCode::Char('['), none, cmd::PagePrev);
|
||||
|
||||
// Group / hide
|
||||
normal.bind_cmd(KeyCode::Char('z'), none, cmd::ToggleGroupUnderCursor);
|
||||
normal.bind_cmd(KeyCode::Char('H'), shift, cmd::HideSelectedRowItem);
|
||||
|
||||
// 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);
|
||||
|
||||
// ── Prefix keys (Emacs-style sub-keymaps) ────────────────────────
|
||||
|
||||
// g-prefix
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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));
|
||||
set.insert(ModeKey::Help, Arc::new(help));
|
||||
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user