refactor(command): update keymaps for command mode and help navigation
Update keymaps to allow entering command mode via ':' in various panels and add help page navigation. - Added ':' command binding to Help, FormulaPanel, CategoryPanel, ViewPanel, and TileSelect modes. - Added navigation bindings (Right/Left, l/h, n/p, Tab/BackTab) to the Help keymap. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
@ -479,6 +479,22 @@ impl KeymapSet {
|
|||||||
"enter-mode",
|
"enter-mode",
|
||||||
vec!["normal".into()],
|
vec!["normal".into()],
|
||||||
);
|
);
|
||||||
|
// Allow entering command mode from Help (important on empty-model launch)
|
||||||
|
help.bind_args(
|
||||||
|
KeyCode::Char(':'),
|
||||||
|
none,
|
||||||
|
"enter-mode",
|
||||||
|
vec!["command".into()],
|
||||||
|
);
|
||||||
|
// Page navigation
|
||||||
|
help.bind(KeyCode::Right, none, "help-page-next");
|
||||||
|
help.bind(KeyCode::Char('l'), none, "help-page-next");
|
||||||
|
help.bind(KeyCode::Char('n'), none, "help-page-next");
|
||||||
|
help.bind(KeyCode::Tab, none, "help-page-next");
|
||||||
|
help.bind(KeyCode::Left, none, "help-page-prev");
|
||||||
|
help.bind(KeyCode::Char('h'), none, "help-page-prev");
|
||||||
|
help.bind(KeyCode::Char('p'), none, "help-page-prev");
|
||||||
|
help.bind(KeyCode::BackTab, none, "help-page-prev");
|
||||||
set.insert(ModeKey::Help, Arc::new(help));
|
set.insert(ModeKey::Help, Arc::new(help));
|
||||||
|
|
||||||
// ── Formula panel ────────────────────────────────────────────────
|
// ── Formula panel ────────────────────────────────────────────────
|
||||||
@ -524,6 +540,12 @@ impl KeymapSet {
|
|||||||
"toggle-panel-and-focus",
|
"toggle-panel-and-focus",
|
||||||
vec!["view".into()],
|
vec!["view".into()],
|
||||||
);
|
);
|
||||||
|
fp.bind_args(
|
||||||
|
KeyCode::Char(':'),
|
||||||
|
none,
|
||||||
|
"enter-mode",
|
||||||
|
vec!["command".into()],
|
||||||
|
);
|
||||||
set.insert(ModeKey::FormulaPanel, Arc::new(fp));
|
set.insert(ModeKey::FormulaPanel, Arc::new(fp));
|
||||||
|
|
||||||
// ── Category panel ───────────────────────────────────────────────
|
// ── Category panel ───────────────────────────────────────────────
|
||||||
@ -577,6 +599,12 @@ impl KeymapSet {
|
|||||||
"toggle-panel-and-focus",
|
"toggle-panel-and-focus",
|
||||||
vec!["view".into()],
|
vec!["view".into()],
|
||||||
);
|
);
|
||||||
|
cp.bind_args(
|
||||||
|
KeyCode::Char(':'),
|
||||||
|
none,
|
||||||
|
"enter-mode",
|
||||||
|
vec!["command".into()],
|
||||||
|
);
|
||||||
set.insert(ModeKey::CategoryPanel, Arc::new(cp));
|
set.insert(ModeKey::CategoryPanel, Arc::new(cp));
|
||||||
|
|
||||||
// ── View panel ───────────────────────────────────────────────────
|
// ── View panel ───────────────────────────────────────────────────
|
||||||
@ -622,6 +650,12 @@ impl KeymapSet {
|
|||||||
"toggle-panel-and-focus",
|
"toggle-panel-and-focus",
|
||||||
vec!["formula".into()],
|
vec!["formula".into()],
|
||||||
);
|
);
|
||||||
|
vp.bind_args(
|
||||||
|
KeyCode::Char(':'),
|
||||||
|
none,
|
||||||
|
"enter-mode",
|
||||||
|
vec!["command".into()],
|
||||||
|
);
|
||||||
set.insert(ModeKey::ViewPanel, Arc::new(vp));
|
set.insert(ModeKey::ViewPanel, Arc::new(vp));
|
||||||
|
|
||||||
// ── Tile select ──────────────────────────────────────────────────
|
// ── Tile select ──────────────────────────────────────────────────
|
||||||
@ -668,6 +702,12 @@ impl KeymapSet {
|
|||||||
"set-axis-for-tile",
|
"set-axis-for-tile",
|
||||||
vec!["none".into()],
|
vec!["none".into()],
|
||||||
);
|
);
|
||||||
|
ts.bind_args(
|
||||||
|
KeyCode::Char(':'),
|
||||||
|
none,
|
||||||
|
"enter-mode",
|
||||||
|
vec!["command".into()],
|
||||||
|
);
|
||||||
set.insert(ModeKey::TileSelect, Arc::new(ts));
|
set.insert(ModeKey::TileSelect, Arc::new(ts));
|
||||||
|
|
||||||
// ── Editing mode ─────────────────────────────────────────────────
|
// ── Editing mode ─────────────────────────────────────────────────
|
||||||
@ -757,3 +797,272 @@ impl KeymapSet {
|
|||||||
set
|
set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// ── Keymap::lookup fallback chain ──────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_exact_match() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind(KeyCode::Char('a'), KeyModifiers::NONE, "cmd-a");
|
||||||
|
let b = km.lookup(KeyCode::Char('a'), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(b, Some(Binding::Cmd { name: "cmd-a", .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_exact_with_ctrl() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind(KeyCode::Char('s'), KeyModifiers::CONTROL, "save");
|
||||||
|
let b = km.lookup(KeyCode::Char('s'), KeyModifiers::CONTROL);
|
||||||
|
assert!(matches!(b, Some(Binding::Cmd { name: "save", .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_char_falls_back_to_none_mods() {
|
||||||
|
// Bind 'A' with NONE. Lookup with SHIFT should still match because
|
||||||
|
// terminals vary in whether they send SHIFT for uppercase chars.
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind(KeyCode::Char('A'), KeyModifiers::NONE, "cmd-A");
|
||||||
|
let b = km.lookup(KeyCode::Char('A'), KeyModifiers::SHIFT);
|
||||||
|
assert!(matches!(b, Some(Binding::Cmd { name: "cmd-A", .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_non_char_does_not_fall_back_to_none_mods() {
|
||||||
|
// Arrow keys with CTRL should NOT fall back to NONE when no match
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind(KeyCode::Up, KeyModifiers::NONE, "move-up");
|
||||||
|
let b = km.lookup(KeyCode::Up, KeyModifiers::CONTROL);
|
||||||
|
// Should NOT match — no AnyChar or Any fallback registered
|
||||||
|
assert!(b.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_char_falls_to_any_char() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind_any_char("append-char", vec![]);
|
||||||
|
let b = km.lookup(KeyCode::Char('z'), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(
|
||||||
|
b,
|
||||||
|
Some(Binding::Cmd {
|
||||||
|
name: "append-char",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_non_char_skips_any_char() {
|
||||||
|
// AnyChar should only match Char keys, not Enter/Esc/arrows
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind_any_char("append-char", vec![]);
|
||||||
|
let b = km.lookup(KeyCode::Enter, KeyModifiers::NONE);
|
||||||
|
assert!(b.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_any_matches_everything() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind_any("catchall");
|
||||||
|
let b = km.lookup(KeyCode::F(12), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(
|
||||||
|
b,
|
||||||
|
Some(Binding::Cmd {
|
||||||
|
name: "catchall",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_exact_takes_priority_over_any_char() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind(KeyCode::Char('n'), KeyModifiers::NONE, "specific");
|
||||||
|
km.bind_any_char("generic", vec![]);
|
||||||
|
let b = km.lookup(KeyCode::Char('n'), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(
|
||||||
|
b,
|
||||||
|
Some(Binding::Cmd {
|
||||||
|
name: "specific",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_any_char_takes_priority_over_any() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind_any_char("char-catch", vec![]);
|
||||||
|
km.bind_any("total-catch");
|
||||||
|
let b = km.lookup(KeyCode::Char('x'), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(
|
||||||
|
b,
|
||||||
|
Some(Binding::Cmd {
|
||||||
|
name: "char-catch",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_non_char_falls_to_any_not_any_char() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind_any_char("char-catch", vec![]);
|
||||||
|
km.bind_any("total-catch");
|
||||||
|
let b = km.lookup(KeyCode::Esc, KeyModifiers::NONE);
|
||||||
|
assert!(matches!(
|
||||||
|
b,
|
||||||
|
Some(Binding::Cmd {
|
||||||
|
name: "total-catch",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_ctrl_char_with_only_none_binding_falls_through() {
|
||||||
|
// Ctrl+S bound; plain 's' should NOT match Ctrl+S
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind(KeyCode::Char('s'), KeyModifiers::CONTROL, "save");
|
||||||
|
let b = km.lookup(KeyCode::Char('s'), KeyModifiers::NONE);
|
||||||
|
assert!(b.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ModeKey::from_app_mode ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_key_normal_no_search() {
|
||||||
|
let mk = ModeKey::from_app_mode(&AppMode::Normal, false);
|
||||||
|
assert_eq!(mk, Some(ModeKey::Normal));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_key_normal_with_search_overrides() {
|
||||||
|
let mk = ModeKey::from_app_mode(&AppMode::Normal, true);
|
||||||
|
assert_eq!(mk, Some(ModeKey::SearchMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_key_help() {
|
||||||
|
let mk = ModeKey::from_app_mode(&AppMode::Help, false);
|
||||||
|
assert_eq!(mk, Some(ModeKey::Help));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_key_quit_returns_none() {
|
||||||
|
let mk = ModeKey::from_app_mode(&AppMode::Quit, false);
|
||||||
|
assert_eq!(mk, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Prefix bindings ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_returns_prefix_binding() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
let sub = Arc::new(Keymap::new());
|
||||||
|
km.bind_prefix(KeyCode::Char('g'), KeyModifiers::NONE, sub);
|
||||||
|
let b = km.lookup(KeyCode::Char('g'), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(b, Some(Binding::Prefix(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lookup_returns_sequence_binding() {
|
||||||
|
let mut km = Keymap::new();
|
||||||
|
km.bind_seq(
|
||||||
|
KeyCode::Char('o'),
|
||||||
|
KeyModifiers::NONE,
|
||||||
|
vec![("step1", vec![]), ("step2", vec![])],
|
||||||
|
);
|
||||||
|
let b = km.lookup(KeyCode::Char('o'), KeyModifiers::NONE);
|
||||||
|
assert!(matches!(b, Some(Binding::Sequence(steps)) if steps.len() == 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── default_keymaps smoke tests ────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_keymaps_has_all_modes() {
|
||||||
|
let ks = KeymapSet::default_keymaps();
|
||||||
|
let expected_modes = vec![
|
||||||
|
ModeKey::Normal,
|
||||||
|
ModeKey::Help,
|
||||||
|
ModeKey::FormulaPanel,
|
||||||
|
ModeKey::CategoryPanel,
|
||||||
|
ModeKey::ViewPanel,
|
||||||
|
ModeKey::TileSelect,
|
||||||
|
ModeKey::Editing,
|
||||||
|
ModeKey::FormulaEdit,
|
||||||
|
ModeKey::CategoryAdd,
|
||||||
|
ModeKey::ItemAdd,
|
||||||
|
ModeKey::ExportPrompt,
|
||||||
|
ModeKey::CommandMode,
|
||||||
|
ModeKey::SearchMode,
|
||||||
|
ModeKey::ImportWizard,
|
||||||
|
];
|
||||||
|
for mode in &expected_modes {
|
||||||
|
assert!(
|
||||||
|
ks.mode_maps.contains_key(mode),
|
||||||
|
"Missing keymap for mode {:?}",
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_mode_has_basic_movement() {
|
||||||
|
let ks = KeymapSet::default_keymaps();
|
||||||
|
let normal = ks.mode_maps.get(&ModeKey::Normal).unwrap();
|
||||||
|
// hjkl should be bound
|
||||||
|
for key in ['h', 'j', 'k', 'l'] {
|
||||||
|
assert!(
|
||||||
|
normal.lookup(KeyCode::Char(key), KeyModifiers::NONE).is_some(),
|
||||||
|
"Normal mode missing binding for '{}'",
|
||||||
|
key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Arrow keys
|
||||||
|
for key in [KeyCode::Up, KeyCode::Down, KeyCode::Left, KeyCode::Right] {
|
||||||
|
assert!(
|
||||||
|
normal.lookup(key, KeyModifiers::NONE).is_some(),
|
||||||
|
"Normal mode missing binding for {:?}",
|
||||||
|
key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn editing_mode_has_any_char_and_esc() {
|
||||||
|
let ks = KeymapSet::default_keymaps();
|
||||||
|
let editing = ks.mode_maps.get(&ModeKey::Editing).unwrap();
|
||||||
|
// Should have AnyChar for text input
|
||||||
|
assert!(editing
|
||||||
|
.lookup(KeyCode::Char('z'), KeyModifiers::NONE)
|
||||||
|
.is_some());
|
||||||
|
// Should have Esc to exit
|
||||||
|
assert!(editing
|
||||||
|
.lookup(KeyCode::Esc, KeyModifiers::NONE)
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_mode_has_any_char_and_esc() {
|
||||||
|
let ks = KeymapSet::default_keymaps();
|
||||||
|
let search = ks.mode_maps.get(&ModeKey::SearchMode).unwrap();
|
||||||
|
assert!(search
|
||||||
|
.lookup(KeyCode::Char('a'), KeyModifiers::NONE)
|
||||||
|
.is_some());
|
||||||
|
assert!(search
|
||||||
|
.lookup(KeyCode::Esc, KeyModifiers::NONE)
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_wizard_has_any_catchall() {
|
||||||
|
let ks = KeymapSet::default_keymaps();
|
||||||
|
let wiz = ks.mode_maps.get(&ModeKey::ImportWizard).unwrap();
|
||||||
|
// Should catch any key at all
|
||||||
|
assert!(wiz.lookup(KeyCode::F(5), KeyModifiers::NONE).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user