From 60b19aa6b4e442652d9518a74bdfc4b2d055b450 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sat, 14 Jul 2018 16:26:03 -0700 Subject: [PATCH] add plumbing for escape sequence parsing --- Cargo.toml | 4 + src/cell.rs | 20 +- src/color.rs | 13 ++ src/escape/csi.rs | 429 +++++++++++++++++++++++++++++++++++++++ src/escape/mod.rs | 116 +++++++++++ src/escape/osc.rs | 37 ++++ src/escape/parser/mod.rs | 152 ++++++++++++++ src/lib.rs | 5 + src/render/terminfo.rs | 10 +- 9 files changed, 778 insertions(+), 8 deletions(-) create mode 100644 src/escape/csi.rs create mode 100644 src/escape/mod.rs create mode 100644 src/escape/osc.rs create mode 100644 src/escape/parser/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 95eb48999..49cd02760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,7 @@ palette = "~0.4" serde = "~1.0" serde_derive = "~1.0" failure = "~0.1" +vte = "0.3.2" +num = "0.2.0" +num-derive = "0.2.2" +num-traits = "0.2.5" diff --git a/src/cell.rs b/src/cell.rs index 7877f0a0d..aabb8558d 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -86,14 +86,22 @@ pub enum Underline { Double = 2, } +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[repr(u16)] +pub enum Blink { + None = 0, + Slow = 1, + Rapid = 2, +} + impl CellAttributes { bitfield!(intensity, set_intensity, Intensity, 0b11, 0); bitfield!(underline, set_underline, Underline, 0b11, 2); - bitfield!(italic, set_italic, 4); - bitfield!(blink, set_blink, 5); - bitfield!(reverse, set_reverse, 6); - bitfield!(strikethrough, set_strikethrough, 7); - bitfield!(invisible, set_invisible, 8); + bitfield!(blink, set_blink, Blink, 0b11, 4); + bitfield!(italic, set_italic, 6); + bitfield!(reverse, set_reverse, 7); + bitfield!(strikethrough, set_strikethrough, 8); + bitfield!(invisible, set_invisible, 9); pub fn set_foreground>(&mut self, foreground: C) -> &mut Self { self.foreground = foreground.into(); @@ -164,7 +172,7 @@ pub enum AttributeChange { Intensity(Intensity), Underline(Underline), Italic(bool), - Blink(bool), + Blink(Blink), Reverse(bool), StrikeThrough(bool), Invisible(bool), diff --git a/src/color.rs b/src/color.rs index cfc2831fd..bdfb498ab 100644 --- a/src/color.rs +++ b/src/color.rs @@ -97,6 +97,7 @@ pub enum ColorSpec { Default, /// Use either a raw number, or use values from the `AnsiColor` enum PaletteIndex(u8), + TrueColor(RgbColor), } impl Default for ColorSpec { @@ -105,6 +106,18 @@ impl Default for ColorSpec { } } +impl From for ColorSpec { + fn from(col: AnsiColor) -> Self { + ColorSpec::PaletteIndex(col as u8) + } +} + +impl From for ColorSpec { + fn from(col: RgbColor) -> Self { + ColorSpec::TrueColor(col) + } +} + #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] pub struct ColorAttribute { /// Used if the terminal supports full color diff --git a/src/escape/csi.rs b/src/escape/csi.rs new file mode 100644 index 000000000..7f28e7a22 --- /dev/null +++ b/src/escape/csi.rs @@ -0,0 +1,429 @@ +use cell::{Blink, Intensity, Underline}; +use color::{AnsiColor, ColorSpec, RgbColor}; +use num; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CSI { + /// SGR: Set Graphics Rendition. + /// These values affect how the character is rendered. + Sgr(Sgr), + + Unspecified { + params: Vec, + // TODO: can we just make intermediates a single u8? + intermediates: Vec, + /// if true, more than two intermediates arrived and the + /// remaining data was ignored + ignored_extra_intermediates: bool, + /// The final character in the CSI sequence; this typically + /// defines how to interpret the other parameters. + control: char, + }, + #[doc(hidden)] + __Nonexhaustive, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Sgr { + /// Resets rendition to defaults. Typically switches off + /// all other Sgr options, but may have greater or lesser impact. + Reset, + /// Set the intensity/bold level + Intensity(Intensity), + Underline(Underline), + Blink(Blink), + Italic(bool), + Inverse(bool), + Invisible(bool), + StrikeThrough(bool), + Font(Font), + Foreground(ColorSpec), + Background(ColorSpec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Font { + Default, + Alternate(u8), +} + +/// Constrol Sequence Initiator (CSI) Parser. +/// Since many sequences allow for composition of actions by separating +/// `;` character, we need to be able to iterate over +/// the set of parsed actions from a given CSI sequence. +/// `CSIParser` implements an Iterator that yields `CSI` instances as +/// it parses them out from the input sequence. +struct CSIParser<'a> { + intermediates: &'a [u8], + /// From vte::Perform: this flag is set when more than two intermediates + /// arrived and subsequent characters were ignored. + ignored_extra_intermediates: bool, + control: char, + /// While params is_some we have more data to consume. The advance_by + /// method updates the slice as we consume data. + /// In a number of cases an empty params list is used to indicate + /// default values, especially for SGR, so we need to be careful not + /// to update params to an empty slice. + params: Option<&'a [i64]>, +} + +impl CSI { + /// Parse a CSI sequence. + /// Returns an iterator that yields individual CSI actions. + /// Why not a single? Because sequences like `CSI [ 1 ; 3 m` + /// embed two separate actions but are sent as a single unit. + /// If no semantic meaning is known for a subsequence, the remainder + /// of the sequence is returned wrapped in a `CSI::Unspecified` container. + pub fn parse<'a>( + params: &'a [i64], + intermediates: &'a [u8], + ignored_extra_intermediates: bool, + control: char, + ) -> impl Iterator + 'a { + CSIParser { + intermediates, + ignored_extra_intermediates, + control, + params: Some(params), + } + } +} + +/// A little helper to convert i64 -> u8 if safe +fn to_u8(v: i64) -> Result { + if v <= u8::max_value() as i64 { + Ok(v as u8) + } else { + Err(()) + } +} + +impl<'a> CSIParser<'a> { + /// Consume some number of elements from params and update it. + /// Take care to avoid setting params back to an empty slice + /// as this would trigger returning a default value and/or + /// an unterminated parse loop. + fn advance_by(&mut self, n: usize, params: &'a [i64], result: T) -> T { + let (_, next) = params.split_at(n); + if !next.is_empty() { + self.params = Some(next); + } + result + } + + fn parse_sgr_color(&mut self, params: &'a [i64]) -> Result { + if params.len() >= 5 && params[1] == 2 { + let red = to_u8(params[2])?; + let green = to_u8(params[3])?; + let blue = to_u8(params[4])?; + let res = RgbColor::new(red, green, blue).into(); + Ok(self.advance_by(5, params, res)) + } else if params.len() >= 3 && params[1] == 5 { + let idx = to_u8(params[2])?; + Ok(self.advance_by(3, params, ColorSpec::PaletteIndex(idx))) + } else { + Err(()) + } + } + + fn sgr(&mut self, params: &'a [i64]) -> Result { + if params.len() == 0 { + // With no parameters, treat as equivalent to Reset. + Ok(Sgr::Reset) + } else { + // Consume a single parameter and return the parsed result + macro_rules! one { + ($t:expr) => { + Ok(self.advance_by(1, params, $t)) + }; + }; + + match num::FromPrimitive::from_i64(params[0]) { + None => Err(()), + Some(sgr) => match sgr { + SgrCode::Reset => one!(Sgr::Reset), + SgrCode::IntensityBold => one!(Sgr::Intensity(Intensity::Bold)), + SgrCode::IntensityDim => one!(Sgr::Intensity(Intensity::Half)), + SgrCode::NormalIntensity => one!(Sgr::Intensity(Intensity::Normal)), + SgrCode::UnderlineOn => one!(Sgr::Underline(Underline::Single)), + SgrCode::UnderlineDouble => one!(Sgr::Underline(Underline::Double)), + SgrCode::UnderlineOff => one!(Sgr::Underline(Underline::None)), + SgrCode::BlinkOn => one!(Sgr::Blink(Blink::Slow)), + SgrCode::RapidBlinkOn => one!(Sgr::Blink(Blink::Rapid)), + SgrCode::BlinkOff => one!(Sgr::Blink(Blink::None)), + SgrCode::ItalicOn => one!(Sgr::Italic(true)), + SgrCode::ItalicOff => one!(Sgr::Italic(false)), + SgrCode::ForegroundColor => { + self.parse_sgr_color(params).map(|c| Sgr::Foreground(c)) + } + SgrCode::ForegroundBlack => one!(Sgr::Foreground(AnsiColor::Black.into())), + SgrCode::ForegroundRed => one!(Sgr::Foreground(AnsiColor::Maroon.into())), + SgrCode::ForegroundGreen => one!(Sgr::Foreground(AnsiColor::Green.into())), + SgrCode::ForegroundYellow => one!(Sgr::Foreground(AnsiColor::Olive.into())), + SgrCode::ForegroundBlue => one!(Sgr::Foreground(AnsiColor::Navy.into())), + SgrCode::ForegroundMagenta => one!(Sgr::Foreground(AnsiColor::Purple.into())), + SgrCode::ForegroundCyan => one!(Sgr::Foreground(AnsiColor::Teal.into())), + SgrCode::ForegroundWhite => one!(Sgr::Foreground(AnsiColor::Silver.into())), + SgrCode::ForegroundDefault => one!(Sgr::Foreground(ColorSpec::Default)), + SgrCode::ForegroundBrightBlack => one!(Sgr::Foreground(AnsiColor::Grey.into())), + SgrCode::ForegroundBrightRed => one!(Sgr::Foreground(AnsiColor::Red.into())), + SgrCode::ForegroundBrightGreen => one!(Sgr::Foreground(AnsiColor::Lime.into())), + SgrCode::ForegroundBrightYellow => { + one!(Sgr::Foreground(AnsiColor::Yellow.into())) + } + SgrCode::ForegroundBrightBlue => one!(Sgr::Foreground(AnsiColor::Blue.into())), + SgrCode::ForegroundBrightMagenta => { + one!(Sgr::Foreground(AnsiColor::Fuschia.into())) + } + SgrCode::ForegroundBrightCyan => one!(Sgr::Foreground(AnsiColor::Aqua.into())), + SgrCode::ForegroundBrightWhite => { + one!(Sgr::Foreground(AnsiColor::White.into())) + } + + SgrCode::BackgroundColor => { + self.parse_sgr_color(params).map(|c| Sgr::Background(c)) + } + SgrCode::BackgroundBlack => one!(Sgr::Background(AnsiColor::Black.into())), + SgrCode::BackgroundRed => one!(Sgr::Background(AnsiColor::Maroon.into())), + SgrCode::BackgroundGreen => one!(Sgr::Background(AnsiColor::Green.into())), + SgrCode::BackgroundYellow => one!(Sgr::Background(AnsiColor::Olive.into())), + SgrCode::BackgroundBlue => one!(Sgr::Background(AnsiColor::Navy.into())), + SgrCode::BackgroundMagenta => one!(Sgr::Background(AnsiColor::Purple.into())), + SgrCode::BackgroundCyan => one!(Sgr::Background(AnsiColor::Teal.into())), + SgrCode::BackgroundWhite => one!(Sgr::Background(AnsiColor::Silver.into())), + SgrCode::BackgroundDefault => one!(Sgr::Background(ColorSpec::Default)), + SgrCode::BackgroundBrightBlack => one!(Sgr::Background(AnsiColor::Grey.into())), + SgrCode::BackgroundBrightRed => one!(Sgr::Background(AnsiColor::Red.into())), + SgrCode::BackgroundBrightGreen => one!(Sgr::Background(AnsiColor::Lime.into())), + SgrCode::BackgroundBrightYellow => { + one!(Sgr::Background(AnsiColor::Yellow.into())) + } + SgrCode::BackgroundBrightBlue => one!(Sgr::Background(AnsiColor::Blue.into())), + SgrCode::BackgroundBrightMagenta => { + one!(Sgr::Background(AnsiColor::Fuschia.into())) + } + SgrCode::BackgroundBrightCyan => one!(Sgr::Background(AnsiColor::Aqua.into())), + SgrCode::BackgroundBrightWhite => { + one!(Sgr::Background(AnsiColor::White.into())) + } + + SgrCode::InverseOn => one!(Sgr::Inverse(true)), + SgrCode::InverseOff => one!(Sgr::Inverse(false)), + SgrCode::InvisibleOn => one!(Sgr::Invisible(true)), + SgrCode::InvisibleOff => one!(Sgr::Invisible(false)), + SgrCode::StrikeThroughOn => one!(Sgr::StrikeThrough(true)), + SgrCode::StrikeThroughOff => one!(Sgr::StrikeThrough(false)), + SgrCode::DefaultFont => one!(Sgr::Font(Font::Default)), + SgrCode::AltFont1 => one!(Sgr::Font(Font::Alternate(1))), + SgrCode::AltFont2 => one!(Sgr::Font(Font::Alternate(2))), + SgrCode::AltFont3 => one!(Sgr::Font(Font::Alternate(3))), + SgrCode::AltFont4 => one!(Sgr::Font(Font::Alternate(4))), + SgrCode::AltFont5 => one!(Sgr::Font(Font::Alternate(5))), + SgrCode::AltFont6 => one!(Sgr::Font(Font::Alternate(6))), + SgrCode::AltFont7 => one!(Sgr::Font(Font::Alternate(7))), + SgrCode::AltFont8 => one!(Sgr::Font(Font::Alternate(8))), + SgrCode::AltFont9 => one!(Sgr::Font(Font::Alternate(9))), + }, + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, FromPrimitive)] +pub enum SgrCode { + Reset = 0, + IntensityBold = 1, + IntensityDim = 2, + ItalicOn = 3, + UnderlineOn = 4, + /// Blinks < 150 times per minute + BlinkOn = 5, + /// Blinks > 150 times per minute + RapidBlinkOn = 6, + InverseOn = 7, + InvisibleOn = 8, + StrikeThroughOn = 9, + DefaultFont = 10, + AltFont1 = 11, + AltFont2 = 12, + AltFont3 = 13, + AltFont4 = 14, + AltFont5 = 15, + AltFont6 = 16, + AltFont7 = 17, + AltFont8 = 18, + AltFont9 = 19, + // Fraktur = 20, + UnderlineDouble = 21, + NormalIntensity = 22, + ItalicOff = 23, + UnderlineOff = 24, + BlinkOff = 25, + InverseOff = 27, + InvisibleOff = 28, + StrikeThroughOff = 29, + ForegroundBlack = 30, + ForegroundRed = 31, + ForegroundGreen = 32, + ForegroundYellow = 33, + ForegroundBlue = 34, + ForegroundMagenta = 35, + ForegroundCyan = 36, + ForegroundWhite = 37, + ForegroundDefault = 39, + BackgroundBlack = 40, + BackgroundRed = 41, + BackgroundGreen = 42, + BackgroundYellow = 43, + BackgroundBlue = 44, + BackgroundMagenta = 45, + BackgroundCyan = 46, + BackgroundWhite = 47, + BackgroundDefault = 49, + + ForegroundBrightBlack = 90, + ForegroundBrightRed = 91, + ForegroundBrightGreen = 92, + ForegroundBrightYellow = 93, + ForegroundBrightBlue = 94, + ForegroundBrightMagenta = 95, + ForegroundBrightCyan = 96, + ForegroundBrightWhite = 97, + + BackgroundBrightBlack = 100, + BackgroundBrightRed = 101, + BackgroundBrightGreen = 102, + BackgroundBrightYellow = 103, + BackgroundBrightBlue = 104, + BackgroundBrightMagenta = 105, + BackgroundBrightCyan = 106, + BackgroundBrightWhite = 107, + + /// Maybe followed either either a 256 color palette index or + /// a sequence describing a true color rgb value + ForegroundColor = 38, + BackgroundColor = 48, +} + +impl<'a> Iterator for CSIParser<'a> { + type Item = CSI; + + fn next(&mut self) -> Option { + let params = self.params.take(); + + match (self.control, self.intermediates, params) { + (_, _, None) => None, + ('m', &[], Some(params)) => match self.sgr(params) { + Ok(sgr) => Some(CSI::Sgr(sgr)), + Err(()) => Some(CSI::Unspecified { + params: params.to_vec(), + intermediates: vec![], + ignored_extra_intermediates: self.ignored_extra_intermediates, + control: self.control, + }), + }, + + // Catch-all: just report the leftovers + (control, intermediates, Some(params)) => Some(CSI::Unspecified { + params: params.to_vec(), + intermediates: intermediates.to_vec(), + ignored_extra_intermediates: self.ignored_extra_intermediates, + control, + }), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn parse(control: char, params: &[i64]) -> Vec { + CSI::parse(params, &[], false, control).collect() + } + + #[test] + fn test_basic() { + assert_eq!(parse('m', &[]), vec![CSI::Sgr(Sgr::Reset)]); + assert_eq!(parse('m', &[0]), vec![CSI::Sgr(Sgr::Reset)]); + assert_eq!( + parse('m', &[1]), + vec![CSI::Sgr(Sgr::Intensity(Intensity::Bold))] + ); + assert_eq!( + parse('m', &[1, 3]), + vec![ + CSI::Sgr(Sgr::Intensity(Intensity::Bold)), + CSI::Sgr(Sgr::Italic(true)), + ] + ); + + // Verify that we propagate Unspecified for codes + // that we don't recognize. + assert_eq!( + parse('m', &[1, 3, 1231231]), + vec![ + CSI::Sgr(Sgr::Intensity(Intensity::Bold)), + CSI::Sgr(Sgr::Italic(true)), + CSI::Unspecified { + params: [1231231].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + }, + ] + ); + assert_eq!( + parse('m', &[1, 1231231, 3]), + vec![ + CSI::Sgr(Sgr::Intensity(Intensity::Bold)), + CSI::Unspecified { + params: [1231231, 3].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + }, + ] + ); + assert_eq!( + parse('m', &[1231231, 3]), + vec![CSI::Unspecified { + params: [1231231, 3].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + }] + ); + } + + #[test] + fn test_color() { + assert_eq!( + parse('m', &[38, 2]), + vec![CSI::Unspecified { + params: [38, 2].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + }] + ); + assert_eq!( + parse('m', &[38, 2, 255, 255, 255]), + vec![CSI::Sgr(Sgr::Foreground(ColorSpec::TrueColor( + RgbColor::new(255, 255, 255), + )))] + ); + assert_eq!( + parse('m', &[38, 5, 220, 255, 255]), + vec![ + CSI::Sgr(Sgr::Foreground(ColorSpec::PaletteIndex(220))), + CSI::Unspecified { + params: [255, 255].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + }, + ] + ); + } +} diff --git a/src/escape/mod.rs b/src/escape/mod.rs new file mode 100644 index 000000000..a2fa0e122 --- /dev/null +++ b/src/escape/mod.rs @@ -0,0 +1,116 @@ +//! This module provides the ability to parse escape sequences and attach +//! semantic meaning to them. It can also encode the semantic values as +//! escape sequences. It provides encoding and decoding functionality +//! only; it does not provide terminal emulation facilities itself. +use num; +pub mod csi; +pub mod osc; +pub mod parser; + +use self::csi::CSI; +use self::osc::OperatingSystemCommand; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Action { + /// Send a single printable character to the display + Print(char), + /// A C0 or C1 control code + Control(Control), + /// Device control. This is uncommon wrt. terminal emulation. + DeviceControl(DeviceControlMode), + /// A command that typically doesn't change the contents of the + /// terminal, but rather influences how it displays or otherwise + /// interacts with the rest of the system + OperatingSystemCommand(OperatingSystemCommand), + CSI(CSI), + Esc(Esc), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Esc { + Unspecified { + params: Vec, + // TODO: can we just make intermediates a single u8? + intermediates: Vec, + /// if true, more than two intermediates arrived and the + /// remaining data was ignored + ignored_extra_intermediates: bool, + /// The final character in the Escape sequence; this typically + /// defines how to interpret the other parameters. + control: u8, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeviceControlMode { + /// Identify device control mode from the encoded parameters. + /// This mode is activated and must remain active until + /// `Exit` is observed. While the mode is + /// active, data is made available to the device mode via + /// the `Data` variant. + Enter { + params: Vec, + // TODO: can we just make intermediates a single u8? + intermediates: Vec, + /// if true, more than two intermediates arrived and the + /// remaining data was ignored + ignored_extra_intermediates: bool, + }, + /// Exit the current device control mode + Exit, + /// Data for the device mode to consume + Data(u8), +} + +/// C0 or C1 control codes +#[derive(Debug, Clone, PartialEq, Eq, FromPrimitive)] +#[repr(u8)] +pub enum ControlCode { + Null = 0, + StartOfHeading = 1, + StartOfText = 2, + EndOfText = 3, + EndOfTransmission = 4, + Enquiry = 5, + Acknowledge = 6, + Bell = 7, + Backspace = 8, + HorizontalTab = b'\t', + LineFeed = b'\n', + VerticalTab = 0xb, + FormFeed = 0xc, + CarriageReturn = b'\r', + ShiftOut = 0xe, + ShiftIn = 0xf, + DataLinkEscape = 0x10, + DeviceControlOne = 0x11, + DeviceControlTwo = 0x12, + DeviceControlThree = 0x13, + DeviceControlFour = 0x14, + NegativeAcknowledge = 0x15, + SynchronousIdle = 0x16, + EndOfTransmissionBlock = 0x17, + Cancel = 0x18, + EndOfMedium = 0x19, + Substitute = 0x1a, + Escape = 0x1b, + FileSeparator = 0x1c, + GroupSeparator = 0x1d, + RecordSeparator = 0x1e, + UnitSeparator = 0x1f, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Control { + Code(ControlCode), + Unspecified(u8), +} + +impl From for Control { + fn from(b: u8) -> Self { + match num::FromPrimitive::from_u8(b) { + Some(result) => Control::Code(result), + None => Control::Unspecified(b), + } + } +} diff --git a/src/escape/osc.rs b/src/escape/osc.rs new file mode 100644 index 000000000..c77330342 --- /dev/null +++ b/src/escape/osc.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OperatingSystemCommand { + Unspecified(Vec>), + #[doc(hidden)] + __Nonexhaustive, +} + +#[derive(Debug, Clone, PartialEq, Eq, FromPrimitive)] +pub enum OperatingSystemCommandCode { + SetIconNameAndWindowTitle = 0, + SetIconName = 1, + SetWindowTitle = 2, + SetXWindowProperty = 3, + ChangeColorNumber = 4, + /// iTerm2 + ChangeTitleTabColor = 6, + SetCurrentWorkingDirectory = 7, + /// See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + Hyperlink = 8, + /// iTerm2 + SystemNotification = 9, + SetTextForegroundColor = 10, + SetTextBackgroundColor = 11, + SetTextCursorColor = 12, + SetMouseForegroundColor = 13, + SetMouseBackgroundColor = 14, + SetTektronixForegroundColor = 15, + SetTektronixBackgroundColor = 16, + SetHighlightColor = 17, + SetTektronixCursorColor = 18, + SetLogFileName = 46, + SetFont = 50, + EmacsShell = 51, + ManipulateSelectionData = 52, + RxvtProprietary = 777, + ITermProprietary = 1337, +} diff --git a/src/escape/parser/mod.rs b/src/escape/parser/mod.rs new file mode 100644 index 000000000..9e7362f97 --- /dev/null +++ b/src/escape/parser/mod.rs @@ -0,0 +1,152 @@ +use escape::{Action, DeviceControlMode, Esc, OperatingSystemCommand, CSI}; +use vte; + +/// The `Parser` struct holds the state machine that is used to decode +/// a sequence of bytes. The byte sequence can be streaming into the +/// state machine. +/// You can either have the parser trigger a callback as `Action`s are +/// decoded, or have it return a `Vec` holding zero-or-more +/// decoded actions. +pub struct Parser { + state_machine: vte::Parser, +} + +impl Parser { + pub fn new() -> Self { + Self { + state_machine: vte::Parser::new(), + } + } + + pub fn parse(&mut self, bytes: &[u8], mut callback: F) { + let mut perform = Performer { + callback: &mut callback, + }; + for b in bytes { + self.state_machine.advance(&mut perform, *b); + } + } + + pub fn parse_as_vec(&mut self, bytes: &[u8]) -> Vec { + let mut result = Vec::new(); + self.parse(bytes, |action| result.push(action)); + result + } +} + +struct Performer<'a, F: FnMut(Action) + 'a> { + callback: &'a mut F, +} + +impl<'a, F: FnMut(Action)> vte::Perform for Performer<'a, F> { + fn print(&mut self, c: char) { + (self.callback)(Action::Print(c)); + } + + fn execute(&mut self, byte: u8) { + (self.callback)(Action::Control(byte.into())); + } + + fn hook(&mut self, params: &[i64], intermediates: &[u8], ignored_extra_intermediates: bool) { + (self.callback)(Action::DeviceControl(DeviceControlMode::Enter { + params: params.to_vec(), + intermediates: intermediates.to_vec(), + ignored_extra_intermediates, + })); + } + + fn put(&mut self, data: u8) { + (self.callback)(Action::DeviceControl(DeviceControlMode::Data(data))); + } + + fn unhook(&mut self) { + (self.callback)(Action::DeviceControl(DeviceControlMode::Exit)); + } + + fn osc_dispatch(&mut self, osc: &[&[u8]]) { + let mut vec = Vec::new(); + for slice in osc { + vec.push(slice.to_vec()); + } + (self.callback)(Action::OperatingSystemCommand( + OperatingSystemCommand::Unspecified(vec), + )); + } + + fn csi_dispatch( + &mut self, + params: &[i64], + intermediates: &[u8], + ignored_extra_intermediates: bool, + control: char, + ) { + for action in CSI::parse(params, intermediates, ignored_extra_intermediates, control) { + (self.callback)(Action::CSI(action)); + } + } + + fn esc_dispatch( + &mut self, + params: &[i64], + intermediates: &[u8], + ignored_extra_intermediates: bool, + control: u8, + ) { + (self.callback)(Action::Esc(Esc::Unspecified { + params: params.to_vec(), + intermediates: intermediates.to_vec(), + ignored_extra_intermediates, + control, + })); + } +} + +#[cfg(test)] +mod test { + use super::*; + use cell::Intensity; + use escape::csi::Sgr; + + #[test] + fn basic_parse() { + let mut p = Parser::new(); + let actions = p.parse_as_vec(b"hello"); + assert_eq!( + vec![ + Action::Print('h'), + Action::Print('e'), + Action::Print('l'), + Action::Print('l'), + Action::Print('o'), + ], + actions + ); + } + + #[test] + fn basic_bold() { + let mut p = Parser::new(); + let actions = p.parse_as_vec(b"\x1b[1mb"); + assert_eq!( + vec![ + Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))), + Action::Print('b'), + ], + actions + ); + } + + #[test] + fn basic_bold_italic() { + let mut p = Parser::new(); + let actions = p.parse_as_vec(b"\x1b[1;3mb"); + assert_eq!( + vec![ + Action::CSI(CSI::Sgr(Sgr::Intensity(Intensity::Bold))), + Action::CSI(CSI::Sgr(Sgr::Italic(true))), + Action::Print('b'), + ], + actions + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 155f9923f..06dc5c45e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,13 @@ extern crate serde; extern crate terminfo; #[macro_use] extern crate serde_derive; +extern crate num; +extern crate vte; +#[macro_use] +extern crate num_derive; pub mod cell; pub mod color; +pub mod escape; pub mod render; pub mod screen; diff --git a/src/render/terminfo.rs b/src/render/terminfo.rs index 1b5069849..5f3de9b5d 100644 --- a/src/render/terminfo.rs +++ b/src/render/terminfo.rs @@ -1,5 +1,5 @@ //! Rendering of Changes using terminfo -use cell::{AttributeChange, CellAttributes, Intensity, Underline}; +use cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline}; use color::ColorSpec; use failure; use render::Renderer; @@ -95,7 +95,7 @@ impl Renderer for TerminfoRenderer { .bold(attr.intensity() == Intensity::Bold) .dim(attr.intensity() == Intensity::Half) .underline(attr.underline() != Underline::None) - .blink(attr.blink()) + .blink(attr.blink() != Blink::None) .reverse(attr.reverse()) .invisible(attr.invisible()) .to(WriteWrapper::new(out))?; @@ -123,6 +123,9 @@ impl Renderer for TerminfoRenderer { tc.blue )?; } + (_, _, ColorSpec::TrueColor(_)) => { + // TrueColor was specified with no fallback :-( + } (_, _, ColorSpec::Default) => { // Terminfo doesn't define a reset color to default, so // we use the ANSI code. @@ -147,6 +150,9 @@ impl Renderer for TerminfoRenderer { tc.blue )?; } + (_, _, ColorSpec::TrueColor(_)) => { + // TrueColor was specified with no fallback :-( + } (_, _, ColorSpec::Default) => { // Terminfo doesn't define a reset color to default, so // we use the ANSI code.