use crossterm::event::{
self, Event, KeyCode as CtCode, KeyEvent, KeyEventKind, KeyModifiers,
};
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyCode {
Char(char),
Enter,
Esc,
Tab,
BackTab,
Backspace,
Delete,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Function(u8),
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyPress {
pub code: KeyCode,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
}
impl KeyPress {
pub fn new(code: KeyCode) -> Self {
Self {
code,
ctrl: false,
alt: false,
shift: false,
}
}
pub fn ctrl(letter: char) -> Self {
Self {
code: KeyCode::Char(letter),
ctrl: true,
alt: false,
shift: false,
}
}
pub fn is_ctrl(&self, letter: char) -> bool {
self.ctrl && self.code == KeyCode::Char(letter)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputEvent {
Key(KeyPress),
Paste(String),
Resize,
}
pub trait EventSource {
fn next_event(&mut self) -> Result<InputEvent>;
fn is_interactive(&self) -> bool {
true
}
}
pub struct CrosstermSource;
impl EventSource for CrosstermSource {
fn next_event(&mut self) -> Result<InputEvent> {
loop {
match event::read()? {
Event::Key(key) if key.kind != KeyEventKind::Release => {
return Ok(InputEvent::Key(map_key(key)));
}
Event::Paste(text) => return Ok(InputEvent::Paste(text)),
Event::Resize(_, _) => return Ok(InputEvent::Resize),
_ => continue,
}
}
}
}
fn map_key(key: KeyEvent) -> KeyPress {
let mods = key.modifiers;
KeyPress {
code: map_code(key.code),
ctrl: mods.contains(KeyModifiers::CONTROL),
alt: mods.contains(KeyModifiers::ALT),
shift: mods.contains(KeyModifiers::SHIFT),
}
}
fn map_code(code: CtCode) -> KeyCode {
match code {
CtCode::Char(c) => KeyCode::Char(c),
CtCode::Enter => KeyCode::Enter,
CtCode::Esc => KeyCode::Esc,
CtCode::Tab => KeyCode::Tab,
CtCode::BackTab => KeyCode::BackTab,
CtCode::Backspace => KeyCode::Backspace,
CtCode::Delete => KeyCode::Delete,
CtCode::Up => KeyCode::Up,
CtCode::Down => KeyCode::Down,
CtCode::Left => KeyCode::Left,
CtCode::Right => KeyCode::Right,
CtCode::Home => KeyCode::Home,
CtCode::End => KeyCode::End,
CtCode::PageUp => KeyCode::PageUp,
CtCode::PageDown => KeyCode::PageDown,
CtCode::F(n) => KeyCode::Function(n),
_ => KeyCode::Unknown,
}
}
#[cfg(test)]
pub(crate) struct ScriptedSource {
events: std::collections::VecDeque<InputEvent>,
}
#[cfg(test)]
impl ScriptedSource {
pub(crate) fn keys(codes: impl IntoIterator<Item = KeyCode>) -> Self {
let events = codes
.into_iter()
.map(|code| InputEvent::Key(KeyPress::new(code)))
.collect();
Self { events }
}
pub(crate) fn events(events: impl IntoIterator<Item = InputEvent>) -> Self {
Self {
events: events.into_iter().collect(),
}
}
}
#[cfg(test)]
impl EventSource for ScriptedSource {
fn next_event(&mut self) -> Result<InputEvent> {
Ok(self
.events
.pop_front()
.unwrap_or(InputEvent::Key(KeyPress::new(KeyCode::Esc))))
}
fn is_interactive(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scripted_source_yields_then_cancels() {
let mut source = ScriptedSource::keys([KeyCode::Char('a')]);
assert_eq!(
source.next_event().unwrap(),
InputEvent::Key(KeyPress::new(KeyCode::Char('a')))
);
assert_eq!(
source.next_event().unwrap(),
InputEvent::Key(KeyPress::new(KeyCode::Esc))
);
}
#[test]
fn ctrl_helper_matches() {
assert!(KeyPress::ctrl('c').is_ctrl('c'));
assert!(!KeyPress::new(KeyCode::Char('c')).is_ctrl('c'));
}
}