Skip to content

Commit

Permalink
Add Curly, Dotted, Dashed and colored underline concept to model
Browse files Browse the repository at this point in the history
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: #415
  • Loading branch information
wez committed Jan 5, 2021
1 parent 386032b commit b35f3aa
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 11 deletions.
3 changes: 3 additions & 0 deletions term/src/terminalstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(_) => {}
}
}
Expand Down
57 changes: 47 additions & 10 deletions termwiz/src/cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ struct FatAttributes {
hyperlink: Option<Arc<Hyperlink>>,
/// The image data, if any
image: Option<Box<ImageCell>>,
/// 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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -200,15 +209,15 @@ impl Into<bool> 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
Expand All @@ -233,6 +242,7 @@ impl CellAttributes {
self.fat.replace(Box::new(FatAttributes {
hyperlink: None,
image: None,
underline_color: ColorAttribute::Default,
}));
}
}
Expand All @@ -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();
Expand Down Expand Up @@ -270,6 +284,21 @@ impl CellAttributes {
}
}

pub fn set_underline_color<C: Into<ColorAttribute>>(
&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 {
Expand All @@ -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
}

Expand All @@ -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")]
Expand Down
73 changes: 73 additions & 0 deletions termwiz/src/escape/csi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,7 @@ pub enum Sgr {
/// Set the intensity/bold level
Intensity(Intensity),
Underline(Underline),
UnderlineColor(ColorSpec),
Blink(Blink),
Italic(bool),
Inverse(bool),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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!(
Expand Down
31 changes: 30 additions & 1 deletion termwiz/src/escape/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions vtparse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
6 changes: 6 additions & 0 deletions wezterm-gui/src/gui/utilsprites.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ impl<T: Texture2d> UtilSprites<T> {
(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,
}
}

Expand Down

0 comments on commit b35f3aa

Please sign in to comment.