use super::{ESC, F_TILDE, InputEvent, Key, KeyCode, Mods, MouseButton, MouseEvent, MouseKind, PASTE_END};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct KittyFields {
pub key: u32,
pub shifted: u32,
pub base: u32,
pub mods: u32,
pub text: Vec<char>,
}
pub(super) fn kitty_fields(raw: &[u8]) -> Option<KittyFields> {
let raw = match raw.strip_prefix(&[ESC]) {
Some(rest) if rest.starts_with(&[ESC]) => rest,
_ => raw,
};
let body = raw.strip_prefix(b"\x1b[")?.strip_suffix(b"u")?;
if !body.first().is_some_and(u8::is_ascii_digit) {
return None;
}
let params = parse_params(body)?;
if params.is_empty() || params.len() > 3 {
return None;
}
Some(KittyFields {
key: param(¶ms, 0),
shifted: sub_param(¶ms, 0, 1),
base: sub_param(¶ms, 0, 2),
mods: param(¶ms, 1).saturating_sub(1),
text: associated_text(¶ms).unwrap_or_default(),
})
}
fn associated_text(params: &[Vec<u32>]) -> Option<Vec<char>> {
let field = params.get(2).filter(|f| f.iter().any(|&cp| cp != 0))?;
field.iter().map(|&cp| char::from_u32(cp).filter(|c| !c.is_control())).collect()
}
const BEL: u8 = 0x07;
pub(super) const MAX_CSI_LEN: usize = 256;
const MAX_STRING_LEN: usize = 64 * 1024;
#[derive(Debug, Default)]
pub struct InputParser {
buf: Vec<u8>,
paste: Option<Vec<u8>>,
pieces: bool,
last_button: Option<MouseButton>,
}
impl InputParser {
pub fn new() -> Self {
Self::default()
}
pub fn feed(&mut self, bytes: &[u8]) -> Vec<InputEvent> {
self.buf.extend_from_slice(bytes);
self.run(false)
}
pub fn has_pending(&self) -> bool {
match &self.paste {
Some(content) => !content.is_empty(),
None => !self.buf.is_empty(),
}
}
pub fn flush(&mut self) -> Vec<InputEvent> {
self.run(true)
}
fn run(&mut self, flush: bool) -> Vec<InputEvent> {
let mut events = Vec::new();
let mut pos = 0;
loop {
if let Some(content) = self.paste.as_mut() {
let rest = &self.buf[pos..];
if let Some(end) = find(rest, PASTE_END) {
content.extend_from_slice(&rest[..end]);
let pieces = std::mem::take(&mut self.pieces);
if !content.is_empty() || pieces {
events.push(InputEvent::Paste(std::mem::take(content)));
}
self.paste = None;
pos += end + PASTE_END.len();
continue;
}
let take = rest.len() - partial_suffix(rest, PASTE_END);
content.extend_from_slice(&rest[..take]);
pos += take;
if (flush || content.len() >= PASTE_PIECE) && !content.is_empty() {
events.push(InputEvent::PastePiece(std::mem::take(content)));
self.pieces = true;
}
break;
}
let rest = &self.buf[pos..];
if rest.is_empty() {
break;
}
match parse_one(rest) {
Step::Done(tok, len) => {
pos += len;
self.accept(tok, &mut events);
}
Step::Incomplete => {
if flush {
events.push(flush_partial(rest));
pos = self.buf.len();
}
break;
}
}
}
self.buf.drain(..pos);
events
}
fn accept(&mut self, tok: Tok, events: &mut Vec<InputEvent>) {
match tok {
Tok::Event(ev) => {
if let InputEvent::Mouse(MouseEvent { kind: MouseKind::Press(button), .. }) = &ev {
self.last_button = Some(*button);
}
events.push(ev);
}
Tok::Keys(keys) => events.extend(keys),
Tok::PasteStart => self.paste = Some(Vec::new()),
Tok::X10Release(mut ev) => {
let button = self.last_button.take().unwrap_or(MouseButton::Left);
ev.kind = MouseKind::Release(button);
events.push(InputEvent::Mouse(ev));
}
}
}
}
const PASTE_PIECE: usize = 1 << 20;
enum Tok {
Event(InputEvent),
Keys(Vec<InputEvent>),
PasteStart,
X10Release(MouseEvent),
}
enum Step {
Done(Tok, usize),
Incomplete,
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn partial_suffix(data: &[u8], marker: &[u8]) -> usize {
(1..marker.len()).rev().find(|&n| data.ends_with(&marker[..n])).unwrap_or(0)
}
fn flush_partial(rest: &[u8]) -> InputEvent {
let (code, mods) = match *rest {
[ESC] => (KeyCode::Esc, Mods::NONE),
[ESC, ESC] => (KeyCode::Esc, Mods::ALT),
[ESC, c @ 0x20..=0x7e] => (KeyCode::Char(char::from(c)), Mods::ALT),
_ => return InputEvent::Raw(rest.to_vec()),
};
InputEvent::Key(make_key(code, mods, rest))
}
fn normalize(code: KeyCode, mut mods: Mods) -> (KeyCode, Mods) {
match code {
KeyCode::Char(c) if c.is_ascii_alphabetic() => {
if c.is_ascii_uppercase() {
mods.shift = true;
}
let c = if mods.ctrl {
c.to_ascii_lowercase()
} else if mods.shift {
c.to_ascii_uppercase()
} else {
c
};
(KeyCode::Char(c), mods)
}
KeyCode::BackTab => (code, Mods { shift: true, ..mods }),
_ => (code, mods),
}
}
fn make_key(code: KeyCode, mods: Mods, raw: &[u8]) -> Key {
let (code, mods) = normalize(code, mods);
Key { code, mods, raw: raw.to_vec() }
}
fn key_tok(code: KeyCode, mods: Mods, raw: &[u8]) -> Tok {
Tok::Event(InputEvent::Key(make_key(code, mods, raw)))
}
fn raw_tok(raw: &[u8]) -> Tok {
Tok::Event(InputEvent::Raw(raw.to_vec()))
}
fn with_alt(mods: Mods) -> Mods {
Mods { alt: true, ..mods }
}
fn parse_one(b: &[u8]) -> Step {
if b[0] == ESC { parse_escape(b) } else { parse_plain(b) }
}
fn parse_plain(b: &[u8]) -> Step {
let byte = b[0];
let one = |code, mods| Step::Done(key_tok(code, mods, &b[..1]), 1);
match byte {
0x00 => one(KeyCode::Char(' '), Mods::CTRL),
b'\t' => one(KeyCode::Tab, Mods::NONE),
b'\r' => one(KeyCode::Enter, Mods::NONE),
0x01..=0x1a => one(KeyCode::Char(char::from(b'a' + byte - 1)), Mods::CTRL),
ESC => one(KeyCode::Esc, Mods::NONE),
0x1c..=0x1f => {
let c = ['\\', ']', '^', '_'][usize::from(byte - 0x1c)];
one(KeyCode::Char(c), Mods::CTRL)
}
0x20..=0x7e => one(KeyCode::Char(char::from(byte)), Mods::NONE),
0x7f => one(KeyCode::Backspace, Mods::NONE),
_ => parse_utf8(b),
}
}
fn parse_utf8(b: &[u8]) -> Step {
let invalid = || Step::Done(raw_tok(&b[..1]), 1);
let need = match b[0] {
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => return invalid(),
};
let avail = b.len().min(need);
if b[1..avail].iter().any(|&c| c & 0xc0 != 0x80) {
return invalid();
}
if b.len() < need {
return Step::Incomplete;
}
match std::str::from_utf8(&b[..need]).ok().and_then(|s| s.chars().next()) {
Some(c) => Step::Done(key_tok(KeyCode::Char(c), Mods::NONE, &b[..need]), need),
None => invalid(),
}
}
fn parse_escape(b: &[u8]) -> Step {
let Some(&next) = b.get(1) else {
return Step::Incomplete;
};
match next {
b'[' => parse_csi(b),
b'O' => parse_ss3(b),
b']' | b'P' | b'_' | b'^' | b'X' => parse_string(b),
ESC => parse_double_escape(b),
_ => match parse_plain(&b[1..]) {
Step::Incomplete => Step::Incomplete,
Step::Done(Tok::Event(InputEvent::Key(k)), n) => {
Step::Done(key_tok(k.code, with_alt(k.mods), &b[..=n]), n + 1)
}
Step::Done(..) => Step::Done(key_tok(KeyCode::Esc, Mods::NONE, &b[..1]), 1),
},
}
}
fn parse_double_escape(b: &[u8]) -> Step {
match b.get(2) {
None => Step::Incomplete,
Some(b'[' | b'O') => match parse_escape(&b[1..]) {
Step::Incomplete => Step::Incomplete,
Step::Done(Tok::Event(InputEvent::Key(k)), n) if n > 2 => {
Step::Done(key_tok(k.code, with_alt(k.mods), &b[..=n]), n + 1)
}
Step::Done(..) => Step::Done(key_tok(KeyCode::Esc, Mods::NONE, &b[..1]), 1),
},
Some(_) => Step::Done(key_tok(KeyCode::Esc, Mods::ALT, &b[..2]), 2),
}
}
fn ss3_code(c: u8) -> Option<KeyCode> {
Some(match c {
b'A' => KeyCode::Up,
b'B' => KeyCode::Down,
b'C' => KeyCode::Right,
b'D' => KeyCode::Left,
b'H' => KeyCode::Home,
b'F' => KeyCode::End,
b'P'..=b'S' => KeyCode::F(c - b'P' + 1),
b'M' => KeyCode::Enter,
b'X' => KeyCode::Char('='),
b'j' => KeyCode::Char('*'),
b'k' => KeyCode::Char('+'),
b'l' => KeyCode::Char(','),
b'm' => KeyCode::Char('-'),
b'n' => KeyCode::Char('.'),
b'o' => KeyCode::Char('/'),
b'p'..=b'y' => KeyCode::Char(char::from(c - b'p' + b'0')),
_ => return None,
})
}
fn parse_ss3(b: &[u8]) -> Step {
let Some(&c) = b.get(2) else {
return Step::Incomplete;
};
let (mods, i) = if c.is_ascii_digit() {
if b.len() < 4 {
return Step::Incomplete;
}
(Mods::from_xterm_param(u32::from(c - b'0')), 3)
} else {
(Mods::NONE, 2)
};
match ss3_code(b[i]) {
Some(code) => Step::Done(key_tok(code, mods, &b[..=i]), i + 1),
None => Step::Done(key_tok(KeyCode::Char('O'), Mods::ALT, &b[..2]), 2),
}
}
fn parse_csi(b: &[u8]) -> Step {
if b.get(2) == Some(&b'M') {
return if b.len() < 6 { Step::Incomplete } else { Step::Done(x10_mouse(&b[..6]), 6) };
}
let mut i = 2;
while i < b.len() && i < MAX_CSI_LEN {
match b[i] {
0x20..=0x3f => i += 1,
0x40..=0x7e => return Step::Done(csi_dispatch(&b[..=i]), i + 1),
_ if i == 2 => return Step::Done(key_tok(KeyCode::Char('['), Mods::ALT, &b[..2]), 2),
_ => return Step::Done(raw_tok(&b[..i]), i),
}
}
if i >= MAX_CSI_LEN { Step::Done(raw_tok(&b[..i]), i) } else { Step::Incomplete }
}
fn parse_string(b: &[u8]) -> Step {
if b.len() == 2 {
return Step::Incomplete;
}
for i in 2..b.len().min(MAX_STRING_LEN) {
match b[i] {
BEL => return Step::Done(raw_tok(&b[..=i]), i + 1),
ESC => {
return match b.get(i + 1) {
None => Step::Incomplete,
Some(b'\\') => Step::Done(raw_tok(&b[..i + 2]), i + 2),
Some(_) => Step::Done(raw_tok(&b[..i]), i),
};
}
_ => {}
}
}
if b.len() >= MAX_STRING_LEN { Step::Done(raw_tok(&b[..MAX_STRING_LEN]), MAX_STRING_LEN) } else { Step::Incomplete }
}
fn parse_params(body: &[u8]) -> Option<Vec<Vec<u32>>> {
if body.is_empty() {
return Some(Vec::new());
}
body.split(|&c| c == b';').map(|param| param.split(|&c| c == b':').map(parse_num).collect()).collect()
}
fn parse_num(s: &[u8]) -> Option<u32> {
s.iter().try_fold(0u32, |acc, &c| {
c.is_ascii_digit().then(|| acc.saturating_mul(10).saturating_add(u32::from(c - b'0')))
})
}
fn param(params: &[Vec<u32>], i: usize) -> u32 {
sub_param(params, i, 0)
}
fn sub_param(params: &[Vec<u32>], i: usize, j: usize) -> u32 {
params.get(i).and_then(|p| p.get(j)).copied().unwrap_or(0)
}
fn legacy_mods(params: &[Vec<u32>], i: usize, locks: bool) -> Option<Mods> {
let m = param(params, i);
let ignored = if locks { 64 | 128 } else { 0 };
(m <= 256 && (m.saturating_sub(1) & !ignored) < 16).then(|| Mods::from_xterm_param(m))
}
fn is_release(params: &[Vec<u32>]) -> bool {
sub_param(params, 1, 1) == 3
}
fn release_tok(seq: &[u8]) -> Tok {
Tok::Event(InputEvent::KittyKey { raw: seq.to_vec(), release: true })
}
fn csi_dispatch(seq: &[u8]) -> Tok {
let final_byte = seq[seq.len() - 1];
let body = &seq[2..seq.len() - 1];
let (marker, body) = match body.first() {
Some(&m @ (b'<' | b'=' | b'>' | b'?')) => (Some(m), &body[1..]),
_ => (None, body),
};
let Some(params) = parse_params(body) else {
return raw_tok(seq);
};
match (marker, final_byte) {
(Some(b'<'), b'M' | b'm') => sgr_mouse(¶ms, final_byte == b'm', seq),
(None, _) => csi_key(¶ms, final_byte, seq),
_ => raw_tok(seq),
}
}
fn csi_key(params: &[Vec<u32>], final_byte: u8, seq: &[u8]) -> Tok {
let code = match final_byte {
b'I' if params.is_empty() => return Tok::Event(InputEvent::FocusGained),
b'O' if params.is_empty() => return Tok::Event(InputEvent::FocusLost),
b'~' => return csi_tilde(params, seq),
b'u' => return csi_u(params, seq),
b'A'..=b'D' | b'H' | b'F' | b'P'..=b'S' => ss3_code(final_byte),
b'Z' => Some(KeyCode::BackTab),
_ => None,
};
if code.is_some() && is_release(params) {
return release_tok(seq);
}
match (code, legacy_mods(params, 1, final_byte != b'R')) {
(Some(code), Some(mods)) if params.len() <= 2 && param(params, 0) <= 1 => key_tok(code, mods, seq),
_ => raw_tok(seq),
}
}
fn csi_tilde(params: &[Vec<u32>], seq: &[u8]) -> Tok {
let n = param(params, 0);
let code = match n {
200 if params.len() == 1 => return Tok::PasteStart,
27 if params.len() == 3 => {
let mods = Mods::from_xterm_param(param(params, 1));
return codepoint_key(param(params, 2), 0, mods, seq);
}
1 | 7 => KeyCode::Home,
2 => KeyCode::Insert,
3 => KeyCode::Delete,
4 | 8 => KeyCode::End,
5 => KeyCode::PageUp,
6 => KeyCode::PageDown,
11..=14 => KeyCode::F((n - 10) as u8),
_ => match F_TILDE.iter().position(|&v| v == n) {
Some(i) => KeyCode::F(i as u8 + 5),
None => return raw_tok(seq),
},
};
if is_release(params) {
return release_tok(seq);
}
match legacy_mods(params, 1, true) {
Some(mods) if params.len() <= 2 => key_tok(code, mods, seq),
_ => raw_tok(seq),
}
}
fn csi_u(params: &[Vec<u32>], seq: &[u8]) -> Tok {
if params.is_empty() || params.len() > 3 {
return raw_tok(seq);
}
if is_release(params) {
return release_tok(seq);
}
let mods = Mods::from_xterm_param(param(params, 1));
if let Some(text) = associated_text(params) {
if param(params, 0) == 0 {
return Tok::Event(InputEvent::Paste(text.iter().collect::<String>().into_bytes()));
}
return text_tok(&text, mods, seq);
}
codepoint_key(param(params, 0), sub_param(params, 0, 1), mods, seq)
}
fn text_tok(text: &[char], mods: Mods, seq: &[u8]) -> Tok {
match text {
[c] => {
let shift = if c.is_alphabetic() { c.is_uppercase() } else { mods.shift };
key_tok(KeyCode::Char(*c), Mods { alt: false, shift, ..mods }, seq)
}
_ => Tok::Keys(
text.iter()
.map(|c| {
InputEvent::Key(make_key(KeyCode::Char(*c), Mods::NONE, c.encode_utf8(&mut [0; 4]).as_bytes()))
})
.collect(),
),
}
}
fn codepoint_key(cp: u32, shifted: u32, mods: Mods, seq: &[u8]) -> Tok {
let code = match cp {
9 if mods.shift => KeyCode::BackTab,
9 => KeyCode::Tab,
13 => KeyCode::Enter,
27 => KeyCode::Esc,
8 | 127 => KeyCode::Backspace,
_ => {
let cp = if mods.shift && shifted != 0 { shifted } else { cp };
match char::from_u32(cp) {
Some(c) if ('\u{e000}'..='\u{f8ff}').contains(&c) => match keypad(cp) {
Some(code) => code,
None => return Tok::Event(InputEvent::KittyKey { raw: seq.to_vec(), release: false }),
},
Some(c) if !c.is_control() => KeyCode::Char(c),
_ => return raw_tok(seq),
}
}
};
key_tok(code, mods, seq)
}
fn keypad(cp: u32) -> Option<KeyCode> {
use KeyCode::*;
Some(match cp {
57399..=57408 => Char(char::from_digit(cp - 57399, 10)?),
57409 => Char('.'),
57410 => Char('/'),
57411 => Char('*'),
57412 => Char('-'),
57413 => Char('+'),
57414 => Enter,
57415 => Char('='),
57416 => Char(','),
57417 => Left,
57418 => Right,
57419 => Up,
57420 => Down,
57421 => PageUp,
57422 => PageDown,
57423 => Home,
57424 => End,
57425 => Insert,
57426 => Delete,
_ => return None,
})
}
fn mouse_mods(cb: u32) -> Mods {
Mods { shift: cb & 4 != 0, alt: cb & 8 != 0, ctrl: cb & 16 != 0 }
}
fn decode_mouse(cb: u32, release: bool) -> Option<MouseKind> {
if cb >= 128 {
return None; }
let button = match cb & 3 {
0 => Some(MouseButton::Left),
1 => Some(MouseButton::Middle),
2 => Some(MouseButton::Right),
_ => None,
};
Some(if cb & 64 != 0 {
match cb & 3 {
0 => MouseKind::ScrollUp,
1 => MouseKind::ScrollDown,
2 => MouseKind::ScrollLeft,
_ => MouseKind::ScrollRight,
}
} else if cb & 32 != 0 {
button.map_or(MouseKind::Move, MouseKind::Drag)
} else if release {
MouseKind::Release(button?)
} else {
MouseKind::Press(button?)
})
}
fn coord(v: u32) -> u16 {
u16::try_from(v.saturating_sub(1)).unwrap_or(u16::MAX)
}
fn sgr_mouse(params: &[Vec<u32>], release: bool, seq: &[u8]) -> Tok {
let cb = param(params, 0);
match decode_mouse(cb, release) {
Some(kind) if params.len() == 3 => Tok::Event(InputEvent::Mouse(MouseEvent {
kind,
col: coord(param(params, 1)),
row: coord(param(params, 2)),
mods: mouse_mods(cb),
})),
_ => raw_tok(seq),
}
}
fn x10_mouse(seq: &[u8]) -> Tok {
if seq[3..6].iter().any(|&v| v < 32) {
return raw_tok(seq);
}
let cb = u32::from(seq[3] - 32);
let mut ev = MouseEvent {
kind: MouseKind::Move,
col: coord(u32::from(seq[4] - 32)),
row: coord(u32::from(seq[5] - 32)),
mods: mouse_mods(cb),
};
if cb & (32 | 64) == 0 && cb & 3 == 3 {
return Tok::X10Release(ev);
}
match decode_mouse(cb, false) {
Some(kind) => {
ev.kind = kind;
Tok::Event(InputEvent::Mouse(ev))
}
None => raw_tok(seq),
}
}