blob: 4cadabbd9db622eecd1d87b173f75aca5c14aa83 [file]
//! Streaming parser for terminal input bytes.
//!
//! [`Parser`] turns bytes from a terminal or PTY into [`Event`] values. It accepts partial input:
//! callers append bytes with [`Parser::parse`], then drain completed events with [`Parser::pop`].
//! This is the same parser used behind [`EventReader`], exposed for code that owns its input source
//! directly.
//!
//! # Implementation Notes
//!
//! The parser is adapted from [crossterm's Unix event parser], with additions for Termina-specific
//! escape sequences and Windows input modes. Crossterm comments call this style of parser a bit
//! scary and probably in need of a refactor. I like the approach though, because it is quite easy
//! to read and test. The main uncertainty is performance: `process_bytes` considers the bytes as an
//! increasing slice of the buffer until that slice becomes valid or invalid. WezTerm and Alacritty
//! use more formal parsers, [`vtparse`] and [`vte`] respectively, but those terminal-emulator
//! parsers may be larger or more complex than an application needs.
//!
//! [crossterm's Unix event parser]: https://docs.rs/crossterm/latest/crossterm/event/index.html
//! [`vtparse`]: https://docs.rs/vtparse/latest/vtparse/
//! [`vte`]: https://docs.rs/vte/latest/vte/
#[cfg(windows)]
pub mod windows;
#[cfg(all(windows, feature = "windows-legacy"))]
use windows::legacy;
#[cfg(windows)]
use windows::InputReaderMode;
use std::{
collections::VecDeque,
io,
num::NonZeroU16,
str,
time::{Duration, Instant},
};
// These are input resource limits, not limits imposed by the terminal protocols.
// Numeric CSI key prefixes are deliberately excluded: they are ambiguous until
// the final byte and have no finite wire-length limit in the existing grammar.
const MAX_TERMINAL_REPLY_BYTES: usize = 256;
const TERMINAL_REPLY_INPUT_TIMEOUT: Duration = Duration::from_millis(100);
#[cfg(doc)]
use crate::EventReader;
use crate::{
escape::{
self,
csi::{self, Csi, KittyKeyboardFlags, ThemeMode},
dcs, osc,
},
event::{
KeyCode, KeyEvent, KeyEventKind, KeyEventState, KittyGraphicsResponse, MediaKeyCode,
ModifierKeyCode, Modifiers, MouseButton, MouseEvent, MouseEventKind, SgrMouseEvent,
},
style, Event,
};
/// An incremental parser for terminal input.
///
/// The parser keeps incomplete escape sequences in an internal buffer. Pass `maybe_more = true`
/// when the current byte slice may end in the middle of a sequence. Completed events are queued
/// until [`Self::pop`] removes them.
///
/// # Examples
///
/// ```
/// use termina::{Event, Parser};
///
/// let mut parser = Parser::default();
/// parser.parse(b"\x1b[5~", false).unwrap();
/// assert!(matches!(parser.pop(), Some(Event::Key(_))));
/// ```
#[derive(Debug)]
pub struct Parser {
buffer: Vec<u8>,
reply_deadline: Option<Instant>,
max_pending_bytes: usize,
failure: Option<(io::ErrorKind, &'static str)>,
/// Events which have been parsed. Pop out with [`Self::pop`].
events: VecDeque<Event>,
#[cfg(windows)]
mode: InputReaderMode,
#[cfg(all(windows, feature = "windows-legacy"))]
surrogate_buffer: Option<u16>,
#[cfg(all(windows, feature = "windows-legacy"))]
mouse_buttons_pressed: legacy::MouseButtonsPressed,
}
impl Default for Parser {
fn default() -> Self {
Self {
buffer: Vec::with_capacity(256),
reply_deadline: None,
max_pending_bytes: usize::MAX,
failure: None,
events: VecDeque::with_capacity(32),
#[cfg(windows)]
mode: InputReaderMode::Vte,
#[cfg(all(windows, feature = "windows-legacy"))]
surrogate_buffer: None,
#[cfg(all(windows, feature = "windows-legacy"))]
mouse_buttons_pressed: legacy::MouseButtonsPressed::default(),
}
}
}
impl Parser {
// The parser is publicly accessible, but we don't currently expose methods for parsing input records, just VTE.
// So there's no need to make this public.
#[cfg(windows)]
pub(crate) fn with_mode(mode: InputReaderMode) -> Self {
Self {
mode,
..Default::default()
}
}
/// Removes and returns the oldest completed event.
pub fn pop(&mut self) -> Option<Event> {
self.events.pop_front()
}
pub(crate) fn is_ready(&self) -> bool {
!self.events.is_empty() || self.failure.is_some()
}
/// Adds bytes to the parser and queues any completed events.
///
/// Set `maybe_more` to `true` when the input source may provide more bytes for the same
/// escape sequence later. Set it to `false` when the buffer should be treated as complete for
/// now; malformed or incomplete sequences can then be discarded instead of held indefinitely.
///
/// Incomplete `CSI ?`, APC, iTerm2 capability, and XTVERSION replies are limited to 256 bytes
/// and 100 milliseconds.
/// Exceeding either limit returns an input error and leaves the parser failed; stop reading
/// this input source rather than interpreting the remaining reply bytes as key presses.
/// Numeric CSI prefixes retain their existing key/paste behavior and are not timed out.
pub fn parse(&mut self, bytes: &[u8], maybe_more: bool) -> io::Result<()> {
self.parse_at(bytes, maybe_more, Instant::now())
}
fn parse_at(&mut self, bytes: &[u8], maybe_more: bool, now: Instant) -> io::Result<()> {
self.expire_reply_at(now)?;
if bytes.is_empty() {
return self.process_bytes_at(maybe_more, now);
}
for (idx, b) in bytes.iter().enumerate() {
self.push_byte_at(*b, maybe_more || idx + 1 < bytes.len(), now)?;
}
Ok(())
}
fn push_byte_at(&mut self, byte: u8, maybe_more: bool, now: Instant) -> io::Result<()> {
self.expire_reply_at(now)?;
if self.buffer.len() >= self.max_pending_bytes {
return self.fail(
io::ErrorKind::InvalidData,
"pending input exceeded its byte limit",
);
}
if self.reply_deadline.is_some() && self.buffer.len() >= MAX_TERMINAL_REPLY_BYTES {
return self.fail(
io::ErrorKind::InvalidData,
"terminal reply exceeded its input byte limit",
);
}
self.buffer.push(byte);
self.process_bytes_at(maybe_more, now)
}
#[cfg(windows)]
fn push_byte(&mut self, byte: u8, maybe_more: bool) -> io::Result<()> {
self.push_byte_at(byte, maybe_more, Instant::now())
}
#[cfg(windows)]
fn process_bytes(&mut self, maybe_more: bool) -> io::Result<()> {
self.process_bytes_at(maybe_more, Instant::now())
}
fn process_bytes_at(&mut self, maybe_more: bool, now: Instant) -> io::Result<()> {
if self.buffer.starts_with(b"\x1b[?")
|| self.buffer.starts_with(b"\x1b_")
|| self.buffer.starts_with(b"\x1b]1337;Capabilities=")
|| self.buffer.starts_with(b"\x1bP>|")
{
self.reply_deadline
.get_or_insert(now + TERMINAL_REPLY_INPUT_TIMEOUT);
}
self.expire_reply_at(now)?;
match parse_event(&self.buffer, maybe_more) {
Ok(Some(event)) => {
self.events.push_back(event);
self.buffer.clear();
self.reply_deadline = None;
}
Ok(None) => {}
Err(_) => {
self.buffer.clear();
self.reply_deadline = None;
}
}
Ok(())
}
pub(crate) fn set_max_pending_bytes(&mut self, limit: usize) -> io::Result<()> {
if limit == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"pending input byte limit must be nonzero",
));
}
self.max_pending_bytes = limit;
self.expire_reply_at(Instant::now())
}
pub(crate) fn expire_reply_at(&mut self, now: Instant) -> io::Result<()> {
if let Some((kind, message)) = self.failure {
return Err(io::Error::new(kind, message));
}
if self.buffer.len() > self.max_pending_bytes {
return self.fail(
io::ErrorKind::InvalidData,
"pending input exceeded its byte limit",
);
}
if let Some(deadline) = self.reply_deadline {
if self.buffer.len() > MAX_TERMINAL_REPLY_BYTES {
return self.fail(
io::ErrorKind::InvalidData,
"terminal reply exceeded its input byte limit",
);
}
if now >= deadline {
return self.fail(
io::ErrorKind::TimedOut,
"incomplete terminal reply exceeded its input deadline",
);
}
}
Ok(())
}
fn fail(&mut self, kind: io::ErrorKind, message: &'static str) -> io::Result<()> {
self.failure = Some((kind, message));
Err(io::Error::new(kind, message))
}
pub(crate) fn reply_poll_timeout(&self, timeout: Option<Duration>) -> Option<Duration> {
self.reply_deadline.map_or(timeout, |deadline| {
let remaining = deadline.saturating_duration_since(Instant::now());
Some(timeout.map_or(remaining, |timeout| timeout.min(remaining)))
})
}
}
#[derive(Debug)]
struct MalformedSequenceError;
// This is a bit hacky but cuts down on boilerplate conversions
impl From<str::Utf8Error> for MalformedSequenceError {
fn from(_: str::Utf8Error) -> Self {
Self
}
}
type Result<T> = std::result::Result<T, MalformedSequenceError>;
macro_rules! bail {
() => {
return Err(MalformedSequenceError)
};
}
fn parse_event(buffer: &[u8], maybe_more: bool) -> Result<Option<Event>> {
if buffer.is_empty() {
return Ok(None);
}
match buffer[0] {
b'\x1B' => {
if buffer.len() == 1 {
if maybe_more {
// Possible Esc sequence
Ok(None)
} else {
Ok(Some(Event::Key(KeyCode::Escape.into())))
}
} else {
match buffer[1] {
b'O' => {
if buffer.len() == 2 {
Ok(None)
} else {
match buffer[2] {
b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))),
b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))),
b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))),
b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))),
b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))),
b'F' => Ok(Some(Event::Key(KeyCode::End.into()))),
// F1-F4
val @ b'P'..=b'S' => {
Ok(Some(Event::Key(KeyCode::Function(1 + val - b'P').into())))
}
_ => bail!(),
}
}
}
b'[' => parse_csi(buffer),
b']' => parse_osc(buffer),
b'P' => parse_dcs(buffer),
b'_' => parse_kitty_graphics_response(buffer),
b'\x1B' => Ok(Some(Event::Key(KeyCode::Escape.into()))),
_ => parse_event(&buffer[1..], maybe_more).map(|event_option| {
event_option.map(|event| {
if let Event::Key(key_event) = event {
let mut alt_key_event = key_event;
alt_key_event.modifiers |= Modifiers::ALT;
Event::Key(alt_key_event)
} else {
event
}
})
}),
}
}
}
b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))),
b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))),
b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))),
b'\0' => Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char(' '),
Modifiers::CONTROL,
)))),
c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char((c - 0x1 + b'a') as char),
Modifiers::CONTROL,
)))),
c @ b'\x1C'..=b'\x1F' => Ok(Some(Event::Key(KeyEvent::new(
KeyCode::Char((c - 0x1C + b'4') as char),
Modifiers::CONTROL,
)))),
_ => parse_utf8_char(buffer).map(|maybe_char| {
maybe_char.map(|ch| {
let modifiers = if ch.is_uppercase() {
Modifiers::SHIFT
} else {
Modifiers::NONE
};
Event::Key(KeyEvent::new(KeyCode::Char(ch), modifiers))
})
}),
}
}
fn parse_kitty_graphics_response(buffer: &[u8]) -> Result<Option<Event>> {
// Keep the whole APC until ST, including malformed or unknown strings. Rejecting an early
// byte would let the rest of a terminal response escape into ordinary keyboard input.
if !buffer.ends_with(b"\x1b\\") {
return Ok(None);
}
let body = buffer
.strip_prefix(b"\x1b_G")
.and_then(|body| body.strip_suffix(b"\x1b\\"))
.ok_or(MalformedSequenceError)?;
let (control, message) = str::from_utf8(body)?
.split_once(';')
.ok_or(MalformedSequenceError)?;
if message.is_empty()
|| !message
.bytes()
.all(|byte| byte.is_ascii_graphic() || byte == b' ')
{
bail!();
}
let (mut image_id, mut image_number, mut placement_id) = (None, None, None);
for field in control.split(',') {
let (key, value) = field.split_once('=').ok_or(MalformedSequenceError)?;
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
bail!();
}
let value = value.parse::<u32>().map_err(|_| MalformedSequenceError)?;
let target = match key {
"i" => &mut image_id,
"I" => &mut image_number,
"p" => &mut placement_id,
_ => bail!(),
};
if target.replace(value).is_some() {
bail!();
}
}
Ok(Some(Event::KittyGraphicsResponse(KittyGraphicsResponse {
image_id: image_id.ok_or(MalformedSequenceError)?,
image_number,
placement_id,
result: if message == "OK" {
Ok(())
} else {
Err(message.to_owned())
},
})))
}
fn parse_utf8_char(buffer: &[u8]) -> Result<Option<char>> {
assert!(!buffer.is_empty());
match str::from_utf8(buffer) {
Ok(s) => Ok(Some(s.chars().next().unwrap())),
Err(_) => {
// `from_utf8` failed but it could be because we don't have enough bytes to make a
// valid UTF-8 codepoint. Check the validity of the bytes so far:
let required_bytes = match buffer[0] {
// https://en.wikipedia.org/wiki/UTF-8#Description
(0x00..=0x7F) => 1, // 0xxxxxxx
(0xC0..=0xDF) => 2, // 110xxxxx 10xxxxxx
(0xE0..=0xEF) => 3, // 1110xxxx 10xxxxxx 10xxxxxx
(0xF0..=0xF7) => 4, // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
(0x80..=0xBF) | (0xF8..=0xFF) => bail!(),
};
if required_bytes > 1 && buffer.len() > 1 {
for byte in &buffer[1..] {
if byte & !0b0011_1111 != 0b1000_0000 {
bail!()
}
}
}
if buffer.len() < required_bytes {
Ok(None)
} else {
bail!()
}
}
}
}
fn parse_csi(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(b"\x1B["));
if buffer.len() == 2 {
return Ok(None);
}
let maybe_event = match buffer[2] {
b'[' => match buffer.get(3) {
None => None,
Some(b @ b'A'..=b'E') => Some(Event::Key(KeyCode::Function(1 + b - b'A').into())),
Some(_) => bail!(),
},
b'D' => Some(Event::Key(KeyCode::Left.into())),
b'C' => Some(Event::Key(KeyCode::Right.into())),
b'A' => Some(Event::Key(KeyCode::Up.into())),
b'B' => Some(Event::Key(KeyCode::Down.into())),
b'H' => Some(Event::Key(KeyCode::Home.into())),
b'F' => Some(Event::Key(KeyCode::End.into())),
b'Z' => Some(Event::Key(KeyEvent {
code: KeyCode::BackTab,
modifiers: Modifiers::SHIFT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
})),
b'M' => return parse_csi_normal_mouse(buffer),
b'<' => return parse_csi_sgr_mouse(buffer),
b'I' => Some(Event::FocusIn),
b'O' => Some(Event::FocusOut),
b';' => return parse_csi_modifier_key_code(buffer),
// P, Q, and S for compatibility with Kitty keyboard protocol,
// as the 1 in 'CSI 1 P' etc. must be omitted if there are no
// modifiers pressed:
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
b'P' => Some(Event::Key(KeyCode::Function(1).into())),
b'Q' => Some(Event::Key(KeyCode::Function(2).into())),
b'S' => Some(Event::Key(KeyCode::Function(4).into())),
b'?' => match buffer[buffer.len() - 1] {
b'S' => return parse_csi_graphics_attributes(buffer),
b'u' => return parse_csi_keyboard_enhancement_flags(buffer),
b'c' => return parse_csi_primary_device_attributes(buffer),
b'n' => return parse_csi_theme_mode(buffer),
b'y' => return parse_csi_mode(buffer),
_ => None,
},
b'>' => match buffer[buffer.len() - 2..buffer.len()] {
[b' ', b'q'] => return parse_csi_cursor_shape_query_response(buffer),
_ => None,
},
b'0'..=b'9' => {
// Numbered escape code.
if buffer.len() == 3 {
None
} else {
// The final byte of a CSI sequence can be in the range 64-126, so
// let's keep reading anything else.
let last_byte = buffer[buffer.len() - 1];
if !(64..=126).contains(&last_byte) {
None
} else {
if buffer.starts_with(b"\x1B[200~") {
return parse_csi_bracketed_paste(buffer);
}
match last_byte {
b'M' => return parse_csi_rxvt_mouse(buffer),
b'~' => return parse_csi_special_key_code(buffer),
b'u' => return parse_csi_u_encoded_key_code(buffer),
b'R' => return parse_csi_cursor_position(buffer),
b't' => return parse_csi_pixel_size(buffer),
_ => return parse_csi_modifier_key_code(buffer),
}
}
}
}
_ => bail!(),
};
Ok(maybe_event)
}
fn parse_osc(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(b"\x1B]"));
// > In addition to the ECMA-48 string terminator (ST), xterm(1) accepts a BEL to
// > terminate an OSC string.
// https://www.man7.org/linux/man-pages/man4/console_codes.4.html
let Some(buffer) = buffer
.strip_suffix(escape::ST.as_bytes())
.or_else(|| buffer.strip_suffix(escape::BEL.as_bytes()))
else {
return Ok(None);
};
let s = str::from_utf8(&buffer[2..buffer.len()])?;
if let Some(features) = s.strip_prefix("1337;Capabilities=") {
if !features
.bytes()
.all(|byte| byte.is_ascii_graphic() || byte == b' ')
{
bail!();
}
return Ok(Some(Event::Iterm2Capabilities(features.to_owned())));
}
let mut split = s.split(';');
let index = next_parsed::<u8>(&mut split)?;
let Some(color_number) = osc::DynamicColorNumber::from_index(index) else {
bail!()
};
let Some(color_or_query) = split.next() else {
bail!()
};
let response = match color_or_query {
"?" => osc::ColorOrQuery::Query,
_ => osc::ColorOrQuery::Color(color_or_query.parse().map_err(|_| MalformedSequenceError)?),
};
// This parsing could be expanded, see <https://terminalguide.namepad.de/seq/osc-4/>.
Ok(Some(Event::Osc(osc::Osc::ChangeDynamicColors(
color_number,
vec![response],
))))
}
fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> Result<T>
where
T: str::FromStr,
{
iter.next()
.ok_or(MalformedSequenceError)?
.parse::<T>()
.map_err(|_| MalformedSequenceError)
}
fn modifier_and_kind_parsed(iter: &mut dyn Iterator<Item = &str>) -> Result<(u8, u8)> {
let mut sub_split = iter.next().ok_or(MalformedSequenceError)?.split(':');
let modifier_mask = next_parsed::<u8>(&mut sub_split)?;
if let Ok(kind_code) = next_parsed::<u8>(&mut sub_split) {
Ok((modifier_mask, kind_code))
} else {
Ok((modifier_mask, 1))
}
}
fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(b"\x1B")); // CSI
assert!(buffer.ends_with(b"u"));
// This function parses `CSI … u` sequences. These are sequences defined in either
// the `CSI u` (a.k.a. "Fix Keyboard Input on Terminals - Please", https://www.leonerd.org.uk/hacks/fixterms/)
// or Kitty Keyboard Protocol (https://sw.kovidgoyal.net/kitty/keyboard-protocol/) specifications.
// This CSI sequence is a tuple of semicolon-separated numbers.
let s = str::from_utf8(&buffer[2..buffer.len() - 1])?;
let mut split = s.split(';');
// In `CSI u`, this is parsed as:
//
// CSI codepoint ; modifiers u
// codepoint: ASCII Dec value
//
// The Kitty Keyboard Protocol extends this with optional components that can be
// enabled progressively. The full sequence is parsed as:
//
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
let mut codepoints = split.next().ok_or(MalformedSequenceError)?.split(':');
let codepoint = codepoints
.next()
.ok_or(MalformedSequenceError)?
.parse::<u32>()
.map_err(|_| MalformedSequenceError)?;
let (mut modifiers, kind, state_from_modifiers) =
if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) {
(
parse_modifiers(modifier_mask),
parse_key_event_kind(kind_code),
parse_modifiers_to_state(modifier_mask),
)
} else {
(Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE)
};
let (mut code, state_from_keycode) = {
if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) {
(special_key_code, state)
} else if let Some(c) = char::from_u32(codepoint) {
(
match c {
'\x1B' => KeyCode::Escape,
'\r' => KeyCode::Enter,
/*
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
// newlines as input is because the terminal converts \r into \n for us. When we
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
// use Ctrl+J. Waiting to handle it here means it gets picked up later
'\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter,
*/
'\t' => {
if modifiers.contains(Modifiers::SHIFT) {
KeyCode::BackTab
} else {
KeyCode::Tab
}
}
'\x7F' => KeyCode::Backspace,
_ => KeyCode::Char(c),
},
KeyEventState::empty(),
)
} else {
bail!();
}
};
if let KeyCode::Modifier(modifier_keycode) = code {
match modifier_keycode {
ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt => {
modifiers.set(Modifiers::ALT, true)
}
ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl => {
modifiers.set(Modifiers::CONTROL, true)
}
ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift => {
modifiers.set(Modifiers::SHIFT, true)
}
ModifierKeyCode::LeftSuper | ModifierKeyCode::RightSuper => {
modifiers.set(Modifiers::SUPER, true)
}
ModifierKeyCode::LeftHyper | ModifierKeyCode::RightHyper => {
modifiers.set(Modifiers::HYPER, true)
}
ModifierKeyCode::LeftMeta | ModifierKeyCode::RightMeta => {
modifiers.set(Modifiers::META, true)
}
_ => {}
}
}
// When the "report alternate keys" flag is enabled in the Kitty Keyboard Protocol
// and the terminal sends a keyboard event containing shift, the sequence will
// contain an additional codepoint separated by a ':' character which contains
// the shifted character according to the keyboard layout.
if modifiers.contains(Modifiers::SHIFT) {
if let Some(shifted_c) = codepoints
.next()
.and_then(|codepoint| codepoint.parse::<u32>().ok())
.and_then(char::from_u32)
{
code = KeyCode::Char(shifted_c);
modifiers.set(Modifiers::SHIFT, false);
}
}
let event = Event::Key(KeyEvent {
code,
modifiers,
kind,
state: state_from_keycode | state_from_modifiers,
});
Ok(Some(event))
}
fn parse_modifiers(mask: u8) -> Modifiers {
let modifier_mask = mask.saturating_sub(1);
let mut modifiers = Modifiers::empty();
if modifier_mask & 1 != 0 {
modifiers |= Modifiers::SHIFT;
}
if modifier_mask & 2 != 0 {
modifiers |= Modifiers::ALT;
}
if modifier_mask & 4 != 0 {
modifiers |= Modifiers::CONTROL;
}
if modifier_mask & 8 != 0 {
modifiers |= Modifiers::SUPER;
}
if modifier_mask & 16 != 0 {
modifiers |= Modifiers::HYPER;
}
if modifier_mask & 32 != 0 {
modifiers |= Modifiers::META;
}
modifiers
}
fn parse_modifiers_to_state(mask: u8) -> KeyEventState {
let modifier_mask = mask.saturating_sub(1);
let mut state = KeyEventState::empty();
if modifier_mask & 64 != 0 {
state |= KeyEventState::CAPS_LOCK;
}
if modifier_mask & 128 != 0 {
state |= KeyEventState::NUM_LOCK;
}
state
}
fn parse_key_event_kind(kind: u8) -> KeyEventKind {
match kind {
1 => KeyEventKind::Press,
2 => KeyEventKind::Repeat,
3 => KeyEventKind::Release,
_ => KeyEventKind::Press,
}
}
fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(b"\x1B[")); // CSI
let s = str::from_utf8(&buffer[2..buffer.len() - 1])?;
let mut split = s.split(';');
split.next();
let (modifiers, kind) =
if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) {
(
parse_modifiers(modifier_mask),
parse_key_event_kind(kind_code),
)
} else if buffer.len() > 3 {
(
parse_modifiers(
(buffer[buffer.len() - 2] as char)
.to_digit(10)
.ok_or(MalformedSequenceError)? as u8,
),
KeyEventKind::Press,
)
} else {
(Modifiers::NONE, KeyEventKind::Press)
};
let key = buffer[buffer.len() - 1];
let code = match key {
b'A' => KeyCode::Up,
b'B' => KeyCode::Down,
b'C' => KeyCode::Right,
b'D' => KeyCode::Left,
b'F' => KeyCode::End,
b'H' => KeyCode::Home,
b'P' => KeyCode::Function(1),
b'Q' => KeyCode::Function(2),
b'R' => KeyCode::Function(3),
b'S' => KeyCode::Function(4),
_ => bail!(),
};
let event = Event::Key(KeyEvent {
code,
modifiers,
kind,
state: KeyEventState::NONE,
});
Ok(Some(event))
}
fn parse_csi_special_key_code(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(b"\x1B[")); // CSI
assert!(buffer.ends_with(b"~"));
let s = str::from_utf8(&buffer[2..buffer.len() - 1])?;
let mut split = s.split(';');
// This CSI sequence can be a list of semicolon-separated numbers.
let first = next_parsed::<u8>(&mut split)?;
let (modifiers, kind, state) =
if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) {
(
parse_modifiers(modifier_mask),
parse_key_event_kind(kind_code),
parse_modifiers_to_state(modifier_mask),
)
} else {
(Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE)
};
let code = match first {
1 | 7 => KeyCode::Home,
2 => KeyCode::Insert,
3 => KeyCode::Delete,
4 | 8 => KeyCode::End,
5 => KeyCode::PageUp,
6 => KeyCode::PageDown,
v @ 11..=15 => KeyCode::Function(v - 10),
v @ 17..=21 => KeyCode::Function(v - 11),
v @ 23..=26 => KeyCode::Function(v - 12),
v @ 28..=29 => KeyCode::Function(v - 15),
v @ 31..=34 => KeyCode::Function(v - 17),
_ => bail!(),
};
let event = Event::Key(KeyEvent {
code,
modifiers,
kind,
state,
});
Ok(Some(event))
}
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
if let Some(keycode) = match codepoint {
57399 => Some(KeyCode::Char('0')),
57400 => Some(KeyCode::Char('1')),
57401 => Some(KeyCode::Char('2')),
57402 => Some(KeyCode::Char('3')),
57403 => Some(KeyCode::Char('4')),
57404 => Some(KeyCode::Char('5')),
57405 => Some(KeyCode::Char('6')),
57406 => Some(KeyCode::Char('7')),
57407 => Some(KeyCode::Char('8')),
57408 => Some(KeyCode::Char('9')),
57409 => Some(KeyCode::Char('.')),
57410 => Some(KeyCode::Char('/')),
57411 => Some(KeyCode::Char('*')),
57412 => Some(KeyCode::Char('-')),
57413 => Some(KeyCode::Char('+')),
57414 => Some(KeyCode::Enter),
57415 => Some(KeyCode::Char('=')),
57416 => Some(KeyCode::Char(',')),
57417 => Some(KeyCode::Left),
57418 => Some(KeyCode::Right),
57419 => Some(KeyCode::Up),
57420 => Some(KeyCode::Down),
57421 => Some(KeyCode::PageUp),
57422 => Some(KeyCode::PageDown),
57423 => Some(KeyCode::Home),
57424 => Some(KeyCode::End),
57425 => Some(KeyCode::Insert),
57426 => Some(KeyCode::Delete),
57427 => Some(KeyCode::KeypadBegin),
_ => None,
} {
return Some((keycode, KeyEventState::KEYPAD));
}
if let Some(keycode) = match codepoint {
57358 => Some(KeyCode::CapsLock),
57359 => Some(KeyCode::ScrollLock),
57360 => Some(KeyCode::NumLock),
57361 => Some(KeyCode::PrintScreen),
57362 => Some(KeyCode::Pause),
57363 => Some(KeyCode::Menu),
57376 => Some(KeyCode::Function(13)),
57377 => Some(KeyCode::Function(14)),
57378 => Some(KeyCode::Function(15)),
57379 => Some(KeyCode::Function(16)),
57380 => Some(KeyCode::Function(17)),
57381 => Some(KeyCode::Function(18)),
57382 => Some(KeyCode::Function(19)),
57383 => Some(KeyCode::Function(20)),
57384 => Some(KeyCode::Function(21)),
57385 => Some(KeyCode::Function(22)),
57386 => Some(KeyCode::Function(23)),
57387 => Some(KeyCode::Function(24)),
57388 => Some(KeyCode::Function(25)),
57389 => Some(KeyCode::Function(26)),
57390 => Some(KeyCode::Function(27)),
57391 => Some(KeyCode::Function(28)),
57392 => Some(KeyCode::Function(29)),
57393 => Some(KeyCode::Function(30)),
57394 => Some(KeyCode::Function(31)),
57395 => Some(KeyCode::Function(32)),
57396 => Some(KeyCode::Function(33)),
57397 => Some(KeyCode::Function(34)),
57398 => Some(KeyCode::Function(35)),
57428 => Some(KeyCode::Media(MediaKeyCode::Play)),
57429 => Some(KeyCode::Media(MediaKeyCode::Pause)),
57430 => Some(KeyCode::Media(MediaKeyCode::PlayPause)),
57431 => Some(KeyCode::Media(MediaKeyCode::Reverse)),
57432 => Some(KeyCode::Media(MediaKeyCode::Stop)),
57433 => Some(KeyCode::Media(MediaKeyCode::FastForward)),
57434 => Some(KeyCode::Media(MediaKeyCode::Rewind)),
57435 => Some(KeyCode::Media(MediaKeyCode::TrackNext)),
57436 => Some(KeyCode::Media(MediaKeyCode::TrackPrevious)),
57437 => Some(KeyCode::Media(MediaKeyCode::Record)),
57438 => Some(KeyCode::Media(MediaKeyCode::LowerVolume)),
57439 => Some(KeyCode::Media(MediaKeyCode::RaiseVolume)),
57440 => Some(KeyCode::Media(MediaKeyCode::MuteVolume)),
57441 => Some(KeyCode::Modifier(ModifierKeyCode::LeftShift)),
57442 => Some(KeyCode::Modifier(ModifierKeyCode::LeftControl)),
57443 => Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt)),
57444 => Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper)),
57445 => Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper)),
57446 => Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta)),
57447 => Some(KeyCode::Modifier(ModifierKeyCode::RightShift)),
57448 => Some(KeyCode::Modifier(ModifierKeyCode::RightControl)),
57449 => Some(KeyCode::Modifier(ModifierKeyCode::RightAlt)),
57450 => Some(KeyCode::Modifier(ModifierKeyCode::RightSuper)),
57451 => Some(KeyCode::Modifier(ModifierKeyCode::RightHyper)),
57452 => Some(KeyCode::Modifier(ModifierKeyCode::RightMeta)),
57453 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift)),
57454 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift)),
_ => None,
} {
return Some((keycode, KeyEventState::empty()));
}
None
}
fn parse_csi_rxvt_mouse(buffer: &[u8]) -> Result<Option<Event>> {
// rxvt mouse encoding:
// CSI Cb ; Cx ; Cy ; M
assert!(buffer.starts_with(b"\x1B[")); // CSI
assert!(buffer.ends_with(b"M"));
let s = str::from_utf8(&buffer[2..buffer.len() - 1])?;
let mut split = s.split(';');
let cb = next_parsed::<u8>(&mut split)?
.checked_sub(32)
.ok_or(MalformedSequenceError)?;
let (kind, modifiers) = parse_cb(cb)?;
let cx = next_parsed::<u16>(&mut split)?.saturating_sub(1);
let cy = next_parsed::<u16>(&mut split)?.saturating_sub(1);
Ok(Some(Event::Mouse(MouseEvent {
kind,
column: cx,
row: cy,
modifiers,
})))
}
fn parse_csi_normal_mouse(buffer: &[u8]) -> Result<Option<Event>> {
// Normal mouse encoding: CSI M CB Cx Cy (6 characters only).
assert!(buffer.starts_with(b"\x1B[M")); // CSI M
if buffer.len() < 6 {
return Ok(None);
}
let cb = buffer[3].checked_sub(32).ok_or(MalformedSequenceError)?;
let (kind, modifiers) = parse_cb(cb)?;
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
// Mouse positions are encoded as (value + 32), but the upper left
// character position on the terminal is denoted as 1,1.
// So, we need to subtract 32 + 1 (33) to keep it synced with the cursor.
let cx = u16::from(buffer[4].saturating_sub(33));
let cy = u16::from(buffer[5].saturating_sub(33));
Ok(Some(Event::Mouse(MouseEvent {
kind,
column: cx,
row: cy,
modifiers,
})))
}
fn parse_csi_sgr_mouse(buffer: &[u8]) -> Result<Option<Event>> {
// CSI < Cb ; Cx ; Cy (M or m)
assert!(buffer.starts_with(b"\x1B[<")); // CSI <
if !buffer.ends_with(b"m") && !buffer.ends_with(b"M") {
return Ok(None);
}
let s = str::from_utf8(&buffer[3..buffer.len() - 1])?;
let mut split = s.split(';');
let cb = split.next().ok_or(MalformedSequenceError)?;
if cb.is_empty() || !cb.bytes().all(|byte| byte.is_ascii_digit()) {
bail!();
}
let cb = cb.parse::<u32>().map_err(|_| MalformedSequenceError)?;
let x = parse_sgr_coordinate(split.next().ok_or(MalformedSequenceError)?)?;
let y = parse_sgr_coordinate(split.next().ok_or(MalformedSequenceError)?)?;
if split.next().is_some() {
bail!();
}
// Kitty's leave extension ignores every other bit and both coordinates. Recognize it before
// interpreting button/modifier bits, without inventing a location or a button release.
if cb & (1 << 8) != 0 {
return Ok(Some(Event::MouseLeave));
}
let (kind, modifiers) = parse_cb(u8::try_from(cb).map_err(|_| MalformedSequenceError)?)?;
// When button 3 in Cb is used to represent mouse release, you can't tell which button was
// released. SGR mode solves this by having the sequence end with a lowercase m if it's a
// button release and an uppercase M if it's a button press.
//
// We've already checked that the last character is a lowercase or uppercase M at the start of
// this function, so we just need one if.
let kind = if buffer.last() == Some(&b'm') {
match kind {
MouseEventKind::Down(button) => MouseEventKind::Up(button),
other => other,
}
} else {
kind
};
Ok(Some(Event::SgrMouse(SgrMouseEvent {
kind,
x,
y,
modifiers,
})))
}
fn parse_sgr_coordinate(parameter: &str) -> Result<i32> {
let digits = parameter.strip_prefix('-').unwrap_or(parameter);
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
bail!();
}
parameter.parse().map_err(|_| MalformedSequenceError)
}
/// Cb is the byte of a mouse input that contains the button being used, the key modifiers being
/// held and whether the mouse is dragging or not.
///
/// Bit layout of cb, from low to high:
///
/// - button number
/// - button number
/// - shift
/// - meta (alt)
/// - control
/// - mouse is dragging
/// - button number
/// - button number
fn parse_cb(cb: u8) -> Result<(MouseEventKind, Modifiers)> {
let button_number = (cb & 0b0000_0011) | ((cb & 0b1100_0000) >> 4);
let dragging = cb & 0b0010_0000 == 0b0010_0000;
let kind = match (button_number, dragging) {
(0, false) => MouseEventKind::Down(MouseButton::Left),
(1, false) => MouseEventKind::Down(MouseButton::Middle),
(2, false) => MouseEventKind::Down(MouseButton::Right),
(0, true) => MouseEventKind::Drag(MouseButton::Left),
(1, true) => MouseEventKind::Drag(MouseButton::Middle),
(2, true) => MouseEventKind::Drag(MouseButton::Right),
(3, false) => MouseEventKind::Up(MouseButton::Left),
(3, true) | (4, true) | (5, true) => MouseEventKind::Moved,
(4, false) => MouseEventKind::ScrollUp,
(5, false) => MouseEventKind::ScrollDown,
(6, false) => MouseEventKind::ScrollLeft,
(7, false) => MouseEventKind::ScrollRight,
// We do not support other buttons.
_ => bail!(),
};
let mut modifiers = Modifiers::empty();
if cb & 0b0000_0100 == 0b0000_0100 {
modifiers |= Modifiers::SHIFT;
}
if cb & 0b0000_1000 == 0b0000_1000 {
modifiers |= Modifiers::ALT;
}
if cb & 0b0001_0000 == 0b0001_0000 {
modifiers |= Modifiers::CONTROL;
}
Ok((kind, modifiers))
}
fn parse_csi_bracketed_paste(buffer: &[u8]) -> Result<Option<Event>> {
// CSI 2 0 0 ~ pasted text CSI 2 0 1 ~
let buffer = buffer
.strip_prefix(b"\x1b[200~")
.expect("asserted by calling functions");
if let Some(contents) = buffer.strip_suffix(b"\x1b[201~") {
let paste = String::from_utf8_lossy(contents).to_string();
Ok(Some(Event::Paste(paste)))
} else {
Ok(None)
}
}
fn parse_csi_cursor_position(buffer: &[u8]) -> Result<Option<Event>> {
// CSI Cy ; Cx R
// Cy - cursor row number (starting from 1)
// Cx - cursor column number (starting from 1)
assert!(buffer.starts_with(b"\x1B[")); // CSI
assert!(buffer.ends_with(b"R"));
let s = str::from_utf8(&buffer[2..buffer.len() - 1])?;
let mut split = s.split(';');
let line = next_parsed::<NonZeroU16>(&mut split)?.into();
let col = next_parsed::<NonZeroU16>(&mut split)?.into();
Ok(Some(Event::Csi(Csi::Cursor(
csi::Cursor::ActivePositionReport { line, col },
))))
}
fn parse_csi_cursor_shape_query_response(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(b"\x1B[>")); // CSI >
assert!(buffer.ends_with(b" q"));
if buffer.len() < 5 {
// Minimum: ESC [ > SP q
return Ok(None);
}
let s = str::from_utf8(&buffer[3..buffer.len() - 2])?;
// An empty parameter string (CSI > SP q) is a query.
if s.is_empty() {
return Ok(Some(Event::Csi(Csi::Cursor(csi::Cursor::QueryCursorShape))));
}
let caps: Vec<csi::MultiCursorCapability> = s
.split(';')
.filter(|part| !part.is_empty())
.map(|part| {
part.parse::<u8>()
.map_err(|_| MalformedSequenceError)
.and_then(|v| {
csi::MultiCursorCapability::try_from(v).map_err(|_| MalformedSequenceError)
})
})
.collect::<Result<Vec<_>>>()?;
Ok(Some(Event::Csi(Csi::Cursor(
csi::Cursor::CursorShapeQueryResponse(caps),
))))
}
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result<Option<Event>> {
// CSI ? flags u
assert!(buffer.starts_with(b"\x1B[?")); // ESC [ ?
assert!(buffer.ends_with(b"u"));
if buffer.len() < 5 {
return Ok(None);
}
let bits = buffer[3];
let mut flags = KittyKeyboardFlags::empty();
if bits & 1 != 0 {
flags |= KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES;
}
if bits & 2 != 0 {
flags |= KittyKeyboardFlags::REPORT_EVENT_TYPES;
}
if bits & 4 != 0 {
flags |= KittyKeyboardFlags::REPORT_ALTERNATE_KEYS;
}
if bits & 8 != 0 {
flags |= KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
}
// TODO: support this
// if bits & 16 != 0 {
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
// }
Ok(Some(Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(
flags,
)))))
}
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> Result<Option<Event>> {
// CSI 64 ; attr1 ; attr2 ; ... ; attrn ; c
assert!(buffer.starts_with(b"\x1B[?"));
assert!(buffer.ends_with(b"c"));
let parameters = str::from_utf8(&buffer[3..buffer.len() - 1])?;
let mut parameters = parameters.split(';');
let terminal_type = parse_reply_parameter(parameters.next().ok_or(MalformedSequenceError)?)?;
let attributes = parameters
.map(parse_reply_parameter)
.collect::<Result<Vec<_>>>()?;
Ok(Some(Event::Csi(Csi::PrimaryDeviceAttributes(
csi::PrimaryDeviceAttributes {
terminal_type,
attributes,
},
))))
}
fn parse_csi_graphics_attributes(buffer: &[u8]) -> Result<Option<Event>> {
let parameters = str::from_utf8(&buffer[3..buffer.len() - 1])?;
let mut parameters = parameters.split(';');
let item = parse_reply_parameter(parameters.next().ok_or(MalformedSequenceError)?)?;
let status = parse_reply_parameter(parameters.next().ok_or(MalformedSequenceError)?)?;
let values = parameters
.map(|parameter| {
if parameter.is_empty() || !parameter.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(MalformedSequenceError);
}
parameter.parse::<u32>().map_err(|_| MalformedSequenceError)
})
.collect::<Result<Vec<_>>>()?;
Ok(Some(Event::Csi(Csi::GraphicsAttributes(
csi::GraphicsAttributeResponse {
item,
status,
values,
},
))))
}
fn parse_csi_pixel_size(buffer: &[u8]) -> Result<Option<Event>> {
let parameters = str::from_utf8(&buffer[2..buffer.len() - 1])?;
let mut parameters = parameters.split(';');
let kind = parameters.next().ok_or(MalformedSequenceError)?;
let height = parse_reply_parameter(parameters.next().ok_or(MalformedSequenceError)?)?;
let width = parse_reply_parameter(parameters.next().ok_or(MalformedSequenceError)?)?;
if height == 0 || width == 0 {
bail!();
}
if parameters.next().is_some() {
bail!();
}
let window = match kind {
"4" => csi::Window::ReportTextAreaSizePixelsResponse {
width: Some(i64::from(width)),
height: Some(i64::from(height)),
},
"6" => csi::Window::ReportCellSizePixelsResponse {
width: Some(i64::from(width)),
height: Some(i64::from(height)),
},
_ => bail!(),
};
Ok(Some(Event::Csi(Csi::Window(Box::new(window)))))
}
fn parse_reply_parameter(parameter: &str) -> Result<u16> {
if parameter.is_empty() || !parameter.bytes().all(|byte| byte.is_ascii_digit()) {
bail!();
}
parameter.parse().map_err(|_| MalformedSequenceError)
}
fn parse_csi_theme_mode(buffer: &[u8]) -> Result<Option<Event>> {
// dark mode: CSI ? 997 ; 1 n
// light mode: CSI ? 997 ; 2 n
assert!(buffer.starts_with(b"\x1B[?"));
assert!(buffer.ends_with(b"n"));
let s = str::from_utf8(&buffer[3..buffer.len() - 1])?;
let mut split = s.split(';');
if next_parsed::<u16>(&mut split)? != 997 {
bail!();
}
let theme_mode = match next_parsed::<u8>(&mut split)? {
1 => ThemeMode::Dark,
2 => ThemeMode::Light,
_ => bail!(),
};
Ok(Some(Event::Csi(Csi::Mode(csi::Mode::ReportTheme(
theme_mode,
)))))
}
fn parse_csi_mode(buffer: &[u8]) -> Result<Option<Event>> {
// pixel mouse mode: CSI ? 1016 ; 2 $ y
// sync output mode: CSI ? 2026 ; 0 $ y
// grapheme clustering: CSI ? 2027 ; 1 $ y
assert!(buffer.starts_with(b"\x1B[?"));
assert!(buffer.ends_with(b"y"));
let s = str::from_utf8(&buffer[3..buffer.len() - 1])?;
let s = match s.strip_suffix('$') {
Some(s) => s,
None => bail!(),
};
let mut split = s.split(';');
let mode = match parse_reply_parameter(split.next().ok_or(MalformedSequenceError)?)? {
80 => csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SixelDisplayMode),
1016 => csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SGRPixelsMouse),
2026 => csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput),
2027 => csi::DecPrivateMode::Code(csi::DecPrivateModeCode::GraphemeClustering),
_ => bail!(),
};
let setting = match parse_reply_parameter(split.next().ok_or(MalformedSequenceError)?)? {
// For synchronized output specifically, 3 is undefined and 0 and 4 are treated as "not
// supported."
0 | 4 if mode == csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput) => {
csi::DecModeSetting::NotRecognized
}
0 => csi::DecModeSetting::NotRecognized,
1 => csi::DecModeSetting::Set,
2 => csi::DecModeSetting::Reset,
3 if matches!(
mode,
csi::DecPrivateMode::Code(
csi::DecPrivateModeCode::SixelDisplayMode
| csi::DecPrivateModeCode::GraphemeClustering
| csi::DecPrivateModeCode::SGRPixelsMouse
)
) =>
{
csi::DecModeSetting::PermanentlySet
}
4 => csi::DecModeSetting::PermanentlyReset,
_ => bail!(),
};
if split.next().is_some() {
bail!();
}
Ok(Some(Event::Csi(Csi::Mode(
csi::Mode::ReportDecPrivateMode { mode, setting },
))))
}
fn parse_dcs(buffer: &[u8]) -> Result<Option<Event>> {
assert!(buffer.starts_with(escape::DCS.as_bytes()));
if !buffer.ends_with(escape::ST.as_bytes()) {
return Ok(None);
}
if let Some(value) = buffer.strip_prefix(b"\x1bP>|") {
let value = &value[..value.len() - escape::ST.len()];
if value.is_empty()
|| !value
.iter()
.all(|byte| byte.is_ascii_graphic() || *byte == b' ')
{
bail!();
}
return Ok(Some(Event::Dcs(dcs::Dcs::TerminalNameAndVersion(
str::from_utf8(value)?.to_owned(),
))));
}
match buffer[buffer.len() - 3] {
// SGR response: DCS Ps $ r SGR m ST
b'm' => {
if buffer.get(3..5) != Some(b"$r") {
bail!();
}
// NOTE: <https://www.xfree86.org/current/ctlseqs.html> says that '1' is a valid
// request and '0' is invalid while the vt100.net docs for DECRQSS say the opposite.
// Kitty and WezTerm both follow the ctlseqs doc.
let is_request_valid = match buffer[2] {
b'1' => true,
// TODO: don't parse attributes if the request isn't valid?
b'0' => false,
_ => bail!(),
};
let s = str::from_utf8(&buffer[5..buffer.len() - 3])?;
let mut sgrs = Vec::new();
// TODO: is this correct? What about terminals that use ';' for true colors?
for sgr in s.split(';') {
sgrs.push(parse_sgr(sgr)?);
}
Ok(Some(Event::Dcs(dcs::Dcs::Response {
is_request_valid,
value: dcs::DcsResponse::GraphicRendition(sgrs),
})))
}
_ => bail!(),
}
}
fn parse_sgr(buffer: &str) -> Result<csi::Sgr> {
use csi::Sgr;
use style::*;
let sgr = match buffer {
"0" => Sgr::Reset,
"22" => Sgr::Intensity(Intensity::Normal),
"1" => Sgr::Intensity(Intensity::Bold),
"2" => Sgr::Intensity(Intensity::Dim),
"24" => Sgr::Underline(Underline::None),
"4" => Sgr::Underline(Underline::Single),
"21" => Sgr::Underline(Underline::Double),
"4:3" => Sgr::Underline(Underline::Curly),
"4:4" => Sgr::Underline(Underline::Dotted),
"4:5" => Sgr::Underline(Underline::Dashed),
"25" => Sgr::Blink(Blink::None),
"5" => Sgr::Blink(Blink::Slow),
"6" => Sgr::Blink(Blink::Rapid),
"3" => Sgr::Italic(true),
"23" => Sgr::Italic(false),
"7" => Sgr::Reverse(true),
"27" => Sgr::Reverse(false),
"8" => Sgr::Invisible(true),
"28" => Sgr::Invisible(false),
"9" => Sgr::StrikeThrough(true),
"29" => Sgr::StrikeThrough(false),
"53" => Sgr::Overline(true),
"55" => Sgr::Overline(false),
"10" => Sgr::Font(Font::Default),
"11" => Sgr::Font(Font::Alternate(1)),
"12" => Sgr::Font(Font::Alternate(2)),
"13" => Sgr::Font(Font::Alternate(3)),
"14" => Sgr::Font(Font::Alternate(4)),
"15" => Sgr::Font(Font::Alternate(5)),
"16" => Sgr::Font(Font::Alternate(6)),
"17" => Sgr::Font(Font::Alternate(7)),
"18" => Sgr::Font(Font::Alternate(8)),
"19" => Sgr::Font(Font::Alternate(9)),
"75" => Sgr::VerticalAlign(VerticalAlign::BaseLine),
"73" => Sgr::VerticalAlign(VerticalAlign::SuperScript),
"74" => Sgr::VerticalAlign(VerticalAlign::SubScript),
"39" => Sgr::Foreground(ColorSpec::Reset),
"30" => Sgr::Foreground(ColorSpec::BLACK),
"31" => Sgr::Foreground(ColorSpec::RED),
"32" => Sgr::Foreground(ColorSpec::GREEN),
"33" => Sgr::Foreground(ColorSpec::YELLOW),
"34" => Sgr::Foreground(ColorSpec::BLUE),
"35" => Sgr::Foreground(ColorSpec::MAGENTA),
"36" => Sgr::Foreground(ColorSpec::CYAN),
"37" => Sgr::Foreground(ColorSpec::WHITE),
"90" => Sgr::Foreground(ColorSpec::BRIGHT_BLACK),
"91" => Sgr::Foreground(ColorSpec::BRIGHT_RED),
"92" => Sgr::Foreground(ColorSpec::BRIGHT_GREEN),
"93" => Sgr::Foreground(ColorSpec::BRIGHT_YELLOW),
"94" => Sgr::Foreground(ColorSpec::BRIGHT_BLUE),
"95" => Sgr::Foreground(ColorSpec::BRIGHT_MAGENTA),
"96" => Sgr::Foreground(ColorSpec::BRIGHT_CYAN),
"97" => Sgr::Foreground(ColorSpec::BRIGHT_WHITE),
"49" => Sgr::Background(ColorSpec::Reset),
"40" => Sgr::Background(ColorSpec::BLACK),
"41" => Sgr::Background(ColorSpec::RED),
"42" => Sgr::Background(ColorSpec::GREEN),
"43" => Sgr::Background(ColorSpec::YELLOW),
"44" => Sgr::Background(ColorSpec::BLUE),
"45" => Sgr::Background(ColorSpec::MAGENTA),
"46" => Sgr::Background(ColorSpec::CYAN),
"47" => Sgr::Background(ColorSpec::WHITE),
"100" => Sgr::Background(ColorSpec::BRIGHT_BLACK),
"101" => Sgr::Background(ColorSpec::BRIGHT_RED),
"102" => Sgr::Background(ColorSpec::BRIGHT_GREEN),
"103" => Sgr::Background(ColorSpec::BRIGHT_YELLOW),
"104" => Sgr::Background(ColorSpec::BRIGHT_BLUE),
"105" => Sgr::Background(ColorSpec::BRIGHT_MAGENTA),
"106" => Sgr::Background(ColorSpec::BRIGHT_CYAN),
"107" => Sgr::Background(ColorSpec::BRIGHT_WHITE),
"59" => Sgr::UnderlineColor(ColorSpec::Reset),
_ => {
let mut split = buffer.split(':').filter(|s| !s.is_empty());
let first = next_parsed::<u8>(&mut split)?;
let color = match next_parsed::<u8>(&mut split)? {
2 => RgbColor {
red: next_parsed::<u8>(&mut split)?,
green: next_parsed::<u8>(&mut split)?,
blue: next_parsed::<u8>(&mut split)?,
}
.into(),
5 => ColorSpec::PaletteIndex(next_parsed::<u8>(&mut split)?),
6 => RgbaColor {
red: next_parsed::<u8>(&mut split)?,
green: next_parsed::<u8>(&mut split)?,
blue: next_parsed::<u8>(&mut split)?,
alpha: next_parsed::<u8>(&mut split)?,
}
.into(),
_ => bail!(),
};
match first {
38 => Sgr::Foreground(color),
48 => Sgr::Background(color),
58 => Sgr::UnderlineColor(color),
_ => bail!(),
}
}
};
Ok(sgr)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn graphics_replies_preserve_input_and_paste_at_every_byte_boundary() {
let wire = concat!(
"你\x1b_Gi=31;OK\x1b\\\x1b]1337;Capabilities=T3FFSxSy;future=1\x1b\\",
"\x1bP>|XTerm(411)\x1b\\\x1b[?1016;2$y\x1b[?64;1;4;22c",
"\x1b[?2;0;1000;800S\x1b[?2;0;0;0S",
"\x1b[?2;1S\x1b[?2;2S\x1b[?2;3S\x1b[?17;99;4294967295S\x10",
"\x1b[6;16;8t\x1b[4;680;960t\x1b_Gp=1,i=32;ENOENT: missing image\x1b\\",
"\x1b[<0;4;5M\x1b[<0;4;5m\x1b[<288;-1;-20M",
"\x1b[200~literal \x1b[?64;4c \x1b[?2;0;1000;800S ",
"\x1b[6;16;8t \x1b_Gi=33;OK\x1b\\\x1b[201~z",
);
let expected = vec![
Event::Key(KeyCode::Char('你').into()),
Event::KittyGraphicsResponse(KittyGraphicsResponse {
image_id: 31,
image_number: None,
placement_id: None,
result: Ok(()),
}),
Event::Iterm2Capabilities("T3FFSxSy;future=1".to_owned()),
Event::Dcs(dcs::Dcs::TerminalNameAndVersion("XTerm(411)".to_owned())),
Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode {
mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SGRPixelsMouse),
setting: csi::DecModeSetting::Reset,
})),
Event::Csi(Csi::PrimaryDeviceAttributes(csi::PrimaryDeviceAttributes {
terminal_type: 64,
attributes: vec![1, 4, 22],
})),
Event::Csi(Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 0,
values: vec![1000, 800],
})),
Event::Csi(Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 0,
values: vec![0, 0],
})),
Event::Csi(Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 1,
values: vec![],
})),
Event::Csi(Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 2,
values: vec![],
})),
Event::Csi(Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 3,
values: vec![],
})),
Event::Csi(Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 17,
status: 99,
values: vec![4294967295],
})),
Event::Key(KeyEvent::new(KeyCode::Char('p'), Modifiers::CONTROL)),
Event::Csi(Csi::Window(Box::new(
csi::Window::ReportCellSizePixelsResponse {
width: Some(8),
height: Some(16),
},
))),
Event::Csi(Csi::Window(Box::new(
csi::Window::ReportTextAreaSizePixelsResponse {
width: Some(960),
height: Some(680),
},
))),
Event::KittyGraphicsResponse(KittyGraphicsResponse {
image_id: 32,
image_number: None,
placement_id: Some(1),
result: Err("ENOENT: missing image".to_owned()),
}),
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
x: 4,
y: 5,
modifiers: Modifiers::NONE,
}),
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
x: 4,
y: 5,
modifiers: Modifiers::NONE,
}),
Event::MouseLeave,
Event::Paste(
concat!(
"literal \x1b[?64;4c \x1b[?2;0;1000;800S ",
"\x1b[6;16;8t \x1b_Gi=33;OK\x1b\\",
)
.to_string(),
),
Event::Key(KeyCode::Char('z').into()),
];
for split in 0..=wire.len() {
let mut parser = Parser::default();
parser.parse(&wire.as_bytes()[..split], true).unwrap();
parser.parse(&wire.as_bytes()[split..], false).unwrap();
let actual: Vec<_> = std::iter::from_fn(|| parser.pop()).collect();
assert_eq!(actual, expected, "split at byte {split}");
}
assert_eq!(
Csi::PrimaryDeviceAttributes(csi::PrimaryDeviceAttributes {
terminal_type: 64,
attributes: vec![1, 4, 22],
})
.to_string(),
"\x1b[?64;1;4;22c"
);
assert_eq!(
Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 0,
values: vec![1000, 800],
})
.to_string(),
"\x1b[?2;0;1000;800S"
);
assert_eq!(
Csi::GraphicsAttributes(csi::GraphicsAttributeResponse {
item: 2,
status: 3,
values: vec![],
})
.to_string(),
"\x1b[?2;3S"
);
}
#[test]
fn kitty_graphics_reply_preserves_identifiers_and_exact_status() {
let mut parser = Parser::default();
parser
.parse(
b"\x1b_GI=13,p=4294967295,i=0;OK\x1b\\\x1b_Gi=99;OK extra\x1b\\",
false,
)
.unwrap();
let response = parser.pop().unwrap();
assert!(response.is_escape());
assert_eq!(
response,
Event::KittyGraphicsResponse(KittyGraphicsResponse {
image_id: 0,
image_number: Some(13),
placement_id: Some(u32::MAX),
result: Ok(()),
})
);
assert_eq!(
parser.pop(),
Some(Event::KittyGraphicsResponse(KittyGraphicsResponse {
image_id: 99,
image_number: None,
placement_id: None,
result: Err("OK extra".to_owned()),
}))
);
assert_eq!(parser.pop(), None);
}
#[test]
fn malformed_or_unknown_apc_never_becomes_keys_and_keeps_following_input() {
for invalid in [
"unknown APC",
"G;OK",
"GI=13;OK",
"Gi=1,i=2;OK",
"Gi=1,p=2,p=3;OK",
"Gi=1,I=2,I=3;OK",
"Gi=+1;OK",
"Gi=-1;OK",
"Gi=4294967296;OK",
"Gi=;OK",
"Gi=1,;OK",
"Gi=1,x=2;OK",
"Gi=1;",
"Gi=1;不行",
"Gi=1;bad\x07message",
"Gi=1;bad\x1b[Amesssage",
] {
let wire = format!("\x1b_{invalid}\x1b\\你\x1b[A\x03");
let mut parser = Parser::default();
parser.parse(wire.as_bytes(), false).unwrap();
let actual: Vec<_> = std::iter::from_fn(|| parser.pop()).collect();
assert_eq!(
actual,
vec![
Event::Key(KeyCode::Char('你').into()),
Event::Key(KeyCode::Up.into()),
Event::Key(KeyEvent::new(KeyCode::Char('c'), Modifiers::CONTROL)),
],
"{invalid:?}"
);
}
}
#[test]
fn graphics_csi_parameters_are_decimal_and_cell_pixels_are_nonzero() {
for invalid in [
b"\x1b[?+64;4c".as_slice(),
b"\x1b[?64;+4c",
b"\x1b[?64;;4c",
b"\x1b[?64;65536c",
b"\x1b[?S",
b"\x1b[?2S",
b"\x1b[?;0S",
b"\x1b[?+2;0;1000;800S",
b"\x1b[?65536;0S",
b"\x1b[?2;+0S",
b"\x1b[?2;65536S",
b"\x1b[?2;;0S",
b"\x1b[?2;0;+1000;800S",
b"\x1b[?2;0;1:2;800S",
b"\x1b[?2;0;4294967296;800S",
b"\x1b[?2;0;1000;S",
b"\x1b[6;+16;8t",
b"\x1b[6;16;+8t",
b"\x1b[6;0;8t",
b"\x1b[6;16;0t",
b"\x1b[6;16;65536t",
b"\x1b[6;16;8;1t",
b"\x1b[4;0;960t",
b"\x1b[4;680;0t",
b"\x1b[4;+680;960t",
b"\x1b[4;680;+960t",
b"\x1b[4;680;65536t",
b"\x1b[4;680;960;1t",
] {
assert!(parse_event(invalid, false).is_err(), "{invalid:?}");
let start = Instant::now();
let mut parser = Parser::default();
parser.parse_at(invalid, false, start).unwrap();
parser
.parse_at(b"\x10", false, start + Duration::from_millis(100))
.unwrap();
assert_eq!(
parser.pop(),
Some(Event::Key(KeyEvent::new(
KeyCode::Char('p'),
Modifiers::CONTROL
))),
"{invalid:?}"
);
assert_eq!(parser.pop(), None, "{invalid:?}");
}
}
#[test]
fn pending_limit_keeps_a_full_64k_paste_and_does_not_sum_completed_events() {
let mut parser = Parser::default();
parser.set_max_pending_bytes(64 * 1024 + 12).unwrap();
let text = "x".repeat(64 * 1024);
let wire = format!("\x1b[200~{text}\x1b[201~");
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(parser.pop(), Some(Event::Paste(text)));
assert_eq!(parser.pop(), None);
let mut parser = Parser::default();
parser.set_max_pending_bytes(1).unwrap();
parser.parse(b"abcdefghijklmnopqrstuvwxyz", false).unwrap();
let actual: Vec<_> = std::iter::from_fn(|| parser.pop()).collect();
assert_eq!(
actual,
"abcdefghijklmnopqrstuvwxyz"
.chars()
.map(|character| Event::Key(KeyCode::Char(character).into()))
.collect::<Vec<_>>()
);
}
#[test]
fn pending_limit_failure_is_sticky_and_never_replays_the_tail() {
let mut parser = Parser::default();
assert_eq!(
parser.set_max_pending_bytes(0).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
parser.set_max_pending_bytes(8).unwrap();
assert_eq!(
parser.parse(b"\x1b[6;2;0000", true).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert!(parser.buffer.len() <= 8);
assert_eq!(
parser.set_max_pending_bytes(1024).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(
parser.parse(b"~z", false).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(parser.pop(), None);
}
#[test]
fn private_csi_replies_expire_without_more_bytes_but_numeric_keys_do_not() {
let start = Instant::now();
let mut parser = Parser::default();
parser.parse_at(b"\x1b[?64;4", true, start).unwrap();
assert_eq!(
parser
.parse_at(b"", false, start + Duration::from_millis(100))
.unwrap_err()
.kind(),
io::ErrorKind::TimedOut
);
assert_eq!(
parser
.parse_at(b"cz", false, start + Duration::from_millis(101))
.unwrap_err()
.kind(),
io::ErrorKind::TimedOut
);
assert_eq!(parser.pop(), None);
let mut parser = Parser::default();
parser.parse_at(b"\x1b[6;2", true, start).unwrap();
parser
.parse_at(b"~", false, start + Duration::from_secs(1))
.unwrap();
assert_eq!(
parser.pop(),
Some(Event::Key(KeyEvent::new(
KeyCode::PageDown,
Modifiers::SHIFT
)))
);
let mut parser = Parser::default();
parser
.parse_at(b"\x1b[200~\x1b[?64;4", true, start)
.unwrap();
parser
.parse_at(b"c\x1b[201~", false, start + Duration::from_secs(1))
.unwrap();
assert_eq!(parser.pop(), Some(Event::Paste("\x1b[?64;4c".to_string())));
}
#[test]
fn terminal_metadata_bounds_keep_completed_replies_and_never_replay_rejected_tails() {
let mut parser = Parser::default();
let wire = format!("\x1b_Gi=31;{}\x1b\\", "E".repeat(246));
assert_eq!(wire.len(), 256);
parser.parse(wire.as_bytes(), false).unwrap();
assert!(matches!(
parser.pop(),
Some(Event::KittyGraphicsResponse(KittyGraphicsResponse { image_id: 31, result: Err(message), .. }))
if message.len() == 246
));
assert_eq!(parser.pop(), None);
let start = Instant::now();
for prefix in [
b"\x1b_".as_slice(),
b"\x1b_Gi=31;OK\x1b",
b"\x1b_unknown",
b"\x1b]1337;Capabilities=FF",
b"\x1bP>|XTerm(411)",
] {
let mut parser = Parser::default();
parser.parse_at(b"x", false, start).unwrap();
parser.parse_at(prefix, false, start).unwrap();
assert_eq!(parser.pop(), Some(Event::Key(KeyCode::Char('x').into())));
assert_eq!(parser.pop(), None);
assert_eq!(
parser
.expire_reply_at(start + Duration::from_millis(100))
.unwrap_err()
.kind(),
io::ErrorKind::TimedOut
);
assert_eq!(
parser.parse(b"\\z", false).unwrap_err().kind(),
io::ErrorKind::TimedOut
);
assert_eq!(parser.pop(), None);
}
for prefix in ["\x1b_Gi=31;", "\x1b]1337;Capabilities=", "\x1bP>|"] {
let mut parser = Parser::default();
let wire = format!("x{prefix}{}", "E".repeat(300));
assert_eq!(
parser.parse(wire.as_bytes(), true).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(parser.pop(), Some(Event::Key(KeyCode::Char('x').into())));
assert!(parser.buffer.len() <= 256);
assert_eq!(
parser.parse(b"\x1b\\z", false).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(parser.pop(), None);
}
// Other OSC 1337 replies retain their existing framing and resource behavior.
let mut parser = Parser::default();
let wire = format!("\x1b]1337;ReportVariable={}\x1b\\z", "E".repeat(300));
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(parser.pop(), Some(Event::Key(KeyCode::Char('z').into())));
assert_eq!(parser.pop(), None);
}
#[test]
fn private_csi_reply_byte_limit_is_sticky_before_its_deadline() {
let mut parser = Parser::default();
let wire = format!("\x1b[?64;{}", "0".repeat(300));
assert_eq!(
parser.parse(wire.as_bytes(), true).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert!(parser.buffer.len() <= 256);
assert_eq!(
parser.parse(b"cz", false).unwrap_err().kind(),
io::ErrorKind::InvalidData
);
assert_eq!(parser.pop(), None);
}
#[test]
fn parse_dcs_sgr_response() {
// Example from <https://vt100.net/docs/vt510-rm/DECRPSS.html>
// > If the current graphic rendition is underline, blinking, and reverse, then the
// > terminal responds with the following DECRPSS sequence:
// > DCS 0 $ r 0 ; 4 ; 5 ; 7 m ST
// NOTE: The vt100.net docs have the Ps part of this reversed. 0 is invalid and 1 is
// valid according to the xterm docs. See `parse_dcs`.
let event = parse_event(b"\x1bP0$r0;4;5;7m\x1b\\", false)
.unwrap()
.unwrap();
assert_eq!(
event,
Event::Dcs(dcs::Dcs::Response {
is_request_valid: false,
value: dcs::DcsResponse::GraphicRendition(vec![
csi::Sgr::Reset,
csi::Sgr::Underline(style::Underline::Single),
csi::Sgr::Blink(style::Blink::Slow),
csi::Sgr::Reverse(true),
])
})
);
}
#[test]
fn parse_dcs_sgr_curly_underline() {
// A DECRPSS reply describing a curly (`4:3`) underline must round-trip through `parse_sgr`.
let event = parse_event(b"\x1bP1$r4:3m\x1b\\", false).unwrap().unwrap();
assert_eq!(
event,
Event::Dcs(dcs::Dcs::Response {
is_request_valid: true,
value: dcs::DcsResponse::GraphicRendition(vec![csi::Sgr::Underline(
style::Underline::Curly
)])
})
);
}
#[test]
fn parse_osc_dynamic_color_response() {
assert_eq!(
parse_event(b"\x1b]11;rgb:2828/2828/2828\x1b\\", false)
.unwrap()
.unwrap(),
Event::Osc(osc::Osc::ChangeDynamicColors(
osc::DynamicColorNumber::TextBackgroundColor,
vec![style::RgbColor::new(40, 40, 40).into()]
))
);
// BEL ending instead of ST
assert_eq!(
parse_event(b"\x1b]11;rgb:2828/2828/2828\x07", false)
.unwrap()
.unwrap(),
Event::Osc(osc::Osc::ChangeDynamicColors(
osc::DynamicColorNumber::TextBackgroundColor,
vec![style::RgbColor::new(40, 40, 40).into()]
))
);
}
#[test]
fn iterm_capabilities_preserve_raw_features_and_complete_osc_framing() {
for (wire, features) in [
("\x1b]1337;Capabilities=\x07", ""),
("\x1b]1337;Capabilities=F\x07", "F"),
("\x1b]1337;Capabilities=FFSxSy\x1b\\", "FFSxSy"),
("\x1b]1337;Capabilities=F0F2;future !\x07", "F0F2;future !"),
] {
let mut parser = Parser::default();
parser.parse(wire.as_bytes(), false).unwrap();
let event = parser.pop().unwrap();
assert!(event.is_escape());
assert_eq!(event, Event::Iterm2Capabilities(features.to_owned()));
assert_eq!(parser.pop(), None);
}
for invalid in [
"\x1b]1337;Capabilities\x07",
"\x1b]1337;capabilities=FF\x07",
"\x1b]1337;Capabilities=FF\u{1}\x07",
"\x1b]1337;Capabilities=图\x07",
"\x1b]1337;File=FF\x1b\\",
] {
let mut parser = Parser::default();
let wire = format!("{invalid}你\x03");
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(parser.pop(), Some(Event::Key(KeyCode::Char('你').into())));
assert_eq!(
parser.pop(),
Some(Event::Key(KeyEvent::new(
KeyCode::Char('c'),
Modifiers::CONTROL
)))
);
assert_eq!(parser.pop(), None);
}
}
#[test]
fn terminal_version_reply_preserves_identity_without_parsing_a_product() {
for value in [
"XTerm(411)",
"kitty(0.45.0)",
"ghostty 1.3.0",
"Other terminal",
] {
let wire = format!("\x1bP>|{value}\x1b\\");
let mut parser = Parser::default();
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(
parser.pop(),
Some(Event::Dcs(dcs::Dcs::TerminalNameAndVersion(
value.to_owned()
)))
);
assert_eq!(parser.pop(), None);
}
for invalid in [
"\x1bP>|\x1b\\",
"\x1bP>|XTerm\u{1}\x1b\\",
"\x1bP>|终端\x1b\\",
"\x1bP>!XTerm(411)\x1b\\",
] {
let mut parser = Parser::default();
let wire = format!("{invalid}z");
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(parser.pop(), Some(Event::Key(KeyCode::Char('z').into())));
assert_eq!(parser.pop(), None);
}
}
#[test]
fn parse_cursor_shape_query() {
// CSI > SP q with no parameters is a query.
let event = parse_event(b"\x1b[> q", false).unwrap().unwrap();
assert_eq!(
event,
Event::Csi(Csi::Cursor(csi::Cursor::QueryCursorShape))
);
}
#[test]
fn parse_cursor_shape_query_response() {
// Kitty responds with the supported operation codes.
let event = parse_event(b"\x1b[>1;2;29;100 q", false).unwrap().unwrap();
assert_eq!(
event,
Event::Csi(Csi::Cursor(csi::Cursor::CursorShapeQueryResponse(vec![
csi::MultiCursorCapability::BlockShape,
csi::MultiCursorCapability::BeamShape,
csi::MultiCursorCapability::FollowMainCursorShape,
csi::MultiCursorCapability::QueryCurrentCursors,
])))
);
}
#[test]
fn parse_cursor_shape_query_response_invalid() {
// Value 7 is not a valid MultiCursorCapability code.
assert!(parse_event(b"\x1b[>7 q", false).is_err());
}
#[test]
fn cursor_shape_query_response_round_trip() {
let response = csi::Cursor::CursorShapeQueryResponse(vec![
csi::MultiCursorCapability::BlockShape,
csi::MultiCursorCapability::FollowMainCursorShape,
csi::MultiCursorCapability::QueryCurrentCursors,
]);
let encoded = Csi::Cursor(response.clone()).to_string();
assert_eq!(encoded, "\x1b[>1;29;100 q");
let parsed = parse_event(encoded.as_bytes(), false).unwrap().unwrap();
assert_eq!(parsed, Event::Csi(Csi::Cursor(response)));
}
#[test]
fn parse_synchronized_output_mode_set() {
let event = parse_event(b"\x1b[?2026;1$y", false).unwrap().unwrap();
assert_eq!(
event,
Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode {
mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput),
setting: csi::DecModeSetting::Set,
}))
);
}
#[test]
fn parse_grapheme_clustering_mode_set() {
let event = parse_event(b"\x1b[?2027;1$y", false).unwrap().unwrap();
assert_eq!(
event,
Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode {
mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::GraphemeClustering),
setting: csi::DecModeSetting::Set,
}))
);
}
#[test]
fn pixel_mouse_and_sixel_mode_reports_preserve_all_five_states() {
for (code, mode) in [
(80, csi::DecPrivateModeCode::SixelDisplayMode),
(1016, csi::DecPrivateModeCode::SGRPixelsMouse),
] {
for (value, setting) in [
(0, csi::DecModeSetting::NotRecognized),
(1, csi::DecModeSetting::Set),
(2, csi::DecModeSetting::Reset),
(3, csi::DecModeSetting::PermanentlySet),
(4, csi::DecModeSetting::PermanentlyReset),
] {
let wire = format!("\x1b[?{code};{value}$y");
let mut parser = Parser::default();
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(
parser.pop(),
Some(Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode {
mode: csi::DecPrivateMode::Code(mode),
setting,
})))
);
assert_eq!(parser.pop(), None);
}
}
for invalid in [
b"\x1b[?+1016;1$y".as_slice(),
b"\x1b[?1016;+1$y",
b"\x1b[?1016;5$y",
b"\x1b[?1016;;1$y",
b"\x1b[?1016;1;2$y",
] {
assert!(parse_event(invalid, false).is_err(), "{invalid:?}");
}
assert_eq!(
Csi::Window(Box::new(csi::Window::ReportTextAreaSizePixelsResponse {
width: Some(960),
height: Some(680),
}))
.to_string(),
"\x1b[4;680;960t"
);
}
#[test]
fn sgr_mouse_keeps_signed_wire_positions_and_legacy_mouse_keeps_cells() {
let mut parser = Parser::default();
parser
.parse(
b"\x1b[<0;0;1M\x1b[<0;1;0m\x1b[<35;-17;70000M\x1b[<60;-2147483648;2147483647M\x1b[<64;10;20M\x1b[32;33;34M\x1b[M !!",
false,
)
.unwrap();
let actual: Vec<_> = std::iter::from_fn(|| parser.pop()).collect();
assert_eq!(
actual,
vec![
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
x: 0,
y: 1,
modifiers: Modifiers::NONE,
}),
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
x: 1,
y: 0,
modifiers: Modifiers::NONE,
}),
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Moved,
x: -17,
y: 70000,
modifiers: Modifiers::NONE,
}),
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
x: i32::MIN,
y: i32::MAX,
modifiers: Modifiers::SHIFT | Modifiers::ALT | Modifiers::CONTROL,
}),
Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::ScrollUp,
x: 10,
y: 20,
modifiers: Modifiers::NONE,
}),
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 32,
row: 33,
modifiers: Modifiers::NONE,
}),
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 0,
row: 0,
modifiers: Modifiers::NONE,
}),
]
);
}
#[test]
fn pixel_mouse_leave_ignores_button_bits_and_coordinates_but_not_bad_framing() {
for cb in [256, 288, 511, u32::MAX] {
let mut parser = Parser::default();
let wire = format!("\x1b[<{cb};-17;-99M\x1b[<0;0;1m");
parser.parse(wire.as_bytes(), false).unwrap();
assert_eq!(parser.pop(), Some(Event::MouseLeave));
assert_eq!(
parser.pop(),
Some(Event::SgrMouse(SgrMouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
x: 0,
y: 1,
modifiers: Modifiers::NONE,
}))
);
assert_eq!(parser.pop(), None);
}
for invalid in [
b"\x1b[<+288;0;0M".as_slice(),
b"\x1b[<-288;0;0M",
b"\x1b[<4294967296;0;0M",
b"\x1b[<288;;0M",
b"\x1b[<288;0M",
b"\x1b[<288;0;0;1M",
b"\x1b[<288;0;+1M",
b"\x1b[<288;0;-M",
b"\x1b[<288;0;2147483648M",
b"\x1b[<288;-2147483649;0M",
b"\x1b[<512;0;0M",
] {
assert!(parse_event(invalid, false).is_err(), "{invalid:?}");
}
}
#[test]
fn parse_bracketed_paste() {
// Incomplete input is not considered a paste.
let event = parse_event(b"\x1b[200~", false).unwrap();
assert_eq!(event, None);
let event = parse_event(b"\x1b[200~Hello, world!\x1b[201~", false).unwrap();
assert_eq!(event, Some(Event::Paste("Hello, world!".to_string())));
let event = parse_event(b"\x1b[200~\x1b[201~", false).unwrap();
assert_eq!(event, Some(Event::Paste("".to_string())));
}
}