use ratatui_crossterm::crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
use crate::vocabulary::{Action, Reply};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reach {
Always,
Unclaimed,
}
struct Shortcut {
key: KeyCode,
modifiers: KeyModifiers,
reply: Reply,
reach: Reach,
}
const SHORTCUTS: &[Shortcut] = &[
Shortcut {
key: KeyCode::Char('Q'),
modifiers: KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
reply: Reply::force_quit(),
reach: Reach::Always,
},
Shortcut {
key: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
reply: Reply::Act(Action::New),
reach: Reach::Unclaimed,
},
Shortcut {
key: KeyCode::Char('o'),
modifiers: KeyModifiers::CONTROL,
reply: Reply::Act(Action::Open),
reach: Reach::Unclaimed,
},
Shortcut {
key: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
reply: Reply::Act(Action::Save),
reach: Reach::Unclaimed,
},
Shortcut {
key: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
reply: Reply::quit(),
reach: Reach::Unclaimed,
},
Shortcut {
key: KeyCode::Char('z'),
modifiers: KeyModifiers::CONTROL,
reply: Reply::Act(Action::Undo),
reach: Reach::Unclaimed,
},
Shortcut {
key: KeyCode::Char('Z'),
modifiers: KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
reply: Reply::Act(Action::Redo),
reach: Reach::Unclaimed,
},
];
pub(crate) fn handle_always(event: &Event) -> Reply {
handle(event, Reach::Always)
}
pub(crate) fn handle_unclaimed(event: &Event) -> Reply {
handle(event, Reach::Unclaimed)
}
fn handle(event: &Event, reach: Reach) -> Reply {
let Event::Key(key) = event else {
return Reply::Ignored;
};
if key.kind != KeyEventKind::Press {
return Reply::Ignored;
}
SHORTCUTS
.iter()
.find(|shortcut| {
shortcut.reach == reach
&& shortcut.key == key.code
&& shortcut.modifiers == key.modifiers
})
.map_or(Reply::Ignored, |shortcut| shortcut.reply.clone())
}
#[cfg(test)]
fn events_at(reach: Reach) -> Vec<Event> {
use ratatui_crossterm::crossterm::event::KeyEvent;
SHORTCUTS
.iter()
.filter(|shortcut| shortcut.reach == reach)
.map(|shortcut| Event::Key(KeyEvent::new(shortcut.key, shortcut.modifiers)))
.collect()
}
#[cfg(test)]
pub(crate) fn events_answered_first() -> Vec<Event> {
events_at(Reach::Always)
}
#[cfg(test)]
pub(crate) fn events_unclaimed() -> Vec<Event> {
events_at(Reach::Unclaimed)
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui_crossterm::crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
fn key(code: KeyCode, modifiers: KeyModifiers) -> Event {
Event::Key(KeyEvent::new(code, modifiers))
}
fn handle_at(event: &Event, reach: Reach) -> Reply {
match reach {
Reach::Always => handle_always(event),
Reach::Unclaimed => handle_unclaimed(event),
}
}
#[test]
fn the_way_out_is_the_shortcut_the_menu_shows() {
let event = key(KeyCode::Char('q'), KeyModifiers::CONTROL);
assert_eq!(handle_unclaimed(&event), Reply::quit());
}
#[test]
fn the_forced_exit_is_answered_first_and_the_way_out_last() {
let forced_exit = key(
KeyCode::Char('Q'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
);
let way_out = key(KeyCode::Char('q'), KeyModifiers::CONTROL);
assert_eq!(handle_always(&forced_exit), Reply::force_quit());
assert_eq!(handle_unclaimed(&way_out), Reply::quit());
assert_eq!(
handle_unclaimed(&forced_exit),
Reply::Ignored,
"the forced exit answered after the chrome"
);
assert_eq!(
handle_always(&way_out),
Reply::Ignored,
"the way out answered before the chrome"
);
}
#[test]
fn a_shortcut_answers_to_nothing_but_a_press() {
for shortcut in SHORTCUTS {
for kind in [KeyEventKind::Release, KeyEventKind::Repeat] {
let event = Event::Key(KeyEvent::new_with_kind(
shortcut.key,
shortcut.modifiers,
kind,
));
assert_eq!(
handle_at(&event, shortcut.reach),
Reply::Ignored,
"{kind:?} answered {:?}",
shortcut.reply
);
}
}
}
#[test]
fn a_shortcut_is_the_combination_it_shows_and_not_another_one() {
for shortcut in SHORTCUTS {
for modifiers in combinations_but(shortcut.modifiers) {
assert_eq!(
handle_at(&key(shortcut.key, modifiers), shortcut.reach),
Reply::Ignored,
"{:?} with {modifiers:?} answered",
shortcut.reply
);
}
}
let another_key = key(KeyCode::Char('x'), KeyModifiers::CONTROL);
assert_eq!(handle_unclaimed(&another_key), Reply::Ignored);
}
fn combinations_but(own: KeyModifiers) -> Vec<KeyModifiers> {
let mut combinations = Vec::new();
for shift in [false, true] {
for control in [false, true] {
for alt in [false, true] {
let mut modifiers = KeyModifiers::NONE;
modifiers.set(KeyModifiers::SHIFT, shift);
modifiers.set(KeyModifiers::CONTROL, control);
modifiers.set(KeyModifiers::ALT, alt);
if modifiers != own {
combinations.push(modifiers);
}
}
}
}
combinations
}
#[test]
fn a_key_that_is_not_a_shortcut_is_not_an_action() {
let event = Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 0,
row: 0,
modifiers: KeyModifiers::NONE,
});
for reach in [Reach::Always, Reach::Unclaimed] {
assert_eq!(handle_at(&event, reach), Reply::Ignored);
}
}
}