use crossterm::event as path_crossterm_event;
#[cfg(test)]
mod tests;
use std::io;
use crossterm::event::Event;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PickerEvent {
Key(PickerKey),
Resize { width: u16, height: u16 },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PickerKey {
Up,
Down,
Enter,
Cancelled,
Ignored,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LogicalKey {
Up,
Down,
Tab,
BackTab,
Enter,
Esc,
CtrlC,
CtrlD,
Char(char),
Unknown,
}
fn logical_to_action(key: LogicalKey) -> PickerKey {
match key {
LogicalKey::Up | LogicalKey::BackTab => PickerKey::Up,
LogicalKey::Down | LogicalKey::Tab => PickerKey::Down,
LogicalKey::Enter => PickerKey::Enter,
LogicalKey::Esc | LogicalKey::CtrlC | LogicalKey::CtrlD => PickerKey::Cancelled,
LogicalKey::Char(c) => match c {
'j' => PickerKey::Down,
'k' => PickerKey::Up,
'q' => PickerKey::Cancelled,
_ => PickerKey::Ignored,
},
LogicalKey::Unknown => PickerKey::Ignored,
}
}
pub(crate) fn read_terminal_event() -> io::Result<PickerEvent> {
loop {
if let Some(event) = terminal_event_to_picker_event(crossterm::event::read()?) {
return Ok(event);
}
}
}
fn terminal_event_to_picker_event(event: Event) -> Option<PickerEvent> {
match event {
Event::Key(key) if key.kind == path_crossterm_event::KeyEventKind::Press => {
let logical = terminal_key_to_logical(key);
Some(PickerEvent::Key(logical_to_action(logical)))
}
Event::Key(_) => None,
Event::Resize(width, height) => Some(PickerEvent::Resize { width, height }),
_ => None,
}
}
fn terminal_key_to_logical(key: crossterm::event::KeyEvent) -> LogicalKey {
use crossterm::event::{KeyCode, KeyModifiers};
match key.code {
KeyCode::Up => LogicalKey::Up,
KeyCode::Down => LogicalKey::Down,
KeyCode::Tab => LogicalKey::Tab,
KeyCode::BackTab => LogicalKey::BackTab,
KeyCode::Enter => LogicalKey::Enter,
KeyCode::Esc => LogicalKey::Esc,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => LogicalKey::CtrlC,
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => LogicalKey::CtrlD,
KeyCode::Char(_)
if key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
LogicalKey::Unknown
}
KeyCode::Char(c) => LogicalKey::Char(c),
_ => LogicalKey::Unknown,
}
}
pub(crate) fn read_byte_key(reader: &mut impl io::Read) -> io::Result<PickerKey> {
let Some(byte) = read_one_byte(reader)? else {
return Ok(PickerKey::Cancelled);
};
let logical = match byte {
0x03 => LogicalKey::CtrlC,
0x04 => LogicalKey::CtrlD,
b'\n' | b'\r' => LogicalKey::Enter,
b'\t' => LogicalKey::Tab,
0x1b => read_escape_sequence(reader)?,
byte if byte.is_ascii() && !byte.is_ascii_control() => LogicalKey::Char(char::from(byte)),
_ => LogicalKey::Unknown,
};
Ok(logical_to_action(logical))
}
fn read_escape_sequence(reader: &mut impl io::Read) -> io::Result<LogicalKey> {
if read_one_byte(reader)? != Some(b'[') {
return Ok(LogicalKey::Esc);
}
let Some(byte) = read_one_byte(reader)? else {
return Ok(LogicalKey::Unknown);
};
Ok(match byte {
b'A' => LogicalKey::Up,
b'B' => LogicalKey::Down,
b'Z' => LogicalKey::BackTab,
_ => LogicalKey::Unknown,
})
}
fn read_one_byte(reader: &mut impl io::Read) -> io::Result<Option<u8>> {
let mut byte = [0_u8; 1];
loop {
match reader.read(&mut byte) {
Ok(0) => return Ok(None),
Ok(_) => return Ok(Some(byte[0])),
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err) => return Err(err),
}
}
}