288 lines
7.9 KiB
Rust
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
|
|
);
|
|
}
|
|
}
|