use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::cmdline::ExCommand;
use crate::state::{AppState, Focus};
#[derive(Clone, Debug, PartialEq)]
pub enum Command {
Quit,
Help,
Reload,
CycleFocus,
Open,
Back,
MoveDown,
MoveUp,
MoveTop,
MoveBottom,
HalfPageDown,
HalfPageUp,
NextProfile,
StartCommand(&'static str),
Backlinks,
MoreResults,
ToggleExpand,
Edit,
Ex(ExCommand),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Bar {
Global,
Context,
}
#[derive(Clone, Copy)]
pub struct KeyChord {
pub code: KeyCode,
pub mods: KeyModifiers,
}
impl KeyChord {
fn matches(&self, key: &KeyEvent) -> bool {
if self.code != key.code {
return false;
}
let mods = match key.code {
KeyCode::Char(_) => key.modifiers.difference(KeyModifiers::SHIFT),
_ => key.modifiers,
};
mods == self.mods
}
}
#[derive(Clone, Copy)]
pub struct When {
pub focus: Option<Focus>,
pub results_only: bool,
}
impl When {
fn matches(&self, state: &AppState) -> bool {
self.focus.is_none_or(|f| f == state.focus)
&& (!self.results_only || !state.source.is_all())
}
}
pub struct Binding {
pub chord: KeyChord,
pub when: When,
pub cmd: Command,
pub bar: Option<Bar>,
pub key_label: &'static str,
pub action_label: &'static str,
}
const NONE: KeyModifiers = KeyModifiers::NONE;
const CTRL: KeyModifiers = KeyModifiers::CONTROL;
const ANY: When = When {
focus: None,
results_only: false,
};
const LIST: When = When {
focus: Some(Focus::List),
results_only: false,
};
const VIEW: When = When {
focus: Some(Focus::View),
results_only: false,
};
const LIST_RESULTS: When = When {
focus: Some(Focus::List),
results_only: true,
};
macro_rules! bind {
($code:expr, $mods:expr, $when:expr, $cmd:expr, $bar:expr, $key:expr, $action:expr) => {
Binding {
chord: KeyChord {
code: $code,
mods: $mods,
},
when: $when,
cmd: $cmd,
bar: $bar,
key_label: $key,
action_label: $action,
}
};
}
use Command as C;
use KeyCode::*;
pub static KEYMAP: &[Binding] = &[
bind!(
Char('q'),
NONE,
ANY,
C::Quit,
Some(Bar::Global),
"q",
"quit"
),
bind!(
Char(':'),
NONE,
ANY,
C::StartCommand(""),
Some(Bar::Global),
":",
"cmd"
),
bind!(
Char('/'),
NONE,
ANY,
C::StartCommand("search "),
Some(Bar::Global),
"/",
"search"
),
bind!(
Char('?'),
NONE,
ANY,
C::Help,
Some(Bar::Global),
"?",
"help"
),
bind!(
Char('p'),
NONE,
ANY,
C::NextProfile,
Some(Bar::Global),
"p",
"profile"
),
bind!(
Char('r'),
NONE,
ANY,
C::Reload,
Some(Bar::Global),
"r",
"reload"
),
bind!(
Tab,
NONE,
ANY,
C::CycleFocus,
Some(Bar::Global),
"Tab",
"focus"
),
bind!(Esc, NONE, VIEW, C::Back, None, "Esc", "back"),
bind!(
Esc,
NONE,
LIST_RESULTS,
C::Back,
Some(Bar::Context),
"Esc",
"all jots"
),
bind!(
Char('j'),
NONE,
LIST,
C::MoveDown,
Some(Bar::Context),
"j/k",
"move"
),
bind!(Char('k'), NONE, LIST, C::MoveUp, None, "k", "up"),
bind!(
Char('j'),
NONE,
VIEW,
C::MoveDown,
Some(Bar::Context),
"j/k",
"scroll"
),
bind!(Char('k'), NONE, VIEW, C::MoveUp, None, "k", "up"),
bind!(Down, NONE, ANY, C::MoveDown, None, "↓", "down"),
bind!(Up, NONE, ANY, C::MoveUp, None, "↑", "up"),
bind!(
Char('g'),
NONE,
ANY,
C::MoveTop,
Some(Bar::Context),
"g/G",
"top/end"
),
bind!(Char('G'), NONE, ANY, C::MoveBottom, None, "G", "end"),
bind!(Home, NONE, ANY, C::MoveTop, None, "Home", "top"),
bind!(End, NONE, ANY, C::MoveBottom, None, "End", "end"),
bind!(
Char('d'),
NONE,
ANY,
C::HalfPageDown,
Some(Bar::Context),
"d/u",
"½page"
),
bind!(Char('u'), NONE, ANY, C::HalfPageUp, None, "u", "½page up"),
bind!(Char('d'), CTRL, ANY, C::HalfPageDown, None, "C-d", "½page"),
bind!(Char('u'), CTRL, ANY, C::HalfPageUp, None, "C-u", "½page up"),
bind!(PageDown, NONE, ANY, C::HalfPageDown, None, "PgDn", "½page"),
bind!(PageUp, NONE, ANY, C::HalfPageUp, None, "PgUp", "½page up"),
bind!(Enter, NONE, LIST, C::Open, Some(Bar::Context), "⏎", "open"),
bind!(Char('l'), NONE, LIST, C::Open, None, "l", "open"),
bind!(Right, NONE, LIST, C::Open, None, "→", "open"),
bind!(
Char('h'),
NONE,
VIEW,
C::Back,
Some(Bar::Context),
"h",
"back"
),
bind!(Left, NONE, VIEW, C::Back, None, "←", "back"),
bind!(
Char('f'),
NONE,
LIST,
C::StartCommand("find "),
Some(Bar::Context),
"f",
"find"
),
bind!(
Char('b'),
NONE,
ANY,
C::Backlinks,
Some(Bar::Context),
"b",
"backlinks"
),
bind!(
Char('m'),
NONE,
LIST_RESULTS,
C::MoreResults,
Some(Bar::Context),
"m",
"more"
),
bind!(
Char('x'),
NONE,
VIEW,
C::ToggleExpand,
Some(Bar::Context),
"x",
"expand"
),
bind!(
Char('e'),
NONE,
ANY,
C::Edit,
Some(Bar::Context),
"e",
"edit"
),
];
pub fn lookup(state: &AppState, key: &KeyEvent) -> Option<Command> {
KEYMAP
.iter()
.find(|b| b.when.matches(state) && b.chord.matches(key))
.map(|b| b.cmd.clone())
}
pub fn bar_items(state: &AppState, bar: Bar) -> Vec<(&'static str, &'static str)> {
KEYMAP
.iter()
.filter(|b| b.bar == Some(bar) && b.when.matches(state))
.map(|b| (b.key_label, b.action_label))
.collect()
}