use super::listener::{Command, CommandDiscriminants};
use crate::config::KeyBindingsConfig;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
use std::{fmt, io, iter, mem, str::FromStr, time::Duration};
pub struct KeyboardListener {
bindings: CommandKeyBindings,
events: Vec<KeyEvent>,
}
impl KeyboardListener {
pub fn new(bindings: CommandKeyBindings) -> Self {
Self { bindings, events: Vec::new() }
}
pub(crate) fn poll_next_command(&mut self, timeout: Duration) -> io::Result<Option<Command>> {
if poll(timeout)? { self.next_command() } else { Ok(None) }
}
pub(crate) fn next_command(&mut self) -> io::Result<Option<Command>> {
let mut events = mem::take(&mut self.events);
let (command, events) = match read()? {
Event::Key(event) if event.kind == KeyEventKind::Release => (None, events),
Event::Key(event) => {
events.push(event);
self.match_events(events)
}
Event::Resize(..) => (Some(Command::Redraw), events),
_ => (None, vec![]),
};
self.events = events;
Ok(command)
}
fn match_events(&self, events: Vec<KeyEvent>) -> (Option<Command>, Vec<KeyEvent>) {
match self.bindings.apply(&events) {
InputAction::Emit(command) => (Some(command), Vec::new()),
InputAction::Buffer => (None, events),
InputAction::Reset => (None, Vec::new()),
}
}
}
enum InputAction {
Buffer,
Reset,
Emit(Command),
}
pub struct CommandKeyBindings {
bindings: Vec<(KeyBinding, CommandDiscriminants)>,
}
impl CommandKeyBindings {
fn apply(&self, events: &[KeyEvent]) -> InputAction {
let mut any_partials = false;
for (binding, identifier) in &self.bindings {
match binding.match_events(events) {
BindingMatch::Full(context) => return Self::instantiate(identifier, context),
BindingMatch::Partial => any_partials = true,
BindingMatch::None => (),
}
}
if any_partials { InputAction::Buffer } else { InputAction::Reset }
}
fn instantiate(discriminant: &CommandDiscriminants, context: MatchContext) -> InputAction {
use CommandDiscriminants::*;
let command = match discriminant {
Redraw => Command::Redraw,
Next => Command::Next,
NextFast => Command::NextFast,
Previous => Command::Previous,
PreviousFast => Command::PreviousFast,
FirstSlide => Command::FirstSlide,
LastSlide => Command::LastSlide,
GoToSlide => {
match context {
MatchContext::None => return InputAction::Reset,
MatchContext::Number(number) => Command::GoToSlide(number),
}
}
RenderAsyncOperations => Command::RenderAsyncOperations,
Exit => Command::Exit,
Suspend => Command::Suspend,
Reload => Command::Reload,
HardReload => Command::HardReload,
ToggleSlideIndex => Command::ToggleSlideIndex,
ToggleKeyBindingsConfig => Command::ToggleKeyBindingsConfig,
ToggleLayoutGrid => Command::ToggleLayoutGrid,
CloseModal => Command::CloseModal,
SkipPauses => Command::SkipPauses,
GoToSlideChunk => panic!("go to slide chunk is not configurable"),
};
InputAction::Emit(command)
}
fn validate_conflicts<'a>(
bindings: impl Iterator<Item = &'a KeyBinding>,
) -> Result<(), KeyBindingsValidationError> {
let mut bindings: Vec<_> = bindings.map(|binding| &binding.0).collect();
bindings.sort_by(|a, b| a.partial_cmp(b).unwrap());
for window in bindings.windows(2) {
if window[0].iter().eq(window[1].iter().take(window[0].len())) {
return Err(KeyBindingsValidationError::Conflict(
KeyBinding(window[0].clone()),
KeyBinding(window[1].clone()),
));
}
}
Ok(())
}
}
impl TryFrom<KeyBindingsConfig> for CommandKeyBindings {
type Error = KeyBindingsValidationError;
fn try_from(config: KeyBindingsConfig) -> Result<Self, Self::Error> {
let zip = |discriminant, bindings: Vec<KeyBinding>| bindings.into_iter().zip(iter::repeat(discriminant));
if !config.go_to_slide.iter().all(|k| k.expects_number()) {
return Err(KeyBindingsValidationError::Invalid("go_to_slide", "<number> matcher required"));
}
let KeyBindingsConfig {
next,
next_fast,
previous,
previous_fast,
first_slide,
last_slide,
go_to_slide,
execute_code,
reload,
toggle_slide_index,
toggle_bindings,
toggle_layout_grid,
close_modal,
exit,
suspend,
skip_pauses,
} = config;
let bindings: Vec<_> = iter::empty()
.chain(zip(CommandDiscriminants::Next, next))
.chain(zip(CommandDiscriminants::NextFast, next_fast))
.chain(zip(CommandDiscriminants::Previous, previous))
.chain(zip(CommandDiscriminants::PreviousFast, previous_fast))
.chain(zip(CommandDiscriminants::FirstSlide, first_slide))
.chain(zip(CommandDiscriminants::LastSlide, last_slide))
.chain(zip(CommandDiscriminants::GoToSlide, go_to_slide))
.chain(zip(CommandDiscriminants::Exit, exit))
.chain(zip(CommandDiscriminants::Suspend, suspend))
.chain(zip(CommandDiscriminants::HardReload, reload))
.chain(zip(CommandDiscriminants::ToggleSlideIndex, toggle_slide_index))
.chain(zip(CommandDiscriminants::ToggleKeyBindingsConfig, toggle_bindings))
.chain(zip(CommandDiscriminants::ToggleLayoutGrid, toggle_layout_grid))
.chain(zip(CommandDiscriminants::RenderAsyncOperations, execute_code))
.chain(zip(CommandDiscriminants::CloseModal, close_modal))
.chain(zip(CommandDiscriminants::SkipPauses, skip_pauses))
.collect();
Self::validate_conflicts(bindings.iter().map(|binding| &binding.0))?;
Ok(Self { bindings })
}
}
#[derive(Debug, thiserror::Error)]
pub enum KeyBindingsValidationError {
#[error("invalid binding for {0}: {1}")]
Invalid(&'static str, &'static str),
#[error("conflicting keybindings: {0} and {1}")]
Conflict(KeyBinding, KeyBinding),
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum BindingMatch {
Full(MatchContext),
Partial,
None,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
pub struct KeyBinding(#[cfg_attr(feature = "json-schema", schemars(with = "String"))] Vec<KeyMatcher>);
crate::utils::impl_deserialize_from_str!(KeyBinding);
impl KeyBinding {
fn match_events(&self, mut events: &[KeyEvent]) -> BindingMatch {
let mut output_context = MatchContext::None;
for (index, matcher) in self.0.iter().enumerate() {
let Some((context, rest)) = matcher.try_match_events(events) else {
return BindingMatch::None;
};
if !matches!(context, MatchContext::None) {
output_context = context;
}
events = rest;
if index != self.0.len() - 1 && events.is_empty() {
return BindingMatch::Partial;
}
}
BindingMatch::Full(output_context)
}
fn expects_number(&self) -> bool {
self.0.iter().any(|m| matches!(m, KeyMatcher::Number))
}
}
impl FromStr for KeyBinding {
type Err = KeyBindingParseError;
fn from_str(mut input: &str) -> Result<Self, Self::Err> {
let mut matchers = Vec::new();
let mut has_numbers = false;
while !input.is_empty() {
let (matcher, rest) = KeyMatcher::parse(input)?;
let is_number = matches!(matcher, KeyMatcher::Number);
if has_numbers && is_number {
return Err(KeyBindingParseError::TooManyNumbers);
}
has_numbers = has_numbers || is_number;
matchers.push(matcher);
input = rest;
}
Ok(Self(matchers))
}
}
impl fmt::Display for KeyBinding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for matcher in &self.0 {
write!(f, "{matcher}")?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum KeyBindingParseError {
#[error("no input")]
NoInput,
#[error("not a valid key: {0}")]
InvalidKey(char),
#[error("too many number placeholders")]
TooManyNumbers,
#[error("invalid control sequence")]
InvalidControlSequence,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
enum KeyMatcher {
Key(KeyCombination),
Number,
}
impl KeyMatcher {
fn try_match_events<'a>(&self, events: &'a [KeyEvent]) -> Option<(MatchContext, &'a [KeyEvent])> {
match self {
Self::Key(combo) => Self::try_match_key(combo, events),
Self::Number => Self::try_match_number(events),
}
}
fn try_match_key<'a>(combo: &KeyCombination, events: &'a [KeyEvent]) -> Option<(MatchContext, &'a [KeyEvent])> {
let event = events.first()?;
let is_control = event.modifiers == KeyModifiers::CONTROL;
if combo.key == event.code && combo.control == is_control {
let rest = &events[1..];
Some((MatchContext::None, rest))
} else {
None
}
}
fn try_match_number(mut events: &[KeyEvent]) -> Option<(MatchContext, &[KeyEvent])> {
let mut number = None;
while let Some((head, rest)) = events.split_first() {
let digit = match head.code {
KeyCode::Char(c) if c.is_ascii_digit() => c.to_digit(10).expect("not a digit"),
_ => break,
};
let next = number.unwrap_or(0u32).checked_mul(10).and_then(|number| number.checked_add(digit));
match next {
Some(n) => {
number = Some(n);
events = rest;
}
None => return None,
}
}
number.map(|number| (MatchContext::Number(number), events))
}
fn parse(input: &str) -> Result<(Self, &str), KeyBindingParseError> {
if let Some(input) = input.strip_prefix("<number>") {
Ok((Self::Number, input))
} else if let Some(input) = Self::try_match_input(input, &["<c-", "<C-"]) {
let (key, input) = Self::parse_key_code(input)?;
let Some(input) = input.strip_prefix('>') else {
return Err(KeyBindingParseError::InvalidControlSequence);
};
let matcher = Self::Key(KeyCombination { key, control: true });
Ok((matcher, input))
} else {
let (key, input) = Self::parse_key_code(input)?;
let matcher = Self::Key(KeyCombination { key, control: false });
Ok((matcher, input))
}
}
fn parse_key_code(input: &str) -> Result<(KeyCode, &str), KeyBindingParseError> {
if let Some(input) = Self::try_match_input(input, &["<PageUp>", "<page_up>"]) {
Ok((KeyCode::PageUp, input))
} else if let Some(input) = Self::try_match_input(input, &["<PageDown>", "<page_down>"]) {
Ok((KeyCode::PageDown, input))
} else if let Some(input) = Self::try_match_input(input, &["<cr>", "<CR>", "<Enter>", "<enter>"]) {
Ok((KeyCode::Enter, input))
} else if let Some(input) = Self::try_match_input(input, &["<Home>", "<home>"]) {
Ok((KeyCode::Home, input))
} else if let Some(input) = Self::try_match_input(input, &["<End>", "<end>"]) {
Ok((KeyCode::End, input))
} else if let Some(input) = Self::try_match_input(input, &["<Left>", "<left>"]) {
Ok((KeyCode::Left, input))
} else if let Some(input) = Self::try_match_input(input, &["<Right>", "<right>"]) {
Ok((KeyCode::Right, input))
} else if let Some(input) = Self::try_match_input(input, &["<Up>", "<up>"]) {
Ok((KeyCode::Up, input))
} else if let Some(input) = Self::try_match_input(input, &["<Down>", "<down>"]) {
Ok((KeyCode::Down, input))
} else if let Some(input) = Self::try_match_input(input, &["<Esc>", "<esc>"]) {
Ok((KeyCode::Esc, input))
} else if let Some(input) = Self::try_match_input(input, &["<Tab>", "<tab>"]) {
Ok((KeyCode::Tab, input))
} else if let Some(input) = Self::try_match_input(input, &["<Backspace>", "<backspace>"]) {
Ok((KeyCode::Backspace, input))
} else if let Some(input) = Self::try_match_input(input, &["<F", "<f"]) {
let (number, rest) = input.split_once('>').ok_or(KeyBindingParseError::InvalidControlSequence)?;
let number: u8 = number.parse().map_err(|_| KeyBindingParseError::InvalidControlSequence)?;
if number > 12 { Err(KeyBindingParseError::InvalidControlSequence) } else { Ok((KeyCode::F(number), rest)) }
} else {
let next = input.chars().next().ok_or(KeyBindingParseError::NoInput)?;
if next == '<' || next == '>' {
Err(KeyBindingParseError::InvalidKey(next))
} else if next.is_alphanumeric() || next.is_ascii_punctuation() || next == ' ' {
let key = KeyCode::Char(next);
Ok((key, &input[next.len_utf8()..]))
} else {
Err(KeyBindingParseError::InvalidKey(next))
}
}
}
fn try_match_input<'a>(input: &'a str, aliases: &[&str]) -> Option<&'a str> {
for alias in aliases {
if let Some(input) = input.strip_prefix(alias) {
return Some(input);
}
}
None
}
}
impl fmt::Display for KeyMatcher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Number => write!(f, "<number>"),
Self::Key(combo) => {
if combo.control {
write!(f, "<c-")?;
}
match combo.key {
KeyCode::Char(' ') => write!(f, "' '")?,
KeyCode::Char(c) => write!(f, "{}", c)?,
other => write!(f, "<{other:?}>")?,
};
if combo.control {
write!(f, ">")?;
}
Ok(())
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum MatchContext {
Number(u32),
None,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
struct KeyCombination {
key: KeyCode,
control: bool,
}
impl KeyCombination {
#[cfg(test)]
fn char(c: char) -> Self {
Self { key: KeyCode::Char(c), control: false }
}
#[cfg(test)]
fn control_char(c: char) -> Self {
Self { key: KeyCode::Char(c), control: true }
}
}
impl From<KeyCode> for KeyCombination {
fn from(key: KeyCode) -> Self {
Self { key, control: false }
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::event::KeyEventState;
use rstest::rstest;
trait KeyEventSource {
fn into_event(self) -> KeyEvent;
}
impl KeyEventSource for KeyCode {
fn into_event(self) -> KeyEvent {
KeyEvent {
code: self,
modifiers: KeyModifiers::empty(),
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
}
impl KeyEventSource for char {
fn into_event(self) -> KeyEvent {
KeyCode::Char(self).into_event()
}
}
trait KeyEventExt {
fn with_control(self) -> Self;
}
impl KeyEventExt for KeyEvent {
fn with_control(mut self) -> Self {
self.modifiers = KeyModifiers::CONTROL;
self
}
}
#[rstest]
#[case::number("<number>", vec![KeyMatcher::Number])]
#[case::char("w", vec![KeyMatcher::Key(KeyCombination::char('w'))])]
#[case::ctrl_char1("<c-w>", vec![KeyMatcher::Key(KeyCombination::control_char('w'))])]
#[case::ctrl_char2("<C-w>", vec![KeyMatcher::Key(KeyCombination::control_char('w'))])]
#[case::dot(".", vec![KeyMatcher::Key(KeyCombination::char('.'))])]
#[case::dot(" ", vec![KeyMatcher::Key(KeyCombination::char(' '))])]
#[case::multi("hi", vec![KeyMatcher::Key(KeyCombination::char('h')), KeyMatcher::Key(KeyCombination::char('i'))])]
#[case::page_up1("<page_up>", vec![KeyMatcher::Key(KeyCode::PageUp.into())])]
#[case::page_up2("<PageUp>", vec![KeyMatcher::Key(KeyCode::PageUp.into())])]
#[case::page_down1("<page_down>", vec![KeyMatcher::Key(KeyCode::PageDown.into())])]
#[case::page_down2("<PageDown>", vec![KeyMatcher::Key(KeyCode::PageDown.into())])]
#[case::enter1("<CR>", vec![KeyMatcher::Key(KeyCode::Enter.into())])]
#[case::enter2("<cr>", vec![KeyMatcher::Key(KeyCode::Enter.into())])]
#[case::enter3("<enter>", vec![KeyMatcher::Key(KeyCode::Enter.into())])]
#[case::home1("<home>", vec![KeyMatcher::Key(KeyCode::Home.into())])]
#[case::home2("<Home>", vec![KeyMatcher::Key(KeyCode::Home.into())])]
#[case::end1("<End>", vec![KeyMatcher::Key(KeyCode::End.into())])]
#[case::end2("<end>", vec![KeyMatcher::Key(KeyCode::End.into())])]
#[case::left1("<Left>", vec![KeyMatcher::Key(KeyCode::Left.into())])]
#[case::left2("<left>", vec![KeyMatcher::Key(KeyCode::Left.into())])]
#[case::right1("<Right>", vec![KeyMatcher::Key(KeyCode::Right.into())])]
#[case::right2("<right>", vec![KeyMatcher::Key(KeyCode::Right.into())])]
#[case::up1("<Up>", vec![KeyMatcher::Key(KeyCode::Up.into())])]
#[case::up2("<up>", vec![KeyMatcher::Key(KeyCode::Up.into())])]
#[case::down1("<Down>", vec![KeyMatcher::Key(KeyCode::Down.into())])]
#[case::down2("<down>", vec![KeyMatcher::Key(KeyCode::Down.into())])]
#[case::esc1("<Esc>", vec![KeyMatcher::Key(KeyCode::Esc.into())])]
#[case::esc2("<esc>", vec![KeyMatcher::Key(KeyCode::Esc.into())])]
#[case::f1("<f1>", vec![KeyMatcher::Key(KeyCode::F(1).into())])]
#[case::f12("<f12>", vec![KeyMatcher::Key(KeyCode::F(12).into())])]
#[case::backspace1("<Backspace>", vec![KeyMatcher::Key(KeyCode::Backspace.into())])]
#[case::backspace2("<backspace>", vec![KeyMatcher::Key(KeyCode::Backspace.into())])]
#[case::tab1("<Tab>", vec![KeyMatcher::Key(KeyCode::Tab.into())])]
#[case::tab2("<tab>", vec![KeyMatcher::Key(KeyCode::Tab.into())])]
fn parse_key_binding(#[case] pattern: &str, #[case] matchers: Vec<KeyMatcher>) {
let binding = KeyBinding::from_str(pattern).expect("failed to parse");
let expected = KeyBinding(matchers);
assert_eq!(binding, expected);
}
#[rstest]
#[case::invalid_tag("<hi>")]
#[case::invalid_char("🚀")]
#[case::too_many_numbers("<number><number>")]
#[case::control_sequence("<C-w")]
#[case::f10("<f13>")]
#[case::unfinished_f("<f1")]
fn invalid_key_bindings(#[case] input: &str) {
let result = KeyBinding::from_str(input);
assert!(result.is_err(), "not an error");
}
#[rstest]
#[case::single("g", &['g'.into_event()])]
#[case::single_uppercase("G", &['G'.into_event()])]
#[case::multi("gg", &['g'.into_event(), 'g'.into_event()])]
#[case::multi_space(" g", &[' '.into_event(), 'g'.into_event()])]
#[case::control("<c-w>", &['w'.into_event().with_control()])]
#[case::page_up("<PageUp>", &[KeyCode::PageUp.into_event()])]
#[case::page_down("<PageDown>", &[KeyCode::PageDown.into_event()])]
#[case::enter("<Enter>", &[KeyCode::Enter.into_event()])]
#[case::home("<Home>", &[KeyCode::Home.into_event()])]
#[case::end("<End>", &[KeyCode::End.into_event()])]
fn matching(#[case] pattern: &str, #[case] events: &[KeyEvent]) {
let binding = KeyBinding::from_str(pattern).expect("failed to parse");
let result = binding.match_events(events);
assert!(matches!(result, BindingMatch::Full(_)), "not full match: {result:?}");
}
#[rstest]
#[case::fewer("gg", &['g'.into_event()])]
#[case::number_something1("<number>G", &['4'.into_event()])]
#[case::number_something2("<number>G", &['4'.into_event(), '2'.into_event()])]
#[case::number_something3(":<number><CR>", &[':'.into_event(), '4'.into_event()])]
fn partial_matching(#[case] pattern: &str, #[case] events: &[KeyEvent]) {
let binding = KeyBinding::from_str(pattern).expect("failed to parse");
let result = binding.match_events(events);
assert!(matches!(result, BindingMatch::Partial), "not partial match: {result:?}");
}
#[rstest]
#[case::number_something("<number>G", &['4'.into_event(), 'K'.into_event()])]
fn no_matching(#[case] pattern: &str, #[case] events: &[KeyEvent]) {
let binding = KeyBinding::from_str(pattern).expect("failed to parse");
let result = binding.match_events(events);
assert!(matches!(result, BindingMatch::None), "some match: {result:?}");
}
#[rstest]
#[case::number_something("<number>G", &['4'.into_event(), '2'.into_event(), 'G'.into_event()])]
#[case::number_something(
":<number><cr>",
&[':'.into_event(), '4'.into_event(), '2'.into_event(), KeyCode::Enter.into_event()]
)]
fn match_number(#[case] pattern: &str, #[case] events: &[KeyEvent]) {
let binding = KeyBinding::from_str(pattern).expect("failed to parse");
let result = binding.match_events(events);
let BindingMatch::Full(MatchContext::Number(number)) = result else {
panic!("unexpected match: {result:?}");
};
assert_eq!(number, 42);
}
#[rstest]
#[case(&["<number>G", "other", "<number>Go"])]
#[case(&["<PageUp><PageDown>", "something", "<PageUp>"])]
#[case(&["<cr><cr>", "<cr><cr>"])]
#[case(&["<c-w>", "<c-w>a"])]
#[case(&["<c-w>", "<c-w>"])]
#[case(&["<number>", "<number>"])]
fn conflicts(#[case] patterns: &[&str]) {
let bindings: Vec<_> = patterns.iter().map(|p| KeyBinding::from_str(p).unwrap()).collect();
let result = CommandKeyBindings::validate_conflicts(bindings.iter());
assert!(result.is_err(), "not an error: {result:?}");
}
#[rstest]
#[case(&["<number>Ga", "<number>Go"])]
#[case(&["<c-a><number>", "<c-a>hi"])]
fn no_conflicts(#[case] patterns: &[&str]) {
let bindings: Vec<_> = patterns.iter().map(|p| KeyBinding::from_str(p).unwrap()).collect();
let result = CommandKeyBindings::validate_conflicts(bindings.iter());
assert!(result.is_ok(), "got error: {result:?}");
}
#[rstest]
#[case("<number>G")]
#[case("<PageUp>potato")]
#[case("<Esc><number><PageUp>")]
fn display(#[case] pattern: &str) {
let binding = KeyBinding::from_str(pattern).expect("invalid pattern");
let rendered = binding.to_string();
assert_eq!(rendered, pattern);
}
}