Skip to content

Commit

Permalink
Add colored underline support
Browse files Browse the repository at this point in the history
This commit adds support for colored underline and refines the dynamic
extra storage. The extra storage now is using `Arc` making cloning it way
faster compared to `Box` approach which scales really well when it comes
to cloning in `Term::write_at_cursor`, since cloning `Arc` is constant
time.

Fixes alacritty#4142.
  • Loading branch information
kchibisov authored Mar 16, 2022
1 parent 589c1e9 commit f4bdf5f
Show file tree
Hide file tree
Showing 14 changed files with 94 additions and 38 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Escape sequence for undercurl, dotted and dashed underlines (`CSI 4 : [3-5] m`)
- `ToggleMaximized` key binding action to (un-)maximize the active window, not bound by default
- Support for OpenGL ES 2.0
- Escape sequence to set underline color (`CSI 58 : 2 : Ps : Ps : Ps m`/`CSI 58 : 5 : Ps m`)
- Escape sequence to reset underline color (`CSI 59 m`)

### Changed

Expand Down
9 changes: 8 additions & 1 deletion alacritty/src/display/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pub struct RenderableCell {
pub fg: Rgb,
pub bg: Rgb,
pub bg_alpha: f32,
pub underline: Rgb,
pub flags: Flags,
}

Expand Down Expand Up @@ -251,14 +252,20 @@ impl RenderableCell {
let cell_point = cell.point;
let point = display::point_to_viewport(display_offset, cell_point).unwrap();

let flags = cell.flags;
let underline = cell
.underline_color()
.map_or(fg, |underline| Self::compute_fg_rgb(content, underline, flags));

RenderableCell {
zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()),
flags: cell.flags,
flags,
character,
bg_alpha,
point,
fg,
bg,
underline,
}
}

Expand Down
1 change: 1 addition & 0 deletions alacritty/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ impl Renderer {
bg_alpha: 1.0,
fg,
bg,
underline: fg,
});

self.draw_cells(size_info, glyph_cache, cells);
Expand Down
7 changes: 5 additions & 2 deletions alacritty/src/renderer/rects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ impl RenderLines {
return;
}

// The underline color escape does not apply to strikeout.
let color = if flag.contains(Flags::STRIKEOUT) { cell.fg } else { cell.underline };

