// SPDX-License-Identifier: Apache-2.0 // // This module was originally part of the `alacritty_terminal` crate, which is // licensed under the Apache License, Version 2.0 and is part of the Alacritty // project (https://github.com/alacritty/alacritty). //! ANSI Terminal Stream Parsing. extern crate alloc; use alloc::borrow::ToOwned; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::convert::TryFrom; use core::fmt::{self, Display, Formatter, Write}; #[cfg(not(feature = "no_std"))] use core::ops::Mul; use core::ops::{Add, Sub}; use core::str::FromStr; use core::time::Duration; use core::{iter, mem, str}; #[cfg(not(feature = "no_std"))] use std::time::Instant; use bitflags::bitflags; #[doc(inline)] pub use cursor_icon; use cursor_icon::CursorIcon; use log::debug; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::{Params, ParamsIter}; /// Maximum time before a synchronized update is aborted. const SYNC_UPDATE_TIMEOUT: Duration = Duration::from_millis(150); /// Maximum number of bytes read in one synchronized update (2MiB). const SYNC_BUFFER_SIZE: usize = 0x20_0000; /// Number of bytes in the BSU/ESU CSI sequences. const SYNC_ESCAPE_LEN: usize = 8; /// BSU CSI sequence for beginning or extending synchronized updates. const BSU_CSI: [u8; SYNC_ESCAPE_LEN] = *b"\x1b[?2026h"; /// ESU CSI sequence for terminating synchronized updates. const ESU_CSI: [u8; SYNC_ESCAPE_LEN] = *b"\x1b[?2026l"; #[derive(Debug, PartialEq, Eq, Hash)] pub struct Hyperlink { /// Identifier for the given hyperlink. pub id: Option, /// Resource identifier of the hyperlink. pub uri: String, } #[derive(Debug, Eq, PartialEq, Copy, Clone, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Rgb { pub r: u8, pub g: u8, pub b: u8, } impl Rgb { /// Implementation of [W3C's luminance algorithm]. /// /// [W3C's luminance algorithm]: https://www.w3.org/TR/WCAG20/#relativeluminancedef #[cfg(not(feature = "no_std"))] pub fn luminance(self) -> f64 { let channel_luminance = |channel| { let channel = channel as f64 / 255.; if channel <= 0.03928 { channel / 12.92 } else { f64::powf((channel + 0.055) / 1.055, 2.4) } }; let r_luminance = channel_luminance(self.r); let g_luminance = channel_luminance(self.g); let b_luminance = channel_luminance(self.b); 0.2126 * r_luminance + 0.7152 * g_luminance + 0.0722 * b_luminance } /// Implementation of [W3C's contrast algorithm]. /// /// [W3C's contrast algorithm]: https://www.w3.org/TR/WCAG20/#contrast-ratiodef #[cfg(not(feature = "no_std"))] pub fn contrast(self, other: Rgb) -> f64 { let self_luminance = self.luminance(); let other_luminance = other.luminance(); let (darker, lighter) = if self_luminance > other_luminance { (other_luminance, self_luminance) } else { (self_luminance, other_luminance) }; (lighter + 0.05) / (darker + 0.05) } } // A multiply function for Rgb, as the default dim is just *2/3. #[cfg(not(feature = "no_std"))] impl Mul for Rgb { type Output = Rgb; fn mul(self, rhs: f32) -> Rgb { let result = Rgb { r: (f32::from(self.r) * rhs).clamp(0.0, 255.0) as u8, g: (f32::from(self.g) * rhs).clamp(0.0, 255.0) as u8, b: (f32::from(self.b) * rhs).clamp(0.0, 255.0) as u8, }; log::trace!("Scaling RGB by {} from {:?} to {:?}", rhs, self, result); result } } impl Add for Rgb { type Output = Rgb; fn add(self, rhs: Rgb) -> Rgb { Rgb { r: self.r.saturating_add(rhs.r), g: self.g.saturating_add(rhs.g), b: self.b.saturating_add(rhs.b), } } } impl Sub for Rgb { type Output = Rgb; fn sub(self, rhs: Rgb) -> Rgb { Rgb { r: self.r.saturating_sub(rhs.r), g: self.g.saturating_sub(rhs.g), b: self.b.saturating_sub(rhs.b), } } } impl Display for Rgb { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b) } } impl FromStr for Rgb { type Err = (); fn from_str(s: &str) -> Result { let chars = if s.starts_with("0x") && s.len() == 8 { &s[2..] } else if s.starts_with('#') && s.len() == 7 { &s[1..] } else { return Err(()); }; match u32::from_str_radix(chars, 16) { Ok(mut color) => { let b = (color & 0xFF) as u8; color >>= 8; let g = (color & 0xFF) as u8; color >>= 8; let r = color as u8; Ok(Rgb { r, g, b }) }, Err(_) => Err(()), } } } /// Parse colors in XParseColor format. fn xparse_color(color: &[u8]) -> Option { if !color.is_empty() && color[0] == b'#' { parse_legacy_color(&color[1..]) } else if color.len() >= 4 && &color[..4] == b"rgb:" { parse_rgb_color(&color[4..]) } else { None } } /// Parse colors in `rgb:r(rrr)/g(ggg)/b(bbb)` format. fn parse_rgb_color(color: &[u8]) -> Option { let colors = str::from_utf8(color).ok()?.split('/').collect::>(); if colors.len() != 3 { return None; } // Scale values instead of filling with `0`s. let scale = |input: &str| { if input.len() > 4 { None } else { let max = u32::pow(16, input.len() as u32) - 1; let value = u32::from_str_radix(input, 16).ok()?; Some((255 * value / max) as u8) } }; Some(Rgb { r: scale(colors[0])?, g: scale(colors[1])?, b: scale(colors[2])? }) } /// Parse colors in `#r(rrr)g(ggg)b(bbb)` format. fn parse_legacy_color(color: &[u8]) -> Option { let item_len = color.len() / 3; // Truncate/Fill to two byte precision. let color_from_slice = |slice: &[u8]| { let col = usize::from_str_radix(str::from_utf8(slice).ok()?, 16).ok()? << 4; Some((col >> (4 * slice.len().saturating_sub(1))) as u8) }; Some(Rgb { r: color_from_slice(&color[0..item_len])?, g: color_from_slice(&color[item_len..item_len * 2])?, b: color_from_slice(&color[item_len * 2..])?, }) } fn parse_number(input: &[u8]) -> Option { if input.is_empty() { return None; } let mut num: u8 = 0; for c in input { let c = *c as char; let digit = c.to_digit(10)?; num = num.checked_mul(10).and_then(|v| v.checked_add(digit as u8))?; } Some(num) } /// Internal state for VTE processor. #[derive(Debug, Default)] struct ProcessorState { /// Last processed character for repetition. preceding_char: Option, /// State for synchronized terminal updates. sync_state: SyncState, } #[derive(Debug)] struct SyncState { /// Handler for synchronized updates. timeout: T, /// Bytes read during the synchronized update. buffer: Vec, } impl Default for SyncState { fn default() -> Self { Self { buffer: Vec::with_capacity(SYNC_BUFFER_SIZE), timeout: Default::default() } } } /// The processor wraps a `crate::Parser` to ultimately call methods on a /// Handler. #[cfg(not(feature = "no_std"))] #[derive(Default)] pub struct Processor { state: ProcessorState, parser: crate::Parser, } /// The processor wraps a `crate::Parser` to ultimately call methods on a /// Handler. #[cfg(feature = "no_std")] #[derive(Default)] pub struct Processor { state: ProcessorState, parser: crate::Parser, } impl Processor { #[inline] pub fn new() -> Self { Self::default() } /// Synchronized update timeout. pub fn sync_timeout(&self) -> &T { &self.state.sync_state.timeout } /// Process a new byte from the PTY. #[inline] pub fn advance(&mut self, handler: &mut H, bytes: &[u8]) where H: Handler, { let mut processed = 0; while processed != bytes.len() { if self.state.sync_state.timeout.pending_timeout() { processed += self.advance_sync(handler, &bytes[processed..]); } else { let mut performer = Performer::new(&mut self.state, handler); processed += self.parser.advance_until_terminated(&mut performer, &bytes[processed..]); } } } /// End a synchronized update. pub fn stop_sync(&mut self, handler: &mut H) where H: Handler, { self.stop_sync_internal(handler, None); } /// End a synchronized update. /// /// The `bsu_offset` parameter should be passed if the sync buffer contains /// a new BSU escape that is not part of the current synchronized /// update. fn stop_sync_internal(&mut self, handler: &mut H, bsu_offset: Option) where H: Handler, { // Process all synchronized bytes. // // NOTE: We do not use `advance_until_terminated` here since BSU sequences are // processed automatically during the synchronized update. let buffer = mem::take(&mut self.state.sync_state.buffer); let offset = bsu_offset.unwrap_or(buffer.len()); let mut performer = Performer::new(&mut self.state, handler); self.parser.advance(&mut performer, &buffer[..offset]); self.state.sync_state.buffer = buffer; match bsu_offset { // Just clear processed bytes if there is a new BSU. // // NOTE: We do not need to re-process for a new ESU since the `advance_sync` // function checks for BSUs in reverse. Some(bsu_offset) => { let new_len = self.state.sync_state.buffer.len() - bsu_offset; self.state.sync_state.buffer.copy_within(bsu_offset.., 0); self.state.sync_state.buffer.truncate(new_len); }, // Report mode and clear state if no new BSU is present. None => { handler.unset_private_mode(NamedPrivateMode::SyncUpdate.into()); self.state.sync_state.timeout.clear_timeout(); self.state.sync_state.buffer.clear(); }, } } /// Number of bytes in the synchronization buffer. #[inline] pub fn sync_bytes_count(&self) -> usize { self.state.sync_state.buffer.len() } /// Process a new byte during a synchronized update. /// /// Returns the number of bytes processed. #[cold] fn advance_sync(&mut self, handler: &mut H, bytes: &[u8]) -> usize where H: Handler, { // Advance sync parser or stop sync if we'd exceed the maximum buffer size. if self.state.sync_state.buffer.len() + bytes.len() >= SYNC_BUFFER_SIZE - 1 { // Terminate the synchronized update. self.stop_sync_internal(handler, None); // Just parse the bytes normally. let mut performer = Performer::new(&mut self.state, handler); self.parser.advance_until_terminated(&mut performer, bytes) } else { self.state.sync_state.buffer.extend(bytes); self.advance_sync_csi(handler, bytes.len()); bytes.len() } } /// Handle BSU/ESU CSI sequences during synchronized update. fn advance_sync_csi(&mut self, handler: &mut H, new_bytes: usize) where H: Handler, { // Get constraints within which a new escape character might be relevant. let buffer_len = self.state.sync_state.buffer.len(); let start_offset = (buffer_len - new_bytes).saturating_sub(SYNC_ESCAPE_LEN - 1); let end_offset = buffer_len.saturating_sub(SYNC_ESCAPE_LEN - 1); let search_buffer = &self.state.sync_state.buffer[start_offset..end_offset]; // Search for termination/extension escapes in the added bytes. // // NOTE: It is technically legal to specify multiple private modes in the same // escape, but we only allow EXACTLY `\e[?2026h`/`\e[?2026l` to keep the parser // more simple. let mut bsu_offset = None; for index in memchr::memchr_iter(0x1B, search_buffer).rev() { let offset = start_offset + index; let escape = &self.state.sync_state.buffer[offset..offset + SYNC_ESCAPE_LEN]; if escape == BSU_CSI { self.state.sync_state.timeout.set_timeout(SYNC_UPDATE_TIMEOUT); bsu_offset = Some(offset); } else if escape == ESU_CSI { self.stop_sync_internal(handler, bsu_offset); break; } } } } /// Helper type that implements `crate::Perform`. /// /// Processor creates a Performer when running advance and passes the Performer /// to `crate::Parser`. struct Performer<'a, H: Handler, T: Timeout> { state: &'a mut ProcessorState, handler: &'a mut H, /// Whether the parser should be prematurely terminated. terminated: bool, } impl<'a, H: Handler + 'a, T: Timeout> Performer<'a, H, T> { /// Create a performer. #[inline] pub fn new<'b>(state: &'b mut ProcessorState, handler: &'b mut H) -> Performer<'b, H, T> { Performer { state, handler, terminated: Default::default() } } } #[cfg(not(feature = "no_std"))] #[derive(Default)] pub struct StdSyncHandler { timeout: Option, } #[cfg(not(feature = "no_std"))] impl StdSyncHandler { /// Synchronized update expiration time. #[inline] pub fn sync_timeout(&self) -> Option { self.timeout } } #[cfg(not(feature = "no_std"))] impl Timeout for StdSyncHandler { #[inline] fn set_timeout(&mut self, duration: Duration) { self.timeout = Some(Instant::now() + duration); } #[inline] fn clear_timeout(&mut self) { self.timeout = None; } #[inline] fn pending_timeout(&self) -> bool { self.timeout.is_some() } } /// Interface for creating timeouts and checking their expiry. /// /// This is internally used by the [`Processor`] to handle synchronized /// updates. pub trait Timeout: Default { /// Sets the timeout for the next synchronized update. /// /// The `duration` parameter specifies the duration of the timeout. Once the /// specified duration has elapsed, the synchronized update rotuine can be /// performed. fn set_timeout(&mut self, duration: Duration); /// Clear the current timeout. fn clear_timeout(&mut self); /// Returns whether a timeout is currently active and has not yet expired. fn pending_timeout(&self) -> bool; } /// Type that handles actions from the parser. /// /// XXX Should probably not provide default impls for everything, but it makes /// writing specific handler impls for tests far easier. pub trait Handler { /// OSC to set window title. fn set_title(&mut self, _: Option) {} /// Set the cursor style. fn set_cursor_style(&mut self, _: Option) {} /// Set the cursor shape. fn set_cursor_shape(&mut self, _shape: CursorShape) {} /// A character to be displayed. fn input(&mut self, _c: char) {} /// Set cursor to position. fn goto(&mut self, _line: i32, _col: usize) {} /// Set cursor to specific row. fn goto_line(&mut self, _line: i32) {} /// Set cursor to specific column. fn goto_col(&mut self, _col: usize) {} /// Insert blank characters in current line starting from cursor. fn insert_blank(&mut self, _: usize) {} /// Move cursor up `rows`. fn move_up(&mut self, _: usize) {} /// Move cursor down `rows`. fn move_down(&mut self, _: usize) {} /// Identify the terminal (should write back to the pty stream). fn identify_terminal(&mut self, _intermediate: Option) {} /// Report device status. fn device_status(&mut self, _: usize) {} /// Move cursor forward `cols`. fn move_forward(&mut self, _col: usize) {} /// Move cursor backward `cols`. fn move_backward(&mut self, _col: usize) {} /// Move cursor down `rows` and set to column 1. fn move_down_and_cr(&mut self, _row: usize) {} /// Move cursor up `rows` and set to column 1. fn move_up_and_cr(&mut self, _row: usize) {} /// Put `count` tabs. fn put_tab(&mut self, _count: u16) {} /// Backspace `count` characters. fn backspace(&mut self) {} /// Carriage return. fn carriage_return(&mut self) {} /// Linefeed. fn linefeed(&mut self) {} /// Ring the bell. /// /// Hopefully this is never implemented. fn bell(&mut self) {} /// Substitute char under cursor. fn substitute(&mut self) {} /// Newline. fn newline(&mut self) {} /// Set current position as a tabstop. fn set_horizontal_tabstop(&mut self) {} /// Scroll up `rows` rows. fn scroll_up(&mut self, _: usize) {} /// Scroll down `rows` rows. fn scroll_down(&mut self, _: usize) {} /// Insert `count` blank lines. fn insert_blank_lines(&mut self, _: usize) {} /// Delete `count` lines. fn delete_lines(&mut self, _: usize) {} /// Erase `count` chars in current line following cursor. /// /// Erase means resetting to the default state (default colors, no content, /// no mode flags). fn erase_chars(&mut self, _: usize) {} /// Delete `count` chars. /// /// Deleting a character is like the delete key on the keyboard - everything /// to the right of the deleted things is shifted left. fn delete_chars(&mut self, _: usize) {} /// Move backward `count` tabs. fn move_backward_tabs(&mut self, _count: u16) {} /// Move forward `count` tabs. fn move_forward_tabs(&mut self, _count: u16) {} /// Save current cursor position. fn save_cursor_position(&mut self) {} /// Restore cursor position. fn restore_cursor_position(&mut self) {} /// Clear current line. fn clear_line(&mut self, _mode: LineClearMode) {} /// Clear screen. fn clear_screen(&mut self, _mode: ClearMode) {} /// Clear tab stops. fn clear_tabs(&mut self, _mode: TabulationClearMode) {} /// Set tab stops at every `interval`. fn set_tabs(&mut self, _interval: u16) {} /// Reset terminal state. fn reset_state(&mut self) {} /// Reverse Index. /// /// Move the active position to the same horizontal position on the /// preceding line. If the active position is at the top margin, a scroll /// down is performed. fn reverse_index(&mut self) {} /// Set a terminal attribute. fn terminal_attribute(&mut self, _attr: Attr) {} /// Set mode. fn set_mode(&mut self, _mode: Mode) {} /// Unset mode. fn unset_mode(&mut self, _mode: Mode) {} /// DECRPM - report mode. fn report_mode(&mut self, _mode: Mode) {} /// Set private mode. fn set_private_mode(&mut self, _mode: PrivateMode) {} /// Unset private mode. fn unset_private_mode(&mut self, _mode: PrivateMode) {} /// DECRPM - report private mode. fn report_private_mode(&mut self, _mode: PrivateMode) {} /// DECSTBM - Set the terminal scrolling region. fn set_scrolling_region(&mut self, _top: usize, _bottom: Option) {} /// DECKPAM - Set keypad to applications mode (ESCape instead of digits). fn set_keypad_application_mode(&mut self) {} /// DECKPNM - Set keypad to numeric mode (digits instead of ESCape seq). fn unset_keypad_application_mode(&mut self) {} /// Set one of the graphic character sets, G0 to G3, as the active charset. /// /// 'Invoke' one of G0 to G3 in the GL area. Also referred to as shift in, /// shift out and locking shift depending on the set being activated. fn set_active_charset(&mut self, _: CharsetIndex) {} /// Assign a graphic character set to G0, G1, G2 or G3. /// /// 'Designate' a graphic character set as one of G0 to G3, so that it can /// later be 'invoked' by `set_active_charset`. fn configure_charset(&mut self, _: CharsetIndex, _: StandardCharset) {} /// Set an indexed color value. fn set_color(&mut self, _: usize, _: Rgb) {} /// Respond to a color query escape sequence. fn dynamic_color_sequence(&mut self, _: String, _: usize, _: &str) {} /// Reset an indexed color to original value. fn reset_color(&mut self, _: usize) {} /// Store data into clipboard. fn clipboard_store(&mut self, _: u8, _: &[u8]) {} /// Load data from clipboard. fn clipboard_load(&mut self, _: u8, _: &str) {} /// Run the decaln routine. fn decaln(&mut self) {} /// Push a title onto the stack. fn push_title(&mut self) {} /// Pop the last title from the stack. fn pop_title(&mut self) {} /// Report text area size in pixels. fn text_area_size_pixels(&mut self) {} /// Report text area size in characters. fn text_area_size_chars(&mut self) {} /// Set hyperlink. fn set_hyperlink(&mut self, _: Option) {} /// Set mouse cursor icon. fn set_mouse_cursor_icon(&mut self, _: CursorIcon) {} /// Report current keyboard mode. fn report_keyboard_mode(&mut self) {} /// Push keyboard mode into the keyboard mode stack. fn push_keyboard_mode(&mut self, _mode: KeyboardModes) {} /// Pop the given amount of keyboard modes from the /// keyboard mode stack. fn pop_keyboard_modes(&mut self, _to_pop: u16) {} /// Set the [`keyboard mode`] using the given [`behavior`]. /// /// [`keyboard mode`]: crate::ansi::KeyboardModes /// [`behavior`]: crate::ansi::KeyboardModesApplyBehavior fn set_keyboard_mode(&mut self, _mode: KeyboardModes, _behavior: KeyboardModesApplyBehavior) {} /// Set XTerm's [`ModifyOtherKeys`] option. fn set_modify_other_keys(&mut self, _mode: ModifyOtherKeys) {} /// Report XTerm's [`ModifyOtherKeys`] state. /// /// The output is of form `CSI > 4 ; mode m`. fn report_modify_other_keys(&mut self) {} // Set SCP control. fn set_scp(&mut self, _char_path: ScpCharPath, _update_mode: ScpUpdateMode) {} } bitflags! { /// A set of [`kitty keyboard protocol'] modes. /// /// [`kitty keyboard protocol']: https://sw.kovidgoyal.net/kitty/keyboard-protocol #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct KeyboardModes : u8 { /// No keyboard protocol mode is set. const NO_MODE = 0b0000_0000; /// Report `Esc`, `alt` + `key`, `ctrl` + `key`, `ctrl` + `alt` + `key`, `shift` /// + `alt` + `key` keys using `CSI u` sequence instead of raw ones. const DISAMBIGUATE_ESC_CODES = 0b0000_0001; /// Report key presses, release, and repetition alongside the escape. Key events /// that result in text are reported as plain UTF-8, unless the /// [`Self::REPORT_ALL_KEYS_AS_ESC`] is enabled. const REPORT_EVENT_TYPES = 0b0000_0010; /// Additionally report shifted key an dbase layout key. const REPORT_ALTERNATE_KEYS = 0b0000_0100; /// Report every key as an escape sequence. const REPORT_ALL_KEYS_AS_ESC = 0b0000_1000; /// Report the text generated by the key event. const REPORT_ASSOCIATED_TEXT = 0b0001_0000; } } /// XTMODKEYS modifyOtherKeys state. /// /// This only applies to keys corresponding to ascii characters. /// /// For the details on how to implement the mode handling correctly, consult /// [`XTerm's implementation`] and the [`output`] of XTerm's provided [`perl /// script`]. Some libraries and implementations also use the [`fixterms`] /// definition of the `CSI u`. /// /// The end escape sequence has a `CSI char; modifiers u` form while the /// original `CSI 27 ; modifier ; char ~`. The clients should prefer the `CSI /// u`, since it has more adoption. /// /// [`XTerm's implementation`]: https://invisible-island.net/xterm/modified-keys.html /// [`perl script`]: https://github.com/ThomasDickey/xterm-snapshots/blob/master/vttests/modify-keys.pl /// [`output`]: https://github.com/alacritty/vte/blob/master/doc/modifyOtherKeys-example.txt /// [`fixterms`]: http://www.leonerd.org.uk/hacks/fixterms/ #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ModifyOtherKeys { /// Reset the state. Reset, /// Enables this feature except for keys with well-known behavior, e.g., /// Tab, Backspace and some special control character cases which are /// built into the X11 library (e.g., Control-Space to make a NUL, or /// Control-3 to make an Escape character). /// /// Escape sequences shouldn't be emitted under the following circumstances: /// - When the key is in range of `[64;127]` and the modifier is either /// Control or Shift /// - When the key combination is a known control combination alias /// /// For more details, consult the [`example`] for the suggested translation. /// /// [`example`]: https://github.com/alacritty/vte/blob/master/doc/modifyOtherKeys-example.txt EnableExceptWellDefined, /// Enables this feature for all keys including the exceptions of /// [`Self::EnableExceptWellDefined`]. XTerm still ignores the special /// cases built into the X11 library. Any shifted (modified) ordinary /// key send an escape sequence. The Alt- and Meta- modifiers cause /// XTerm to send escape sequences. /// /// For more details, consult the [`example`] for the suggested translation. /// /// [`example`]: https://github.com/alacritty/vte/blob/master/doc/modifyOtherKeys-example.txt EnableAll, } /// Describes how the new [`KeyboardModes`] should be applied. #[repr(u8)] #[derive(Default, Clone, Copy, PartialEq, Eq)] pub enum KeyboardModesApplyBehavior { /// Replace the active flags with the new ones. #[default] Replace, /// Merge the given flags with currently active ones. Union, /// Remove the given flags from the active ones. Difference, } /// Terminal cursor configuration. #[derive(Default, Debug, Eq, PartialEq, Copy, Clone, Hash)] pub struct CursorStyle { pub shape: CursorShape, pub blinking: bool, } /// Terminal cursor shape. #[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)] pub enum CursorShape { /// Cursor is a block like `▒`. #[default] Block, /// Cursor is an underscore like `_`. Underline, /// Cursor is a vertical bar `⎸`. Beam, /// Cursor is a box like `☐`. HollowBlock, /// Invisible cursor. Hidden, } /// Wrapper for the ANSI modes. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Mode { /// Known ANSI mode. Named(NamedMode), /// Unidentified publc mode. Unknown(u16), } impl Mode { fn new(mode: u16) -> Self { match mode { 4 => Self::Named(NamedMode::Insert), 20 => Self::Named(NamedMode::LineFeedNewLine), _ => Self::Unknown(mode), } } /// Get the raw value of the mode. pub fn raw(self) -> u16 { match self { Self::Named(named) => named as u16, Self::Unknown(mode) => mode, } } } impl From for Mode { fn from(value: NamedMode) -> Self { Self::Named(value) } } /// ANSI modes. #[repr(u16)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum NamedMode { /// IRM Insert Mode. Insert = 4, LineFeedNewLine = 20, } /// Wrapper for the private DEC modes. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum PrivateMode { /// Known private mode. Named(NamedPrivateMode), /// Unknown private mode. Unknown(u16), } impl PrivateMode { fn new(mode: u16) -> Self { match mode { 1 => Self::Named(NamedPrivateMode::CursorKeys), 3 => Self::Named(NamedPrivateMode::ColumnMode), 6 => Self::Named(NamedPrivateMode::Origin), 7 => Self::Named(NamedPrivateMode::LineWrap), 12 => Self::Named(NamedPrivateMode::BlinkingCursor), 25 => Self::Named(NamedPrivateMode::ShowCursor), 1000 => Self::Named(NamedPrivateMode::ReportMouseClicks), 1002 => Self::Named(NamedPrivateMode::ReportCellMouseMotion), 1003 => Self::Named(NamedPrivateMode::ReportAllMouseMotion), 1004 => Self::Named(NamedPrivateMode::ReportFocusInOut), 1005 => Self::Named(NamedPrivateMode::Utf8Mouse), 1006 => Self::Named(NamedPrivateMode::SgrMouse), 1007 => Self::Named(NamedPrivateMode::AlternateScroll), 1042 => Self::Named(NamedPrivateMode::UrgencyHints), 1049 => Self::Named(NamedPrivateMode::SwapScreenAndSetRestoreCursor), 2004 => Self::Named(NamedPrivateMode::BracketedPaste), 2026 => Self::Named(NamedPrivateMode::SyncUpdate), _ => Self::Unknown(mode), } } /// Get the raw value of the mode. pub fn raw(self) -> u16 { match self { Self::Named(named) => named as u16, Self::Unknown(mode) => mode, } } } impl From for PrivateMode { fn from(value: NamedPrivateMode) -> Self { Self::Named(value) } } /// Private DEC modes. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum NamedPrivateMode { CursorKeys = 1, /// Select 80 or 132 columns per page (DECCOLM). /// /// CSI ? 3 h -> set 132 column font. /// CSI ? 3 l -> reset 80 column font. /// /// Additionally, /// /// * set margins to default positions /// * erases all data in page memory /// * resets DECLRMM to unavailable /// * clears data from the status line (if set to host-writable) ColumnMode = 3, Origin = 6, LineWrap = 7, BlinkingCursor = 12, ShowCursor = 25, ReportMouseClicks = 1000, ReportCellMouseMotion = 1002, ReportAllMouseMotion = 1003, ReportFocusInOut = 1004, Utf8Mouse = 1005, SgrMouse = 1006, AlternateScroll = 1007, UrgencyHints = 1042, SwapScreenAndSetRestoreCursor = 1049, BracketedPaste = 2004, /// The mode is handled automatically by [`Processor`]. SyncUpdate = 2026, } /// Mode for clearing line. /// /// Relative to cursor. #[derive(Debug)] pub enum LineClearMode { /// Clear right of cursor. Right, /// Clear left of cursor. Left, /// Clear entire line. All, } /// Mode for clearing terminal. /// /// Relative to cursor. #[derive(Debug)] pub enum ClearMode { /// Clear below cursor. Below, /// Clear above cursor. Above, /// Clear entire terminal. All, /// Clear 'saved' lines (scrollback). Saved, } /// Mode for clearing tab stops. #[derive(Debug)] pub enum TabulationClearMode { /// Clear stop under cursor. Current, /// Clear all stops. All, } /// Standard colors. /// /// The order here matters since the enum should be castable to a `usize` for /// indexing a color list. #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum NamedColor { /// Black. Black = 0, /// Red. Red, /// Green. Green, /// Yellow. Yellow, /// Blue. Blue, /// Magenta. Magenta, /// Cyan. Cyan, /// White. White, /// Bright black. BrightBlack, /// Bright red. BrightRed, /// Bright green. BrightGreen, /// Bright yellow. BrightYellow, /// Bright blue. BrightBlue, /// Bright magenta. BrightMagenta, /// Bright cyan. BrightCyan, /// Bright white. BrightWhite, /// The foreground color. Foreground = 256, /// The background color. Background, /// Color for the cursor itself. Cursor, /// Dim black. DimBlack, /// Dim red. DimRed, /// Dim green. DimGreen, /// Dim yellow. DimYellow, /// Dim blue. DimBlue, /// Dim magenta. DimMagenta, /// Dim cyan. DimCyan, /// Dim white. DimWhite, /// The bright foreground color. BrightForeground, /// Dim foreground. DimForeground, } impl NamedColor { #[must_use] pub fn to_bright(self) -> Self { match self { NamedColor::Foreground => NamedColor::BrightForeground, NamedColor::Black => NamedColor::BrightBlack, NamedColor::Red => NamedColor::BrightRed, NamedColor::Green => NamedColor::BrightGreen, NamedColor::Yellow => NamedColor::BrightYellow, NamedColor::Blue => NamedColor::BrightBlue, NamedColor::Magenta => NamedColor::BrightMagenta, NamedColor::Cyan => NamedColor::BrightCyan, NamedColor::White => NamedColor::BrightWhite, NamedColor::DimForeground => NamedColor::Foreground, NamedColor::DimBlack => NamedColor::Black, NamedColor::DimRed => NamedColor::Red, NamedColor::DimGreen => NamedColor::Green, NamedColor::DimYellow => NamedColor::Yellow, NamedColor::DimBlue => NamedColor::Blue, NamedColor::DimMagenta => NamedColor::Magenta, NamedColor::DimCyan => NamedColor::Cyan, NamedColor::DimWhite => NamedColor::White, val => val, } } #[must_use] pub fn to_dim(self) -> Self { match self { NamedColor::Black => NamedColor::DimBlack, NamedColor::Red => NamedColor::DimRed, NamedColor::Green => NamedColor::DimGreen, NamedColor::Yellow => NamedColor::DimYellow, NamedColor::Blue => NamedColor::DimBlue, NamedColor::Magenta => NamedColor::DimMagenta, NamedColor::Cyan => NamedColor::DimCyan, NamedColor::White => NamedColor::DimWhite, NamedColor::Foreground => NamedColor::DimForeground, NamedColor::BrightBlack => NamedColor::Black, NamedColor::BrightRed => NamedColor::Red, NamedColor::BrightGreen => NamedColor::Green, NamedColor::BrightYellow => NamedColor::Yellow, NamedColor::BrightBlue => NamedColor::Blue, NamedColor::BrightMagenta => NamedColor::Magenta, NamedColor::BrightCyan => NamedColor::Cyan, NamedColor::BrightWhite => NamedColor::White, NamedColor::BrightForeground => NamedColor::Foreground, val => val, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Color { Named(NamedColor), Spec(Rgb), Indexed(u8), } /// Terminal character attributes. #[derive(Debug, Eq, PartialEq)] pub enum Attr { /// Clear all special abilities. Reset, /// Bold text. Bold, /// Dim or secondary color. Dim, /// Italic text. Italic, /// Underline text. Underline, /// Underlined twice. DoubleUnderline, /// Undercurled text. Undercurl, /// Dotted underlined text. DottedUnderline, /// Dashed underlined text. DashedUnderline, /// Blink cursor slowly. BlinkSlow, /// Blink cursor fast. BlinkFast, /// Invert colors. Reverse, /// Do not display characters. Hidden, /// Strikeout text. Strike, /// Cancel bold. CancelBold, /// Cancel bold and dim. CancelBoldDim, /// Cancel italic. CancelItalic, /// Cancel all underlines. CancelUnderline, /// Cancel blink. CancelBlink, /// Cancel inversion. CancelReverse, /// Cancel text hiding. CancelHidden, /// Cancel strikeout. CancelStrike, /// Set indexed foreground color. Foreground(Color), /// Set indexed background color. Background(Color), /// Underline color. UnderlineColor(Option), } /// Identifiers which can be assigned to a graphic character set. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CharsetIndex { /// Default set, is designated as ASCII at startup. #[default] G0, G1, G2, G3, } /// Standard or common character sets which can be designated as G0-G3. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum StandardCharset { #[default] Ascii, SpecialCharacterAndLineDrawing, } impl StandardCharset { /// Switch/Map character to the active charset. Ascii is the common case and /// for that we want to do as little as possible. #[inline] pub fn map(self, c: char) -> char { match self { StandardCharset::Ascii => c, StandardCharset::SpecialCharacterAndLineDrawing => match c { '_' => ' ', '`' => '◆', 'a' => '▒', 'b' => '\u{2409}', // Symbol for horizontal tabulation 'c' => '\u{240c}', // Symbol for form feed 'd' => '\u{240d}', // Symbol for carriage return 'e' => '\u{240a}', // Symbol for line feed 'f' => '°', 'g' => '±', 'h' => '\u{2424}', // Symbol for newline 'i' => '\u{240b}', // Symbol for vertical tabulation 'j' => '┘', 'k' => '┐', 'l' => '┌', 'm' => '└', 'n' => '┼', 'o' => '⎺', 'p' => '⎻', 'q' => '─', 'r' => '⎼', 's' => '⎽', 't' => '├', 'u' => '┤', 'v' => '┴', 'w' => '┬', 'x' => '│', 'y' => '≤', 'z' => '≥', '{' => 'π', '|' => '≠', '}' => '£', '~' => '·', _ => c, }, } } } /// SCP control's first parameter which determines character path. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ScpCharPath { /// SCP's first parameter value of 0. Behavior is implementation defined. Default, /// SCP's first parameter value of 1 which sets character path to /// LEFT-TO-RIGHT. LTR, /// SCP's first parameter value of 2 which sets character path to /// RIGHT-TO-LEFT. RTL, } /// SCP control's second parameter which determines update mode/direction /// between components. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ScpUpdateMode { /// SCP's second parameter value of 0 (the default). Implementation /// dependant update. ImplementationDependant, /// SCP's second parameter value of 1. /// /// Reflect data component changes in the presentation component. DataToPresentation, /// SCP's second parameter value of 2. /// /// Reflect presentation component changes in the data component. PresentationToData, } impl<'a, H, T> crate::Perform for Performer<'a, H, T> where H: Handler + 'a, T: Timeout, { #[inline] fn print(&mut self, c: char) { self.handler.input(c); self.state.preceding_char = Some(c); } #[inline] fn execute(&mut self, byte: u8) { match byte { C0::HT => self.handler.put_tab(1), C0::BS => self.handler.backspace(), C0::CR => self.handler.carriage_return(), C0::LF | C0::VT | C0::FF => self.handler.linefeed(), C0::BEL => self.handler.bell(), C0::SUB => self.handler.substitute(), C0::SI => self.handler.set_active_charset(CharsetIndex::G0), C0::SO => self.handler.set_active_charset(CharsetIndex::G1), _ => debug!("[unhandled] execute byte={:02x}", byte), } } #[inline] fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { debug!( "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", params, intermediates, ignore, action ); } #[inline] fn put(&mut self, byte: u8) { debug!("[unhandled put] byte={:?}", byte); } #[inline] fn unhook(&mut self) { debug!("[unhandled unhook]"); } #[inline] fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { let terminator = if bell_terminated { "\x07" } else { "\x1b\\" }; fn unhandled(params: &[&[u8]]) { let mut buf = String::new(); for items in params { buf.push('['); for item in *items { let _ = write!(buf, "{:?}", *item as char); } buf.push_str("],"); } debug!("[unhandled osc_dispatch]: [{}] at line {}", &buf, line!()); } if params.is_empty() || params[0].is_empty() { return; } match params[0] { // Set window title. b"0" | b"2" => { if params.len() >= 2 { let title = params[1..] .iter() .flat_map(|x| str::from_utf8(x)) .collect::>() .join(";") .trim() .to_owned(); self.handler.set_title(Some(title)); return; } unhandled(params); }, // Set color index. b"4" => { if params.len() <= 1 || params.len() % 2 == 0 { unhandled(params); return; } for chunk in params[1..].chunks(2) { let index = match parse_number(chunk[0]) { Some(index) => index, None => { unhandled(params); continue; }, }; if let Some(c) = xparse_color(chunk[1]) { self.handler.set_color(index as usize, c); } else if chunk[1] == b"?" { let prefix = alloc::format!("4;{index}"); self.handler.dynamic_color_sequence(prefix, index as usize, terminator); } else { unhandled(params); } } }, // Hyperlink. b"8" if params.len() > 2 => { let link_params = params[1]; // NOTE: The escape sequence is of form 'OSC 8 ; params ; URI ST', where // URI is URL-encoded. However `;` is a special character and might be // passed as is, thus we need to rebuild the URI. let mut uri = str::from_utf8(params[2]).unwrap_or_default().to_string(); for param in params[3..].iter() { uri.push(';'); uri.push_str(str::from_utf8(param).unwrap_or_default()); } // The OSC 8 escape sequence must be stopped when getting an empty `uri`. if uri.is_empty() { self.handler.set_hyperlink(None); return; } // Link parameters are in format of `key1=value1:key2=value2`. Currently only // key `id` is defined. let id = link_params .split(|&b| b == b':') .find_map(|kv| kv.strip_prefix(b"id=")) .and_then(|kv| str::from_utf8(kv).ok().map(|e| e.to_owned())); self.handler.set_hyperlink(Some(Hyperlink { id, uri })); }, // Get/set Foreground, Background, Cursor colors. b"10" | b"11" | b"12" => { if params.len() >= 2 { if let Some(mut dynamic_code) = parse_number(params[0]) { for param in ¶ms[1..] { // 10 is the first dynamic color, also the foreground. let offset = dynamic_code as usize - 10; let index = NamedColor::Foreground as usize + offset; // End of setting dynamic colors. if index > NamedColor::Cursor as usize { unhandled(params); break; } if let Some(color) = xparse_color(param) { self.handler.set_color(index, color); } else if param == b"?" { self.handler.dynamic_color_sequence( dynamic_code.to_string(), index, terminator, ); } else { unhandled(params); } dynamic_code += 1; } return; } } unhandled(params); }, // Set mouse cursor shape. b"22" if params.len() == 2 => { let shape = String::from_utf8_lossy(params[1]); match CursorIcon::from_str(&shape) { Ok(cursor_icon) => self.handler.set_mouse_cursor_icon(cursor_icon), Err(_) => debug!("[osc 22] unrecognized cursor icon shape: {shape:?}"), } }, // Set cursor style. b"50" => { if params.len() >= 2 && params[1].len() >= 13 && params[1][0..12] == *b"CursorShape=" { let shape = match params[1][12] as char { '0' => CursorShape::Block, '1' => CursorShape::Beam, '2' => CursorShape::Underline, _ => return unhandled(params), }; self.handler.set_cursor_shape(shape); return; } unhandled(params); }, // Set clipboard. b"52" => { if params.len() < 3 { return unhandled(params); } let clipboard = params[1].first().unwrap_or(&b'c'); match params[2] { b"?" => self.handler.clipboard_load(*clipboard, terminator), base64 => self.handler.clipboard_store(*clipboard, base64), } }, // Reset color index. b"104" => { // Reset all color indexes when no parameters are given. if params.len() == 1 || params[1].is_empty() { for i in 0..256 { self.handler.reset_color(i); } return; } // Reset color indexes given as parameters. for param in ¶ms[1..] { match parse_number(param) { Some(index) => self.handler.reset_color(index as usize), None => unhandled(params), } } }, // Reset foreground color. b"110" => self.handler.reset_color(NamedColor::Foreground as usize), // Reset background color. b"111" => self.handler.reset_color(NamedColor::Background as usize), // Reset text cursor color. b"112" => self.handler.reset_color(NamedColor::Cursor as usize), _ => unhandled(params), } } #[allow(clippy::cognitive_complexity)] #[inline] fn csi_dispatch( &mut self, params: &Params, intermediates: &[u8], has_ignored_intermediates: bool, action: char, ) { macro_rules! unhandled { () => {{ debug!( "[Unhandled CSI] action={:?}, params={:?}, intermediates={:?}", action, params, intermediates ); }}; } if has_ignored_intermediates || intermediates.len() > 2 { unhandled!(); return; } let mut params_iter = params.iter(); let handler = &mut self.handler; let mut next_param_or = |default: u16| match params_iter.next() { Some(&[param, ..]) if param != 0 => param, _ => default, }; match (action, intermediates) { ('@', []) => handler.insert_blank(next_param_or(1) as usize), ('A', []) => handler.move_up(next_param_or(1) as usize), ('B', []) | ('e', []) => handler.move_down(next_param_or(1) as usize), ('b', []) => { if let Some(c) = self.state.preceding_char { for _ in 0..next_param_or(1) { handler.input(c); } } else { debug!("tried to repeat with no preceding char"); } }, ('C', []) | ('a', []) => handler.move_forward(next_param_or(1) as usize), ('c', intermediates) if next_param_or(0) == 0 => { handler.identify_terminal(intermediates.first().map(|&i| i as char)) }, ('D', []) => handler.move_backward(next_param_or(1) as usize), ('d', []) => handler.goto_line(next_param_or(1) as i32 - 1), ('E', []) => handler.move_down_and_cr(next_param_or(1) as usize), ('F', []) => handler.move_up_and_cr(next_param_or(1) as usize), ('G', []) | ('`', []) => handler.goto_col(next_param_or(1) as usize - 1), ('W', [b'?']) if next_param_or(0) == 5 => handler.set_tabs(8), ('g', []) => { let mode = match next_param_or(0) { 0 => TabulationClearMode::Current, 3 => TabulationClearMode::All, _ => { unhandled!(); return; }, }; handler.clear_tabs(mode); }, ('H', []) | ('f', []) => { let y = next_param_or(1) as i32; let x = next_param_or(1) as usize; handler.goto(y - 1, x - 1); }, ('h', []) => { for param in params_iter.map(|param| param[0]) { handler.set_mode(Mode::new(param)) } }, ('h', [b'?']) => { for param in params_iter.map(|param| param[0]) { // Handle sync updates opaquely. if param == NamedPrivateMode::SyncUpdate as u16 { self.state.sync_state.timeout.set_timeout(SYNC_UPDATE_TIMEOUT); self.terminated = true; } handler.set_private_mode(PrivateMode::new(param)) } }, ('I', []) => handler.move_forward_tabs(next_param_or(1)), ('J', []) => { let mode = match next_param_or(0) { 0 => ClearMode::Below, 1 => ClearMode::Above, 2 => ClearMode::All, 3 => ClearMode::Saved, _ => { unhandled!(); return; }, }; handler.clear_screen(mode); }, ('K', []) => { let mode = match next_param_or(0) { 0 => LineClearMode::Right, 1 => LineClearMode::Left, 2 => LineClearMode::All, _ => { unhandled!(); return; }, }; handler.clear_line(mode); }, ('k', [b' ']) => { // SCP control. let char_path = match next_param_or(0) { 0 => ScpCharPath::Default, 1 => ScpCharPath::LTR, 2 => ScpCharPath::RTL, _ => { unhandled!(); return; }, }; let update_mode = match next_param_or(0) { 0 => ScpUpdateMode::ImplementationDependant, 1 => ScpUpdateMode::DataToPresentation, 2 => ScpUpdateMode::PresentationToData, _ => { unhandled!(); return; }, }; handler.set_scp(char_path, update_mode); }, ('L', []) => handler.insert_blank_lines(next_param_or(1) as usize), ('l', []) => { for param in params_iter.map(|param| param[0]) { handler.unset_mode(Mode::new(param)) } }, ('l', [b'?']) => { for param in params_iter.map(|param| param[0]) { handler.unset_private_mode(PrivateMode::new(param)) } }, ('M', []) => handler.delete_lines(next_param_or(1) as usize), ('m', []) => { if params.is_empty() { handler.terminal_attribute(Attr::Reset); } else { attrs_from_sgr_parameters(*handler, &mut params_iter); } }, ('m', [b'>']) => { let mode = match (next_param_or(1) == 4).then(|| next_param_or(0)) { Some(0) => ModifyOtherKeys::Reset, Some(1) => ModifyOtherKeys::EnableExceptWellDefined, Some(2) => ModifyOtherKeys::EnableAll, _ => return unhandled!(), }; handler.set_modify_other_keys(mode); }, ('m', [b'?']) => { if params_iter.next() == Some(&[4]) { handler.report_modify_other_keys(); } else { unhandled!() } }, ('n', []) => handler.device_status(next_param_or(0) as usize), ('P', []) => handler.delete_chars(next_param_or(1) as usize), ('p', [b'$']) => { let mode = next_param_or(0); handler.report_mode(Mode::new(mode)); }, ('p', [b'?', b'$']) => { let mode = next_param_or(0); handler.report_private_mode(PrivateMode::new(mode)); }, ('q', [b' ']) => { // DECSCUSR (CSI Ps SP q) -- Set Cursor Style. let cursor_style_id = next_param_or(0); let shape = match cursor_style_id { 0 => None, 1 | 2 => Some(CursorShape::Block), 3 | 4 => Some(CursorShape::Underline), 5 | 6 => Some(CursorShape::Beam), _ => { unhandled!(); return; }, }; let cursor_style = shape.map(|shape| CursorStyle { shape, blinking: cursor_style_id % 2 == 1 }); handler.set_cursor_style(cursor_style); }, ('r', []) => { let top = next_param_or(1) as usize; let bottom = params_iter.next().map(|param| param[0] as usize).filter(|¶m| param != 0); handler.set_scrolling_region(top, bottom); }, ('S', []) => handler.scroll_up(next_param_or(1) as usize), ('s', []) => handler.save_cursor_position(), ('T', []) => handler.scroll_down(next_param_or(1) as usize), ('t', []) => match next_param_or(1) as usize { 14 => handler.text_area_size_pixels(), 18 => handler.text_area_size_chars(), 22 => handler.push_title(), 23 => handler.pop_title(), _ => unhandled!(), }, ('u', [b'?']) => handler.report_keyboard_mode(), ('u', [b'=']) => { let mode = KeyboardModes::from_bits_truncate(next_param_or(0) as u8); let behavior = match next_param_or(1) { 3 => KeyboardModesApplyBehavior::Difference, 2 => KeyboardModesApplyBehavior::Union, // Default is replace. _ => KeyboardModesApplyBehavior::Replace, }; handler.set_keyboard_mode(mode, behavior); }, ('u', [b'>']) => { let mode = KeyboardModes::from_bits_truncate(next_param_or(0) as u8); handler.push_keyboard_mode(mode); }, ('u', [b'<']) => { // The default is 1. handler.pop_keyboard_modes(next_param_or(1)); }, ('u', []) => handler.restore_cursor_position(), ('X', []) => handler.erase_chars(next_param_or(1) as usize), ('Z', []) => handler.move_backward_tabs(next_param_or(1)), _ => unhandled!(), } } #[inline] fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { macro_rules! unhandled { () => {{ debug!( "[unhandled] esc_dispatch ints={:?}, byte={:?} ({:02x})", intermediates, byte as char, byte ); }}; } macro_rules! configure_charset { ($charset:path, $intermediates:expr) => {{ let index: CharsetIndex = match $intermediates { [b'('] => CharsetIndex::G0, [b')'] => CharsetIndex::G1, [b'*'] => CharsetIndex::G2, [b'+'] => CharsetIndex::G3, _ => { unhandled!(); return; }, }; self.handler.configure_charset(index, $charset) }}; } match (byte, intermediates) { (b'B', intermediates) => configure_charset!(StandardCharset::Ascii, intermediates), (b'D', []) => self.handler.linefeed(), (b'E', []) => { self.handler.linefeed(); self.handler.carriage_return(); }, (b'H', []) => self.handler.set_horizontal_tabstop(), (b'M', []) => self.handler.reverse_index(), (b'Z', []) => self.handler.identify_terminal(None), (b'c', []) => self.handler.reset_state(), (b'0', intermediates) => { configure_charset!(StandardCharset::SpecialCharacterAndLineDrawing, intermediates) }, (b'7', []) => self.handler.save_cursor_position(), (b'8', [b'#']) => self.handler.decaln(), (b'8', []) => self.handler.restore_cursor_position(), (b'=', []) => self.handler.set_keypad_application_mode(), (b'>', []) => self.handler.unset_keypad_application_mode(), // String terminator, do nothing (parser handles as string terminator). (b'\\', []) => (), _ => unhandled!(), } } #[inline] fn terminated(&self) -> bool { self.terminated } } #[inline] fn attrs_from_sgr_parameters(handler: &mut H, params: &mut ParamsIter<'_>) { while let Some(param) = params.next() { let attr = match param { [0] => Some(Attr::Reset), [1] => Some(Attr::Bold), [2] => Some(Attr::Dim), [3] => Some(Attr::Italic), [4, 0] => Some(Attr::CancelUnderline), [4, 2] => Some(Attr::DoubleUnderline), [4, 3] => Some(Attr::Undercurl), [4, 4] => Some(Attr::DottedUnderline), [4, 5] => Some(Attr::DashedUnderline), [4, ..] => Some(Attr::Underline), [5] => Some(Attr::BlinkSlow), [6] => Some(Attr::BlinkFast), [7] => Some(Attr::Reverse), [8] => Some(Attr::Hidden), [9] => Some(Attr::Strike), [21] => Some(Attr::CancelBold), [22] => Some(Attr::CancelBoldDim), [23] => Some(Attr::CancelItalic), [24] => Some(Attr::CancelUnderline), [25] => Some(Attr::CancelBlink), [27] => Some(Attr::CancelReverse), [28] => Some(Attr::CancelHidden), [29] => Some(Attr::CancelStrike), [30] => Some(Attr::Foreground(Color::Named(NamedColor::Black))), [31] => Some(Attr::Foreground(Color::Named(NamedColor::Red))), [32] => Some(Attr::Foreground(Color::Named(NamedColor::Green))), [33] => Some(Attr::Foreground(Color::Named(NamedColor::Yellow))), [34] => Some(Attr::Foreground(Color::Named(NamedColor::Blue))), [35] => Some(Attr::Foreground(Color::Named(NamedColor::Magenta))), [36] => Some(Attr::Foreground(Color::Named(NamedColor::Cyan))), [37] => Some(Attr::Foreground(Color::Named(NamedColor::White))), [38] => { let mut iter = params.map(|param| param[0]); parse_sgr_color(&mut iter).map(Attr::Foreground) }, [38, params @ ..] => handle_colon_rgb(params).map(Attr::Foreground), [39] => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))), [40] => Some(Attr::Background(Color::Named(NamedColor::Black))), [41] => Some(Attr::Background(Color::Named(NamedColor::Red))), [42] => Some(Attr::Background(Color::Named(NamedColor::Green))), [43] => Some(Attr::Background(Color::Named(NamedColor::Yellow))), [44] => Some(Attr::Background(Color::Named(NamedColor::Blue))), [45] => Some(Attr::Background(Color::Named(NamedColor::Magenta))), [46] => Some(Attr::Background(Color::Named(NamedColor::Cyan))), [47] => Some(Attr::Background(Color::Named(NamedColor::White))), [48] => { let mut iter = params.map(|param| param[0]); parse_sgr_color(&mut iter).map(Attr::Background) }, [48, params @ ..] => handle_colon_rgb(params).map(Attr::Background), [49] => Some(Attr::Background(Color::Named(NamedColor::Background))), [58] => { let mut iter = params.map(|param| param[0]); parse_sgr_color(&mut iter).map(|color| Attr::UnderlineColor(Some(color))) }, [58, params @ ..] => { handle_colon_rgb(params).map(|color| Attr::UnderlineColor(Some(color))) }, [59] => Some(Attr::UnderlineColor(None)), [90] => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlack))), [91] => Some(Attr::Foreground(Color::Named(NamedColor::BrightRed))), [92] => Some(Attr::Foreground(Color::Named(NamedColor::BrightGreen))), [93] => Some(Attr::Foreground(Color::Named(NamedColor::BrightYellow))), [94] => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlue))), [95] => Some(Attr::Foreground(Color::Named(NamedColor::BrightMagenta))), [96] => Some(Attr::Foreground(Color::Named(NamedColor::BrightCyan))), [97] => Some(Attr::Foreground(Color::Named(NamedColor::BrightWhite))), [100] => Some(Attr::Background(Color::Named(NamedColor::BrightBlack))), [101] => Some(Attr::Background(Color::Named(NamedColor::BrightRed))), [102] => Some(Attr::Background(Color::Named(NamedColor::BrightGreen))), [103] => Some(Attr::Background(Color::Named(NamedColor::BrightYellow))), [104] => Some(Attr::Background(Color::Named(NamedColor::BrightBlue))), [105] => Some(Attr::Background(Color::Named(NamedColor::BrightMagenta))), [106] => Some(Attr::Background(Color::Named(NamedColor::BrightCyan))), [107] => Some(Attr::Background(Color::Named(NamedColor::BrightWhite))), _ => None, }; match attr { Some(attr) => handler.terminal_attribute(attr), None => continue, } } } /// Handle colon separated rgb color escape sequence. #[inline] fn handle_colon_rgb(params: &[u16]) -> Option { let rgb_start = if params.len() > 4 { 2 } else { 1 }; let rgb_iter = params[rgb_start..].iter().copied(); let mut iter = iter::once(params[0]).chain(rgb_iter); parse_sgr_color(&mut iter) } /// Parse a color specifier from list of attributes. fn parse_sgr_color(params: &mut dyn Iterator) -> Option { match params.next() { Some(2) => Some(Color::Spec(Rgb { r: u8::try_from(params.next()?).ok()?, g: u8::try_from(params.next()?).ok()?, b: u8::try_from(params.next()?).ok()?, })), Some(5) => Some(Color::Indexed(u8::try_from(params.next()?).ok()?)), _ => None, } } /// C0 set of 7-bit control characters (from ANSI X3.4-1977). #[allow(non_snake_case)] pub mod C0 { /// Null filler, terminal should ignore this character. pub const NUL: u8 = 0x00; /// Start of Header. pub const SOH: u8 = 0x01; /// Start of Text, implied end of header. pub const STX: u8 = 0x02; /// End of Text, causes some terminal to respond with ACK or NAK. pub const ETX: u8 = 0x03; /// End of Transmission. pub const EOT: u8 = 0x04; /// Enquiry, causes terminal to send ANSWER-BACK ID. pub const ENQ: u8 = 0x05; /// Acknowledge, usually sent by terminal in response to ETX. pub const ACK: u8 = 0x06; /// Bell, triggers the bell, buzzer, or beeper on the terminal. pub const BEL: u8 = 0x07; /// Backspace, can be used to define overstruck characters. pub const BS: u8 = 0x08; /// Horizontal Tabulation, move to next predetermined position. pub const HT: u8 = 0x09; /// Linefeed, move to same position on next line (see also NL). pub const LF: u8 = 0x0A; /// Vertical Tabulation, move to next predetermined line. pub const VT: u8 = 0x0B; /// Form Feed, move to next form or page. pub const FF: u8 = 0x0C; /// Carriage Return, move to first character of current line. pub const CR: u8 = 0x0D; /// Shift Out, switch to G1 (other half of character set). pub const SO: u8 = 0x0E; /// Shift In, switch to G0 (normal half of character set). pub const SI: u8 = 0x0F; /// Data Link Escape, interpret next control character specially. pub const DLE: u8 = 0x10; /// (DC1) Terminal is allowed to resume transmitting. pub const XON: u8 = 0x11; /// Device Control 2, causes ASR-33 to activate paper-tape reader. pub const DC2: u8 = 0x12; /// (DC2) Terminal must pause and refrain from transmitting. pub const XOFF: u8 = 0x13; /// Device Control 4, causes ASR-33 to deactivate paper-tape reader. pub const DC4: u8 = 0x14; /// Negative Acknowledge, used sometimes with ETX and ACK. pub const NAK: u8 = 0x15; /// Synchronous Idle, used to maintain timing in Sync communication. pub const SYN: u8 = 0x16; /// End of Transmission block. pub const ETB: u8 = 0x17; /// Cancel (makes VT100 abort current escape sequence if any). pub const CAN: u8 = 0x18; /// End of Medium. pub const EM: u8 = 0x19; /// Substitute (VT100 uses this to display parity errors). pub const SUB: u8 = 0x1A; /// Prefix to an escape sequence. pub const ESC: u8 = 0x1B; /// File Separator. pub const FS: u8 = 0x1C; /// Group Separator. pub const GS: u8 = 0x1D; /// Record Separator (sent by VT132 in block-transfer mode). pub const RS: u8 = 0x1E; /// Unit Separator. pub const US: u8 = 0x1F; /// Delete, should be ignored by terminal. pub const DEL: u8 = 0x7F; } // Tests for parsing escape sequences. // // Byte sequences used in these tests are recording of pty stdout. #[cfg(test)] mod tests { use super::*; #[derive(Default)] pub struct TestSyncHandler { is_sync: usize, } impl Timeout for TestSyncHandler { #[inline] fn set_timeout(&mut self, _: Duration) { self.is_sync += 1; } #[inline] fn clear_timeout(&mut self) { self.is_sync = 0; } #[inline] fn pending_timeout(&self) -> bool { self.is_sync != 0 } } struct MockHandler { index: CharsetIndex, charset: StandardCharset, attr: Option, identity_reported: bool, color: Option, reset_colors: Vec, } impl Handler for MockHandler { fn terminal_attribute(&mut self, attr: Attr) { self.attr = Some(attr); } fn configure_charset(&mut self, index: CharsetIndex, charset: StandardCharset) { self.index = index; self.charset = charset; } fn set_active_charset(&mut self, index: CharsetIndex) { self.index = index; } fn identify_terminal(&mut self, _intermediate: Option) { self.identity_reported = true; } fn reset_state(&mut self) { *self = Self::default(); } fn set_color(&mut self, _: usize, c: Rgb) { self.color = Some(c); } fn reset_color(&mut self, index: usize) { self.reset_colors.push(index) } } impl Default for MockHandler { fn default() -> MockHandler { MockHandler { index: CharsetIndex::G0, charset: StandardCharset::Ascii, attr: None, identity_reported: false, color: None, reset_colors: Vec::new(), } } } #[test] fn parse_control_attribute() { static BYTES: &[u8] = &[0x1B, b'[', b'1', b'm']; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, BYTES); assert_eq!(handler.attr, Some(Attr::Bold)); } #[test] fn parse_terminal_identity_csi() { let bytes: &[u8] = &[0x1B, b'[', b'1', b'c']; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); assert!(!handler.identity_reported); handler.reset_state(); let bytes: &[u8] = &[0x1B, b'[', b'c']; parser.advance(&mut handler, bytes); assert!(handler.identity_reported); handler.reset_state(); let bytes: &[u8] = &[0x1B, b'[', b'0', b'c']; parser.advance(&mut handler, bytes); assert!(handler.identity_reported); } #[test] fn parse_terminal_identity_esc() { let bytes: &[u8] = &[0x1B, b'Z']; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); assert!(handler.identity_reported); handler.reset_state(); let bytes: &[u8] = &[0x1B, b'#', b'Z']; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); assert!(!handler.identity_reported); handler.reset_state(); } #[test] fn parse_truecolor_attr() { static BYTES: &[u8] = &[ 0x1B, b'[', b'3', b'8', b';', b'2', b';', b'1', b'2', b'8', b';', b'6', b'6', b';', b'2', b'5', b'5', b'm', ]; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, BYTES); let spec = Rgb { r: 128, g: 66, b: 255 }; assert_eq!(handler.attr, Some(Attr::Foreground(Color::Spec(spec)))); } /// No exactly a test; useful for debugging. #[test] fn parse_zsh_startup() { static BYTES: &[u8] = &[ 0x1B, b'[', b'1', b'm', 0x1B, b'[', b'7', b'm', b'%', 0x1B, b'[', b'2', b'7', b'm', 0x1B, b'[', b'1', b'm', 0x1B, b'[', b'0', b'm', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b' ', b'\r', b' ', b'\r', b'\r', 0x1B, b'[', b'0', b'm', 0x1B, b'[', b'2', b'7', b'm', 0x1B, b'[', b'2', b'4', b'm', 0x1B, b'[', b'J', b'j', b'w', b'i', b'l', b'm', b'@', b'j', b'w', b'i', b'l', b'm', b'-', b'd', b'e', b's', b'k', b' ', 0x1B, b'[', b'0', b'1', b';', b'3', b'2', b'm', 0xE2, 0x9E, 0x9C, b' ', 0x1B, b'[', b'0', b'1', b';', b'3', b'2', b'm', b' ', 0x1B, b'[', b'3', b'6', b'm', b'~', b'/', b'c', b'o', b'd', b'e', ]; let mut handler = MockHandler::default(); let mut parser = Processor::::new(); parser.advance(&mut handler, BYTES); } #[test] fn parse_designate_g0_as_line_drawing() { static BYTES: &[u8] = &[0x1B, b'(', b'0']; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, BYTES); assert_eq!(handler.index, CharsetIndex::G0); assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing); } #[test] fn parse_designate_g1_as_line_drawing_and_invoke() { static BYTES: &[u8] = &[0x1B, b')', b'0', 0x0E]; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, &BYTES[..3]); assert_eq!(handler.index, CharsetIndex::G1); assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing); let mut handler = MockHandler::default(); parser.advance(&mut handler, &[BYTES[3]]); assert_eq!(handler.index, CharsetIndex::G1); } #[test] fn parse_valid_rgb_colors() { assert_eq!(xparse_color(b"rgb:f/e/d"), Some(Rgb { r: 0xFF, g: 0xEE, b: 0xDD })); assert_eq!(xparse_color(b"rgb:11/aa/ff"), Some(Rgb { r: 0x11, g: 0xAA, b: 0xFF })); assert_eq!(xparse_color(b"rgb:f/ed1/cb23"), Some(Rgb { r: 0xFF, g: 0xEC, b: 0xCA })); assert_eq!(xparse_color(b"rgb:ffff/0/0"), Some(Rgb { r: 0xFF, g: 0x0, b: 0x0 })); } #[test] fn parse_valid_legacy_rgb_colors() { assert_eq!(xparse_color(b"#1af"), Some(Rgb { r: 0x10, g: 0xA0, b: 0xF0 })); assert_eq!(xparse_color(b"#11aaff"), Some(Rgb { r: 0x11, g: 0xAA, b: 0xFF })); assert_eq!(xparse_color(b"#110aa0ff0"), Some(Rgb { r: 0x11, g: 0xAA, b: 0xFF })); assert_eq!(xparse_color(b"#1100aa00ff00"), Some(Rgb { r: 0x11, g: 0xAA, b: 0xFF })); } #[test] fn parse_invalid_rgb_colors() { assert_eq!(xparse_color(b"rgb:0//"), None); assert_eq!(xparse_color(b"rgb://///"), None); } #[test] fn parse_invalid_legacy_rgb_colors() { assert_eq!(xparse_color(b"#"), None); assert_eq!(xparse_color(b"#f"), None); } #[test] fn parse_invalid_number() { assert_eq!(parse_number(b"1abc"), None); } #[test] fn parse_valid_number() { assert_eq!(parse_number(b"123"), Some(123)); } #[test] fn parse_number_too_large() { assert_eq!(parse_number(b"321"), None); } #[test] fn parse_osc4_set_color() { let bytes: &[u8] = b"\x1b]4;0;#fff\x1b\\"; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); assert_eq!(handler.color, Some(Rgb { r: 0xF0, g: 0xF0, b: 0xF0 })); } #[test] fn parse_osc104_reset_color() { let bytes: &[u8] = b"\x1b]104;1;\x1b\\"; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); assert_eq!(handler.reset_colors, vec![1]); } #[test] fn parse_osc104_reset_all_colors() { let bytes: &[u8] = b"\x1b]104;\x1b\\"; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); let expected: Vec = (0..256).collect(); assert_eq!(handler.reset_colors, expected); } #[test] fn parse_osc104_reset_all_colors_no_semicolon() { let bytes: &[u8] = b"\x1b]104\x1b\\"; let mut parser = Processor::::new(); let mut handler = MockHandler::default(); parser.advance(&mut handler, bytes); let expected: Vec = (0..256).collect(); assert_eq!(handler.reset_colors, expected); } #[test] fn partial_sync_updates() { let mut parser = Processor::::new(); let mut handler = MockHandler::default(); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_none()); // Start synchronized update. parser.advance(&mut handler, b"\x1b[?20"); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_none()); parser.advance(&mut handler, b"26h"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); // Dispatch some data. parser.advance(&mut handler, b"random \x1b[31m stuff"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); // Extend synchronized update. parser.advance(&mut handler, b"\x1b[?20"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); parser.advance(&mut handler, b"26h"); assert_eq!(parser.state.sync_state.timeout.is_sync, 2); assert!(handler.attr.is_none()); // Terminate synchronized update. parser.advance(&mut handler, b"\x1b[?20"); assert_eq!(parser.state.sync_state.timeout.is_sync, 2); assert!(handler.attr.is_none()); parser.advance(&mut handler, b"26l"); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_some()); } #[test] fn sync_bursts_buffer() { let mut parser = Processor::::new(); let mut handler = MockHandler::default(); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_none()); // Repeat test twice to ensure internal state is reset properly. for _ in 0..2 { // Start synchronized update. parser.advance(&mut handler, b"\x1b[?2026h"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); // Ensure sync works. parser.advance(&mut handler, b"\x1b[31m"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); // Exceed sync buffer dimensions. parser.advance(&mut handler, "a".repeat(SYNC_BUFFER_SIZE).as_bytes()); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.take().is_some()); // Ensure new events are dispatched directly. parser.advance(&mut handler, b"\x1b[31m"); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.take().is_some()); } } #[test] fn mixed_sync_escape() { let mut parser = Processor::::new(); let mut handler = MockHandler::default(); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_none()); // Start synchronized update with immediate SGR. parser.advance(&mut handler, b"\x1b[?2026h\x1b[31m"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); // Terminate synchronized update and check for SGR. parser.advance(&mut handler, b"\x1b[?2026l"); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_some()); } #[test] fn sync_bsu_with_esu() { let mut parser = Processor::::new(); let mut handler = MockHandler::default(); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert!(handler.attr.is_none()); // Start synchronized update with immediate SGR. parser.advance(&mut handler, b"\x1b[?2026h\x1b[1m"); assert_eq!(parser.state.sync_state.timeout.is_sync, 1); assert!(handler.attr.is_none()); // Terminate synchronized update, but immediately start a new one. parser.advance(&mut handler, b"\x1b[?2026l\x1b[?2026h\x1b[4m"); assert_eq!(parser.state.sync_state.timeout.is_sync, 2); assert_eq!(handler.attr.take(), Some(Attr::Bold)); // Terminate again, expecting one buffered SGR. parser.advance(&mut handler, b"\x1b[?2026l"); assert_eq!(parser.state.sync_state.timeout.is_sync, 0); assert_eq!(handler.attr.take(), Some(Attr::Underline)); } #[test] #[cfg(not(feature = "no_std"))] fn contrast() { let rgb1 = Rgb { r: 0xFF, g: 0xFF, b: 0xFF }; let rgb2 = Rgb { r: 0x00, g: 0x00, b: 0x00 }; assert!((rgb1.contrast(rgb2) - 21.).abs() < f64::EPSILON); let rgb1 = Rgb { r: 0xFF, g: 0xFF, b: 0xFF }; assert!((rgb1.contrast(rgb1) - 1.).abs() < f64::EPSILON); let rgb1 = Rgb { r: 0xFF, g: 0x00, b: 0xFF }; let rgb2 = Rgb { r: 0x00, g: 0xFF, b: 0x00 }; assert!((rgb1.contrast(rgb2) - 2.285_543_608_124_253_3).abs() < f64::EPSILON); let rgb1 = Rgb { r: 0x12, g: 0x34, b: 0x56 }; let rgb2 = Rgb { r: 0xFE, g: 0xDC, b: 0xBA }; assert!((rgb1.contrast(rgb2) - 9.786_558_997_257_74).abs() < f64::EPSILON); } }