pub use ratada::keymap::{Conflict as ChordConflict, KeyChord};
pub type Keymap = ratada::keymap::Keymap<Action>;
pub type Conflict = ChordConflict<Action>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Up,
Down,
Top,
Bottom,
PageUp,
PageDown,
HalfPageUp,
HalfPageDown,
TabGit,
TabFiles,
ToggleSelect,
ExtendUp,
ExtendDown,
ExtendPageUp,
ExtendPageDown,
Jump,
Open,
GitTool,
JumpCd,
OpenApp,
Filter,
ChangesFilter,
Github,
Preview,
PreviewPosition,
PreviewScrollUp,
PreviewScrollDown,
PreviewPageUp,
PreviewPageDown,
PreviewShrink,
PreviewGrow,
Columns,
Sort,
ToggleGrouping,
ToggleFavFloat,
SectionJump,
ManageSections,
ReorderUp,
ReorderDown,
Add,
Edit,
Delete,
Undo,
ToggleFav,
Zip,
ZipAll,
Archive,
Slug,
ToggleSlugs,
CopyPath,
RepairPath,
Errors,
Reload,
ReloadFetch,
RefreshOne,
RefreshOneFetch,
Help,
Quit,
}
struct ActionSpec {
action: Action,
config_name: &'static str,
description: &'static str,
default_keys: &'static [&'static str],
}
const ACTIONS: &[ActionSpec] = &[
ActionSpec {
action: Action::Up,
config_name: "up",
description: "up",
default_keys: &["up"],
},
ActionSpec {
action: Action::Down,
config_name: "down",
description: "down",
default_keys: &["down"],
},
ActionSpec {
action: Action::Top,
config_name: "top",
description: "top",
default_keys: &["home", "g"],
},
ActionSpec {
action: Action::Bottom,
config_name: "bottom",
description: "bottom",
default_keys: &["end", "G"],
},
ActionSpec {
action: Action::PageUp,
config_name: "page_up",
description: "page up",
default_keys: &["pgup"],
},
ActionSpec {
action: Action::PageDown,
config_name: "page_down",
description: "page down",
default_keys: &["pgdn"],
},
ActionSpec {
action: Action::HalfPageUp,
config_name: "half_page_up",
description: "half page up",
default_keys: &["ctrl+u"],
},
ActionSpec {
action: Action::HalfPageDown,
config_name: "half_page_down",
description: "half page down",
default_keys: &["ctrl+d"],
},
ActionSpec {
action: Action::TabGit,
config_name: "tab_git",
description: "git repos",
default_keys: &["1"],
},
ActionSpec {
action: Action::TabFiles,
config_name: "tab_files",
description: "files",
default_keys: &["2"],
},
ActionSpec {
action: Action::ToggleSelect,
config_name: "select",
description: "select",
default_keys: &["space"],
},
ActionSpec {
action: Action::ExtendUp,
config_name: "extend_up",
description: "extend",
default_keys: &["shift+up"],
},
ActionSpec {
action: Action::ExtendDown,
config_name: "extend_down",
description: "extend",
default_keys: &["shift+down"],
},
ActionSpec {
action: Action::ExtendPageUp,
config_name: "extend_page_up",
description: "extend a page",
default_keys: &["shift+pgup"],
},
ActionSpec {
action: Action::ExtendPageDown,
config_name: "extend_page_down",
description: "extend a page",
default_keys: &["shift+pgdn"],
},
ActionSpec {
action: Action::Jump,
config_name: "jump",
description: "cd",
default_keys: &["enter"],
},
ActionSpec {
action: Action::Open,
config_name: "open",
description: "open",
default_keys: &["L"],
},
ActionSpec {
action: Action::GitTool,
config_name: "git_tool",
description: "lazygit",
default_keys: &["l"],
},
ActionSpec {
action: Action::JumpCd,
config_name: "jump_cd",
description: "cd",
default_keys: &["o"],
},
ActionSpec {
action: Action::OpenApp,
config_name: "open_app",
description: "open in app",
default_keys: &["O"],
},
ActionSpec {
action: Action::Filter,
config_name: "filter",
description: "filter",
default_keys: &["f"],
},
ActionSpec {
action: Action::ChangesFilter,
config_name: "changes_filter",
description: "changes",
default_keys: &["F"],
},
ActionSpec {
action: Action::Github,
config_name: "github",
description: "github",
default_keys: &["b"],
},
ActionSpec {
action: Action::Preview,
config_name: "preview",
description: "detail panel",
default_keys: &["v"],
},
ActionSpec {
action: Action::PreviewPosition,
config_name: "preview_position",
description: "right/bottom",
default_keys: &["V"],
},
ActionSpec {
action: Action::PreviewScrollUp,
config_name: "preview_scroll_up",
description: "scroll panel",
default_keys: &["ctrl+up"],
},
ActionSpec {
action: Action::PreviewScrollDown,
config_name: "preview_scroll_down",
description: "scroll panel",
default_keys: &["ctrl+down"],
},
ActionSpec {
action: Action::PreviewPageUp,
config_name: "preview_page_up",
description: "panel page up",
default_keys: &["ctrl+pgup"],
},
ActionSpec {
action: Action::PreviewPageDown,
config_name: "preview_page_down",
description: "panel page down",
default_keys: &["ctrl+pgdn"],
},
ActionSpec {
action: Action::PreviewShrink,
config_name: "preview_shrink",
description: "smaller",
default_keys: &["ctrl+left"],
},
ActionSpec {
action: Action::PreviewGrow,
config_name: "preview_grow",
description: "bigger",
default_keys: &["ctrl+right"],
},
ActionSpec {
action: Action::Columns,
config_name: "columns",
description: "columns",
default_keys: &["c"],
},
ActionSpec {
action: Action::Sort,
config_name: "sort",
description: "sort",
default_keys: &["t"],
},
ActionSpec {
action: Action::ToggleGrouping,
config_name: "grouping",
description: "group",
default_keys: &["."],
},
ActionSpec {
action: Action::ToggleFavFloat,
config_name: "fav_float",
description: "favs on top",
default_keys: &[","],
},
ActionSpec {
action: Action::SectionJump,
config_name: "section_jump",
description: "jump to section",
default_keys: &["s"],
},
ActionSpec {
action: Action::ManageSections,
config_name: "manage_sections",
description: "sections",
default_keys: &["M"],
},
ActionSpec {
action: Action::ReorderUp,
config_name: "reorder_up",
description: "reorder up",
default_keys: &["alt+up"],
},
ActionSpec {
action: Action::ReorderDown,
config_name: "reorder_down",
description: "reorder down",
default_keys: &["alt+down"],
},
ActionSpec {
action: Action::Add,
config_name: "add",
description: "add",
default_keys: &["n"],
},
ActionSpec {
action: Action::Edit,
config_name: "edit",
description: "edit",
default_keys: &["e"],
},
ActionSpec {
action: Action::Delete,
config_name: "delete",
description: "del",
default_keys: &["d", "del", "backspace"],
},
ActionSpec {
action: Action::Undo,
config_name: "undo",
description: "undo",
default_keys: &["u"],
},
ActionSpec {
action: Action::ToggleFav,
config_name: "fav",
description: "fav",
default_keys: &["*"],
},
ActionSpec {
action: Action::Zip,
config_name: "zip",
description: "zip",
default_keys: &["z"],
},
ActionSpec {
action: Action::ZipAll,
config_name: "zip_all",
description: "zip all",
default_keys: &["Z"],
},
ActionSpec {
action: Action::Archive,
config_name: "archive",
description: "archive/restore",
default_keys: &["A"],
},
ActionSpec {
action: Action::Slug,
config_name: "slug",
description: "slug",
default_keys: &["S"],
},
ActionSpec {
action: Action::ToggleSlugs,
config_name: "toggle_slugs",
description: "slugs",
default_keys: &["i"],
},
ActionSpec {
action: Action::CopyPath,
config_name: "copy_path",
description: "copy path",
default_keys: &["y"],
},
ActionSpec {
action: Action::RepairPath,
config_name: "repair_path",
description: "fix path",
default_keys: &["p"],
},
ActionSpec {
action: Action::Errors,
config_name: "errors",
description: "errors",
default_keys: &["!"],
},
ActionSpec {
action: Action::Reload,
config_name: "reload",
description: "reload/check",
default_keys: &["r"],
},
ActionSpec {
action: Action::ReloadFetch,
config_name: "reload_fetch",
description: "reload+fetch",
default_keys: &["R"],
},
ActionSpec {
action: Action::RefreshOne,
config_name: "refresh_one",
description: "refresh one",
default_keys: &["x"],
},
ActionSpec {
action: Action::RefreshOneFetch,
config_name: "refresh_one_fetch",
description: "refresh+fetch",
default_keys: &["X"],
},
ActionSpec {
action: Action::Help,
config_name: "help",
description: "help",
default_keys: &["?"],
},
ActionSpec {
action: Action::Quit,
config_name: "quit",
description: "quit",
default_keys: &["q"],
},
];
impl Action {
pub fn all() -> impl Iterator<Item = Action> + Clone {
ACTIONS.iter().map(|spec| spec.action)
}
fn spec(self) -> &'static ActionSpec {
ACTIONS
.iter()
.find(|spec| spec.action == self)
.expect("every action has an ACTIONS entry")
}
pub fn config_name(self) -> &'static str {
self.spec().config_name
}
pub fn description(self) -> &'static str {
self.spec().description
}
pub fn default_keys(self) -> &'static [&'static str] {
self.spec().default_keys
}
}
impl ratada::keymap::Action for Action {
fn all() -> impl Iterator<Item = Self> + Clone {
Action::all()
}
fn config_name(&self) -> &'static str {
(*self).config_name()
}
fn description(&self) -> &'static str {
(*self).description()
}
fn default_keys(&self) -> &'static [&'static str] {
(*self).default_keys()
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::*;
fn key(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, mods)
}
#[test]
fn parses_chars_function_keys_and_named_keys() {
assert_eq!(KeyChord::parse("a").unwrap().display(), "a");
assert_eq!(KeyChord::parse("G").unwrap().display(), "G");
assert_eq!(KeyChord::parse("pgup").unwrap().display(), "pgup");
assert_eq!(KeyChord::parse("enter").unwrap().display(), "enter");
assert_eq!(KeyChord::parse("ctrl+u").unwrap().display(), "ctrl+u");
assert_eq!(KeyChord::parse("alt+up").unwrap().display(), "alt+up");
assert!(KeyChord::parse("nonsense").is_none());
assert!(KeyChord::parse("").is_none());
}
#[test]
fn matches_distinguishes_ctrl_and_alt_arrows() {
let plain = KeyChord::parse("up").unwrap();
let ctrl = KeyChord::parse("ctrl+up").unwrap();
let alt = KeyChord::parse("alt+up").unwrap();
assert!(plain.matches(&key(KeyCode::Up, KeyModifiers::NONE)));
assert!(!plain.matches(&key(KeyCode::Up, KeyModifiers::CONTROL)));
assert!(ctrl.matches(&key(KeyCode::Up, KeyModifiers::CONTROL)));
assert!(alt.matches(&key(KeyCode::Up, KeyModifiers::ALT)));
}
#[test]
fn default_map_binds_expected_actions() {
let map = Keymap::default();
assert_eq!(
map.action_for(&key(KeyCode::Char('n'), KeyModifiers::NONE)),
Some(Action::Add),
);
assert_eq!(
map.action_for(&key(KeyCode::Char('z'), KeyModifiers::NONE)),
Some(Action::Zip),
);
assert_eq!(
map.action_for(&key(KeyCode::Char('Z'), KeyModifiers::SHIFT)),
Some(Action::ZipAll),
);
assert_eq!(
map.action_for(&key(KeyCode::Up, KeyModifiers::ALT)),
Some(Action::ReorderUp),
);
assert_eq!(map.keys_for(Action::Delete), vec!["d", "del", "backspace"]);
}
#[test]
fn ctrl_and_plain_letters_do_not_collide() {
let map = Keymap::default();
assert_eq!(
map.action_for(&key(KeyCode::Char('u'), KeyModifiers::CONTROL)),
Some(Action::HalfPageUp),
);
assert_eq!(
map.action_for(&key(KeyCode::Char('u'), KeyModifiers::NONE)),
Some(Action::Undo),
);
assert_eq!(
map.action_for(&key(KeyCode::Char('d'), KeyModifiers::CONTROL)),
Some(Action::HalfPageDown),
);
assert_eq!(
map.action_for(&key(KeyCode::Char('d'), KeyModifiers::NONE)),
Some(Action::Delete),
);
}
#[test]
fn override_replaces_an_action_and_keeps_others() {
let mut overrides = BTreeMap::new();
overrides.insert("add".to_string(), vec!["N".to_string()]);
let map = Keymap::from_overrides(&overrides);
assert_eq!(
map.action_for(&key(KeyCode::Char('N'), KeyModifiers::SHIFT)),
Some(Action::Add),
);
assert_ne!(
map.action_for(&key(KeyCode::Char('n'), KeyModifiers::NONE)),
Some(Action::Add),
);
assert_eq!(map.keys_for(Action::Edit), vec!["e"]);
}
#[test]
fn default_map_has_no_conflicts() {
assert!(Keymap::default().conflicts().is_empty());
}
#[test]
fn every_action_has_a_reachable_default_key() {
let map = Keymap::default();
for action in Action::all() {
assert!(
!map.keys_for(action).is_empty(),
"'{}' has no bound key",
action.config_name()
);
for text in action.default_keys() {
let chord =
KeyChord::parse(text).expect("default key must parse");
assert_eq!(
map.action_for(&chord.to_key()),
Some(action),
"'{text}' does not resolve back to '{}'",
action.config_name()
);
}
}
}
#[test]
fn hints_pair_keys_with_descriptions() {
let map = Keymap::default();
let hints = map.hints(&[Action::Add, Action::Quit]);
assert_eq!(hints[0], ("n".to_string(), "add".to_string()));
assert_eq!(hints[1], ("q".to_string(), "quit".to_string()));
}
#[test]
fn actions_table_matches_the_enum() {
fn covered(action: Action) {
match action {
Action::Up
| Action::Down
| Action::Top
| Action::Bottom
| Action::PageUp
| Action::PageDown
| Action::HalfPageUp
| Action::HalfPageDown
| Action::TabGit
| Action::TabFiles
| Action::ToggleSelect
| Action::ExtendUp
| Action::ExtendDown
| Action::ExtendPageUp
| Action::ExtendPageDown
| Action::Jump
| Action::Open
| Action::GitTool
| Action::JumpCd
| Action::OpenApp
| Action::Filter
| Action::ChangesFilter
| Action::Github
| Action::Preview
| Action::PreviewPosition
| Action::PreviewScrollUp
| Action::PreviewScrollDown
| Action::PreviewPageUp
| Action::PreviewPageDown
| Action::PreviewShrink
| Action::PreviewGrow
| Action::Columns
| Action::Sort
| Action::ToggleGrouping
| Action::ToggleFavFloat
| Action::SectionJump
| Action::ManageSections
| Action::ReorderUp
| Action::ReorderDown
| Action::Add
| Action::Edit
| Action::Delete
| Action::Undo
| Action::ToggleFav
| Action::Zip
| Action::ZipAll
| Action::Archive
| Action::Slug
| Action::ToggleSlugs
| Action::CopyPath
| Action::RepairPath
| Action::Errors
| Action::Reload
| Action::ReloadFetch
| Action::RefreshOne
| Action::RefreshOneFetch
| Action::Help
| Action::Quit => {}
}
}
let _ = covered;
let mut names: Vec<&str> =
ACTIONS.iter().map(|spec| spec.config_name).collect();
names.sort_unstable();
let total = names.len();
names.dedup();
assert_eq!(names.len(), total, "duplicate config_name in ACTIONS");
for action in Action::all() {
let _ = action.spec();
}
}
}