blob: 592266efeb45def690f5edf7fdc736f004f6df92 [file] [log] [blame]
// 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(feature = "std")]
use core::ops::Mul;
use core::ops::{Add, Sub};
use core::str::FromStr;
use core::time::Duration;
use core::{iter, mem, str};
#[cfg(feature = "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<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(feature = "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(feature = "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(feature = "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,
};
log::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;
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<T: Timeout> {
/// Last processed character for repetition.
preceding_char: Option<char>,
/// State for synchronized terminal updates.
sync_state: SyncState<T>,
}
#[derive(Debug)]
struct SyncState<T: Timeout> {
/// Handler for synchronized updates.
timeout: T,
/// 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), timeout: Default::default() }
}
}
/// The processor wraps a `crate::Parser` to ultimately call methods on a
/// Handler.
#[cfg(feature = "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(not(feature = "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, 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<H>(&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<H>(&mut self, handler: &mut H, bsu_offset: Option<usize>)
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<H>(&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<H>(&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<T>,
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<T>, handler: &'b mut H) -> Performer<'b, H, T> {
Performer { state, handler, terminated: Default::default() }
}
}
#[cfg(feature = "std")]
#[derive(Default)]
pub struct StdSyncHandler {
timeout: Option<Instant>,
}
#[cfg(feature = "std")]
impl StdSyncHandler {
/// Synchronized update expiration time.
#[inline]
pub fn sync_timeout(&self) -> Option<Instant> {
self.timeout
}
}
#[cfg(feature = "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) {}
/// 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<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>) {}
/// 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<NamedMode> 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<NamedPrivateMode> 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<Color>),
}
/// 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::<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 &params[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 &params[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(|&param| 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<H: Handler>(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<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 {
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<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();
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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::new();
let mut handler = MockHandler::default();
parser.advance(&mut handler, bytes);
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();
parser.advance(&mut handler, bytes);
let expected: Vec<usize> = (0..256).collect();
assert_eq!(handler.reset_colors, expected);
}
#[test]
fn partial_sync_updates() {
let mut parser = Processor::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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::<TestSyncHandler>::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(feature = "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);
}
}