Skip to content

Commit 92c83ae

Browse files
sudormrfbinthe-mikedavis
authored andcommitted
Support different kinds of underline rendering
Adds four new modifiers that can be used in themes: - undercurled - underdashed - underdotted - double-underline
1 parent ed29f2c commit 92c83ae

File tree

10 files changed

+148
-30
lines changed

10 files changed

+148
-30
lines changed

Cargo.lock

Lines changed: 7 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: 17 additions & 13 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 = "#ff0000", modifiers = ["bold", "italic", "undercurled"] }
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 color (only meaningful if an underline modifier is enabled), 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,21 @@ 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` |
80+
| Modifier |
81+
| --- |
82+
| `bold` |
83+
| `dim` |
84+
| `italic` |
85+
| `underlined` |
86+
| `undercurled` |
87+
| `underdashed` |
88+
| `underdotted` |
89+
| `double-underlined` |
90+
| `slow_blink` |
91+
| `rapid_blink` |
92+
| `reversed` |
93+
| `hidden` |
94+
| `crossed_out` |
9195

9296
### Rainbow
9397

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.9"
2222
crossterm = { version = "0.25", optional = true }
23+
cxterminfo = "0.2"
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: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,56 @@ use crossterm::{
44
execute, queue,
55
style::{
66
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
7-
SetForegroundColor,
7+
SetForegroundColor, SetUnderlineColor,
88
},
99
terminal::{self, Clear, ClearType},
1010
};
1111
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
1212
use std::io::{self, Write};
1313

14+
fn vte_version() -> Option<usize> {
15+
std::env::var("VTE_VERSION").ok()?.parse().ok()
16+
}
17+
18+
/// Describes terminal capabilities like extended underline, truecolor, etc.
19+
#[derive(Copy, Clone, Debug, Default)]
20+
struct Capabilities {
21+
/// Support for undercurled, underdashed, etc.
22+
has_extended_underlines: bool,
23+
}
24+
25+
impl Capabilities {
26+
/// Detect capabilities from the terminfo database located based
27+
/// on the $TERM environment variable. If detection fails, returns
28+
/// a default value where no capability is supported.
29+
pub fn from_env_or_default() -> Self {
30+
match cxterminfo::terminfo::TermInfo::from_env() {
31+
Err(_) => Capabilities::default(),
32+
Ok(t) => Capabilities {
33+
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
34+
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
35+
has_extended_underlines: t.get_ext_string("Smulx").is_some()
36+
|| *t.get_ext_bool("Su").unwrap_or(&false)
37+
|| vte_version() >= Some(5102),
38+
},
39+
}
40+
}
41+
}
42+
1443
pub struct CrosstermBackend<W: Write> {
1544
buffer: W,
45+
capabilities: Capabilities,
1646
}
1747

1848
impl<W> CrosstermBackend<W>
1949
where
2050
W: Write,
2151
{
2252
pub fn new(buffer: W) -> CrosstermBackend<W> {
23-
CrosstermBackend { buffer }
53+
CrosstermBackend {
54+
buffer,
55+
capabilities: Capabilities::from_env_or_default(),
56+
}
2457
}
2558
}
2659

@@ -47,6 +80,7 @@ where
4780
{
4881
let mut fg = Color::Reset;
4982
let mut bg = Color::Reset;
83+
let mut underline = Color::Reset;
5084
let mut modifier = Modifier::empty();
5185
let mut last_pos: Option<(u16, u16)> = None;
5286
for (x, y, cell) in content {
@@ -60,7 +94,7 @@ where
6094
from: modifier,
6195
to: cell.modifier,
6296
};
63-
diff.queue(&mut self.buffer)?;
97+
diff.queue(&mut self.buffer, self.capabilities)?;
6498
modifier = cell.modifier;
6599
}
66100
if cell.fg != fg {
@@ -73,6 +107,11 @@ where
73107
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
74108
bg = cell.bg;
75109
}
110+
if cell.underline != underline {
111+
let color = CColor::from(cell.underline);
112+
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
113+
underline = cell.underline;
114+
}
76115

77116
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
78117
}
@@ -135,7 +174,7 @@ struct ModifierDiff {
135174
}
136175

137176
impl ModifierDiff {
138-
fn queue<W>(&self, mut w: W) -> io::Result<()>
177+
fn queue<W>(&self, mut w: W, caps: Capabilities) -> io::Result<()>
139178
where
140179
W: io::Write,
141180
{
@@ -153,7 +192,7 @@ impl ModifierDiff {
153192
if removed.contains(Modifier::ITALIC) {
154193
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
155194
}
156-
if removed.contains(Modifier::UNDERLINED) {
195+
if removed.intersects(Modifier::ANY_UNDERLINE) {
157196
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
158197
}
159198
if removed.contains(Modifier::DIM) {
@@ -166,6 +205,14 @@ impl ModifierDiff {
166205
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
167206
}
168207

208+
let queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> {
209+
let underline = match caps.has_extended_underlines {
210+
true => styled_underline,
211+
false => CAttribute::Underlined,
212+
};
213+
map_error(queue!(w, SetAttribute(underline)))
214+
};
215+
169216
let added = self.to - self.from;
170217
if added.contains(Modifier::REVERSED) {
171218
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
@@ -179,6 +226,18 @@ impl ModifierDiff {
179226
if added.contains(Modifier::UNDERLINED) {
180227
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
181228
}
229+
if added.contains(Modifier::UNDERCURLED) {
230+
queue_styled_underline(CAttribute::Undercurled, &mut w)?;
231+
}
232+
if added.contains(Modifier::UNDERDOTTED) {
233+
queue_styled_underline(CAttribute::Underdotted, &mut w)?;
234+
}
235+
if added.contains(Modifier::UNDERDASHED) {
236+
queue_styled_underline(CAttribute::Underdashed, &mut w)?;
237+
}
238+
if added.contains(Modifier::DOUBLE_UNDERLINED) {
239+
queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?;
240+
}
182241
if added.contains(Modifier::DIM) {
183242
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
184243
}

helix-tui/src/buffer.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Cell {
1111
pub symbol: String,
1212
pub fg: Color,
1313
pub bg: Color,
14+
pub underline: Color,
1415
pub modifier: Modifier,
1516
}
1617

@@ -44,6 +45,9 @@ impl Cell {
4445
if let Some(c) = style.bg {
4546
self.bg = c;
4647
}
48+
if let Some(c) = style.underline {
49+
self.underline = c;
50+
}
4751
self.modifier.insert(style.add_modifier);
4852
self.modifier.remove(style.sub_modifier);
4953
self
@@ -53,6 +57,7 @@ impl Cell {
5357
Style::default()
5458
.fg(self.fg)
5559
.bg(self.bg)
60+
.underline(self.underline)
5661
.add_modifier(self.modifier)
5762
}
5863

@@ -61,6 +66,7 @@ impl Cell {
6166
self.symbol.push(' ');
6267
self.fg = Color::Reset;
6368
self.bg = Color::Reset;
69+
self.underline = Color::Reset;
6470
self.modifier = Modifier::empty();
6571
}
6672
}
@@ -71,6 +77,7 @@ impl Default for Cell {
7177
symbol: " ".into(),
7278
fg: Color::Reset,
7379
bg: Color::Reset,
80+
underline: Color::Reset,
7481
modifier: Modifier::empty(),
7582
}
7683
}
@@ -97,7 +104,8 @@ impl Default for Cell {
97104
/// symbol: String::from("r"),
98105
/// fg: Color::Red,
99106
/// bg: Color::White,
100-
/// modifier: Modifier::empty()
107+
/// underline: Color::Reset,
108+
/// modifier: Modifier::empty(),
101109
/// });
102110
/// buf[(5, 0)].set_char('x');
103111
/// assert_eq!(buf[(5, 0)].symbol, "x");

helix-tui/src/text.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ impl<'a> Span<'a> {
134134
/// style: Style {
135135
/// fg: Some(Color::Yellow),
136136
/// bg: Some(Color::Black),
137+
/// underline: None,
137138
/// add_modifier: Modifier::empty(),
138139
/// sub_modifier: Modifier::empty(),
139140
/// },
@@ -143,6 +144,7 @@ impl<'a> Span<'a> {
143144
/// style: Style {
144145
/// fg: Some(Color::Yellow),
145146
/// bg: Some(Color::Black),
147+
/// underline: None,
146148
/// add_modifier: Modifier::empty(),
147149
/// sub_modifier: Modifier::empty(),
148150
/// },
@@ -152,6 +154,7 @@ impl<'a> Span<'a> {
152154
/// style: Style {
153155
/// fg: Some(Color::Yellow),
154156
/// bg: Some(Color::Black),
157+
/// underline: None,
155158
/// add_modifier: Modifier::empty(),
156159
/// sub_modifier: Modifier::empty(),
157160
/// },
@@ -161,6 +164,7 @@ impl<'a> Span<'a> {
161164
/// style: Style {
162165
/// fg: Some(Color::Yellow),
163166
/// bg: Some(Color::Black),
167+
/// underline: None,
164168
/// add_modifier: Modifier::empty(),
165169
/// sub_modifier: Modifier::empty(),
166170
/// },

helix-view/src/graphics.rs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -329,15 +329,25 @@ bitflags! {
329329
/// ```
330330
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
331331
pub struct Modifier: u16 {
332-
const BOLD = 0b0000_0000_0001;
333-
const DIM = 0b0000_0000_0010;
334-
const ITALIC = 0b0000_0000_0100;
335-
const UNDERLINED = 0b0000_0000_1000;
336-
const SLOW_BLINK = 0b0000_0001_0000;
337-
const RAPID_BLINK = 0b0000_0010_0000;
338-
const REVERSED = 0b0000_0100_0000;
339-
const HIDDEN = 0b0000_1000_0000;
340-
const CROSSED_OUT = 0b0001_0000_0000;
332+
const BOLD = 0b0000_0000_0000_0001;
333+
const DIM = 0b0000_0000_0000_0010;
334+
const ITALIC = 0b0000_0000_0000_0100;
335+
const UNDERLINED = 0b0000_0000_0000_1000;
336+
const SLOW_BLINK = 0b0000_0000_0001_0000;
337+
const RAPID_BLINK = 0b0000_0000_0010_0000;
338+
const REVERSED = 0b0000_0000_0100_0000;
339+
const HIDDEN = 0b0000_0000_1000_0000;
340+
const CROSSED_OUT = 0b0000_0001_0000_0000;
341+
const UNDERCURLED = 0b0000_0010_0000_0000;
342+
const UNDERDOTTED = 0b0000_0100_0000_0000;
343+
const UNDERDASHED = 0b0000_1000_0000_0000;
344+
const DOUBLE_UNDERLINED = 0b0001_0000_0000_0000;
345+
346+
const ANY_UNDERLINE = Self::UNDERLINED.bits
347+
| Self::UNDERCURLED.bits
348+
| Self::UNDERDOTTED.bits
349+
| Self::UNDERDASHED.bits
350+
| Self::DOUBLE_UNDERLINED.bits;
341351
}
342352
}
343353

