diff options
author | Theodore Dubois <tblodt@icloud.com> | 2019-04-28 06:24:58 -0700 |
---|---|---|
committer | Christian Duerr <chrisduerr@users.noreply.github.com> | 2019-04-28 13:24:58 +0000 |
commit | dbd8538762ef8968a493e1bf996e8693479ca783 (patch) | |
tree | 32ac2a6a5e01238a272d4ba534551d2e42903c7a /alacritty_terminal/src/ansi.rs | |
parent | 9c6d12ea2c863ba76015bdedc00db13b7307725a (diff) | |
download | alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.tar.gz alacritty-dbd8538762ef8968a493e1bf996e8693479ca783.zip |
Split alacritty into a separate crates
The crate containing the entry point is called alacritty, and the crate
containing everything else is called alacritty_terminal.
Diffstat (limited to 'alacritty_terminal/src/ansi.rs')
-rw-r--r-- | alacritty_terminal/src/ansi.rs | 1568 |
1 files changed, 1568 insertions, 0 deletions
diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs new file mode 100644 index 00000000..c0ebb79c --- /dev/null +++ b/alacritty_terminal/src/ansi.rs @@ -0,0 +1,1568 @@ +// Copyright 2016 Joe Wilm, The Alacritty Project Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//! ANSI Terminal Stream Parsing +use std::io; +use std::ops::Range; +use std::str; + +use crate::index::{Column, Contains, Line}; +use base64; +use glutin::MouseCursor; +use vte; + +use crate::term::color::Rgb; + +// Parse color arguments +// +// Expect that color argument looks like "rgb:xx/xx/xx" or "#xxxxxx" +fn parse_rgb_color(color: &[u8]) -> Option<Rgb> { + let mut iter = color.iter(); + + macro_rules! next { + () => { + iter.next().map(|v| *v as char) + }; + } + + macro_rules! parse_hex { + () => {{ + let mut digit: u8 = 0; + let next = next!().and_then(|v| v.to_digit(16)); + if let Some(value) = next { + digit = value as u8; + } + + let next = next!().and_then(|v| v.to_digit(16)); + if let Some(value) = next { + digit <<= 4; + digit += value as u8; + } + digit + }}; + } + + match next!() { + Some('r') => { + if next!() != Some('g') { + return None; + } + if next!() != Some('b') { + return None; + } + if next!() != Some(':') { + return None; + } + + let r = parse_hex!(); + let val = next!(); + if val != Some('/') { + return None; + } + let g = parse_hex!(); + if next!() != Some('/') { + return None; + } + let b = parse_hex!(); + + Some(Rgb { r, g, b }) + }, + Some('#') => Some(Rgb { r: parse_hex!(), g: parse_hex!(), b: parse_hex!() }), + _ => None, + } +} + +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) +} + +/// The processor wraps a `vte::Parser` to ultimately call methods on a Handler +pub struct Processor { + state: ProcessorState, + parser: vte::Parser, +} + +/// Internal state for VTE processor +struct ProcessorState { + preceding_char: Option<char>, +} + +/// Helper type that implements `vte::Perform`. +/// +/// Processor creates a Performer when running advance and passes the Performer +/// to `vte::Parser`. +struct Performer<'a, H: Handler + TermInfo, W: io::Write> { + _state: &'a mut ProcessorState, + handler: &'a mut H, + writer: &'a mut W, +} + +impl<'a, H: Handler + TermInfo + 'a, W: io::Write> Performer<'a, H, W> { + /// Create a performer + #[inline] + pub fn new<'b>( + state: &'b mut ProcessorState, + handler: &'b mut H, + writer: &'b mut W, + ) -> Performer<'b, H, W> { + Performer { _state: state, handler, writer } + } +} + +impl Default for Processor { + fn default() -> Processor { + Processor { state: ProcessorState { preceding_char: None }, parser: vte::Parser::new() } + } +} + +impl Processor { + pub fn new() -> Processor { + Default::default() + } + + #[inline] + pub fn advance<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W) + where + H: Handler + TermInfo, + W: io::Write, + { + let mut performer = Performer::new(&mut self.state, handler, writer); + self.parser.advance(&mut performer, byte); + } +} + +/// Trait that provides properties of terminal +pub trait TermInfo { + fn lines(&self) -> Line; + fn cols(&self) -> Column; +} + +/// 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, _: &str) {} + + /// Set the window's mouse cursor + fn set_mouse_cursor(&mut self, _: MouseCursor) {} + + /// Set the cursor style + fn set_cursor_style(&mut self, _: Option<CursorStyle>) {} + + /// A character to be displayed + fn input(&mut self, _c: char) {} + + /// Set cursor to position + fn goto(&mut self, _: Line, _: Column) {} + + /// Set cursor to specific row + fn goto_line(&mut self, _: Line) {} + + /// Set cursor to specific column + fn goto_col(&mut self, _: Column) {} + + /// Insert blank characters in current line starting from cursor + fn insert_blank(&mut self, _: Column) {} + + /// Move cursor up `rows` + fn move_up(&mut self, _: Line) {} + + /// Move cursor down `rows` + fn move_down(&mut self, _: Line) {} + + /// Identify the terminal (should write back to the pty stream) + /// + /// TODO this should probably return an io::Result + fn identify_terminal<W: io::Write>(&mut self, _: &mut W) {} + + // Report device status + fn device_status<W: io::Write>(&mut self, _: &mut W, _: usize) {} + + /// Move cursor forward `cols` + fn move_forward(&mut self, _: Column) {} + + /// Move cursor backward `cols` + fn move_backward(&mut self, _: Column) {} + + /// Move cursor down `rows` and set to column 1 + fn move_down_and_cr(&mut self, _: Line) {} + + /// Move cursor up `rows` and set to column 1 + fn move_up_and_cr(&mut self, _: Line) {} + + /// Put `count` tabs + fn put_tab(&mut self, _count: i64) {} + + /// 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, _: Line) {} + + /// Scroll down `rows` rows + fn scroll_down(&mut self, _: Line) {} + + /// Insert `count` blank lines + fn insert_blank_lines(&mut self, _: Line) {} + + /// Delete `count` lines + fn delete_lines(&mut self, _: Line) {} + + /// 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, _: Column) {} + + /// 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, _: Column) {} + + /// Move backward `count` tabs + fn move_backward_tabs(&mut self, _count: i64) {} + + /// Move forward `count` tabs + fn move_forward_tabs(&mut self, _count: i64) {} + + /// 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, _: Range<Line>) {} + + /// 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) {} + + /// Reset an indexed color to original value + fn reset_color(&mut self, _: usize) {} + + /// Set the clipboard + fn set_clipboard(&mut self, _: &str) {} + + /// Run the dectest routine + fn dectest(&mut self) {} +} + +/// Describes shape of cursor +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)] +pub enum CursorStyle { + /// 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 CursorStyle { + fn default() -> CursorStyle { + CursorStyle::Block + } +} + +/// Terminal modes +#[derive(Debug, Eq, PartialEq)] +pub enum Mode { + /// ?1 + CursorKeys = 1, + /// 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) + DECCOLM = 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, + /// ?1006 + SgrMouse = 1006, + /// ?1049 + SwapScreenAndSetRestoreCursor = 1049, + /// ?2004 + BracketedPaste = 2004, +} + +impl Mode { + /// Create mode from a primitive + /// + /// TODO lots of unhandled values.. + pub fn from_primitive(private: bool, num: i64) -> Option<Mode> { + 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, + 1006 => Mode::SgrMouse, + 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, 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 text under the cursor + CursorText, + /// 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 { + 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, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, 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, + /// Underscore text + Underscore, + /// 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 underline + 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, 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<'a, H, W> vte::Perform for Performer<'a, H, W> +where + H: Handler + TermInfo + 'a, + W: io::Write + 'a, +{ + #[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), + C1::NEL => self.handler.newline(), + C1::HTS => self.handler.set_horizontal_tabstop(), + C1::DECID => self.handler.identify_terminal(self.writer), + _ => debug!("[unhandled] execute byte={:02x}", byte), + } + } + + #[inline] + fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool) { + debug!( + "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}", + params, intermediates, ignore + ); + } + + #[inline] + fn put(&mut self, byte: u8) { + debug!("[unhandled put] byte={:?}", byte); + } + + #[inline] + fn unhook(&mut self) { + debug!("[unhandled unhook]"); + } + + // TODO replace OSC parsing with parser combinators + #[inline] + fn osc_dispatch(&mut self, params: &[&[u8]]) { + fn unhandled(params: &[&[u8]]) { + let mut buf = String::new(); + for items in params { + buf.push_str("["); + for item in *items { + buf.push_str(&format!("{:?},", *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 { + if let Ok(utf8_title) = str::from_utf8(params[1]) { + self.handler.set_title(utf8_title); + return; + } + } + unhandled(params); + }, + + // Set icon name + // This is ignored, since alacritty has no concept of tabs + b"1" => return, + + // Set color index + b"4" => { + if params.len() > 1 && params.len() % 2 != 0 { + for chunk in params[1..].chunks(2) { + let index = parse_number(chunk[0]); + let color = parse_rgb_color(chunk[1]); + if let (Some(i), Some(c)) = (index, color) { + self.handler.set_color(i as usize, c); + return; + } + } + } + unhandled(params); + }, + + // Set foreground color + b"10" => { + if params.len() >= 2 { + if let Some(color) = parse_rgb_color(params[1]) { + self.handler.set_color(NamedColor::Foreground as usize, color); + return; + } + } + unhandled(params); + }, + + // Set background color + b"11" => { + if params.len() >= 2 { + if let Some(color) = parse_rgb_color(params[1]) { + self.handler.set_color(NamedColor::Background as usize, color); + return; + } + } + unhandled(params); + }, + + // Set text cursor color + b"12" => { + if params.len() >= 2 { + if let Some(color) = parse_rgb_color(params[1]) { + self.handler.set_color(NamedColor::Cursor as usize, color); + return; + } + } + unhandled(params); + }, + + // Set cursor style + b"50" => { + if params.len() >= 2 + && params[1].len() >= 13 + && params[1][0..12] == *b"CursorShape=" + { + let style = match params[1][12] as char { + '0' => CursorStyle::Block, + '1' => CursorStyle::Beam, + '2' => CursorStyle::Underline, + _ => return unhandled(params), + }; + self.handler.set_cursor_style(Some(style)); + return; + } + unhandled(params); + }, + + // Set clipboard + b"52" => { + if params.len() < 3 { + return unhandled(params); + } + + match params[2] { + b"?" => unhandled(params), + selection => { + if let Ok(string) = base64::decode(selection) { + if let Ok(utf8_string) = str::from_utf8(&string) { + self.handler.set_clipboard(utf8_string); + } + } + }, + } + }, + + // Reset color index + b"104" => { + // Reset all color indexes when no parameters are given + if params.len() == 1 { + 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), + } + } + + #[inline] + fn csi_dispatch(&mut self, args: &[i64], intermediates: &[u8], _ignore: bool, action: char) { + let private = intermediates.get(0).map(|b| *b == b'?').unwrap_or(false); + let handler = &mut self.handler; + let writer = &mut self.writer; + + macro_rules! unhandled { + () => {{ + debug!( + "[Unhandled CSI] action={:?}, args={:?}, intermediates={:?}", + action, args, intermediates + ); + return; + }}; + } + + macro_rules! arg_or_default { + (idx: $idx:expr, default: $default:expr) => { + args.get($idx) + .and_then(|v| if *v == 0 { None } else { Some(*v) }) + .unwrap_or($default) + }; + } + + match action { + '@' => handler.insert_blank(Column(arg_or_default!(idx: 0, default: 1) as usize)), + 'A' => { + handler.move_up(Line(arg_or_default!(idx: 0, default: 1) as usize)); + }, + 'b' => { + if let Some(c) = self._state.preceding_char { + for _ in 0..arg_or_default!(idx: 0, default: 1) { + handler.input(c); + } + } else { + debug!("tried to repeat with no preceding char"); + } + }, + 'B' | 'e' => handler.move_down(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'c' => handler.identify_terminal(writer), + 'C' | 'a' => handler.move_forward(Column(arg_or_default!(idx: 0, default: 1) as usize)), + 'D' => handler.move_backward(Column(arg_or_default!(idx: 0, default: 1) as usize)), + 'E' => handler.move_down_and_cr(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'F' => handler.move_up_and_cr(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'g' => { + let mode = match arg_or_default!(idx: 0, default: 0) { + 0 => TabulationClearMode::Current, + 3 => TabulationClearMode::All, + _ => unhandled!(), + }; + + handler.clear_tabs(mode); + }, + 'G' | '`' => handler.goto_col(Column(arg_or_default!(idx: 0, default: 1) as usize - 1)), + 'H' | 'f' => { + let y = arg_or_default!(idx: 0, default: 1) as usize; + let x = arg_or_default!(idx: 1, default: 1) as usize; + handler.goto(Line(y - 1), Column(x - 1)); + }, + 'I' => handler.move_forward_tabs(arg_or_default!(idx: 0, default: 1)), + 'J' => { + let mode = match arg_or_default!(idx: 0, default: 0) { + 0 => ClearMode::Below, + 1 => ClearMode::Above, + 2 => ClearMode::All, + 3 => ClearMode::Saved, + _ => unhandled!(), + }; + + handler.clear_screen(mode); + }, + 'K' => { + let mode = match arg_or_default!(idx: 0, default: 0) { + 0 => LineClearMode::Right, + 1 => LineClearMode::Left, + 2 => LineClearMode::All, + _ => unhandled!(), + }; + + handler.clear_line(mode); + }, + 'S' => handler.scroll_up(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'T' => handler.scroll_down(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'L' => handler.insert_blank_lines(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'l' => { + for arg in args { + let mode = Mode::from_primitive(private, *arg); + match mode { + Some(mode) => handler.unset_mode(mode), + None => unhandled!(), + } + } + }, + 'M' => handler.delete_lines(Line(arg_or_default!(idx: 0, default: 1) as usize)), + 'X' => handler.erase_chars(Column(arg_or_default!(idx: 0, default: 1) as usize)), + 'P' => handler.delete_chars(Column(arg_or_default!(idx: 0, default: 1) as usize)), + 'Z' => handler.move_backward_tabs(arg_or_default!(idx: 0, default: 1)), + 'd' => handler.goto_line(Line(arg_or_default!(idx: 0, default: 1) as usize - 1)), + 'h' => { + for arg in args { + let mode = Mode::from_primitive(private, *arg); + match mode { + Some(mode) => handler.set_mode(mode), + None => unhandled!(), + } + } + }, + 'm' => { + // Sometimes a C-style for loop is just what you need + let mut i = 0; // C-for initializer + if args.is_empty() { + handler.terminal_attribute(Attr::Reset); + return; + } + loop { + if i >= args.len() { + // C-for condition + break; + } + + let attr = match args[i] { + 0 => Attr::Reset, + 1 => Attr::Bold, + 2 => Attr::Dim, + 3 => Attr::Italic, + 4 => Attr::Underscore, + 5 => Attr::BlinkSlow, + 6 => Attr::BlinkFast, + 7 => Attr::Reverse, + 8 => Attr::Hidden, + 9 => Attr::Strike, + 21 => Attr::CancelBold, + 22 => Attr::CancelBoldDim, + 23 => Attr::CancelItalic, + 24 => Attr::CancelUnderline, + 25 => Attr::CancelBlink, + 27 => Attr::CancelReverse, + 28 => Attr::CancelHidden, + 29 => Attr::CancelStrike, + 30 => Attr::Foreground(Color::Named(NamedColor::Black)), + 31 => Attr::Foreground(Color::Named(NamedColor::Red)), + 32 => Attr::Foreground(Color::Named(NamedColor::Green)), + 33 => Attr::Foreground(Color::Named(NamedColor::Yellow)), + 34 => Attr::Foreground(Color::Named(NamedColor::Blue)), + 35 => Attr::Foreground(Color::Named(NamedColor::Magenta)), + 36 => Attr::Foreground(Color::Named(NamedColor::Cyan)), + 37 => Attr::Foreground(Color::Named(NamedColor::White)), + 38 => { + let mut start = 0; + if let Some(color) = parse_color(&args[i..], &mut start) { + i += start; + Attr::Foreground(color) + } else { + break; + } + }, + 39 => Attr::Foreground(Color::Named(NamedColor::Foreground)), + 40 => Attr::Background(Color::Named(NamedColor::Black)), + 41 => Attr::Background(Color::Named(NamedColor::Red)), + 42 => Attr::Background(Color::Named(NamedColor::Green)), + 43 => Attr::Background(Color::Named(NamedColor::Yellow)), + 44 => Attr::Background(Color::Named(NamedColor::Blue)), + 45 => Attr::Background(Color::Named(NamedColor::Magenta)), + 46 => Attr::Background(Color::Named(NamedColor::Cyan)), + 47 => Attr::Background(Color::Named(NamedColor::White)), + 48 => { + let mut start = 0; + if let Some(color) = parse_color(&args[i..], &mut start) { + i += start; + Attr::Background(color) + } else { + break; + } + }, + 49 => Attr::Background(Color::Named(NamedColor::Background)), + 90 => Attr::Foreground(Color::Named(NamedColor::BrightBlack)), + 91 => Attr::Foreground(Color::Named(NamedColor::BrightRed)), + 92 => Attr::Foreground(Color::Named(NamedColor::BrightGreen)), + 93 => Attr::Foreground(Color::Named(NamedColor::BrightYellow)), + 94 => Attr::Foreground(Color::Named(NamedColor::BrightBlue)), + 95 => Attr::Foreground(Color::Named(NamedColor::BrightMagenta)), + 96 => Attr::Foreground(Color::Named(NamedColor::BrightCyan)), + 97 => Attr::Foreground(Color::Named(NamedColor::BrightWhite)), + 100 => Attr::Background(Color::Named(NamedColor::BrightBlack)), + 101 => Attr::Background(Color::Named(NamedColor::BrightRed)), + 102 => Attr::Background(Color::Named(NamedColor::BrightGreen)), + 103 => Attr::Background(Color::Named(NamedColor::BrightYellow)), + 104 => Attr::Background(Color::Named(NamedColor::BrightBlue)), + 105 => Attr::Background(Color::Named(NamedColor::BrightMagenta)), + 106 => Attr::Background(Color::Named(NamedColor::BrightCyan)), + 107 => Attr::Background(Color::Named(NamedColor::BrightWhite)), + _ => unhandled!(), + }; + + handler.terminal_attribute(attr); + + i += 1; // C-for expr + } + }, + 'n' => handler.device_status(writer, arg_or_default!(idx: 0, default: 0) as usize), + 'r' => { + if private { + unhandled!(); + } + let arg0 = arg_or_default!(idx: 0, default: 1) as usize; + let top = Line(arg0 - 1); + // Bottom should be included in the range, but range end is not + // usually included. One option would be to use an inclusive + // range, but instead we just let the open range end be 1 + // higher. + let arg1 = arg_or_default!(idx: 1, default: handler.lines().0 as _) as usize; + let bottom = Line(arg1); + + handler.set_scrolling_region(top..bottom); + }, + 's' => handler.save_cursor_position(), + 'u' => handler.restore_cursor_position(), + 'q' => { + let style = match arg_or_default!(idx: 0, default: 0) { + 0 => None, + 1 | 2 => Some(CursorStyle::Block), + 3 | 4 => Some(CursorStyle::Underline), + 5 | 6 => Some(CursorStyle::Beam), + _ => unhandled!(), + }; + + handler.set_cursor_style(style); + }, + _ => unhandled!(), + } + } + + #[inline] + fn esc_dispatch(&mut self, params: &[i64], intermediates: &[u8], _ignore: bool, byte: u8) { + macro_rules! unhandled { + () => {{ + debug!( + "[unhandled] esc_dispatch params={:?}, ints={:?}, byte={:?} ({:02x})", + params, intermediates, byte as char, byte + ); + return; + }}; + } + + macro_rules! configure_charset { + ($charset:path) => {{ + let index: CharsetIndex = match intermediates.first().cloned() { + Some(b'(') => CharsetIndex::G0, + Some(b')') => CharsetIndex::G1, + Some(b'*') => CharsetIndex::G2, + Some(b'+') => CharsetIndex::G3, + _ => unhandled!(), + }; + self.handler.configure_charset(index, $charset) + }}; + } + + match byte { + b'B' => configure_charset!(StandardCharset::Ascii), + 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(self.writer), + b'c' => self.handler.reset_state(), + b'0' => configure_charset!(StandardCharset::SpecialCharacterAndLineDrawing), + b'7' => self.handler.save_cursor_position(), + b'8' => { + if !intermediates.is_empty() && intermediates[0] == b'#' { + self.handler.dectest(); + } else { + self.handler.restore_cursor_position(); + } + }, + b'=' => self.handler.set_keypad_application_mode(), + b'>' => self.handler.unset_keypad_application_mode(), + b'\\' => (), // String terminator, do nothing (parser handles as string terminator) + _ => unhandled!(), + } + } +} + +/// Parse a color specifier from list of attributes +fn parse_color(attrs: &[i64], i: &mut usize) -> Option<Color> { + if attrs.len() < 2 { + return None; + } + + match attrs[*i + 1] { + 2 => { + // RGB color spec + if attrs.len() < 5 { + debug!("Expected RGB color spec; got {:?}", attrs); + return None; + } + + let r = attrs[*i + 2]; + let g = attrs[*i + 3]; + let b = attrs[*i + 4]; + + *i += 4; + + let range = 0..256; + if !range.contains_(r) || !range.contains_(g) || !range.contains_(b) { + debug!("Invalid RGB color spec: ({}, {}, {})", r, g, b); + return None; + } + + Some(Color::Spec(Rgb { r: r as u8, g: g as u8, b: b as u8 })) + }, + 5 => { + if attrs.len() < 3 { + debug!("Expected color index; got {:?}", attrs); + None + } else { + *i += 2; + let idx = attrs[*i]; + match idx { + 0..=255 => Some(Color::Indexed(idx as u8)), + _ => { + debug!("Invalid color index: {}", idx); + None + }, + } + } + }, + _ => { + debug!("Unexpected color attr: {}", attrs[*i + 1]); + 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; +} + +/// C1 set of 8-bit control characters (from ANSI X3.64-1979) +/// +/// 0x80 (@), 0x81 (A), 0x82 (B), 0x83 (C) are reserved +/// 0x98 (X), 0x99 (Y) are reserved +/// 0x9a (Z) is 'reserved', but causes DEC terminals to respond with DA codes +#[allow(non_snake_case)] +pub mod C1 { + /// Reserved + pub const PAD: u8 = 0x80; + /// Reserved + pub const HOP: u8 = 0x81; + /// Reserved + pub const BPH: u8 = 0x82; + /// Reserved + pub const NBH: u8 = 0x83; + /// Index, moves down one line same column regardless of NL + pub const IND: u8 = 0x84; + /// New line, moves done one line and to first column (CR+LF) + pub const NEL: u8 = 0x85; + /// Start of Selected Area to be sent to auxiliary output device + pub const SSA: u8 = 0x86; + /// End of Selected Area to be sent to auxiliary output device + pub const ESA: u8 = 0x87; + /// Horizontal Tabulation Set at current position + pub const HTS: u8 = 0x88; + /// Hor Tab Justify, moves string to next tab position + pub const HTJ: u8 = 0x89; + /// Vertical Tabulation Set at current line + pub const VTS: u8 = 0x8A; + /// Partial Line Down (subscript) + pub const PLD: u8 = 0x8B; + /// Partial Line Up (superscript) + pub const PLU: u8 = 0x8C; + /// Reverse Index, go up one line, reverse scroll if necessary + pub const RI: u8 = 0x8D; + /// Single Shift to G2 + pub const SS2: u8 = 0x8E; + /// Single Shift to G3 (VT100 uses this for sending PF keys) + pub const SS3: u8 = 0x8F; + /// Device Control String, terminated by ST (VT125 enters graphics) + pub const DCS: u8 = 0x90; + /// Private Use 1 + pub const PU1: u8 = 0x91; + /// Private Use 2 + pub const PU2: u8 = 0x92; + /// Set Transmit State + pub const STS: u8 = 0x93; + /// Cancel character, ignore previous character + pub const CCH: u8 = 0x94; + /// Message Waiting, turns on an indicator on the terminal + pub const MW: u8 = 0x95; + /// Start of Protected Area + pub const SPA: u8 = 0x96; + /// End of Protected Area + pub const EPA: u8 = 0x97; + /// SOS + pub const SOS: u8 = 0x98; + /// SGCI + pub const SGCI: u8 = 0x99; + /// DECID - Identify Terminal + pub const DECID: u8 = 0x9a; + /// Control Sequence Introducer + pub const CSI: u8 = 0x9B; + /// String Terminator (VT125 exits graphics) + pub const ST: u8 = 0x9C; + /// Operating System Command (reprograms intelligent terminal) + pub const OSC: u8 = 0x9D; + /// Privacy Message (password verification), terminated by ST + pub const PM: u8 = 0x9E; + /// Application Program Command (to word processor), term by ST + pub const APC: u8 = 0x9F; +} + +// Tests for parsing escape sequences +// +// Byte sequences used in these tests are recording of pty stdout. +#[cfg(test)] +mod tests { + use super::{ + parse_number, parse_rgb_color, Attr, CharsetIndex, Color, Handler, Processor, + StandardCharset, TermInfo, + }; + use crate::index::{Column, Line}; + use crate::term::color::Rgb; + use std::io; + + /// The /dev/null of `io::Write` + struct Void; + + impl io::Write for Void { + fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { + Ok(bytes.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[derive(Default)] + struct AttrHandler { + attr: Option<Attr>, + } + + impl Handler for AttrHandler { + fn terminal_attribute(&mut self, attr: Attr) { + self.attr = Some(attr); + } + } + + impl TermInfo for AttrHandler { + fn lines(&self) -> Line { + Line(24) + } + + fn cols(&self) -> Column { + Column(80) + } + } + + #[test] + fn parse_control_attribute() { + static BYTES: &'static [u8] = &[0x1b, 0x5b, 0x31, 0x6d]; + + let mut parser = Processor::new(); + let mut handler = AttrHandler::default(); + + for byte in &BYTES[..] { + parser.advance(&mut handler, *byte, &mut Void); + } + + assert_eq!(handler.attr, Some(Attr::Bold)); + } + + #[test] + fn parse_truecolor_attr() { + static BYTES: &'static [u8] = &[ + 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x32, 0x3b, 0x31, 0x32, 0x38, 0x3b, 0x36, 0x36, 0x3b, + 0x32, 0x35, 0x35, 0x6d, + ]; + + let mut parser = Processor::new(); + let mut handler = AttrHandler::default(); + + for byte in &BYTES[..] { + parser.advance(&mut handler, *byte, &mut Void); + } + + 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: &'static [u8] = &[ + 0x1b, 0x5b, 0x31, 0x6d, 0x1b, 0x5b, 0x37, 0x6d, 0x25, 0x1b, 0x5b, 0x32, 0x37, 0x6d, + 0x1b, 0x5b, 0x31, 0x6d, 0x1b, 0x5b, 0x30, 0x6d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x0d, 0x20, 0x0d, 0x0d, 0x1b, 0x5b, 0x30, 0x6d, 0x1b, 0x5b, 0x32, + 0x37, 0x6d, 0x1b, 0x5b, 0x32, 0x34, 0x6d, 0x1b, 0x5b, 0x4a, 0x6a, 0x77, 0x69, 0x6c, + 0x6d, 0x40, 0x6a, 0x77, 0x69, 0x6c, 0x6d, 0x2d, 0x64, 0x65, 0x73, 0x6b, 0x20, 0x1b, + 0x5b, 0x30, 0x31, 0x3b, 0x33, 0x32, 0x6d, 0xe2, 0x9e, 0x9c, 0x20, 0x1b, 0x5b, 0x30, + 0x31, 0x3b, 0x33, 0x32, 0x6d, 0x20, 0x1b, 0x5b, 0x33, 0x36, 0x6d, 0x7e, 0x2f, 0x63, + 0x6f, 0x64, 0x65, + ]; + + let mut handler = AttrHandler::default(); + let mut parser = Processor::new(); + + for byte in &BYTES[..] { + parser.advance(&mut handler, *byte, &mut Void); + } + } + + struct CharsetHandler { + index: CharsetIndex, + charset: StandardCharset, + } + + impl Default for CharsetHandler { + fn default() -> CharsetHandler { + CharsetHandler { index: CharsetIndex::G0, charset: StandardCharset::Ascii } + } + } + + impl Handler for CharsetHandler { + 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; + } + } + + impl TermInfo for CharsetHandler { + fn lines(&self) -> Line { + Line(200) + } + + fn cols(&self) -> Column { + Column(90) + } + } + + #[test] + fn parse_designate_g0_as_line_drawing() { + static BYTES: &'static [u8] = &[0x1b, b'(', b'0']; + let mut parser = Processor::new(); + let mut handler = CharsetHandler::default(); + + for byte in &BYTES[..] { + parser.advance(&mut handler, *byte, &mut Void); + } + + assert_eq!(handler.index, CharsetIndex::G0); + assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing); + } + + #[test] + fn parse_designate_g1_as_line_drawing_and_invoke() { + static BYTES: &'static [u8] = &[0x1b, 0x29, 0x30, 0x0e]; + let mut parser = Processor::new(); + let mut handler = CharsetHandler::default(); + + for byte in &BYTES[..3] { + parser.advance(&mut handler, *byte, &mut Void); + } + + assert_eq!(handler.index, CharsetIndex::G1); + assert_eq!(handler.charset, StandardCharset::SpecialCharacterAndLineDrawing); + + let mut handler = CharsetHandler::default(); + parser.advance(&mut handler, BYTES[3], &mut Void); + + assert_eq!(handler.index, CharsetIndex::G1); + } + + #[test] + fn parse_valid_rgb_color() { + assert_eq!(parse_rgb_color(b"rgb:11/aa/ff"), Some(Rgb { r: 0x11, g: 0xaa, b: 0xff })); + } + + #[test] + fn parse_valid_rgb_color2() { + assert_eq!(parse_rgb_color(b"#11aaff"), Some(Rgb { r: 0x11, g: 0xaa, b: 0xff })); + } + + #[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); + } +} |