Skip to content

Commit 418a622

Browse files
authored
Merge pull request #4061 from pascalkuthe/undercurl-modifier
Support different kinds of underline rendering (updated)
2 parents faf0c52 + 66a4908 commit 418a622

File tree

11 files changed

+307
-33
lines changed

11 files changed

+307
-33
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

book/src/themes.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix
1313
Each line in the theme file is specified as below:
1414

1515
```toml
16-
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
16+
key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] }
1717
```
1818

19-
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
19+
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults.
2020

2121
To specify only the foreground color:
2222

@@ -77,17 +77,35 @@ The following values may be used as modifiers.
7777

7878
Less common modifiers might not be supported by your terminal emulator.
7979

80+
| Modifier |
81+
| --- |
82+
| `bold` |
83+
| `dim` |
84+
| `italic` |
85+
| `underlined` |
86+
| `slow_blink` |
87+
| `rapid_blink` |
88+
| `reversed` |
89+
| `hidden` |
90+
| `crossed_out` |
91+
92+
> Note: The `underlined` modifier is deprecated and only available for backwards compatibility.
93+
> Its behavior is equivalent to setting `underline.style="line"`.
94+
95+
### Underline Style
96+
97+
One of the following values may be used as a value for `underline.style`.
98+
99+
Some styles might not be supported by your terminal emulator.
100+
80101
| Modifier |
81102
| --- |
82-
| `bold` |
83-
| `dim` |
84-
| `italic` |
85-
| `underlined` |
86-
| `slow_blink` |
87-
| `rapid_blink` |
88-
| `reversed` |
89-
| `hidden` |
90-
| `crossed_out` |
103+
| `line` |
104+
| `curl` |
105+
| `dashed` |
106+
| `dot` |
107+
| `double_line` |
108+
91109

92110
### Inheritance
93111

helix-tui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ bitflags = "1.3"
2020
cassowary = "0.3"
2121
unicode-segmentation = "1.10"
2222
crossterm = { version = "0.25", optional = true }
23+
termini = "0.1"
2324
serde = { version = "1", "optional" = true, features = ["derive"]}
2425
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
2526
helix-core = { version = "0.6", path = "../helix-core" }

helix-tui/src/backend/crossterm.rs

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,56 @@ use crossterm::{
77
SetForegroundColor,
88
},
99
terminal::{self, Clear, ClearType},
10+
Command,
1011
};
11-
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
12-
use std::io::{self, Write};
12+
use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
13+
use std::{
14+
fmt,
15+
io::{self, Write},
16+
};
17+
fn vte_version() -> Option<usize> {
18+
std::env::var("VTE_VERSION").ok()?.parse().ok()
19+
}
20+
21+
/// Describes terminal capabilities like extended underline, truecolor, etc.
22+
#[derive(Copy, Clone, Debug, Default)]
23+
struct Capabilities {
24+
/// Support for undercurled, underdashed, etc.
25+
has_extended_underlines: bool,
26+
}
27+
28+
impl Capabilities {
29+
/// Detect capabilities from the terminfo database located based
30+
/// on the $TERM environment variable. If detection fails, returns
31+
/// a default value where no capability is supported.
32+
pub fn from_env_or_default() -> Self {
33+
match termini::TermInfo::from_env() {
34+
Err(_) => Capabilities::default(),
35+
Ok(t) => Capabilities {
36+
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
37+
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
38+
has_extended_underlines: t.extended_cap("Smulx").is_some()
39+
|| t.extended_cap("Su").is_some()
40+
|| vte_version() >= Some(5102),
41+
},
42+
}
43+
}
44+
}
1345

1446
pub struct CrosstermBackend<W: Write> {
1547
buffer: W,
48+
capabilities: Capabilities,
1649
}
1750

1851
impl<W> CrosstermBackend<W>
1952
where
2053
W: Write,
2154
{
2255
pub fn new(buffer: W) -> CrosstermBackend<W> {
23-
CrosstermBackend { buffer }
56+
CrosstermBackend {
57+
buffer,
58+
capabilities: Capabilities::from_env_or_default(),
59+
}
2460
}
2561
}
2662