// Include wide char spacer if the current cell is a wide char.
let mut end = cell.point;
if cell.flags.contains(Flags::WIDE_CHAR) {
Expand All @@ -201,7 +204,7 @@ impl RenderLines {

// Check if there's an active line.
if let Some(line) = self.inner.get_mut(&flag).and_then(|lines| lines.last_mut()) {
if cell.fg == line.color
if color == line.color
&& cell.point.column == line.end.column + 1
&& cell.point.line == line.end.line
{
Expand All @@ -212,7 +215,7 @@ impl RenderLines {
}

// Start new line if there currently is none.
let line = RenderLine { start: cell.point, end, color: cell.fg };
let line = RenderLine { start: cell.point, end, color };
match self.inner.get_mut(&flag) {
Some(lines) => lines.push(line),
None => {
Expand Down
2 changes: 1 addition & 1 deletion alacritty_terminal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ version = "0.1.0"
libc = "0.2"
bitflags = "1"
parking_lot = "0.11.0"
serde = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive", "rc"] }
serde_yaml = "0.8"
vte = { version = "0.10.0", default-features = false }
mio = "0.6.20"
Expand Down
36 changes: 22 additions & 14 deletions alacritty_terminal/src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,8 @@ pub enum Attr {
Foreground(Color),
/// Set indexed background color.
Background(Color),
/// Underline color.
UnderlineColor(Option<Color>),
}

/// Identifiers which can be assigned to a graphic character set.
Expand Down Expand Up @@ -1364,13 +1366,7 @@ fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
let mut iter = params.map(|param| param[0]);
parse_sgr_color(&mut iter).map(Attr::Foreground)
},
[38, params @ ..] => {
let rgb_start = if params.len() > 4 { 2 } else { 1 };
let rgb_iter = params[rgb_start..].iter().copied();
let mut iter = iter::once(params[0]).chain(rgb_iter);

parse_sgr_color(&mut iter).map(Attr::Foreground)
},
[38, params @ ..] => handle_colon_rgb(params).map(Attr::Foreground),
[39] => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))),
[40] => Some(Attr::Background(Color::Named(NamedColor::Black))),
[41] => Some(Attr::Background(Color::Named(NamedColor::Red))),
Expand All @@ -1384,14 +1380,16 @@ fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
let mut iter = params.map(|param| param[0]);
parse_sgr_color(&mut iter).map(Attr::Background)
},
[48, params @ ..] => {
let rgb_start = if params.len() > 4 { 2 } else { 1 };
let rgb_iter = params[rgb_start..].iter().copied();
let mut iter = iter::once(params[0]).chain(rgb_iter);

parse_sgr_color(&mut iter).map(Attr::Background)
},
[48, params @ ..] => handle_colon_rgb(params).map(Attr::Background),
[49] => Some(Attr::Background(Color::Named(NamedColor::Background))),
[58] => {
let mut iter = params.map(|param| param[0]);
parse_sgr_color(&mut iter).map(|color| Attr::UnderlineColor(Some(color)))
},
[58, params @ ..] => {
handle_colon_rgb(params).map(|color| Attr::UnderlineColor(Some(color)))
},
[59] => Some(Attr::UnderlineColor(None)),
[90] => Some(Attr::Foreground(Color::Named(NamedColor::BrightBlack))),
[91] => Some(Attr::Foreground(Color::Named(NamedColor::BrightRed))),
[92] => Some(Attr::Foreground(Color::Named(NamedColor::BrightGreen))),
Expand All @@ -1416,6 +1414,16 @@ fn attrs_from_sgr_parameters(params: &mut ParamsIter<'_>) -> Vec<Option<Attr>> {
attrs
}

/// Handle colon separated rgb color escape sequence.
#[inline]
fn handle_colon_rgb(params: &[u16]) -> Option<Color> {
let rgb_start = if params.len() > 4 { 2 } else { 1 };
let rgb_iter = params[rgb_start..].iter().copied();
let mut iter = iter::once(params[0]).chain(rgb_iter);

parse_sgr_color(&mut iter)
}

/// Parse a color specifier from list of attributes.
fn parse_sgr_color(params: &mut dyn Iterator<Item = u16>) -> Option<Color> {
match params.next() {
Expand Down
60 changes: 43 additions & 17 deletions alacritty_terminal/src/term/cell.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::boxed::Box;
use std::sync::Arc;

use bitflags::bitflags;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -57,19 +57,20 @@ impl ResetDiscriminant<Color> for Cell {
/// allocation required ahead of time for every cell, with some additional overhead when the extra
/// storage is actually required.
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
struct CellExtra {
pub struct CellExtra {
zerowidth: Vec<char>,

underline_color: Option<Color>,
}

/// Content and attributes of a single cell in the terminal grid.
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct Cell {
pub c: char,
pub fg: Color,
pub bg: Color,
pub flags: Flags,
#[serde(default)]
extra: Option<Box<CellExtra>>,
pub extra: Option<Arc<CellExtra>>,
}

impl Default for Cell {
Expand All @@ -94,25 +95,39 @@ impl Cell {

/// Write a new zerowidth character to this cell.
#[inline]
pub fn push_zerowidth(&mut self, c: char) {
self.extra.get_or_insert_with(Default::default).zerowidth.push(c);
}

/// Free all dynamically allocated cell storage.
#[inline]
pub fn drop_extra(&mut self) {
if self.extra.is_some() {
self.extra = None;
}
pub fn push_zerowidth(&mut self, character: char) {
let extra = self.extra.get_or_insert(Default::default());
Arc::make_mut(extra).zerowidth.push(character);
}

/// Remove all wide char data from a cell.
#[inline(never)]
pub fn clear_wide(&mut self) {
self.flags.remove(Flags::WIDE_CHAR);
self.drop_extra();
if let Some(extra) = self.extra.as_mut() {
Arc::make_mut(extra).zerowidth = Vec::new();
}
self.c = ' ';
}

/// Set underline color on the cell.
pub fn set_underline_color(&mut self, color: Option<Color>) {
// If we reset color and we don't have zerowidth we should drop extra storage.
if color.is_none() && self.extra.as_ref().map_or(true, |extra| !extra.zerowidth.is_empty())
{
self.extra = None;
return;
}

let extra = self.extra.get_or_insert(Default::default());
Arc::make_mut(extra).underline_color = color;
}

/// Underline color stored in this cell.
#[inline]
pub fn underline_color(&self) -> Option<Color> {
self.extra.as_ref()?.underline_color
}
}

impl GridCell for Cell {
Expand Down Expand Up @@ -184,11 +199,22 @@ impl LineLength for grid::Row<Cell> {

#[cfg(test)]
mod tests {
use super::{Cell, LineLength};
use super::*;

use std::mem;

use crate::grid::Row;
use crate::index::Column;

#[test]
fn cell_size_is_below_cap() {
// Expected cell size on 64-bit architectures.
const EXPECTED_CELL_SIZE: usize = 24;

// Ensure that cell size isn't growning by accident.
assert!(mem::size_of::<Cell>() <= EXPECTED_CELL_SIZE);
}

#[test]
fn line_length_works() {
let mut row = Row::<Cell>::new(10);
Expand Down
6 changes: 4 additions & 2 deletions alacritty_terminal/src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,7 @@ impl<T> Term<T> {
let fg = self.grid.cursor.template.fg;
let bg = self.grid.cursor.template.bg;
let flags = self.grid.cursor.template.flags;
let extra = self.grid.cursor.template.extra.clone();

let mut cursor_cell = self.grid.cursor_cell();

Expand All @@ -1070,12 +1071,11 @@ impl<T> Term<T> {
cursor_cell = self.grid.cursor_cell();
}

cursor_cell.drop_extra();

cursor_cell.c = c;
cursor_cell.fg = fg;
cursor_cell.bg = bg;
cursor_cell.flags = flags;
cursor_cell.extra = extra;
}

#[inline]
Expand Down Expand Up @@ -1826,10 +1826,12 @@ impl<T: EventListener> Handler for Term<T> {
match attr {
Attr::Foreground(color) => cursor.template.fg = color,
Attr::Background(color) => cursor.template.bg = color,
Attr::UnderlineColor(color) => cursor.template.set_underline_color(color),
Attr::Reset => {
cursor.template.fg = Color::Named(NamedColor::Foreground);
cursor.template.bg = Color::Named(NamedColor::Background);
cursor.template.flags = Flags::empty();
cursor.template.set_underline_color(None);
},
Attr::Reverse => cursor.template.flags.insert(Flags::INVERSE),
Attr::CancelReverse => cursor.template.flags.remove(Flags::INVERSE),
Expand Down
1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ ref_tests! {
zerowidth
selective_erasure
colored_reset
colored_underline
delete_lines
delete_chars_reset
alt_reset
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  colored-underlines-v3 via 🦀 v1.59.0 ➜ [?2004hcargo run -- --ref-test --config-file /dev/null -o scrolling.history=0echo -e '\e[58;2;255;0;255m\e[4:1mUNDERLINE\e[4:2mDOUBLE\e[58:5:196m\e[4:3mUh̷̗ERCURL\e[4:4mDOTTED\e[4:5mDASHED\e[59mNOT_COLORED_DASH\e[0m'[?2004l
]2;echo -e '\e[58;2;255;0;255m\e[4:1mUNDERLINE\e[4:2mDOUBLE\e[58:5:196m\e[4:3mUh̷̗ERCURL\e[4:4mDOTTED\e[4:5mDASHED\e[59mNOT_COLORED_DASH\e[0m'[4:1mUNDERLINE[4:2mDOUBLE[58:5:196m[4:3mUh̷̗ERCURL[4:4mDOTTED[4:5mDASHEDNOT_COLORED_DASH
% ]133;A]2;kchibisov@Zodiac:~/src/rust/alacritty-workspace/forkfork on  colored-underlines-v3 via 🦀 v1.59.0 ➜ [?2004h[?2004l
Expand Down
1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref/colored_underline/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"history_size":0}
1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref/colored_underline/grid.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions alacritty_terminal/tests/ref/colored_underline/size.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"width":1262.0,"height":690.0,"cell_width":9.0,"cell_height":18.0,"padding_x":0.0,"padding_y":0.0,"screen_lines":38,"columns":140}
2 changes: 1 addition & 1 deletion docs/escape_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ brevity.
| `CSI l` | PARTIAL | See `CSI h` for supported modes |
| `CSI ? l` | PARTIAL | See `CSI ? h` for supported modes |
| `CSI M` | IMPLEMENTED | |
| `CSI m` | PARTIAL | Only singular straight underlines are supported |
| `CSI m` | IMPLEMENTED | |
| `CSI n` | IMPLEMENTED | |
| `CSI P` | IMPLEMENTED | |
| `CSI SP q` | IMPLEMENTED | |
Expand Down

0 comments on commit f4bdf5f

Please sign in to comment.