use ::termwiz::input::{
InputEvent, KeyCode as K, KeyEvent, Modifiers as M, MouseButtons, MouseEvent,
};
use super::{Button, Event, HeldButton, Key, KeyCode, Modifiers, Mouse, MouseKind, char_key};
pub fn from_termwiz(event: InputEvent, held: &mut HeldButton) -> Option<Event> {
translate(event, held, false)
}
pub fn from_termwiz_kitty(event: InputEvent, held: &mut HeldButton) -> Option<Event> {
translate(event, held, true)
}
fn translate(event: InputEvent, held: &mut HeldButton, exact: bool) -> Option<Event> {
Some(match event {
InputEvent::Key(KeyEvent { key, modifiers }) => {
let mut read = self::key(key, modifiers)?;
read.exact = exact;
Event::Key(read)
}
InputEvent::Mouse(MouseEvent {
x,
y,
mouse_buttons,
modifiers,
}) => {
let kind = if mouse_buttons.contains(MouseButtons::VERT_WHEEL) {
if mouse_buttons.contains(MouseButtons::WHEEL_POSITIVE) {
MouseKind::ScrollUp
} else {
MouseKind::ScrollDown
}
} else if mouse_buttons.contains(MouseButtons::HORZ_WHEEL) {
return None;
} else {
let down = if mouse_buttons.contains(MouseButtons::LEFT) {
Some(Button::Left)
} else if mouse_buttons.contains(MouseButtons::RIGHT) {
Some(Button::Right)
} else if mouse_buttons.contains(MouseButtons::MIDDLE) {
Some(Button::Middle)
} else {
None
};
let kind = match (held.0, down) {
(Some(was), Some(now)) if was == now => MouseKind::Drag(now),
(_, Some(now)) => MouseKind::Down(now),
(Some(was), None) => MouseKind::Up(was),
(None, None) => MouseKind::Moved,
};
held.0 = down;
kind
};
Event::Mouse(Mouse {
kind,
column: x.saturating_sub(1),
row: y.saturating_sub(1),
modifiers: self::modifiers(modifiers),
pixel: None,
})
}
InputEvent::Resized { cols, rows } => Event::Resize {
columns: u16::try_from(cols).unwrap_or(u16::MAX),
rows: u16::try_from(rows).unwrap_or(u16::MAX),
},
InputEvent::Paste(text) => Event::Paste(text),
InputEvent::PixelMouse(_) | InputEvent::Wake => return None,
})
}
fn modifiers(held: M) -> Modifiers {
Modifiers {
shift: held.contains(M::SHIFT),
ctrl: held.contains(M::CTRL),
alt: held.contains(M::ALT),
}
}
fn key(key: K, held: M) -> Option<Key> {
if held.intersects(M::SUPER) {
return None;
}
let mut modifiers = self::modifiers(held);
let code = match key {
K::Char(c) if c.is_control() => return Some(char_key(c, modifiers)),
K::Char(c) => {
if modifiers.shift && c.is_ascii_lowercase() {
modifiers.shift = false;
KeyCode::Char(c.to_ascii_uppercase())
} else {
if c.is_uppercase() {
modifiers.shift = false;
}
KeyCode::Char(c)
}
}
K::Tab if modifiers.shift => {
modifiers.shift = false;
KeyCode::BackTab
}
K::Tab => KeyCode::Tab,
K::Enter => KeyCode::Enter,
K::Backspace => KeyCode::Backspace,
K::Escape => KeyCode::Escape,
K::Delete => KeyCode::Delete,
K::Insert => KeyCode::Insert,
K::Applications => KeyCode::Menu,
K::UpArrow | K::ApplicationUpArrow => KeyCode::Up,
K::DownArrow | K::ApplicationDownArrow => KeyCode::Down,
K::LeftArrow | K::ApplicationLeftArrow => KeyCode::Left,
K::RightArrow | K::ApplicationRightArrow => KeyCode::Right,
K::Home | K::KeyPadHome => KeyCode::Home,
K::End | K::KeyPadEnd => KeyCode::End,
K::PageUp | K::KeyPadPageUp => KeyCode::PageUp,
K::PageDown | K::KeyPadPageDown => KeyCode::PageDown,
K::Function(n) => KeyCode::F(n),
_ => return None,
};
Some(Key::with(code, modifiers))
}
#[cfg(test)]
mod tests {
use super::*;
use ::termwiz::input::InputParser;
fn read(bytes: &[u8], exact: bool) -> Vec<Event> {
let mut held = HeldButton::default();
InputParser::new()
.parse_as_vec(bytes, false)
.into_iter()
.filter_map(|event| translate(event, &mut held, exact))
.collect()
}
fn key(name: &str) -> Event {
Event::Key(Key::parse(name).unwrap())
}
#[test]
fn keys_read_as_a_legacy_terminal_sends_them() {
for (bytes, name) in [
(&b"a"[..], "a"),
(b"A", "A"),
(b"\x01", "ctrl+a"),
(b"\t", "tab"),
(b"\r", "enter"),
(b"\x7f", "backspace"),
(b"\x1b", "esc"),
(b"\x1bx", "alt+x"),
(b"\x1b[Z", "shift+tab"),
(b"\x1b[A", "up"),
(b"\x1b[1;5C", "ctrl+right"),
(b"\x1b[3~", "delete"),
(b"\x1b[6~", "pagedown"),
(b"\x1bOP", "f1"),
(b"\x1b[15~", "f5"),
] {
let read = self::read(bytes, false);
assert_eq!(read, [key(name)], "{bytes:?}");
assert!(!read[0].key().unwrap().is_exact(), "{bytes:?}");
}
let tab = read(b"\t", false)[0].key().unwrap();
assert!(tab.matches(&Key::parse("ctrl+i").unwrap()));
assert_eq!(
read(b"\x1b[200~pasted\x1b[201~", false),
[Event::Paste("pasted".into())]
);
}
#[test]
fn kitty_keys_are_exact() {
for (bytes, name) in [
(&b"\x1b[105;5u"[..], "ctrl+i"),
(b"\t", "tab"),
(b"\x1b[27u", "esc"),
(b"\x1b[99;5u", "ctrl+c"),
(b"\x1b[97;6u", "ctrl+shift+a"),
(b"\x1b[13;2u", "shift+enter"),
] {
let read = self::read(bytes, true);
assert_eq!(read, [key(name)], "{bytes:?}");
assert!(read[0].key().unwrap().is_exact(), "{bytes:?}");
}
let ctrl_i = read(b"\x1b[105;5u", true)[0].key().unwrap();
assert!(!ctrl_i.matches(&Key::parse("tab").unwrap()));
}
#[test]
fn the_mouse_reports_what_changed() {
let at = |kind, column, row| Event::Mouse(Mouse::new(kind, column, row));
assert_eq!(
read(
b"\x1b[<0;5;3M\x1b[<32;6;3M\x1b[<0;6;3m\x1b[<35;7;3M\x1b[<64;1;1M\x1b[<65;1;1M",
false
),
[
at(MouseKind::Down(Button::Left), 4, 2),
at(MouseKind::Drag(Button::Left), 5, 2),
at(MouseKind::Up(Button::Left), 5, 2),
at(MouseKind::Moved, 6, 2),
at(MouseKind::ScrollUp, 0, 0),
at(MouseKind::ScrollDown, 0, 0),
]
);
let Event::Mouse(shifted) = read(b"\x1b[<6;1;1M", false)[0] else {
panic!("a mouse event");
};
assert_eq!(shifted.kind, MouseKind::Down(Button::Right));
assert!(shifted.modifiers.shift);
}
}