diff options
author | Anhad Singh <62820092+Andy-Python-Programmer@users.noreply.github.com> | 2023-05-15 08:54:30 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-14 22:54:30 +0000 |
commit | 257b925513c8bf47895dac90d8ccb815bec88492 (patch) | |
tree | 3f0f8cb7ac9817d857dd1be3a43e300b3a4c9e7d | |
parent | aba7d319b621609ab4244d1fe25187a319b04664 (diff) | |
download | r-alacritty-vte-257b925513c8bf47895dac90d8ccb815bec88492.tar.gz r-alacritty-vte-257b925513c8bf47895dac90d8ccb815bec88492.tar.bz2 r-alacritty-vte-257b925513c8bf47895dac90d8ccb815bec88492.zip |
Migrate `ansi` from `alacritty_terminal`
Signed-off-by: Andy-Python-Programmer <andypythonappdeveloper@gmail.com>
Signed-off-by: Anhad Singh <andypythonappdeveloper@gmail.com>
Co-authored-by: Nicholas Sim <nsim@posteo.net>
Co-authored-by: Christian Duerr <contact@christianduerr.com>
-rw-r--r-- | .builds/linux.yml | 8 | ||||
-rw-r--r-- | CHANGELOG.md | 7 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/ansi.rs | 2088 | ||||
-rw-r--r-- | src/definitions.rs | 9 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | utf8parse/src/types.rs | 9 |
7 files changed, 2111 insertions, 17 deletions
diff --git a/.builds/linux.yml b/.builds/linux.yml index a574963..502f5fb 100644 --- a/.builds/linux.yml +++ b/.builds/linux.yml @@ -7,15 +7,17 @@ tasks: - stable: | cd vte $HOME/.cargo/bin/cargo +stable test + $HOME/.cargo/bin/cargo +stable test --features=ansi - clippy: | cd vte $HOME/.cargo/bin/cargo +stable clippy + $HOME/.cargo/bin/cargo +stable clippy --features=ansi - rustfmt: | $HOME/.cargo/bin/rustup toolchain install nightly -c rustfmt cd vte $HOME/.cargo/bin/cargo +nightly fmt -- --check - - 1-56-0: | - $HOME/.cargo/bin/rustup toolchain install --profile minimal 1.56.0 + - 1-62-1: | + $HOME/.cargo/bin/rustup toolchain install --profile minimal 1.62.1 cd vte rm Cargo.lock - $HOME/.cargo/bin/cargo +1.56.0 test + $HOME/.cargo/bin/cargo +1.62.1 test diff --git a/CHANGELOG.md b/CHANGELOG.md index b19cc66..344ad50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +## Unreleased + +- Minimum rust version has been bumped to 1.62.1 +- Support for ANSI terminal stream parsing under the `ansi` feature. +- Addition of the `serde` feature which derives `Serialize` and `Deserialize` + for the types provided in the `ansi` module. + ## 0.11.0 - Minimum rust version has been bumped to 1.56.0 @@ -16,10 +16,15 @@ rust-version = "1.56.0" [dependencies] vte_generate_state_changes = { version = "0.1.0", path = "vte_generate_state_changes" } arrayvec = { version = "0.7.2", default-features = false, optional = true } +log = { version = "0.4.17", optional = true } utf8parse = { version = "0.2.0", path = "utf8parse" } +serde = { version = "1.0.160", features = ["derive"], optional = true } [features] +ansi = ["log"] +serde = ["dep:serde"] default = ["no_std"] +floats = [] no_std = ["arrayvec"] nightly = ["utf8parse/nightly"] diff --git a/src/ansi.rs b/src/ansi.rs new file mode 100644 index 0000000..282be0f --- /dev/null +++ b/src/ansi.rs @@ -0,0 +1,2088 @@ +// 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}; +use core::ops::{Add, Sub}; +use core::str::FromStr; +use core::time::Duration; +use core::{iter, str}; + +#[cfg(not(feature = "no_std"))] +use core::ops::Mul; + +#[cfg(not(feature = "no_std"))] +use std::time::Instant; + +use log::{debug, trace}; +#[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 synchronized update DCS sequence before the passthrough parameters. +const SYNC_ESCAPE_START_LEN: usize = 5; + +/// Start of the DCS sequence for beginning synchronized updates. +const SYNC_START_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = [b'\x1b', b'P', b'=', b'1', b's']; + +/// Start of the DCS sequence for terminating synchronized updates. +const SYNC_END_ESCAPE_START: [u8; SYNC_ESCAPE_START_LEN] = [b'\x1b', b'P', b'=', b'2', b's']; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Hyperlink { + /// Identifier for the given hyperlink. + pub id: Option<String>, + /// 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<f32> 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, + }; + + trace!("Scaling RGB by {} from {:?} to {:?}", rhs, self, result); + result + } +} + +impl Add<Rgb> 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<Rgb> 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<Rgb, ()> { + 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<Rgb> { + 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<Rgb> { + let colors = str::from_utf8(color).ok()?.split('/').collect::<Vec<_>>(); + + 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<Rgb> { + 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<u8> { + if input.is_empty() { + return None; + } + let mut num: u8 = 0; + for c in input { + let c = *c as char; + if let Some(digit) = c.to_digit(10) { + num = match num.checked_mul(10).and_then(|v| v.checked_add(digit as u8)) { + Some(v) => v, + None => return None, + } + } else { + return None; + } + } + Some(num) +} + +/// Internal state for VTE processor. +#[derive(Debug, Default)] +struct ProcessorState<T: Timeout> { + /// Last processed character for repetition. + preceding_char: Option<char>, + + /// DCS sequence waiting for termination. + dcs: Option<Dcs>, + + /// State for synchronized terminal updates. + sync_state: SyncState<T>, +} + +#[derive(Debug)] +struct SyncState<T: Timeout> { + /// Handler for synchronized updates. + timeout: T, + + /// Sync DCS waiting for termination sequence. + pending_dcs: Option<Dcs>, + + /// Bytes read during the synchronized update. + buffer: Vec<u8>, +} + +impl<T: Timeout> Default for SyncState<T> { + fn default() -> Self { + Self { + buffer: Vec::with_capacity(SYNC_BUFFER_SIZE), + pending_dcs: None, + timeout: T::default(), + } + } +} + +/// Pending DCS sequence. +#[derive(Debug)] +enum Dcs { + /// Begin of the synchronized update. + SyncStart, + + /// End of the synchronized update. + SyncEnd, +} + +/// The processor wraps a `crate::Parser` to ultimately call methods on a Handler. +#[cfg(not(feature = "no_std"))] +#[derive(Default)] +pub struct Processor<T: Timeout = StdSyncHandler> { + state: ProcessorState<T>, + 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<T: Timeout> { + state: ProcessorState<T>, + parser: crate::Parser, +} + +impl<T: Timeout> Processor<T> { + #[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<H>(&mut self, handler: &mut H, byte: u8) + where + H: Handler, + { + if self.state.sync_state.timeout.pending_timeout() { + self.advance_sync(handler, byte); + } else { + let mut performer = Performer::new(&mut self.state, handler); + self.parser.advance(&mut performer, byte); + } + } + + /// End a synchronized update. + pub fn stop_sync<H>(&mut self, handler: &mut H) + where + H: Handler, + { + // Process all synchronized bytes. + for i in 0..self.state.sync_state.buffer.len() { + let byte = self.state.sync_state.buffer[i]; + let mut performer = Performer::new(&mut self.state, handler); + self.parser.advance(&mut performer, byte); + } + + // Resetting state after processing makes sure we don't interpret buffered sync escapes. + self.state.sync_state.buffer.clear(); + self.state.sync_state.timeout.clear_timeout(); + } + + /// 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. + #[cold] + fn advance_sync<H>(&mut self, handler: &mut H, byte: u8) + where + H: Handler, + { + self.state.sync_state.buffer.push(byte); + + // Handle sync DCS escape sequences. + match self.state.sync_state.pending_dcs { + Some(_) => self.advance_sync_dcs_end(handler, byte), + None => self.advance_sync_dcs_start(), + } + } + + /// Find the start of sync DCS sequences. + fn advance_sync_dcs_start(&mut self) { + // Get the last few bytes for comparison. + let len = self.state.sync_state.buffer.len(); + let offset = len.saturating_sub(SYNC_ESCAPE_START_LEN); + let end = &self.state.sync_state.buffer[offset..]; + + // Check for extension/termination of the synchronized update. + if end == SYNC_START_ESCAPE_START { + self.state.sync_state.pending_dcs = Some(Dcs::SyncStart); + } else if end == SYNC_END_ESCAPE_START || len >= SYNC_BUFFER_SIZE - 1 { + self.state.sync_state.pending_dcs = Some(Dcs::SyncEnd); + } + } + + /// Parse the DCS termination sequence for synchronized updates. + fn advance_sync_dcs_end<H>(&mut self, handler: &mut H, byte: u8) + where + H: Handler, + { + match byte { + // Ignore DCS passthrough characters. + 0x00..=0x17 | 0x19 | 0x1c..=0x7f | 0xa0..=0xff => (), + // Cancel the DCS sequence. + 0x18 | 0x1a | 0x80..=0x9f => self.state.sync_state.pending_dcs = None, + // Dispatch on ESC. + 0x1b => match self.state.sync_state.pending_dcs.take() { + Some(Dcs::SyncStart) => { + self.state.sync_state.timeout.set_timeout(SYNC_UPDATE_TIMEOUT); + }, + Some(Dcs::SyncEnd) => self.stop_sync(handler), + None => (), + }, + } + } +} + +/// 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<T>, + handler: &'a mut H, +} + +impl<'a, H: Handler + 'a, T: Timeout> Performer<'a, H, T> { + /// Create a performer. + #[inline] + pub fn new<'b>(state: &'b mut ProcessorState<T>, handler: &'b mut H) -> Performer<'b, H, T> { + Performer { state, handler } + } +} + +#[cfg(not(feature = "no_std"))] +#[derive(Default)] +pub struct StdSyncHandler { + timeout: Option<Instant>, +} + +#[cfg(not(feature = "no_std"))] +impl StdSyncHandler { + /// Synchronized update expiration time. + #[inline] + pub fn sync_timeout(&self) -> Option<Instant> { + 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<String>) {} + + /// Set the cursor style. + fn set_cursor_style(&mut self, _: Option<CursorStyle>) {} + + /// 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<char>) {} + + /// 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) {} + + /// 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) {} + + /// DECSTBM - Set the terminal scrolling region. + fn set_scrolling_region(&mut self, _top: usize, _bottom: Option<usize>) {} + + /// 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<Hyperlink>) {} +} + +/// 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, Eq, PartialEq, Copy, Clone, Hash)] +pub enum CursorShape { + /// Cursor is a block like `▒`. + Block, + + /// Cursor is an underscore like `_`. + Underline, + + /// Cursor is a vertical bar `⎸`. + Beam, + + /// Cursor is a box like `☐`. + HollowBlock, + + /// Invisible cursor. + Hidden, +} + +impl Default for CursorShape { + fn default() -> CursorShape { + CursorShape::Block + } +} + +/// Terminal modes. +#[derive(Debug, Eq, PartialEq)] +pub enum Mode { + /// ?1 + 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, + /// IRM Insert Mode. + /// + /// NB should be part of non-private mode enum. + /// + /// * `CSI 4 h` change to insert mode + /// * `CSI 4 l` reset to replacement mode + Insert = 4, + /// ?6 + Origin = 6, + /// ?7 + LineWrap = 7, + /// ?12 + BlinkingCursor = 12, + /// 20 + /// + /// NB This is actually a private mode. We should consider adding a second + /// enumeration for public/private modesets. + LineFeedNewLine = 20, + /// ?25 + ShowCursor = 25, + /// ?1000 + ReportMouseClicks = 1000, + /// ?1002 + ReportCellMouseMotion = 1002, + /// ?1003 + ReportAllMouseMotion = 1003, + /// ?1004 + ReportFocusInOut = 1004, + /// ?1005 + Utf8Mouse = 1005, + /// ?1006 + SgrMouse = 1006, + /// ?1007 + AlternateScroll = 1007, + /// ?1042 + UrgencyHints = 1042, + /// ?1049 + SwapScreenAndSetRestoreCursor = 1049, + /// ?2004 + BracketedPaste = 2004, +} + +impl Mode { + /// Create mode from a primitive. + pub fn from_primitive(intermediate: Option<&u8>, num: u16) -> Option<Mode> { + let private = match intermediate { + Some(b'?') => true, + None => false, + _ => return None, + }; + + if private { + Some(match num { + 1 => Mode::CursorKeys, + 3 => Mode::ColumnMode, + 6 => Mode::Origin, + 7 => Mode::LineWrap, + 12 => Mode::BlinkingCursor, + 25 => Mode::ShowCursor, + 1000 => Mode::ReportMouseClicks, + 1002 => Mode::ReportCellMouseMotion, + 1003 => Mode::ReportAllMouseMotion, + 1004 => Mode::ReportFocusInOut, + 1005 => Mode::Utf8Mouse, + 1006 => Mode::SgrMouse, + 1007 => Mode::AlternateScroll, + 1042 => Mode::UrgencyHints, + 1049 => Mode::SwapScreenAndSetRestoreCursor, + 2004 => Mode::BracketedPaste, + _ => { + trace!("[unimplemented] primitive mode: {}", num); + return None; + }, + }) + } else { + Some(match num { + 4 => Mode::Insert, + 20 => Mode::LineFeedNewLine, + _ => return None, + }) + } + } +} + +/// 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<Color>), +} + +/// Identifiers which can be assigned to a graphic character set. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CharsetIndex { + /// Default set, is designated as ASCII at startup. + G0, + G1, + G2, + G3, +} + +impl Default for CharsetIndex { + fn default() -> Self { + CharsetIndex::G0 + } +} + +/// Standard or common character sets which can be designated as G0-G3. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum StandardCharset { + Ascii, + SpecialCharacterAndLineDrawing, +} + +impl Default for StandardCharset { + fn default() -> Self { + StandardCharset::Ascii + } +} + +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, + }, + } + } +} + +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) { + match (action, intermediates) { + ('s', [b'=']) => { + // Start a synchronized update. The end is handled with a separate parser. + if params.iter().next().map_or(false, |param| param[0] == 1) { + self.state.dcs = Some(Dcs::SyncStart); + } + }, + _ => 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) { + match self.state.dcs { + Some(Dcs::SyncStart) => { + self.state.sync_state.timeout.set_timeout(SYNC_UPDATE_TIMEOUT); + }, + Some(Dcs::SyncEnd) => (), + _ => 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::<Vec<&str>>() + .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 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() > 1 { + 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), + ('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', intermediates) => { + for param in params_iter.map(|param| param[0]) { + match Mode::from_primitive(intermediates.first(), param) { + Some(mode) => handler.set_mode(mode), + None => unhandled!(), + } + } + }, + ('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); + }, + ('L', []) => handler.insert_blank_lines(next_param_or(1) as usize), + ('l', intermediates) => { + for param in params_iter.map(|param| param[0]) { + match Mode::from_primitive(intermediates.first(), param) { + Some(mode) => handler.unset_mode(mode), + None => unhandled!(), + } + } + }, + ('M', []) => handler.delete_lines(next_param_or(1) as usize), + ('m', []) => { + if params.is_empty() { + handler.terminal_attribute(Attr::Reset); + } else { + for attr in attrs_from_sgr_parameters(&mut params_iter) { + match attr { + Some(attr) => handler.terminal_attribute(attr), + None => unhandled!(), + } + } + } + }, + ('n', []) => handler.device_status(next_param_or(0) as usize), + ('P', []) => handler.delete_chars(next_param_or(1) as usize), + ('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', []) => 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 attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> { + let mut attrs = Vec::with_capacity(params.size_hint().0); + + 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, + }; + attrs.push(attr); + } + + attrs +} + +/// Handle colon separated rgb color escape sequence. +#[inline] +fn handle_colon_rgb(params: &[u16]) -> Option<Color> { + 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<Item = u16>) -> Option<Color> { + 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; + + impl Timeout for TestSyncHandler { + #[inline] + fn set_timeout(&mut self, _: Duration) { + unreachable!() + } + + #[inline] + fn clear_timeout(&mut self) { + unreachable!() + } + + #[inline] + fn pending_timeout(&self) -> bool { + false + } + } + + struct MockHandler { + index: CharsetIndex, + charset: StandardCharset, + attr: Option<Attr>, + identity_reported: bool, + color: Option<Rgb>, + reset_colors: Vec<usize>, + } + + 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<char>) { + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in BYTES { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + assert!(!handler.identity_reported); + handler.reset_state(); + + let bytes: &[u8] = &[0x1b, b'[', b'c']; + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + assert!(handler.identity_reported); + handler.reset_state(); + + let bytes: &[u8] = &[0x1b, b'[', b'0', b'c']; + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + assert!(handler.identity_reported); + } + + #[test] + fn parse_terminal_identity_esc() { + let bytes: &[u8] = &[0x1b, b'Z']; + + let mut parser = Processor::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + assert!(handler.identity_reported); + handler.reset_state(); + + let bytes: &[u8] = &[0x1b, b'#', b'Z']; + + let mut parser = Processor::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in BYTES { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + + for byte in BYTES { + parser.advance(&mut handler, *byte); + } + } + + #[test] + fn parse_designate_g0_as_line_drawing() { + static BYTES: &[u8] = &[0x1b, b'(', b'0']; + let mut parser = Processor::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in BYTES { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in &BYTES[..3] { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + 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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + let expected: Vec<usize> = (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::<TestSyncHandler>::new(); + let mut handler = MockHandler::default(); + + for byte in bytes { + parser.advance(&mut handler, *byte); + } + + let expected: Vec<usize> = (0..256).collect(); + assert_eq!(handler.reset_colors, expected); + } +} diff --git a/src/definitions.rs b/src/definitions.rs index fe7952d..a1f264d 100644 --- a/src/definitions.rs +++ b/src/definitions.rs @@ -1,7 +1,7 @@ use core::mem; #[allow(dead_code)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Default, Copy, Clone)] pub enum State { Anywhere = 0, CsiEntry = 1, @@ -15,18 +15,13 @@ pub enum State { DcsPassthrough = 9, Escape = 10, EscapeIntermediate = 11, + #[default] Ground = 12, OscString = 13, SosPmApcString = 14, Utf8 = 15, } -impl Default for State { - fn default() -> State { - State::Ground - } -} - #[allow(dead_code)] #[derive(Debug, Clone, Copy)] pub enum Action { @@ -44,6 +44,8 @@ mod definitions; mod params; mod table; +#[cfg(feature = "ansi")] +pub mod ansi; pub use params::{Params, ParamsIter}; use definitions::{unpack, Action, State}; diff --git a/utf8parse/src/types.rs b/utf8parse/src/types.rs index f57a94d..8a52c67 100644 --- a/utf8parse/src/types.rs +++ b/utf8parse/src/types.rs @@ -26,9 +26,10 @@ pub enum Action { /// There is a state for each initial input of the 3 and 4 byte sequences since /// the following bytes are subject to different conditions than a tail byte. #[allow(non_camel_case_types)] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub enum State { /// Ground state; expect anything + #[default] Ground = 0, /// 3 tail bytes Tail3 = 1, @@ -46,12 +47,6 @@ pub enum State { Utf8_4_3_f4 = 7, } -impl Default for State { - fn default() -> State { - State::Ground - } -} - impl State { /// Advance the parser state. /// |