@@ -47,6 +83,8 @@ where
4783
{
4884
let mut fg = Color::Reset;
4985
let mut bg = Color::Reset;
86+
let mut underline_color = Color::Reset;
87+
let mut underline_style = UnderlineStyle::Reset;
5088
let mut modifier = Modifier::empty();
5189
let mut last_pos: Option<(u16, u16)> = None;
5290
for (x, y, cell) in content {
@@ -74,11 +112,32 @@ where
74112
bg = cell.bg;
75113
}
76114

115+
let mut new_underline_style = cell.underline_style;
116+
if self.capabilities.has_extended_underlines {
117+
if cell.underline_color != underline_color {
118+
let color = CColor::from(cell.underline_color);
119+
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
120+
underline_color = cell.underline_color;
121+
}
122+
} else {
123+
match new_underline_style {
124+
UnderlineStyle::Reset | UnderlineStyle::Line => (),
125+
_ => new_underline_style = UnderlineStyle::Line,
126+
}
127+
}
128+
129+
if new_underline_style != underline_style {
130+
let attr = CAttribute::from(new_underline_style);
131+
map_error(queue!(self.buffer, SetAttribute(attr)))?;
132+
underline_style = new_underline_style;
133+
}
134+
77135
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
78136
}
79137

80138
map_error(queue!(
81139
self.buffer,
140+
SetUnderlineColor(CColor::Reset),
82141
SetForegroundColor(CColor::Reset),
83142
SetBackgroundColor(CColor::Reset),
84143
SetAttribute(CAttribute::Reset)
@@ -153,9 +212,6 @@ impl ModifierDiff {
153212
if removed.contains(Modifier::ITALIC) {
154213
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
155214
}
156-
if removed.contains(Modifier::UNDERLINED) {
157-
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
158-
}
159215
if removed.contains(Modifier::DIM) {
160216
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
161217
}
@@ -176,9 +232,6 @@ impl ModifierDiff {
176232
if added.contains(Modifier::ITALIC) {
177233
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
178234
}
179-
if added.contains(Modifier::UNDERLINED) {
180-
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
181-
}
182235
if added.contains(Modifier::DIM) {
183236
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
184237
}
@@ -195,3 +248,58 @@ impl ModifierDiff {
195248
Ok(())
196249
}
197250
}
251+
252+
/// Crossterm uses semicolon as a seperator for colors
253+
/// this is actually not spec compliant (altough commonly supported)
254+
/// However the correct approach is to use colons as a seperator.
255+
/// This usually doesn't make a difference for emulators that do support colored underlines.
256+
/// However terminals that do not support colored underlines will ignore underlines colors with colons
257+
/// while escape sequences with semicolons are always processed which leads to weird visual artifacts.
258+
/// See [this nvim issue](https://github.com/neovim/neovim/issues/9270) for details
259+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260+
pub struct SetUnderlineColor(pub CColor);
261+
262+
impl Command for SetUnderlineColor {
263+
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
264+
let color = self.0;
265+
266+
if color == CColor::Reset {
267+
write!(f, "\x1b[59m")?;
268+
return Ok(());
269+
}
270+
f.write_str("\x1b[58:")?;
271+
272+
let res = match color {
273+
CColor::Black => f.write_str("5:0"),
274+
CColor::DarkGrey => f.write_str("5:8"),
275+
CColor::Red => f.write_str("5:9"),
276+
CColor::DarkRed => f.write_str("5:1"),
277+
CColor::Green => f.write_str("5:10"),
278+
CColor::DarkGreen => f.write_str("5:2"),
279+
CColor::Yellow => f.write_str("5:11"),
280+
CColor::DarkYellow => f.write_str("5:3"),
281+
CColor::Blue => f.write_str("5:12"),
282+
CColor::DarkBlue => f.write_str("5:4"),
283+
CColor::Magenta => f.write_str("5:13"),
284+
CColor::DarkMagenta => f.write_str("5:5"),
285+
CColor::Cyan => f.write_str("5:14"),
286+
CColor::DarkCyan => f.write_str("5:6"),
287+
CColor::White => f.write_str("5:15"),
288+
CColor::Grey => f.write_str("5:7"),
289+
CColor::Rgb { r, g, b } => write!(f, "2::{}:{}:{}", r, g, b),
290+
CColor::AnsiValue(val) => write!(f, "5:{}", val),
291+
_ => Ok(()),
292+
};
293+
res?;
294+
write!(f, "m")?;
295+
Ok(())
296+
}
297+
298+
#[cfg(windows)]
299+
fn execute_winapi(&self) -> crossterm::Result<()> {
300+
Err(std::io::Error::new(
301+
std::io::ErrorKind::Other,
302+
"SetUnderlineColor not supported by winapi.",
303+
))
304+
}
305+
}

helix-tui/src/buffer.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ use helix_core::unicode::width::UnicodeWidthStr;
33
use std::cmp::min;
44
use unicode_segmentation::UnicodeSegmentation;
55

6-
use helix_view::graphics::{Color, Modifier, Rect, Style};
6+
use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle};
77

88
/// A buffer cell
99
#[derive(Debug, Clone, PartialEq)]
1010
pub struct Cell {
1111
pub symbol: String,
1212
pub fg: Color,
1313
pub bg: Color,
14+
pub underline_color: Color,
15+
pub underline_style: UnderlineStyle,
1416
pub modifier: Modifier,
1517
}
1618

@@ -44,6 +46,13 @@ impl Cell {
4446
if let Some(c) = style.bg {
4547
self.bg = c;
4648
}
49+
if let Some(c) = style.underline_color {
50+
self.underline_color = c;
51+
}
52+
if let Some(style) = style.underline_style {
53+
self.underline_style = style;
54+
}
55+
4756
self.modifier.insert(style.add_modifier);
4857
self.modifier.remove(style.sub_modifier);
4958
self
@@ -53,6 +62,8 @@ impl Cell {
5362
Style::default()
5463
.fg(self.fg)
5564
.bg(self.bg)
65+
.underline_color(self.underline_color)
66+
.underline_style(self.underline_style)
5667
.add_modifier(self.modifier)
5768
}
5869

@@ -61,6 +72,8 @@ impl Cell {
6172
self.symbol.push(' ');
6273
self.fg = Color::Reset;
6374
self.bg = Color::Reset;
75+
self.underline_color = Color::Reset;
76+
self.underline_style = UnderlineStyle::Reset;
6477
self.modifier = Modifier::empty();
6578
}
6679
}
@@ -71,6 +84,8 @@ impl Default for Cell {
7184
symbol: " ".into(),
7285
fg: Color::Reset,
7386
bg: Color::Reset,
87+
underline_color: Color::Reset,
88+
underline_style: UnderlineStyle::Reset,
7489
modifier: Modifier::empty(),
7590
}
7691
}
@@ -87,7 +102,7 @@ impl Default for Cell {
87102
///
88103
/// ```
89104
/// use helix_tui::buffer::{Buffer, Cell};
90-
/// use helix_view::graphics::{Rect, Color, Style, Modifier};
105+
/// use helix_view::graphics::{Rect, Color, UnderlineStyle, Style, Modifier};
91106
///
92107
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
93108
/// buf[(0, 2)].set_symbol("x");
@@ -97,7 +112,9 @@ impl Default for Cell {
97112
/// symbol: String::from("r"),
98113
/// fg: Color::Red,
99114
/// bg: Color::White,
100-
/// modifier: Modifier::empty()
115+
/// underline_color: Color::Reset,
116+
/// underline_style: UnderlineStyle::Reset,
117+
/// modifier: Modifier::empty(),
101118
/// });
102119
/// buf[(5, 0)].set_char('x');
103120
/// assert_eq!(buf[(5, 0)].symbol, "x");

helix-tui/src/text.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ impl<'a> Span<'a> {
134134
/// style: Style {
135135
/// fg: Some(Color::Yellow),
136136
/// bg: Some(Color::Black),
137+
/// underline_color: None,
138+
/// underline_style: None,
137139
/// add_modifier: Modifier::empty(),
138140
/// sub_modifier: Modifier::empty(),
139141
/// },
@@ -143,6 +145,8 @@ impl<'a> Span<'a> {
143145
/// style: Style {
144146
/// fg: Some(Color::Yellow),
145147
/// bg: Some(Color::Black),
148+
/// underline_color: None,
149+
/// underline_style: None,
146150
/// add_modifier: Modifier::empty(),
147151
/// sub_modifier: Modifier::empty(),
148152
/// },
@@ -152,6 +156,8 @@ impl<'a> Span<'a> {
152156
/// style: Style {
153157
/// fg: Some(Color::Yellow),
154158
/// bg: Some(Color::Black),
159+
/// underline_color: None,
160+
/// underline_style: None,
155161
/// add_modifier: Modifier::empty(),
156162
/// sub_modifier: Modifier::empty(),
157163
/// },
@@ -161,6 +167,8 @@ impl<'a> Span<'a> {
161167
/// style: Style {
162168
/// fg: Some(Color::Yellow),
163169
/// bg: Some(Color::Black),
170+
/// underline_color: None,
171+
/// underline_style: None,
164172
/// add_modifier: Modifier::empty(),
165173
/// sub_modifier: Modifier::empty(),
166174
/// },

0 commit comments

Comments
 (0)