Lines
84.29 %
Functions
66.67 %
Branches
100 %
//! Decoder for keystroke byte streams arriving over a russh data
//! channel.
//!
//! `crossterm::event::read` can't be used here because it reads from
//! `/dev/tty`. Russh hands us raw bytes from the wire instead, so we
//! parse the relevant subset of xterm / VT100 escape sequences in
//! process and emit [`tui::transport::RawEvent`]s shaped exactly
//! like the ones [`crate::handler`] would receive from a local
//! transport.
//! Coverage matches what crossterm produces against a typical PTY:
//! - ASCII 0x20–0x7E → `Char(c)` (Shift implied for upper case to
//! match crossterm's behaviour).
//! - Ctrl-letter (0x01–0x1A, with 0x09/0x0a/0x0d/0x1b carrying
//! their own [`KeyCode`]).
//! - Tab, Enter, Backspace, Esc, plus arrow keys, Home/End/PageUp/
//! PageDown/Insert/Delete, F1–F12 (`ESC O P` and `ESC [ NN ~`
//! forms).
//! - Modifier-encoded forms `ESC [ 1 ; 5 A` per xterm convention.
//! - `ESC` + printable as Alt-letter.
//! - UTF-8 continuation bytes are accumulated until a full code
//! point is decoded.
//! The parser is deliberately minimal — bracketed paste, mouse
//! reports, and the rest of the VT200/CSI zoo are unrecognised and
//! left as inert bytes (logged at trace level so we can grow
//! coverage if a real client trips on something).
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui::transport::RawEvent;
/// Stateful byte → `RawEvent` decoder.
#[derive(Debug, Default)]
pub struct KeyDecoder {
buf: Vec<u8>,
}
impl KeyDecoder {
#[must_use]
pub fn new() -> Self {
Self::default()
/// Append `bytes` to the internal accumulator and yield every
/// `RawEvent` we can decode without consuming an incomplete
/// trailing sequence.
pub fn feed(&mut self, bytes: &[u8]) -> Vec<RawEvent> {
self.buf.extend_from_slice(bytes);
let mut events = Vec::new();
let mut consumed = 0usize;
while consumed < self.buf.len() {
match decode_one(&self.buf[consumed..]) {
Decoded::Event(ev, n) => {
events.push(ev);
consumed += n;
Decoded::Skip(n) => consumed += n,
Decoded::Incomplete => break,
if consumed > 0 {
self.buf.drain(..consumed);
events
enum Decoded {
Event(RawEvent, usize),
/// Bytes we recognise but don't translate (e.g. mouse reports).
/// Drop them to keep the buffer flowing.
Skip(usize),
/// Need more bytes before we can finalise.
Incomplete,
fn decode_one(input: &[u8]) -> Decoded {
if input.is_empty() {
return Decoded::Incomplete;
let b0 = input[0];
match b0 {
0x09 => key(KeyCode::Tab, KeyModifiers::NONE, 1),
0x0a | 0x0d => key(KeyCode::Enter, KeyModifiers::NONE, 1),
0x7f | 0x08 => key(KeyCode::Backspace, KeyModifiers::NONE, 1),
0x1b => decode_escape(input),
0x01..=0x1a => {
let letter = (b0 - 1 + b'a') as char;
key(KeyCode::Char(letter), KeyModifiers::CONTROL, 1)
0x20..=0x7e => key(KeyCode::Char(b0 as char), modifiers_for_ascii(b0), 1),
_ => decode_utf8(input),
fn modifiers_for_ascii(b: u8) -> KeyModifiers {
if b.is_ascii_uppercase() {
KeyModifiers::SHIFT
} else {
KeyModifiers::NONE
fn key(code: KeyCode, mods: KeyModifiers, consumed: usize) -> Decoded {
Decoded::Event(RawEvent::Key(KeyEvent::new(code, mods)), consumed)
fn decode_utf8(input: &[u8]) -> Decoded {
// Fast path for ASCII-only callers; we only land here for >=0x80.
let needed = utf8_width(input[0]);
if needed == 0 {
return Decoded::Skip(1);
if input.len() < needed {
match std::str::from_utf8(&input[..needed]) {
Ok(s) => {
if let Some(c) = s.chars().next() {
key(KeyCode::Char(c), KeyModifiers::NONE, needed)
Decoded::Skip(needed)
Err(_) => Decoded::Skip(1),
const fn utf8_width(b: u8) -> usize {
match b {
0x00..=0x7f => 1,
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => 0,
fn decode_escape(input: &[u8]) -> Decoded {
if input.len() == 1 {
// ambiguous: lone Esc or start of an escape sequence
// — wait one round to disambiguate.
let b1 = input[1];
match b1 {
b'[' => decode_csi(input),
b'O' => decode_ss3(input),
0x20..=0x7e => {
// Esc + printable → Alt-letter
let code = KeyCode::Char(b1 as char);
let mut mods = KeyModifiers::ALT;
if b1.is_ascii_uppercase() {
mods |= KeyModifiers::SHIFT;
key(code, mods, 2)
_ => {
// Fallback: surface a bare Esc and let the next byte be
// re-decoded on its own.
key(KeyCode::Esc, KeyModifiers::NONE, 1)
fn decode_csi(input: &[u8]) -> Decoded {
// Format: ESC [ <params> <final>
// Params are ASCII digits and ';'. Final is 0x40..=0x7e.
let mut idx = 2;
while idx < input.len() {
let b = input[idx];
if (0x40..=0x7e).contains(&b) {
let params = &input[2..idx];
return finalise_csi(params, b, idx + 1);
idx += 1;
Decoded::Incomplete
fn finalise_csi(params: &[u8], final_byte: u8, consumed: usize) -> Decoded {
let parts = parse_params(params);
let mods = parts
.get(1)
.copied()
.map_or(KeyModifiers::NONE, modifiers_from_xterm);
match final_byte {
b'A' => key(KeyCode::Up, mods, consumed),
b'B' => key(KeyCode::Down, mods, consumed),
b'C' => key(KeyCode::Right, mods, consumed),
b'D' => key(KeyCode::Left, mods, consumed),
b'H' => key(KeyCode::Home, mods, consumed),
b'F' => key(KeyCode::End, mods, consumed),
b'Z' => key(KeyCode::BackTab, KeyModifiers::SHIFT, consumed),
b'~' => match parts.first().copied().unwrap_or(0) {
1 | 7 => key(KeyCode::Home, mods, consumed),
2 => key(KeyCode::Insert, mods, consumed),
3 => key(KeyCode::Delete, mods, consumed),
4 | 8 => key(KeyCode::End, mods, consumed),
5 => key(KeyCode::PageUp, mods, consumed),
6 => key(KeyCode::PageDown, mods, consumed),
// xterm convention for F5-F12; F1-F4 take the SS3 form
// `ESC O P/Q/R/S` instead.
15 => key(KeyCode::F(5), mods, consumed),
17 => key(KeyCode::F(6), mods, consumed),
18 => key(KeyCode::F(7), mods, consumed),
19 => key(KeyCode::F(8), mods, consumed),
20 => key(KeyCode::F(9), mods, consumed),
21 => key(KeyCode::F(10), mods, consumed),
23 => key(KeyCode::F(11), mods, consumed),
24 => key(KeyCode::F(12), mods, consumed),
// rxvt-style F1-F4 fallback so screen/tmux clients aren't
// mute on those keys.
11 => key(KeyCode::F(1), mods, consumed),
12 => key(KeyCode::F(2), mods, consumed),
13 => key(KeyCode::F(3), mods, consumed),
14 => key(KeyCode::F(4), mods, consumed),
_ => Decoded::Skip(consumed),
},
fn decode_ss3(input: &[u8]) -> Decoded {
if input.len() < 3 {
let code = match input[2] {
b'P' => KeyCode::F(1),
b'Q' => KeyCode::F(2),
b'R' => KeyCode::F(3),
b'S' => KeyCode::F(4),
b'A' => KeyCode::Up,
b'B' => KeyCode::Down,
b'C' => KeyCode::Right,
b'D' => KeyCode::Left,
b'H' => KeyCode::Home,
b'F' => KeyCode::End,
_ => return Decoded::Skip(3),
};
key(code, KeyModifiers::NONE, 3)
fn parse_params(raw: &[u8]) -> Vec<u32> {
raw.split(|b| *b == b';')
.map(|seg| std::str::from_utf8(seg).ok().and_then(|s| s.parse().ok()))
.map(|v| v.unwrap_or(0))
.collect()
fn modifiers_from_xterm(code: u32) -> KeyModifiers {
if code == 0 {
return KeyModifiers::NONE;
let bits = code.saturating_sub(1);
let mut mods = KeyModifiers::NONE;
if bits & 0b001 != 0 {
if bits & 0b010 != 0 {
mods |= KeyModifiers::ALT;
if bits & 0b100 != 0 {
mods |= KeyModifiers::CONTROL;
mods
#[cfg(test)]
mod tests {
use super::*;
fn first_key(events: &[RawEvent]) -> KeyEvent {
match events.first().unwrap() {
RawEvent::Key(k) => *k,
other => panic!("expected key, got {other:?}"),
#[test]
fn ascii_letter_lower() {
let mut d = KeyDecoder::new();
let evs = d.feed(b"a");
assert_eq!(first_key(&evs).code, KeyCode::Char('a'));
assert_eq!(first_key(&evs).modifiers, KeyModifiers::NONE);
fn ascii_letter_upper_carries_shift() {
let evs = d.feed(b"A");
assert_eq!(first_key(&evs).modifiers, KeyModifiers::SHIFT);
fn ctrl_letter() {
let evs = d.feed(&[0x03]); // Ctrl-C
let key = first_key(&evs);
assert_eq!(key.code, KeyCode::Char('c'));
assert_eq!(key.modifiers, KeyModifiers::CONTROL);
fn tab_enter_backspace() {
let evs = d.feed(&[0x09, 0x0d, 0x7f]);
assert_eq!(evs.len(), 3);
assert_eq!(first_key(&evs[..1]).code, KeyCode::Tab);
fn esc_alone_is_buffered_then_emitted_on_next_byte() {
let evs1 = d.feed(&[0x1b]);
assert!(evs1.is_empty(), "lone Esc must wait for disambiguation");
let evs2 = d.feed(b"x");
// ESC followed by printable is Alt-x, not Esc + 'x'.
assert_eq!(first_key(&evs2).code, KeyCode::Char('x'));
assert!(first_key(&evs2).modifiers.contains(KeyModifiers::ALT));
fn arrow_keys() {
let evs = d.feed(b"\x1b[A\x1b[B\x1b[C\x1b[D");
let codes: Vec<_> = evs
.iter()
.map(|e| match e {
RawEvent::Key(k) => k.code,
_ => unreachable!(),
})
.collect();
assert_eq!(
codes,
vec![KeyCode::Up, KeyCode::Down, KeyCode::Right, KeyCode::Left]
);
fn modified_arrow_ctrl_up() {
let evs = d.feed(b"\x1b[1;5A");
assert_eq!(key.code, KeyCode::Up);
fn page_up_page_down() {
let evs = d.feed(b"\x1b[5~\x1b[6~");
assert_eq!(codes, vec![KeyCode::PageUp, KeyCode::PageDown]);
fn function_keys_via_ss3() {
let evs = d.feed(b"\x1bOP\x1bOQ\x1bOR\x1bOS");
vec![KeyCode::F(1), KeyCode::F(2), KeyCode::F(3), KeyCode::F(4)]
fn function_keys_via_csi() {
let evs = d.feed(b"\x1b[15~\x1b[17~\x1b[24~");
assert_eq!(codes, vec![KeyCode::F(5), KeyCode::F(6), KeyCode::F(12)]);
fn split_csi_across_feeds() {
assert!(d.feed(b"\x1b[").is_empty());
assert!(d.feed(b"1;").is_empty());
let evs = d.feed(b"5A");
assert_eq!(first_key(&evs).code, KeyCode::Up);
assert_eq!(first_key(&evs).modifiers, KeyModifiers::CONTROL);
fn utf8_multi_byte() {
// 'ñ' = 0xc3 0xb1
assert!(d.feed(&[0xc3]).is_empty());
let evs = d.feed(&[0xb1]);
assert_eq!(first_key(&evs).code, KeyCode::Char('ñ'));