use crate::action::Action;
use crate::mode::Mode;
use ratatui::crossterm::event::KeyCode;
pub fn hints(mode: Mode) -> &'static [(&'static str, &'static str)] {
match mode {
Mode::Browse => &[
("j/k", "move"),
("h/l", "out/in"),
("J/K", "preview line"),
("/", "filter"),
("n/N", "match"),
("v", "visual"),
("␣", "leader"),
(":", "command"),
("?", "keys"),
("q", "quit"),
],
Mode::Search => &[("type", "filter"), ("enter", "commit"), ("esc", "cancel")],
Mode::Find => &[("type", "query"), ("enter", "jump"), ("esc", "cancel")],
Mode::Insert => &[("enter", "submit"), ("tab", "results"), ("esc", "normal")],
Mode::Normal => &[("i", "insert"), ("tab", "switch"), ("esc", "close")],
Mode::Leader => &[
("s", "search"),
("f", "find file"),
("g", "grep"),
("b", "branches"),
("p", "preview"),
("/", "find in file"),
("y", "yank url"),
("c", "clear marks"),
("d", "del org"),
("r", "reload"),
("q", "quit"),
("esc", "back"),
],
Mode::Visual => &[
("j/k", "move"),
("␣", "select"),
("h/l", "out/in"),
(":", "command"),
("v", "exit"),
],
Mode::History => &[
("j/k", "commit"),
("enter", "file at commit"),
("y", "yank at commit"),
("/", "filter"),
("esc", "back"),
],
Mode::Preview => &[
("j/k", "line"),
("gg/G", "top/bottom"),
("^D/^U", "½ page"),
("{/}", "paragraph"),
("%", "match bracket"),
(":42", "goto line"),
("/", "find"),
("h", "history"),
("b", "blame"),
("v", "select lines"),
("Y", "copy lines"),
("y", "yank url"),
("enter", "editor or commit"),
("esc", "back"),
],
}
}
pub fn preview_named(code: KeyCode) -> Action {
match code {
KeyCode::Char(' ') => Action::Leader,
KeyCode::Char('/') => Action::LeaderFindInFile,
KeyCode::Char(':') => Action::CommandLine,
KeyCode::Char('h') => Action::LeaderHistory,
KeyCode::Char('b') => Action::BlameToggle,
KeyCode::Char('y') => Action::LeaderYank,
KeyCode::Char('n') => Action::FindNext,
KeyCode::Char('N') => Action::FindPrev,
KeyCode::Enter => Action::PreviewEnter,
KeyCode::Esc | KeyCode::Char('q') => Action::ExitPreview,
_ => Action::Noop,
}
}
pub fn history(code: KeyCode) -> Action {
match code {
KeyCode::Char('j') | KeyCode::Down => Action::HistoryDown,
KeyCode::Char('k') | KeyCode::Up => Action::HistoryUp,
KeyCode::Enter => Action::HistoryOpen,
KeyCode::Char('y') => Action::HistoryYank,
KeyCode::Char('/') => Action::HistoryFilterBegin,
KeyCode::Esc => Action::HistoryClose,
_ => Action::Noop,
}
}
pub fn browsing(code: KeyCode) -> Action {
match code {
KeyCode::Char('q') => Action::Quit,
KeyCode::Char(' ') => Action::Leader,
KeyCode::Char('j') | KeyCode::Down => Action::MoveDown,
KeyCode::Char('k') | KeyCode::Up => Action::MoveUp,
KeyCode::Char('J') => Action::PreviewLineDown,
KeyCode::Char('K') => Action::PreviewLineUp,
KeyCode::Char('l') | KeyCode::Right => Action::DrillIn,
KeyCode::Char('h') | KeyCode::Left => Action::DrillOut,
KeyCode::Char('/') => Action::EnterSearch,
KeyCode::Char('n') => Action::FindNext,
KeyCode::Char('N') => Action::FindPrev,
KeyCode::Char('v') => Action::Visual,
KeyCode::Char('?') => Action::KeybindsPopup,
KeyCode::Char(':') => Action::CommandLine,
KeyCode::Enter => Action::OpenSelected,
KeyCode::Esc => Action::ClearFilter,
_ => Action::Noop,
}
}
pub fn visual(code: KeyCode) -> Action {
match code {
KeyCode::Char('j') | KeyCode::Down => Action::MoveDown,
KeyCode::Char('k') | KeyCode::Up => Action::MoveUp,
KeyCode::Char('h') | KeyCode::Left => Action::DrillOut,
KeyCode::Char('l') | KeyCode::Right => Action::DrillIn,
KeyCode::Char(' ') => Action::ToggleSelect,
KeyCode::Char(':') => Action::CommandLine,
KeyCode::Char('?') => Action::KeybindsPopup,
KeyCode::Char('v') | KeyCode::Esc => Action::ExitVisual,
_ => Action::Noop,
}
}
pub fn leader(code: KeyCode) -> Action {
match code {
KeyCode::Char('s') => Action::LeaderSearch,
KeyCode::Char('f') => Action::LeaderFileFind,
KeyCode::Char('g') => Action::LeaderGrep,
KeyCode::Char('/') => Action::LeaderFindInFile,
KeyCode::Char('y') => Action::LeaderYank,
KeyCode::Char('b') => Action::LeaderRefs,
KeyCode::Char('p') => Action::LeaderPreview,
KeyCode::Char('c') => Action::ClearMarks,
KeyCode::Char('d') => Action::DeleteMarked,
KeyCode::Char('r') => Action::LeaderReload,
KeyCode::Char('q') => Action::LeaderQuit,
KeyCode::Esc => Action::ClosePopup, _ => Action::Noop,
}
}
pub fn search_results() -> &'static [(&'static str, &'static str)] {
&[
("enter", "file"),
("j/k", "hits"),
("/", "filter"),
("tab", "fields"),
("esc", "close"),
]
}
pub fn search_facets() -> &'static [(&'static str, &'static str)] {
&[
("h/l", "chips"),
("enter", "toggle"),
("tab", "fields"),
("esc", "clear/close"),
]
}
pub fn search_file() -> &'static [(&'static str, &'static str)] {
&[
("j/k", "lines"),
("gg/G", "top/bottom"),
("^D/^U", "½ page"),
(":42", "goto line"),
("v", "select lines"),
("Y", "copy lines"),
("y", "yank url"),
("b", "blame"),
("enter", "open"),
("/", "find"),
("n/N", "match"),
("esc/h", "results"),
]
}
pub fn hint_row(rows: &[(&'static str, &'static str)]) -> String {
let body = rows
.iter()
.map(|(key, desc)| format!("{key} {desc}"))
.collect::<Vec<_>>()
.join(" · ");
format!(" {body} ")
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::crossterm::event::KeyCode;
fn code_of(key: &str) -> Option<KeyCode> {
let single = |c: char| Some(KeyCode::Char(c));
match key {
"esc" => Some(KeyCode::Esc),
"enter" => Some(KeyCode::Enter),
"tab" => Some(KeyCode::Tab),
"/" => single('/'),
":" | ":42" => single(':'),
"space" | "␣" => single(' '),
"?" => single('?'),
"y" => single('y'),
"Y" => single('Y'),
"b" => single('b'),
"v" => single('v'),
"q" => single('q'),
"h" => single('h'),
"l" => single('l'),
"g" => single('g'),
"f" => single('f'),
"s" => single('s'),
"p" => single('p'),
"r" => single('r'),
"c" => single('c'),
"d" => single('d'),
"i" => single('i'),
"n" => single('n'),
"N" => single('N'),
_ => None, }
}
#[test]
fn keymap_tables_resolve_to_real_actions() {
for (key, _desc) in hints(Mode::Leader) {
if let Some(code) = code_of(key) {
assert_ne!(
leader(code),
Action::Noop,
"leader table row {key:?} resolves"
);
}
}
{
use crate::action::Action as A;
use crate::components::global_search::{GlobalSearch, SearchKind, mock};
let mut view = GlobalSearch::new(SearchKind::Grep, None, None, None, None);
view.update(&A::GlobalSearchResults {
hits: mock::hits(SearchKind::Grep, "query", ""),
clipped: false,
index: None,
client_filtered: 0,
unfiltered: vec![],
});
for _ in 0..4 {
view.handle_key(ratatui::crossterm::event::KeyEvent::new(
KeyCode::Tab,
ratatui::crossterm::event::KeyModifiers::NONE,
));
}
view.handle_key(ratatui::crossterm::event::KeyEvent::new(
KeyCode::Enter,
ratatui::crossterm::event::KeyModifiers::NONE,
));
for (key, action) in [
("y", A::LeaderYank),
(":", A::CommandLine),
("b", A::BlameToggle),
("Y", A::PreviewCopy),
] {
let code = code_of(key).unwrap();
assert_eq!(
view.handle_key(ratatui::crossterm::event::KeyEvent::new(
code,
ratatui::crossterm::event::KeyModifiers::NONE,
)),
action,
"search_file row {key:?} resolves in the pane"
);
}
}
for (key, _desc) in hints(Mode::History) {
if let Some(code) = code_of(key) {
assert_ne!(history(code), Action::Noop, "history row {key:?} resolves");
}
}
const PREVIEW_NAMED: &[&str] = &[
"/", ":", "h", "b", "y", "n", "N", "enter", "esc", "q", "space",
];
for (key, _desc) in hints(Mode::Preview) {
if PREVIEW_NAMED.contains(key)
&& let Some(code) = code_of(key)
{
assert_ne!(
preview_named(code),
Action::Noop,
"preview row {key:?} resolves"
);
}
}
for (key, _desc) in hints(Mode::Browse) {
if let Some(code) = code_of(key) {
assert_ne!(browsing(code), Action::Noop, "browse row {key:?} resolves");
}
}
}
}