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:
parent
386032bdee
commit
b35f3aa199
@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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!(
|
||||
|
@ -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();
|
||||
|
@ -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!(
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user