Files
IronStorage/apps/tui/src/action.rs
2026-08-10 06:07:05 +00:00

288 lines
7.9 KiB
Rust

//! Central action metadata shared by dispatch, contextual help, and command mode.
use crossterm::event::{KeyCode, KeyModifiers};
use crate::app::Mode;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Quit,
Help,
Command,
Cancel,
Lock,
Refresh,
Next,
Previous,
PageDown,
PageUp,
First,
Last,
Parent,
Child,
Activate,
Filter,
NextMatch,
PreviousMatch,
FocusNext,
FocusPrevious,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct KeyBinding {
pub code: KeyCode,
pub modifiers: KeyModifiers,
pub display: &'static str,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ActionSpec {
pub action: Action,
pub label: &'static str,
pub command: &'static str,
pub bindings: &'static [KeyBinding],
modes: &'static [Mode],
}
impl ActionSpec {
pub fn is_available(self, mode: Mode) -> bool {
self.modes.contains(&mode)
}
}
const BROWSER_LIKE: &[Mode] = &[Mode::Browser, Mode::Viewer];
const UNLOCKED: &[Mode] = &[Mode::Browser, Mode::Viewer, Mode::Editor];
const OVERLAYS: &[Mode] = &[Mode::Help, Mode::Command, Mode::Dialog];
#[cfg(test)]
const ALL: &[Mode] = &[
Mode::Browser,
Mode::Viewer,
Mode::Editor,
Mode::Dialog,
Mode::Help,
Mode::Command,
Mode::Locked,
];
macro_rules! keys {
($(($code:expr, $modifiers:expr, $display:expr)),+ $(,)?) => {
&[$(KeyBinding { code: $code, modifiers: $modifiers, display: $display }),+]
};
}
pub static ACTIONS: &[ActionSpec] = &[
ActionSpec {
action: Action::Quit,
label: "quit",
command: "quit",
bindings: keys!((KeyCode::Char('q'), KeyModifiers::NONE, "q")),
modes: BROWSER_LIKE,
},
ActionSpec {
action: Action::Help,
label: "help",
command: "help",
bindings: keys!((KeyCode::Char('?'), KeyModifiers::NONE, "?")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::Command,
label: "command",
command: "command",
bindings: keys!((KeyCode::Char(':'), KeyModifiers::NONE, ":")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::Cancel,
label: "back",
command: "cancel",
bindings: keys!((KeyCode::Esc, KeyModifiers::NONE, "Esc")),
modes: OVERLAYS,
},
ActionSpec {
action: Action::Lock,
label: "lock",
command: "lock",
bindings: keys!((KeyCode::Char('l'), KeyModifiers::CONTROL, "C-l")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::Refresh,
label: "refresh",
command: "refresh",
bindings: keys!((KeyCode::Char('r'), KeyModifiers::NONE, "r")),
modes: BROWSER_LIKE,
},
ActionSpec {
action: Action::Next,
label: "next",
command: "next",
bindings: keys!(
(KeyCode::Char('j'), KeyModifiers::NONE, "j"),
(KeyCode::Down, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Previous,
label: "previous",
command: "previous",
bindings: keys!(
(KeyCode::Char('k'), KeyModifiers::NONE, "k"),
(KeyCode::Up, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::PageDown,
label: "page down",
command: "page-down",
bindings: keys!((KeyCode::PageDown, KeyModifiers::NONE, "PgDn")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::PageUp,
label: "page up",
command: "page-up",
bindings: keys!((KeyCode::PageUp, KeyModifiers::NONE, "PgUp")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::First,
label: "first",
command: "first",
bindings: keys!((KeyCode::Home, KeyModifiers::NONE, "Home")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Last,
label: "last",
command: "last",
bindings: keys!((KeyCode::End, KeyModifiers::NONE, "End")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Parent,
label: "parent",
command: "parent",
bindings: keys!(
(KeyCode::Char('h'), KeyModifiers::NONE, "h"),
(KeyCode::Left, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Child,
label: "child",
command: "child",
bindings: keys!(
(KeyCode::Char('l'), KeyModifiers::NONE, "l"),
(KeyCode::Right, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Activate,
label: "open/toggle",
command: "open",
bindings: keys!((KeyCode::Enter, KeyModifiers::NONE, "Enter")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Filter,
label: "filter",
command: "filter",
bindings: keys!((KeyCode::Char('/'), KeyModifiers::NONE, "/")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::NextMatch,
label: "next match",
command: "next-match",
bindings: keys!((KeyCode::Char('n'), KeyModifiers::NONE, "n")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::PreviousMatch,
label: "previous match",
command: "previous-match",
bindings: keys!((KeyCode::Char('N'), KeyModifiers::SHIFT, "N")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::FocusNext,
label: "next pane",
command: "focus-next",
bindings: keys!((KeyCode::Tab, KeyModifiers::NONE, "Tab")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::FocusPrevious,
label: "previous pane",
command: "focus-previous",
bindings: keys!((KeyCode::BackTab, KeyModifiers::SHIFT, "S-Tab")),
modes: UNLOCKED,
},
];
pub fn resolve_key(mode: Mode, code: KeyCode, modifiers: KeyModifiers) -> Option<Action> {
ACTIONS
.iter()
.find(|spec| {
spec.is_available(mode)
&& spec
.bindings
.iter()
.any(|binding| binding.code == code && binding.modifiers == modifiers)
})
.map(|spec| spec.action)
}
pub fn available_actions(mode: Mode) -> impl Iterator<Item = &'static ActionSpec> {
ACTIONS.iter().filter(move |spec| spec.is_available(mode))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_has_unique_commands_and_bindings_per_mode() {
for (index, action) in ACTIONS.iter().enumerate() {
assert!(!action.command.is_empty());
assert!(!action.label.is_empty());
assert!(!action.modes.is_empty());
for other in &ACTIONS[index + 1..] {
assert_ne!(action.command, other.command);
for mode in ALL {
if action.is_available(*mode) && other.is_available(*mode) {
for binding in action.bindings {
for other_binding in other.bindings {
assert_ne!(binding, other_binding);
}
}
}
}
}
}
}
#[test]
fn resolution_and_help_consume_the_same_registry() {
for mode in ALL {
for spec in available_actions(*mode) {
for binding in spec.bindings {
assert_eq!(
resolve_key(*mode, binding.code, binding.modifiers),
Some(spec.action)
);
}
}
}
assert_eq!(
resolve_key(Mode::Editor, KeyCode::Char('q'), KeyModifiers::NONE),
None
);
}
}