From b35f3aa1999e1c065d614e4aa98deb44908ad3ca Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Tue, 5 Jan 2021 10:04:31 -0800 Subject: [PATCH] Add Curly, Dotted, Dashed and colored underline concept to model These aren't currently rendered, but the parser and model now support recognizing expanded underline sequences: ``` CSI 24 m -> No underline CSI 4 m -> Single underline CSI 21 m -> Double underline CSI 60 m -> Curly underline CSI 61 m -> Dotted underline CSI 62 m -> Dashed underline CSI 58 ; 2 ; R ; G ; B m -> set underline color to specified true color RGB CSI 58 ; 5 ; I m -> set underline color to palette index I (0-255) CSI 59 -> restore underline color to default ``` The Curly, Dotted and Dashed CSI codes are a wezterm assignment in the SGR space. This is by no means official; I just picked some numbers that were not used based on the xterm ctrl sequences. The color assignment codes 58 and 59 are prior art from Kitty. refs: https://github.com/wez/wezterm/issues/415 --- term/src/terminalstate.rs | 3 ++ termwiz/src/cell.rs | 57 +++++++++++++++++++---- termwiz/src/escape/csi.rs | 73 ++++++++++++++++++++++++++++++ termwiz/src/escape/parser/mod.rs | 31 ++++++++++++- vtparse/src/lib.rs | 22 +++++++++ wezterm-gui/src/gui/utilsprites.rs | 6 +++ 6 files changed, 181 insertions(+), 11 deletions(-) diff --git a/term/src/terminalstate.rs b/term/src/terminalstate.rs index e8848580f4a..e6e6f2cd0b2 100644 --- a/term/src/terminalstate.rs +++ b/term/src/terminalstate.rs @@ -2514,6 +2514,9 @@ impl TerminalState { Sgr::Background(col) => { self.pen.set_background(col); } + Sgr::UnderlineColor(col) => { + self.pen.set_underline_color(col); + } Sgr::Font(_) => {} } } diff --git a/termwiz/src/cell.rs b/termwiz/src/cell.rs index c8208161b24..2eb1255443f 100644 --- a/termwiz/src/cell.rs +++ b/termwiz/src/cell.rs @@ -56,6 +56,9 @@ struct FatAttributes { hyperlink: Option>, /// The image data, if any image: Option>, + /// The color of the underline. If None, then + /// the foreground color is to be used + underline_color: ColorAttribute, } /// Define getter and setter for the attributes bitfield. @@ -162,6 +165,12 @@ pub enum Underline { Single = 1, /// The cell is underlined with two lines Double = 2, + /// Curly underline + Curly = 3, + /// Dotted underline + Dotted = 4, + /// Dashed underline + Dashed = 5, } impl Default for Underline { @@ -200,15 +209,15 @@ impl Into for Blink { impl CellAttributes { bitfield!(intensity, set_intensity, Intensity, 0b11, 0); - bitfield!(underline, set_underline, Underline, 0b11, 2); - 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); - bitfield!(wrapped, set_wrapped, 10); - bitfield!(overline, set_overline, 11); - bitfield!(semantic_type, set_semantic_type, SemanticType, 0b11, 12); + bitfield!(underline, set_underline, Underline, 0b111, 2); + bitfield!(blink, set_blink, Blink, 0b11, 5); + bitfield!(italic, set_italic, 7); + bitfield!(reverse, set_reverse, 8); + bitfield!(strikethrough, set_strikethrough, 9); + bitfield!(invisible, set_invisible, 10); + bitfield!(wrapped, set_wrapped, 11); + bitfield!(overline, set_overline, 12); + bitfield!(semantic_type, set_semantic_type, SemanticType, 0b11, 13); /// Returns true if the attribute bits in both objects are equal. /// This can be used to cheaply test whether the styles of the two @@ -233,6 +242,7 @@ impl CellAttributes { self.fat.replace(Box::new(FatAttributes { hyperlink: None, image: None, + underline_color: ColorAttribute::Default, })); } } @@ -241,7 +251,11 @@ impl CellAttributes { let deallocate = self .fat .as_ref() - .map(|fat| fat.image.is_none() && fat.hyperlink.is_none()) + .map(|fat| { + fat.image.is_none() + && fat.hyperlink.is_none() + && fat.underline_color == ColorAttribute::Default + }) .unwrap_or(false); if deallocate { self.fat.take(); @@ -270,6 +284,21 @@ impl CellAttributes { } } + pub fn set_underline_color>( + &mut self, + underline_color: C, + ) -> &mut Self { + let underline_color = underline_color.into(); + if underline_color == ColorAttribute::Default && self.fat.is_none() { + self + } else { + self.allocate_fat_attributes(); + self.fat.as_mut().unwrap().underline_color = underline_color; + self.deallocate_fat_attributes_if_none(); + self + } + } + /// Clone the attributes, but exclude fancy extras such /// as hyperlinks or future sprite things pub fn clone_sgr_only(&self) -> Self { @@ -284,6 +313,7 @@ impl CellAttributes { // be deterministically tagged as Output so that we have an // easier time in get_semantic_zones. res.set_semantic_type(SemanticType::default()); + res.set_underline_color(self.underline_color()); res } @@ -296,6 +326,13 @@ impl CellAttributes { .as_ref() .and_then(|fat| fat.image.as_ref().map(|im| im.as_ref())) } + + pub fn underline_color(&self) -> ColorAttribute { + self.fat + .as_ref() + .map(|fat| fat.underline_color) + .unwrap_or(ColorAttribute::Default) + } } #[cfg(feature = "use_serde")] diff --git a/termwiz/src/escape/csi.rs b/termwiz/src/escape/csi.rs index 5c97d3a7e47..a459b7949b0 100644 --- a/termwiz/src/escape/csi.rs +++ b/termwiz/src/escape/csi.rs @@ -1086,6 +1086,7 @@ pub enum Sgr { /// Set the intensity/bold level Intensity(Intensity), Underline(Underline), + UnderlineColor(ColorSpec), Blink(Blink), Italic(bool), Inverse(bool), @@ -1124,6 +1125,9 @@ impl Display for Sgr { Sgr::Intensity(Intensity::Normal) => code!(NormalIntensity), Sgr::Underline(Underline::Single) => code!(UnderlineOn), Sgr::Underline(Underline::Double) => code!(UnderlineDouble), + Sgr::Underline(Underline::Curly) => code!(UnderlineCurly), + Sgr::Underline(Underline::Dotted) => code!(UnderlineDotted), + Sgr::Underline(Underline::Dashed) => code!(UnderlineDashed), Sgr::Underline(Underline::None) => code!(UnderlineOff), Sgr::Blink(Blink::Slow) => code!(BlinkOn), Sgr::Blink(Blink::Rapid) => code!(RapidBlinkOn), @@ -1213,6 +1217,18 @@ impl Display for Sgr { c.green, c.blue )?, + Sgr::UnderlineColor(ColorSpec::Default) => code!(ResetUnderlineColor), + Sgr::UnderlineColor(ColorSpec::TrueColor(c)) => write!( + f, + "{};2;{};{};{}m", + SgrCode::UnderlineColor as i64, + c.red, + c.green, + c.blue + )?, + Sgr::UnderlineColor(ColorSpec::PaletteIndex(idx)) => { + write!(f, "{};5;{}m", SgrCode::UnderlineColor as i64, *idx)? + } } Ok(()) } @@ -1814,7 +1830,14 @@ impl<'a> CSIParser<'a> { SgrCode::NormalIntensity => one!(Sgr::Intensity(Intensity::Normal)), SgrCode::UnderlineOn => one!(Sgr::Underline(Underline::Single)), SgrCode::UnderlineDouble => one!(Sgr::Underline(Underline::Double)), + SgrCode::UnderlineCurly => one!(Sgr::Underline(Underline::Curly)), + SgrCode::UnderlineDotted => one!(Sgr::Underline(Underline::Dotted)), + SgrCode::UnderlineDashed => one!(Sgr::Underline(Underline::Dashed)), SgrCode::UnderlineOff => one!(Sgr::Underline(Underline::None)), + SgrCode::UnderlineColor => { + self.parse_sgr_color(params).map(Sgr::UnderlineColor) + } + SgrCode::ResetUnderlineColor => one!(Sgr::UnderlineColor(ColorSpec::default())), SgrCode::BlinkOn => one!(Sgr::Blink(Blink::Slow)), SgrCode::RapidBlinkOn => one!(Sgr::Blink(Blink::Rapid)), SgrCode::BlinkOff => one!(Sgr::Blink(Blink::None)), @@ -1948,6 +1971,12 @@ pub enum SgrCode { OverlineOn = 53, OverlineOff = 55, + UnderlineColor = 58, + ResetUnderlineColor = 59, + UnderlineCurly = 60, + UnderlineDotted = 61, + UnderlineDashed = 62, + ForegroundBrightBlack = 90, ForegroundBrightRed = 91, ForegroundBrightGreen = 92, @@ -2073,6 +2102,50 @@ mod test { ); } + #[test] + fn underlines() { + assert_eq!( + parse('m', &[21], "\x1b[21m"), + vec![CSI::Sgr(Sgr::Underline(Underline::Double))] + ); + assert_eq!( + parse('m', &[4], "\x1b[4m"), + vec![CSI::Sgr(Sgr::Underline(Underline::Single))] + ); + } + + #[test] + fn underline_color() { + assert_eq!( + parse('m', &[58, 2], "\x1b[58;2m"), + vec![CSI::Unspecified(Box::new(Unspecified { + params: [58, 2].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + }))] + ); + + assert_eq!( + parse('m', &[58, 2, 255, 255, 255], "\x1b[58;2;255;255;255m"), + vec![CSI::Sgr(Sgr::UnderlineColor(ColorSpec::TrueColor( + RgbColor::new(255, 255, 255), + )))] + ); + assert_eq!( + parse('m', &[58, 5, 220, 255, 255], "\x1b[58;5;220m\x1b[255;255m"), + vec![ + CSI::Sgr(Sgr::UnderlineColor(ColorSpec::PaletteIndex(220))), + CSI::Unspecified(Box::new(Unspecified { + params: [255, 255].to_vec(), + intermediates: vec![], + ignored_extra_intermediates: false, + control: 'm', + })), + ] + ); + } + #[test] fn color() { assert_eq!( diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 8349f7964f0..e0236a22025 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -418,7 +418,7 @@ impl SixelBuilder { #[cfg(test)] mod test { use super::*; - use crate::cell::Intensity; + use crate::cell::{Intensity, Underline}; use crate::escape::csi::Sgr; use crate::escape::EscCode; use std::io::Write; @@ -478,6 +478,35 @@ mod test { assert_eq!(encode(&actions), "\x1b[1m\x1b[3mb"); } + #[test] + fn fancy_underline() { + let mut p = Parser::new(); + + // Kitty underline sequences use a `:` which is explicitly invalid + // and deleted by the dec/ansi vtparser + let actions = p.parse_as_vec(b"\x1b[4:0mb"); + assert_eq!( + vec![ + // NO: Action::CSI(CSI::Sgr(Sgr::Underline(Underline::None))), + Action::Print('b'), + ], + actions + ); + + let actions = p.parse_as_vec(b"\x1b[60;61;62mb"); + assert_eq!( + vec![ + Action::CSI(CSI::Sgr(Sgr::Underline(Underline::Curly))), + Action::CSI(CSI::Sgr(Sgr::Underline(Underline::Dotted))), + Action::CSI(CSI::Sgr(Sgr::Underline(Underline::Dashed))), + Action::Print('b'), + ], + actions + ); + + assert_eq!(encode(&actions), "\x1b[60m\x1b[61m\x1b[62mb"); + } + #[test] fn basic_osc() { let mut p = Parser::new(); diff --git a/vtparse/src/lib.rs b/vtparse/src/lib.rs index e0ac83ca07c..07b3793f50f 100644 --- a/vtparse/src/lib.rs +++ b/vtparse/src/lib.rs @@ -707,6 +707,28 @@ mod test { ); } + #[test] + fn test_fancy_underline() { + assert_eq!( + parse_as_vec(b"\x1b[4m"), + vec![VTAction::CsiDispatch { + params: vec![4], + intermediates: b"".to_vec(), + ignored_excess_intermediates: false, + byte: b'm' + }] + ); + + assert_eq!( + // This is the kitty curly underline sequence. + // The : is explicitly set to be ignored by + // the state machine tables, so this whole sequence + // is discarded during parsing. + parse_as_vec(b"\x1b[4:3m"), + vec![] + ); + } + #[test] fn test_csi_omitted_param() { assert_eq!( diff --git a/wezterm-gui/src/gui/utilsprites.rs b/wezterm-gui/src/gui/utilsprites.rs index 8b35b2660b4..61c9101c7e0 100644 --- a/wezterm-gui/src/gui/utilsprites.rs +++ b/wezterm-gui/src/gui/utilsprites.rs @@ -369,6 +369,12 @@ impl UtilSprites { (false, true, Underline::None, true) => &self.strike_over, (false, true, Underline::Single, true) => &self.single_strike_over, (false, true, Underline::Double, true) => &self.double_strike_over, + + // FIXME: these are just placeholders under we render + // these things properly + (_, _, Underline::Curly, _) => &self.double_underline, + (_, _, Underline::Dotted, _) => &self.double_underline, + (_, _, Underline::Dashed, _) => &self.double_underline, } }