// The code in this file is adapted from the vte crate (an Alacritty project) // under the Apache license; see: crates/warp_terminal/src/model/LICENSE-ALACRITTY. //! This module exports abstractions for parameters of control sequence actions; //! e.g. actions to be executed after receiving a control sequence from the pty. //! //! Examples of such actions include repositioning the cursor, changing text //! styles, and setting terminal modes. use std::convert::TryFrom; use std::{iter, str}; use anyhow::bail; use get_size::GetSize; use log::trace; use pathfinder_color::ColorU; use serde::{Deserialize, Serialize}; use thiserror::Error; use vte::ParamsIter; /// 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, } /// Terminal modes. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Mode { /// ?1 CursorKeys, /// Select 80 or 132 columns per page. /// /// 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) #[allow(clippy::upper_case_acronyms)] DECCOLM, /// 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, /// ?6 Origin, /// ?7 LineWrap, /// ?12 BlinkingCursor, /// 20 /// /// NB This is actually a private mode. We should consider adding a second /// enumeration for public/private modesets. LineFeedNewLine, /// ?25 ShowCursor, /// ?1000 ReportMouseClicks, /// ?1002 ReportCellMouseMotion, /// ?1003 ReportAllMouseMotion, /// ?1004 ReportFocusInOut, /// ?1005 Utf8Mouse, /// ?1006 SgrMouse, /// ?1007 AlternateScroll, /// ?1042 UrgencyHints, /// ?1049, 47 SwapScreen { save_cursor_and_clear_screen: bool }, /// ?2004 BracketedPaste, /// ?2026 /// See https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036. SyncOutput, } impl Mode { /// Create mode from a primitive. /// /// TODO lots of unhandled values. pub fn from_primitive(intermediate: Option<&u8>, num: u16) -> Option { // 0 is not a valid DEC mode. if num == 0 { return None; }; let private = match intermediate { Some(b'?') => true, None => false, _ => return None, }; if private { Some(match num { 1 => Mode::CursorKeys, 3 => Mode::DECCOLM, 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, 47 => Mode::SwapScreen { save_cursor_and_clear_screen: false, }, 1049 => Mode::SwapScreen { save_cursor_and_clear_screen: true, }, 2004 => Mode::BracketedPaste, 2026 => Mode::SyncOutput, _ => { 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, Copy, Clone)] 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, Copy, Clone)] pub enum ClearMode { /// Clear below cursor. Below, /// Clear above cursor. Above, /// Clear entire terminal. All, /// Clear 'saved' lines (scrollback). Saved, /// Clears all the lines in the terminal, putting the prompt on the first line. ResetAndClear, /// A synthetic mode used to clear the active block only. /// When it comes to interacting with the PTY, this is equivalent to a [ClearMode::ResetAndClear]. ActiveBlock, } /// Mode for clearing tab stops. #[derive(Debug, Copy, Clone)] pub enum TabulationClearMode { /// Clear stop under cursor. Current, /// Clear all stops. All, } /// Standard colors. /// /// Note: These are explicitly not given values to match the Color list, as we want this enum to /// fit into a single byte. See the comment on `terminal::model::cell::Cell` for more details about /// the specific memory alignment. #[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] pub enum NamedColor { Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, BrightBlack, BrightRed, BrightGreen, BrightYellow, BrightBlue, BrightMagenta, BrightCyan, BrightWhite, Foreground, Background, Cursor, DimBlack, DimRed, DimGreen, DimYellow, DimBlue, DimMagenta, DimCyan, DimWhite, BrightForeground, DimForeground, } impl NamedColor { 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, } } 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, } } // This can fail if the caller asks for a background color but self is // NamedColor::Foreground, for example pub fn to_ansi_bg_escape_code(&self) -> anyhow::Result { let code = match self { NamedColor::Black | NamedColor::DimBlack => 40, NamedColor::Red | NamedColor::DimRed => 41, NamedColor::Green | NamedColor::DimGreen => 42, NamedColor::Yellow | NamedColor::DimYellow => 43, NamedColor::Blue | NamedColor::DimBlue => 44, NamedColor::Magenta | NamedColor::DimMagenta => 45, NamedColor::Cyan | NamedColor::DimCyan => 46, NamedColor::White | NamedColor::DimWhite => 47, NamedColor::Background => 49, NamedColor::BrightBlack => 100, NamedColor::BrightRed => 101, NamedColor::BrightGreen => 102, NamedColor::BrightYellow => 103, NamedColor::BrightBlue => 104, NamedColor::BrightMagenta => 105, NamedColor::BrightCyan => 106, NamedColor::BrightWhite => 107, _ => bail!("{:?} is not a valid background", self), }; Ok(code) } pub fn to_ansi_fg_escape_code(&self) -> anyhow::Result { let code = match self { NamedColor::Black | NamedColor::DimBlack => 30, NamedColor::Red | NamedColor::DimRed => 31, NamedColor::Green | NamedColor::DimGreen => 32, NamedColor::Yellow | NamedColor::DimYellow => 33, NamedColor::Blue | NamedColor::DimBlue => 34, NamedColor::Magenta | NamedColor::DimMagenta => 35, NamedColor::Cyan | NamedColor::DimCyan => 36, NamedColor::White | NamedColor::DimWhite => 37, NamedColor::Foreground | NamedColor::BrightForeground | NamedColor::DimForeground => 39, NamedColor::BrightBlack => 90, NamedColor::BrightRed => 91, NamedColor::BrightGreen => 92, NamedColor::BrightYellow => 93, NamedColor::BrightBlue => 94, NamedColor::BrightMagenta => 95, NamedColor::BrightCyan => 96, NamedColor::BrightWhite => 97, _ => bail!("{:?} is not a valid foreground", self), }; Ok(code) } pub fn into_color_index(self) -> usize { use NamedColor::*; match self { Black => color_index::BLACK, Red => color_index::RED, Green => color_index::GREEN, Yellow => color_index::YELLOW, Blue => color_index::BLUE, Magenta => color_index::MAGENTA, Cyan => color_index::CYAN, White => color_index::WHITE, BrightBlack => color_index::BRIGHT_BLACK, BrightRed => color_index::BRIGHT_RED, BrightGreen => color_index::BRIGHT_GREEN, BrightYellow => color_index::BRIGHT_YELLOW, BrightBlue => color_index::BRIGHT_BLUE, BrightMagenta => color_index::BRIGHT_MAGENTA, BrightCyan => color_index::BRIGHT_CYAN, BrightWhite => color_index::BRIGHT_WHITE, Foreground => color_index::FOREGROUND, Background => color_index::BACKGROUND, Cursor => color_index::CURSOR, DimBlack => color_index::DIM_BLACK, DimRed => color_index::DIM_RED, DimGreen => color_index::DIM_GREEN, DimYellow => color_index::DIM_YELLOW, DimBlue => color_index::DIM_BLUE, DimMagenta => color_index::DIM_MAGENTA, DimCyan => color_index::DIM_CYAN, DimWhite => color_index::DIM_WHITE, BrightForeground => color_index::BRIGHT_FOREGROUND, DimForeground => color_index::DIM_FOREGROUND, } } } #[derive(Serialize, Deserialize, Debug)] #[serde(remote = "ColorU")] //TODO write a deserializer (so #ff00aa could be used) pub struct ColorUDef { pub r: u8, pub g: u8, pub b: u8, #[serde(skip, default = "default_alpha")] pub a: u8, } fn default_alpha() -> u8 { 255 } #[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] pub enum Color { Named(NamedColor), #[serde(with = "ColorUDef")] Spec(ColorU), Indexed(u8), } impl GetSize for Color {} /// Terminal character attributes. #[derive(Copy, Clone, 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, /// 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), } /// 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, SpecialCharacterAndusizeDrawing, } 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] #[allow(dead_code)] pub fn map(self, c: char) -> char { match self { StandardCharset::Ascii => c, StandardCharset::SpecialCharacterAndusizeDrawing => match c { '`' => '◆', 'a' => '▒', 'b' => '\t', 'c' => '\u{000c}', 'd' => '\r', 'e' => '\n', 'f' => '°', 'g' => '±', 'h' => '\u{2424}', 'i' => '\u{000b}', 'j' => '┘', 'k' => '┐', 'l' => '┌', 'm' => '└', 'n' => '┼', 'o' => '⎺', 'p' => '⎻', 'q' => '─', 'r' => '⎼', 's' => '⎽', 't' => '├', 'u' => '┤', 'v' => '┴', 'w' => '┬', 'x' => '│', 'y' => '≤', 'z' => '≥', '{' => 'π', '|' => '≠', '}' => '£', '~' => '·', _ => c, }, } } } pub fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec> { 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, ..] => 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 @ ..] => { 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).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 @ ..] => { 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).map(Attr::Background) } [49] => Some(Attr::Background(Color::Named(NamedColor::Background))), [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 } /// Parse a color specifier from list of attributes. fn parse_sgr_color(params: &mut dyn Iterator) -> Option { match params.next() { Some(2) => Some(Color::Spec(ColorU::new( u8::try_from(params.next()?).ok()?, u8::try_from(params.next()?).ok()?, u8::try_from(params.next()?).ok()?, 0xff, ))), Some(5) => Some(Color::Indexed(u8::try_from(params.next()?).ok()?)), _ => None, } } /// Defines the varieties of prompt marker sequences we can process. #[derive(Copy, Clone, Debug)] pub enum PromptMarker { /// A marker indicating that the shell is starting to write out /// a prompt of the given kind. StartPrompt { kind: PromptKind }, /// A marker indicating that the shell has finished writing out /// the in-progress prompt. EndPrompt, } #[derive(Debug, Error)] pub enum PromptMarkerParseError { #[error("unknown parameter encountered")] UnknownParam, #[error("malformed option encountered")] MalformedOption, } impl TryFrom<&[&[u8]]> for PromptMarker { type Error = PromptMarkerParseError; /// Try to parse prompt marker information from an OSC 133 /// sequence. /// /// See the "semantic prompts" spec from terminal-wg for more /// details on the grammar and parameters: /// https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md fn try_from(params: &[&[u8]]) -> Result { match params.first() { Some(&b"A") => Ok(PromptMarker::StartPrompt { kind: PromptKind::Initial, }), Some(&b"B") => Ok(PromptMarker::EndPrompt), Some(&b"P") => { // Default to "Initial" as the kind, if one is not specified as an option. let mut kind = PromptKind::Initial; // Loop through and parse out any options, which are expected to be of the form // "key=value". We ignore unknown options, but return an error for any malformed // ones. for param in ¶ms[1..] { let Some(eq_index) = param.iter().position(|byte| byte == &b"="[0]) else { return Err(Self::Error::MalformedOption); }; if eq_index + 1 >= param.len() { return Err(Self::Error::MalformedOption); } let key = ¶m[..eq_index]; let value = ¶m[eq_index + 1..]; // "k" represents the prompt kind; try to parse the value into our // PromptKind enum. if let b"k" = key { if let Ok(k) = PromptKind::try_from(value) { kind = k; } } } Ok(PromptMarker::StartPrompt { kind }) } _ => Err(Self::Error::UnknownParam), } } } /// An enumeration of the kinds of prompts we support. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PromptKind { /// The initial (left) prompt. Initial, /// The right-side prompt. Right, } #[derive(Debug, Error)] pub enum PromptKindParseError { #[error("unknown value")] UnknownValue, } impl TryFrom<&[u8]> for PromptKind { type Error = PromptKindParseError; fn try_from(value: &[u8]) -> Result { match value { b"i" => Ok(PromptKind::Initial), b"r" => Ok(PromptKind::Right), _ => Err(Self::Error::UnknownValue), } } } pub mod color_index { pub const BLACK: usize = 0; pub const RED: usize = 1; pub const GREEN: usize = 2; pub const YELLOW: usize = 3; pub const BLUE: usize = 4; pub const MAGENTA: usize = 5; pub const CYAN: usize = 6; pub const WHITE: usize = 7; pub const BRIGHT_BLACK: usize = 8; pub const BRIGHT_RED: usize = 9; pub const BRIGHT_GREEN: usize = 10; pub const BRIGHT_YELLOW: usize = 11; pub const BRIGHT_BLUE: usize = 12; pub const BRIGHT_MAGENTA: usize = 13; pub const BRIGHT_CYAN: usize = 14; pub const BRIGHT_WHITE: usize = 15; pub const FOREGROUND: usize = 256; pub const BACKGROUND: usize = 257; pub const CURSOR: usize = 258; pub const DIM_BLACK: usize = 259; pub const DIM_RED: usize = 260; pub const DIM_GREEN: usize = 261; pub const DIM_YELLOW: usize = 262; pub const DIM_BLUE: usize = 263; pub const DIM_MAGENTA: usize = 264; pub const DIM_CYAN: usize = 265; pub const DIM_WHITE: usize = 266; pub const BRIGHT_FOREGROUND: usize = 267; pub const DIM_FOREGROUND: usize = 268; }