1
1
mirror of https://github.com/wez/wezterm.git synced 2025-01-03 19:21:57 +03:00

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
This commit is contained in:
Wez Furlong 2021-01-05 10:04:31 -08:00
parent 386032bdee
commit b35f3aa199
6 changed files with 181 additions and 11 deletions

View File

@ -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(_) => {}
}
}

View File

@ -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.
@ -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<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
@ -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<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 {
@ -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")]

View File

@ -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!(

View File

@ -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();

View File

@ -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!(

View File

@ -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,
}
}