@@ -355,6 +365,10 @@ impl FromStr for Modifier {
355365
"reversed" => Ok(Self::REVERSED),
356366
"hidden" => Ok(Self::HIDDEN),
357367
"crossed_out" => Ok(Self::CROSSED_OUT),
368+
"undercurled" => Ok(Self::UNDERCURLED),
369+
"underdotted" => Ok(Self::UNDERDOTTED),
370+
"underdashed" => Ok(Self::UNDERDASHED),
371+
"double_underlined" => Ok(Self::DOUBLE_UNDERLINED),
358372
_ => Err("Invalid modifier"),
359373
}
360374
}
@@ -426,6 +440,7 @@ impl FromStr for Modifier {
426440
pub struct Style {
427441
pub fg: Option<Color>,
428442
pub bg: Option<Color>,
443+
pub underline: Option<Color>,
429444
pub add_modifier: Modifier,
430445
pub sub_modifier: Modifier,
431446
}
@@ -435,6 +450,7 @@ impl Default for Style {
435450
Style {
436451
fg: None,
437452
bg: None,
453+
underline: None,
438454
add_modifier: Modifier::empty(),
439455
sub_modifier: Modifier::empty(),
440456
}
@@ -447,6 +463,7 @@ impl Style {
447463
Style {
448464
fg: Some(Color::Reset),
449465
bg: Some(Color::Reset),
466+
underline: Some(Color::Reset),
450467
add_modifier: Modifier::empty(),
451468
sub_modifier: Modifier::all(),
452469
}
@@ -482,6 +499,21 @@ impl Style {
482499
self
483500
}
484501

502+
/// Changes the underline color.
503+
///
504+
/// ## Examples
505+
///
506+
/// ```rust
507+
/// # use helix_view::graphics::{Color, Style};
508+
/// let style = Style::default().underline(Color::Blue);
509+
/// let diff = Style::default().underline(Color::Red);
510+
/// assert_eq!(style.patch(diff), Style::default().underline(Color::Red));
511+
/// ```
512+
pub fn underline(mut self, color: Color) -> Style {
513+
self.underline = Some(color);
514+
self
515+
}
516+
485517
/// Changes the text emphasis.
486518
///
487519
/// When applied, it adds the given modifier to the `Style` modifiers.
@@ -538,6 +570,7 @@ impl Style {
538570
pub fn patch(mut self, other: Style) -> Style {
539571
self.fg = other.fg.or(self.fg);
540572
self.bg = other.bg.or(self.bg);
573+
self.underline = other.underline.or(self.underline);
541574

542575
self.add_modifier.remove(other.sub_modifier);
543576
self.add_modifier.insert(other.add_modifier);

0 commit comments

Comments
 (0)