mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-25 14:05:41 -05:00
Add support for double underlines
This adds support for double underlines using the colon separated escape sequence `CSI 4 : 2 m`. Alacritty will now also always fallback to the normal underline in case any of the other underlines like the undercurl are specified. The escape sequence `CSI 4 : 0 m` can now be used to clear all underlines. Some terminals support `CSI 21 m` for double underline, but since Alacritty already uses that as cancel bold which is a little more consistent, that behavior has not changed. So the colon separated variant must be used.
This commit is contained in:
parent
96ea5c445e
commit
b904207b19
11 changed files with 143 additions and 60 deletions
|
@ -18,11 +18,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Secondary device attributes escape (`CSI > 0 c`)
|
- Secondary device attributes escape (`CSI > 0 c`)
|
||||||
- Support for colon separated SGR 38/48
|
- Support for colon separated SGR 38/48
|
||||||
- New Ctrl+C binding to cancel search and leave vi mode
|
- New Ctrl+C binding to cancel search and leave vi mode
|
||||||
|
- Escapes for double underlines (`CSI 4 : 2 m`) and underline reset (`CSI 4 : 0 m`)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Cursors are now inverted when their fixed color is similar to the cell's background
|
- Cursors are now inverted when their fixed color is similar to the cell's background
|
||||||
- Use working directory of active process instead of shell for SpawnNewInstance action
|
- Use working directory of active process instead of shell for SpawnNewInstance action
|
||||||
|
- Fallback to normal underline for unsupported underline types in `CSI 4 : ? m` escapes
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use crossfont::Metrics;
|
use crossfont::Metrics;
|
||||||
|
|
||||||
use alacritty_terminal::index::{Column, Point};
|
use alacritty_terminal::index::{Line, Point};
|
||||||
use alacritty_terminal::term::cell::Flags;
|
use alacritty_terminal::term::cell::Flags;
|
||||||
use alacritty_terminal::term::color::Rgb;
|
use alacritty_terminal::term::color::Rgb;
|
||||||
use alacritty_terminal::term::{RenderableCell, SizeInfo};
|
use alacritty_terminal::term::{RenderableCell, SizeInfo};
|
||||||
|
@ -35,51 +35,91 @@ impl RenderLine {
|
||||||
let mut rects = Vec::new();
|
let mut rects = Vec::new();
|
||||||
|
|
||||||
let mut start = self.start;
|
let mut start = self.start;
|
||||||
while start.line < self.end.line {
|
for line in start.line.0..=self.end.line.0 {
|
||||||
let mut end = start;
|
let mut end = Point::new(Line(line), self.end.col);
|
||||||
end.col = size.cols() - 1;
|
if line != self.end.line.0 {
|
||||||
rects.push(Self::create_rect(metrics, size, flag, start, end, self.color));
|
end.col = size.cols() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
start.col = Column(0);
|
Self::push_rects(&mut rects, metrics, size, flag, start, end, self.color);
|
||||||
start.line += 1;
|
|
||||||
|
start.col.0 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
rects.push(Self::create_rect(metrics, size, flag, start, self.end, self.color));
|
|
||||||
|
|
||||||
rects
|
rects
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_rect(
|
/// Push all rects required to draw the cell's line.
|
||||||
|
fn push_rects(
|
||||||
|
rects: &mut Vec<RenderRect>,
|
||||||
metrics: &Metrics,
|
metrics: &Metrics,
|
||||||
size: &SizeInfo,
|
size: &SizeInfo,
|
||||||
flag: Flags,
|
flag: Flags,
|
||||||
start: Point,
|
start: Point,
|
||||||
end: Point,
|
end: Point,
|
||||||
color: Rgb,
|
color: Rgb,
|
||||||
) -> RenderRect {
|
) {
|
||||||
let start_x = start.col.0 as f32 * size.cell_width;
|
let (position, thickness) = match flag {
|
||||||
let end_x = (end.col.0 + 1) as f32 * size.cell_width;
|
Flags::DOUBLE_UNDERLINE => {
|
||||||
let width = end_x - start_x;
|
// Position underlines so each one has 50% of descent available.
|
||||||
|
let top_pos = 0.25 * metrics.descent;
|
||||||
|
let bottom_pos = 0.75 * metrics.descent;
|
||||||
|
|
||||||
let (position, mut height) = match flag {
|
rects.push(Self::create_rect(
|
||||||
|
size,
|
||||||
|
metrics.descent,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
top_pos,
|
||||||
|
metrics.underline_thickness,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
|
||||||
|
(bottom_pos, metrics.underline_thickness)
|
||||||
|
},
|
||||||
Flags::UNDERLINE => (metrics.underline_position, metrics.underline_thickness),
|
Flags::UNDERLINE => (metrics.underline_position, metrics.underline_thickness),
|
||||||
Flags::STRIKEOUT => (metrics.strikeout_position, metrics.strikeout_thickness),
|
Flags::STRIKEOUT => (metrics.strikeout_position, metrics.strikeout_thickness),
|
||||||
_ => unimplemented!("Invalid flag for cell line drawing specified"),
|
_ => unimplemented!("Invalid flag for cell line drawing specified"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rects.push(Self::create_rect(
|
||||||
|
size,
|
||||||
|
metrics.descent,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
position,
|
||||||
|
thickness,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a line's rect at a position relative to the baseline.
|
||||||
|
fn create_rect(
|
||||||
|
size: &SizeInfo,
|
||||||
|
descent: f32,
|
||||||
|
start: Point,
|
||||||
|
end: Point,
|
||||||
|
position: f32,
|
||||||
|
mut thickness: f32,
|
||||||
|
color: Rgb,
|
||||||
|
) -> RenderRect {
|
||||||
|
let start_x = start.col.0 as f32 * size.cell_width;
|
||||||
|
let end_x = (end.col.0 + 1) as f32 * size.cell_width;
|
||||||
|
let width = end_x - start_x;
|
||||||
|
|
||||||
// Make sure lines are always visible.
|
// Make sure lines are always visible.
|
||||||
height = height.max(1.);
|
thickness = thickness.max(1.);
|
||||||
|
|
||||||
let line_bottom = (start.line.0 as f32 + 1.) * size.cell_height;
|
let line_bottom = (start.line.0 as f32 + 1.) * size.cell_height;
|
||||||
let baseline = line_bottom + metrics.descent;
|
let baseline = line_bottom + descent;
|
||||||
|
|
||||||
let mut y = (baseline - position - height / 2.).ceil();
|
let mut y = (baseline - position - thickness / 2.).ceil();
|
||||||
let max_y = line_bottom - height;
|
let max_y = line_bottom - thickness;
|
||||||
if y > max_y {
|
if y > max_y {
|
||||||
y = max_y;
|
y = max_y;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderRect::new(start_x + size.padding_x, y + size.padding_y, width, height, color, 1.)
|
RenderRect::new(start_x + size.padding_x, y + size.padding_y, width, thickness, color, 1.)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,10 +130,12 @@ pub struct RenderLines {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderLines {
|
impl RenderLines {
|
||||||
|
#[inline]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn rects(&self, metrics: &Metrics, size: &SizeInfo) -> Vec<RenderRect> {
|
pub fn rects(&self, metrics: &Metrics, size: &SizeInfo) -> Vec<RenderRect> {
|
||||||
self.inner
|
self.inner
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -105,32 +147,38 @@ impl RenderLines {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the stored lines with the next cell info.
|
/// Update the stored lines with the next cell info.
|
||||||
|
#[inline]
|
||||||
pub fn update(&mut self, cell: RenderableCell) {
|
pub fn update(&mut self, cell: RenderableCell) {
|
||||||
for flag in &[Flags::UNDERLINE, Flags::STRIKEOUT] {
|
self.update_flag(cell, Flags::UNDERLINE);
|
||||||
if !cell.flags.contains(*flag) {
|
self.update_flag(cell, Flags::DOUBLE_UNDERLINE);
|
||||||
continue;
|
self.update_flag(cell, Flags::STRIKEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's an active line.
|
/// Update the lines for a specific flag.
|
||||||
if let Some(line) = self.inner.get_mut(flag).and_then(|lines| lines.last_mut()) {
|
fn update_flag(&mut self, cell: RenderableCell, flag: Flags) {
|
||||||
if cell.fg == line.color
|
if !cell.flags.contains(flag) {
|
||||||
&& cell.column == line.end.col + 1
|
return;
|
||||||
&& cell.line == line.end.line
|
}
|
||||||
{
|
|
||||||
// Update the length of the line.
|
|
||||||
line.end = cell.into();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start new line if there currently is none.
|
// Check if there's an active line.
|
||||||
let line = RenderLine { start: cell.into(), end: cell.into(), color: cell.fg };
|
if let Some(line) = self.inner.get_mut(&flag).and_then(|lines| lines.last_mut()) {
|
||||||
match self.inner.get_mut(flag) {
|
if cell.fg == line.color
|
||||||
Some(lines) => lines.push(line),
|
&& cell.column == line.end.col + 1
|
||||||
None => {
|
&& cell.line == line.end.line
|
||||||
self.inner.insert(*flag, vec![line]);
|
{
|
||||||
},
|
// Update the length of the line.
|
||||||
|
line.end = cell.into();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start new line if there currently is none.
|
||||||
|
let line = RenderLine { start: cell.into(), end: cell.into(), color: cell.fg };
|
||||||
|
match self.inner.get_mut(&flag) {
|
||||||
|
Some(lines) => lines.push(line),
|
||||||
|
None => {
|
||||||
|
self.inner.insert(flag, vec![line]);
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -618,6 +618,8 @@ pub enum Attr {
|
||||||
Italic,
|
Italic,
|
||||||
/// Underline text.
|
/// Underline text.
|
||||||
Underline,
|
Underline,
|
||||||
|
/// Underlined twice.
|
||||||
|
DoubleUnderline,
|
||||||
/// Blink cursor slowly.
|
/// Blink cursor slowly.
|
||||||
BlinkSlow,
|
BlinkSlow,
|
||||||
/// Blink cursor fast.
|
/// Blink cursor fast.
|
||||||
|
@ -634,7 +636,7 @@ pub enum Attr {
|
||||||
CancelBoldDim,
|
CancelBoldDim,
|
||||||
/// Cancel italic.
|
/// Cancel italic.
|
||||||
CancelItalic,
|
CancelItalic,
|
||||||
/// Cancel underline.
|
/// Cancel all underlines.
|
||||||
CancelUnderline,
|
CancelUnderline,
|
||||||
/// Cancel blink.
|
/// Cancel blink.
|
||||||
CancelBlink,
|
CancelBlink,
|
||||||
|
@ -1151,7 +1153,9 @@ fn attrs_from_sgr_parameters(params: &mut ParamsIter) -> Vec<Option<Attr>> {
|
||||||
[1] => Some(Attr::Bold),
|
[1] => Some(Attr::Bold),
|
||||||
[2] => Some(Attr::Dim),
|
[2] => Some(Attr::Dim),
|
||||||
[3] => Some(Attr::Italic),
|
[3] => Some(Attr::Italic),
|
||||||
[4] => Some(Attr::Underline),
|
[4, 0] => Some(Attr::CancelUnderline),
|
||||||
|
[4, 2] => Some(Attr::DoubleUnderline),
|
||||||
|
[4, ..] => Some(Attr::Underline),
|
||||||
[5] => Some(Attr::BlinkSlow),
|
[5] => Some(Attr::BlinkSlow),
|
||||||
[6] => Some(Attr::BlinkFast),
|
[6] => Some(Attr::BlinkFast),
|
||||||
[7] => Some(Attr::Reverse),
|
[7] => Some(Attr::Reverse),
|
||||||
|
|
|
@ -12,19 +12,20 @@ pub const MAX_ZEROWIDTH_CHARS: usize = 5;
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Flags: u16 {
|
pub struct Flags: u16 {
|
||||||
const INVERSE = 0b000_0000_0001;
|
const INVERSE = 0b0000_0000_0000_0001;
|
||||||
const BOLD = 0b000_0000_0010;
|
const BOLD = 0b0000_0000_0000_0010;
|
||||||
const ITALIC = 0b000_0000_0100;
|
const ITALIC = 0b0000_0000_0000_0100;
|
||||||
const BOLD_ITALIC = 0b000_0000_0110;
|
const BOLD_ITALIC = 0b0000_0000_0000_0110;
|
||||||
const UNDERLINE = 0b000_0000_1000;
|
const UNDERLINE = 0b0000_0000_0000_1000;
|
||||||
const WRAPLINE = 0b000_0001_0000;
|
const WRAPLINE = 0b0000_0000_0001_0000;
|
||||||
const WIDE_CHAR = 0b000_0010_0000;
|
const WIDE_CHAR = 0b0000_0000_0010_0000;
|
||||||
const WIDE_CHAR_SPACER = 0b000_0100_0000;
|
const WIDE_CHAR_SPACER = 0b0000_0000_0100_0000;
|
||||||
const DIM = 0b000_1000_0000;
|
const DIM = 0b0000_0000_1000_0000;
|
||||||
const DIM_BOLD = 0b000_1000_0010;
|
const DIM_BOLD = 0b0000_0000_1000_0010;
|
||||||
const HIDDEN = 0b001_0000_0000;
|
const HIDDEN = 0b0000_0001_0000_0000;
|
||||||
const STRIKEOUT = 0b010_0000_0000;
|
const STRIKEOUT = 0b0000_0010_0000_0000;
|
||||||
const LEADING_WIDE_CHAR_SPACER = 0b100_0000_0000;
|
const LEADING_WIDE_CHAR_SPACER = 0b0000_0100_0000_0000;
|
||||||
|
const DOUBLE_UNDERLINE = 0b0000_1000_0000_0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +59,7 @@ impl GridCell for Cell {
|
||||||
&& !self.flags.intersects(
|
&& !self.flags.intersects(
|
||||||
Flags::INVERSE
|
Flags::INVERSE
|
||||||
| Flags::UNDERLINE
|
| Flags::UNDERLINE
|
||||||
|
| Flags::DOUBLE_UNDERLINE
|
||||||
| Flags::STRIKEOUT
|
| Flags::STRIKEOUT
|
||||||
| Flags::WRAPLINE
|
| Flags::WRAPLINE
|
||||||
| Flags::WIDE_CHAR_SPACER
|
| Flags::WIDE_CHAR_SPACER
|
||||||
|
|
|
@ -311,7 +311,7 @@ impl RenderableCell {
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.bg_alpha == 0.
|
self.bg_alpha == 0.
|
||||||
&& !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT)
|
&& !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT | Flags::DOUBLE_UNDERLINE)
|
||||||
&& self.inner == RenderableCellContent::Chars([' '; cell::MAX_ZEROWIDTH_CHARS + 1])
|
&& self.inner == RenderableCellContent::Chars([' '; cell::MAX_ZEROWIDTH_CHARS + 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2017,8 +2017,17 @@ impl<T: EventListener> Handler for Term<T> {
|
||||||
Attr::CancelBoldDim => cursor.template.flags.remove(Flags::BOLD | Flags::DIM),
|
Attr::CancelBoldDim => cursor.template.flags.remove(Flags::BOLD | Flags::DIM),
|
||||||
Attr::Italic => cursor.template.flags.insert(Flags::ITALIC),
|
Attr::Italic => cursor.template.flags.insert(Flags::ITALIC),
|
||||||
Attr::CancelItalic => cursor.template.flags.remove(Flags::ITALIC),
|
Attr::CancelItalic => cursor.template.flags.remove(Flags::ITALIC),
|
||||||
Attr::Underline => cursor.template.flags.insert(Flags::UNDERLINE),
|
Attr::Underline => {
|
||||||
Attr::CancelUnderline => cursor.template.flags.remove(Flags::UNDERLINE),
|
cursor.template.flags.remove(Flags::DOUBLE_UNDERLINE);
|
||||||
|
cursor.template.flags.insert(Flags::UNDERLINE);
|
||||||
|
},
|
||||||
|
Attr::DoubleUnderline => {
|
||||||
|
cursor.template.flags.remove(Flags::UNDERLINE);
|
||||||
|
cursor.template.flags.insert(Flags::DOUBLE_UNDERLINE);
|
||||||
|
},
|
||||||
|
Attr::CancelUnderline => {
|
||||||
|
cursor.template.flags.remove(Flags::UNDERLINE | Flags::DOUBLE_UNDERLINE);
|
||||||
|
},
|
||||||
Attr::Hidden => cursor.template.flags.insert(Flags::HIDDEN),
|
Attr::Hidden => cursor.template.flags.insert(Flags::HIDDEN),
|
||||||
Attr::CancelHidden => cursor.template.flags.remove(Flags::HIDDEN),
|
Attr::CancelHidden => cursor.template.flags.remove(Flags::HIDDEN),
|
||||||
Attr::Strike => cursor.template.flags.insert(Flags::STRIKEOUT),
|
Attr::Strike => cursor.template.flags.insert(Flags::STRIKEOUT),
|
||||||
|
|
|
@ -66,6 +66,7 @@ ref_tests! {
|
||||||
saved_cursor
|
saved_cursor
|
||||||
saved_cursor_alt
|
saved_cursor_alt
|
||||||
sgr
|
sgr
|
||||||
|
underline
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_u8<P>(path: P) -> Vec<u8>
|
fn read_u8<P>(path: P) -> Vec<u8>
|
||||||
|
|
13
alacritty_terminal/tests/ref/underline/alacritty.recording
Normal file
13
alacritty_terminal/tests/ref/underline/alacritty.recording
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[undeadleech@undeadlap underline]$
[K[undeadleech@undeadlap underline]$ echo -e "\e[4mUNDERLINED\e[0m"
|
||||||
|
[4mUNDERLINED[0m
|
||||||
|
[undeadleech@undeadlap underline]$ echo -e "\e[4:1mUNDERLINED\e[4:0m"
|
||||||
|
[4:1mUNDERLINED[4:0m
|
||||||
|
[undeadleech@undeadlap underline]$ echo -e "\e[4:2mUNDERLINED\e[24m"
|
||||||
|
[4:2mUNDERLINED[24m
|
||||||
|
[undeadleech@undeadlap underline]$ echo -e "\e[4:3mUNDERLINED\e[21m";mUNDERLINED\e[21m"2mUNDERLINED\e[21m"1mUNDERLINED\e[21m"[C[C[C[C[C[C[C[C[C[C[C[C[C[C[C[C[C[1Pm"[1Pm"0m"
|
||||||
|
[4:3;21mUNDERLINED[0m
|
||||||
|
[undeadleech@undeadlap underline]$ echo -e "\e[4;4:2mUNDERLINED\e[0m"
|
||||||
|
[4;4:2mUNDERLINED[0m
|
||||||
|
[undeadleech@undeadlap underline]$ echo -e "\e[4;4:2mUNDERLINED\e[0m":;4:2mUNDERLINED\e[0m"1;4:2mUNDERLINED\e[0m"[C[C[C[1P;4:2mUNDERLINED\e[0m"2;4:2mUNDERLINED\e[0m"[C[C[C[C[1PmUNDERLINED\e[0m"1mUNDERLINED\e[0m"
|
||||||
|
[4:2;4:1mUNDERLINED[0m
|
||||||
|
[undeadleech@undeadlap underline]$
|
1
alacritty_terminal/tests/ref/underline/config.json
Normal file
1
alacritty_terminal/tests/ref/underline/config.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"history_size":0}
|
1
alacritty_terminal/tests/ref/underline/grid.json
Normal file
1
alacritty_terminal/tests/ref/underline/grid.json
Normal file
File diff suppressed because one or more lines are too long
1
alacritty_terminal/tests/ref/underline/size.json
Normal file
1
alacritty_terminal/tests/ref/underline/size.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"width":939.0,"height":503.0,"cell_width":8.0,"cell_height":16.0,"padding_x":5.0,"padding_y":3.0,"dpr":1.0}
|
|
@ -106,3 +106,4 @@ alacritty+common|base fragment for alacritty,
|
||||||
Cr=\E]112\007, Cs=\E]12;%p1%s\007,
|
Cr=\E]112\007, Cs=\E]12;%p1%s\007,
|
||||||
Ms=\E]52;%p1%s;%p2%s\007, Se=\E[0 q, Ss=\E[%p1%d q,
|
Ms=\E]52;%p1%s;%p2%s\007, Se=\E[0 q, Ss=\E[%p1%d q,
|
||||||
hs, dsl=\E]2;\007, fsl=^G, tsl=\E]2;,
|
hs, dsl=\E]2;\007, fsl=^G, tsl=\E]2;,
|
||||||
|
Smulx=\E[4\:%p1%dm,
|
||||||
|
|
Loading…
Reference in a new issue