Introduced new commands: JumpToEdge (first/last row/col), PageScroll, and OpenRecordRow. Updated command registry to use these commands and unified key handling. Added format module for formatting functions. Updated main.rs to include format module. Updated keymap to bind new commands and page scroll. Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
650 lines
24 KiB
Rust
650 lines
24 KiB
Rust
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::sync::Arc;
|
|
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
|
|
use crate::ui::app::AppMode;
|
|
use crate::ui::effect::Effect;
|
|
|
|
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).
|
|
AnyChar,
|
|
/// Matches any key at all (lowest priority fallback).
|
|
Any,
|
|
}
|
|
|
|
/// Identifies which mode a binding applies to.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub enum ModeKey {
|
|
Normal,
|
|
Help,
|
|
FormulaPanel,
|
|
CategoryPanel,
|
|
ViewPanel,
|
|
TileSelect,
|
|
Editing,
|
|
FormulaEdit,
|
|
CategoryAdd,
|
|
ItemAdd,
|
|
ExportPrompt,
|
|
CommandMode,
|
|
SearchMode,
|
|
ImportWizard,
|
|
}
|
|
|
|
impl ModeKey {
|
|
pub fn from_app_mode(mode: &AppMode, search_mode: bool) -> Option<Self> {
|
|
match mode {
|
|
AppMode::Normal if search_mode => Some(ModeKey::SearchMode),
|
|
AppMode::Normal => Some(ModeKey::Normal),
|
|
AppMode::Help => Some(ModeKey::Help),
|
|
AppMode::FormulaPanel => Some(ModeKey::FormulaPanel),
|
|
AppMode::CategoryPanel => Some(ModeKey::CategoryPanel),
|
|
AppMode::ViewPanel => Some(ModeKey::ViewPanel),
|
|
AppMode::TileSelect => Some(ModeKey::TileSelect),
|
|
AppMode::Editing { .. } => Some(ModeKey::Editing),
|
|
AppMode::FormulaEdit { .. } => Some(ModeKey::FormulaEdit),
|
|
AppMode::CategoryAdd { .. } => Some(ModeKey::CategoryAdd),
|
|
AppMode::ItemAdd { .. } => Some(ModeKey::ItemAdd),
|
|
AppMode::ExportPrompt { .. } => Some(ModeKey::ExportPrompt),
|
|
AppMode::CommandMode { .. } => Some(ModeKey::CommandMode),
|
|
AppMode::ImportWizard => Some(ModeKey::ImportWizard),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<String>,
|
|
},
|
|
/// A prefix sub-keymap (Emacs-style).
|
|
Prefix(Arc<Keymap>),
|
|
}
|
|
|
|
/// A keymap maps key patterns to bindings (command names or prefix sub-keymaps).
|
|
#[derive(Default)]
|
|
pub struct Keymap {
|
|
bindings: HashMap<KeyPattern, Binding>,
|
|
}
|
|
|
|
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 {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
bindings: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// 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![] },
|
|
);
|
|
}
|
|
|
|
/// Bind a key to a command name with arguments.
|
|
pub fn bind_args(
|
|
&mut self,
|
|
key: KeyCode,
|
|
mods: KeyModifiers,
|
|
name: &'static str,
|
|
args: Vec<String>,
|
|
) {
|
|
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<Keymap>) {
|
|
self.bindings
|
|
.insert(KeyPattern::Key(key, mods), Binding::Prefix(sub));
|
|
}
|
|
|
|
/// Bind a catch-all for any Char key.
|
|
pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) {
|
|
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.
|
|
/// For Char keys, if exact (key, mods) match fails, retries with NONE
|
|
/// modifiers since terminals vary in whether they send SHIFT for
|
|
/// uppercase/symbol characters.
|
|
pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> {
|
|
self.bindings
|
|
.get(&KeyPattern::Key(key, mods))
|
|
.or_else(|| {
|
|
// Retry Char keys without modifiers (shift is implicit in the char)
|
|
if matches!(key, KeyCode::Char(_)) && mods != KeyModifiers::NONE {
|
|
self.bindings
|
|
.get(&KeyPattern::Key(key, KeyModifiers::NONE))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.or_else(|| {
|
|
if matches!(key, KeyCode::Char(_)) {
|
|
self.bindings.get(&KeyPattern::AnyChar)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.or_else(|| self.bindings.get(&KeyPattern::Any))
|
|
}
|
|
|
|
/// Dispatch a key: look up binding, resolve through registry, return effects.
|
|
pub fn dispatch(
|
|
&self,
|
|
registry: &CmdRegistry,
|
|
ctx: &CmdContext,
|
|
key: KeyCode,
|
|
mods: KeyModifiers,
|
|
) -> Option<Vec<Box<dyn Effect>>> {
|
|
let binding = self.lookup(key, mods)?;
|
|
match binding {
|
|
Binding::Cmd { name, args } => {
|
|
let cmd = registry.interactive(name, args, ctx).ok()?;
|
|
Some(cmd.execute(ctx))
|
|
}
|
|
Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.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 + owns the command registry.
|
|
pub struct KeymapSet {
|
|
mode_maps: HashMap<ModeKey, Arc<Keymap>>,
|
|
registry: CmdRegistry,
|
|
}
|
|
|
|
impl KeymapSet {
|
|
pub fn new(registry: CmdRegistry) -> Self {
|
|
Self {
|
|
mode_maps: HashMap::new(),
|
|
registry,
|
|
}
|
|
}
|
|
|
|
pub fn insert(&mut self, mode: ModeKey, keymap: Arc<Keymap>) {
|
|
self.mode_maps.insert(mode, keymap);
|
|
}
|
|
|
|
/// 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 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)
|
|
}
|
|
|
|
/// Dispatch against a specific keymap (for transient/prefix keymaps).
|
|
pub fn dispatch_transient(
|
|
&self,
|
|
keymap: &Keymap,
|
|
ctx: &CmdContext,
|
|
key: KeyCode,
|
|
mods: KeyModifiers,
|
|
) -> Option<Vec<Box<dyn Effect>>> {
|
|
keymap.dispatch(&self.registry, ctx, key, mods)
|
|
}
|
|
|
|
/// 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;
|
|
|
|
// ── Normal mode ──────────────────────────────────────────────────
|
|
let mut normal = Keymap::new();
|
|
|
|
// Navigation
|
|
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(KeyCode::Char('G'), none, "jump-last-row");
|
|
normal.bind(KeyCode::Char('0'), none, "jump-first-col");
|
|
normal.bind(KeyCode::Char('$'), none, "jump-last-col");
|
|
normal.bind(KeyCode::Home, none, "jump-first-col");
|
|
normal.bind(KeyCode::End, none, "jump-last-col");
|
|
|
|
// Scroll
|
|
normal.bind_args(KeyCode::Char('d'), ctrl, "scroll-rows", vec!["5".into()]);
|
|
normal.bind_args(KeyCode::Char('u'), ctrl, "scroll-rows", vec!["-5".into()]);
|
|
normal.bind_args(KeyCode::PageDown, none, "page-scroll", vec!["1".into()]);
|
|
normal.bind_args(KeyCode::PageUp, none, "page-scroll", vec!["-1".into()]);
|
|
|
|
// Cell operations
|
|
normal.bind(KeyCode::Char('x'), none, "clear-cell");
|
|
normal.bind(KeyCode::Char('p'), none, "paste");
|
|
|
|
// View
|
|
normal.bind(KeyCode::Char('t'), none, "transpose");
|
|
|
|
// Mode changes
|
|
normal.bind(KeyCode::Char('q'), ctrl, "force-quit");
|
|
normal.bind_args(
|
|
KeyCode::Char(':'),
|
|
none,
|
|
"enter-mode",
|
|
vec!["command".into()],
|
|
);
|
|
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('?'), none, "enter-mode", vec!["help".into()]);
|
|
|
|
// Panel toggles
|
|
normal.bind_args(
|
|
KeyCode::Char('F'),
|
|
none,
|
|
"toggle-panel-and-focus",
|
|
vec!["formula".into()],
|
|
);
|
|
normal.bind_args(
|
|
KeyCode::Char('C'),
|
|
none,
|
|
"toggle-panel-and-focus",
|
|
vec!["category".into()],
|
|
);
|
|
normal.bind_args(
|
|
KeyCode::Char('V'),
|
|
none,
|
|
"toggle-panel-and-focus",
|
|
vec!["view".into()],
|
|
);
|
|
normal.bind_args(
|
|
KeyCode::Char('f'),
|
|
ctrl,
|
|
"toggle-panel-visibility",
|
|
vec!["formula".into()],
|
|
);
|
|
normal.bind_args(
|
|
KeyCode::Char('c'),
|
|
ctrl,
|
|
"toggle-panel-visibility",
|
|
vec!["category".into()],
|
|
);
|
|
normal.bind_args(
|
|
KeyCode::Char('v'),
|
|
ctrl,
|
|
"toggle-panel-visibility",
|
|
vec!["view".into()],
|
|
);
|
|
normal.bind(KeyCode::Tab, none, "cycle-panel-focus");
|
|
|
|
// Editing entry — i/a drill into aggregated cells, else edit
|
|
normal.bind(KeyCode::Char('i'), none, "edit-or-drill");
|
|
normal.bind(KeyCode::Char('a'), none, "edit-or-drill");
|
|
normal.bind(KeyCode::Enter, none, "enter-advance");
|
|
normal.bind(KeyCode::Char('e'), ctrl, "enter-export-prompt");
|
|
|
|
// Search / category add
|
|
normal.bind_args(
|
|
KeyCode::Char('n'),
|
|
none,
|
|
"search-navigate",
|
|
vec!["forward".into()],
|
|
);
|
|
normal.bind(KeyCode::Char('N'), none, "search-or-category-add");
|
|
|
|
// Page navigation
|
|
normal.bind(KeyCode::Char(']'), none, "page-next");
|
|
normal.bind(KeyCode::Char('['), none, "page-prev");
|
|
|
|
// Group / hide
|
|
normal.bind(KeyCode::Char('z'), none, "toggle-group-under-cursor");
|
|
normal.bind(KeyCode::Char('H'), none, "hide-selected-row-item");
|
|
|
|
// Drill into aggregated cell / view history / add row
|
|
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
|
|
normal.bind(KeyCode::Char('<'), none, "view-back");
|
|
normal.bind(KeyCode::Char('o'), none, "open-record-row");
|
|
|
|
// Records mode toggle and prune toggle
|
|
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
|
|
normal.bind(KeyCode::Char('P'), none, "toggle-prune-empty");
|
|
|
|
// Tile select
|
|
normal.bind(KeyCode::Char('T'), none, "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
|
|
let mut g_map = Keymap::new();
|
|
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));
|
|
|
|
let mut y_map = Keymap::new();
|
|
y_map.bind(KeyCode::Char('y'), none, "yank");
|
|
normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map));
|
|
|
|
let mut z_map = Keymap::new();
|
|
z_map.bind(KeyCode::Char('Z'), none, "save-and-quit");
|
|
normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map));
|
|
|
|
set.insert(ModeKey::Normal, Arc::new(normal));
|
|
|
|
// ── Help mode ────────────────────────────────────────────────────
|
|
let mut help = Keymap::new();
|
|
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 ────────────────────────────────────────────────
|
|
let mut fp = Keymap::new();
|
|
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");
|
|
fp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]);
|
|
fp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]);
|
|
fp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]);
|
|
set.insert(ModeKey::FormulaPanel, Arc::new(fp));
|
|
|
|
// ── Category panel ───────────────────────────────────────────────
|
|
let mut cp = Keymap::new();
|
|
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, "filter-to-item");
|
|
cp.bind(KeyCode::Char(' '), none, "cycle-axis-at-cursor");
|
|
cp.bind_args(
|
|
KeyCode::Char('n'),
|
|
none,
|
|
"enter-mode",
|
|
vec!["category-add".into()],
|
|
);
|
|
cp.bind(KeyCode::Char('a'), none, "open-item-add-at-cursor");
|
|
cp.bind(KeyCode::Char('o'), none, "open-item-add-at-cursor");
|
|
cp.bind(KeyCode::Char('d'), none, "delete-category-at-cursor");
|
|
cp.bind(KeyCode::Delete, none, "delete-category-at-cursor");
|
|
// C/F/V in panel modes: close panel (toggle-panel-and-focus sees focused=true)
|
|
cp.bind_args(
|
|
KeyCode::Char('C'),
|
|
none,
|
|
"toggle-panel-and-focus",
|
|
vec!["category".into()],
|
|
);
|
|
cp.bind_args(
|
|
KeyCode::Char('F'),
|
|
none,
|
|
"toggle-panel-and-focus",
|
|
vec!["formula".into()],
|
|
);
|
|
cp.bind_args(
|
|
KeyCode::Char('V'),
|
|
none,
|
|
"toggle-panel-and-focus",
|
|
vec!["view".into()],
|
|
);
|
|
set.insert(ModeKey::CategoryPanel, Arc::new(cp));
|
|
|
|
// ── View panel ───────────────────────────────────────────────────
|
|
let mut vp = Keymap::new();
|
|
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");
|
|
vp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]);
|
|
vp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]);
|
|
vp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]);
|
|
set.insert(ModeKey::ViewPanel, Arc::new(vp));
|
|
|
|
// ── Tile select ──────────────────────────────────────────────────
|
|
let mut ts = Keymap::new();
|
|
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_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]);
|
|
ed.bind(KeyCode::Enter, none, "commit-cell-edit");
|
|
ed.bind(KeyCode::Tab, none, "commit-and-advance-right");
|
|
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 ─────────────────────────────────────────────────
|
|
let mut fe = Keymap::new();
|
|
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 ─────────────────────────────────────────────────
|
|
let mut ca = Keymap::new();
|
|
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 ─────────────────────────────────────────────────────
|
|
let mut ia = Keymap::new();
|
|
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 ────────────────────────────────────────────────
|
|
let mut ep = Keymap::new();
|
|
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_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(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 ────────────────────────────────────────────────
|
|
let mut wiz = Keymap::new();
|
|
wiz.bind_any("handle-wizard-key");
|
|
set.insert(ModeKey::ImportWizard, Arc::new(wiz));
|
|
|
|
set
|
|
}
|
|
}
|