on master help sign in
Merge pull request #4061 from pascalkuthe/undercurl-modifier
418a622Blaž Hrastnik
Cargo.lock+10
@@ -508,6 +508,7 @@ dependencies = [
508 "helix-core",
508 "helix-core",
509 "helix-view",
509 "helix-view",
510 "serde",
510 "serde",
511+ "termini",
511 "unicode-segmentation",
512 "unicode-segmentation",
512]
513]
513
514
@@ -1100,6 +1101,15 @@ dependencies = [
1100 "winapi",
1101 "winapi",
1101]
1102]
1102
1103
1104+[[package]]
1105+name = "termini"
1106+version = "0.1.2"
1107+source = "registry+https://github.com/rust-lang/crates.io-index"
1108+checksum = "394766021ef3dae8077f080518cdf5360831990f77f5708d5e3594c9b3efa2f9"
1109+dependencies = [
1110+ "dirs-next",
1111+]
1112+
1103[[package]]
1113[[package]]
1104name = "textwrap"
1114name = "textwrap"
1105version = "0.15.1"
1115version = "0.15.1"
book/src/themes.md+29−11
@@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix
13Each line in the theme file is specified as below:
13Each line in the theme file is specified as below:
14
14
15```toml
15```toml
16−key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
16+key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] }
17```
17```
18
18
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.
20
20
21To specify only the foreground color:
21To specify only the foreground color:
22
22
@@ -77,17 +77,35 @@ The following values may be used as modifiers.
77
77
78Less common modifiers might not be supported by your terminal emulator.
78Less common modifiers might not be supported by your terminal emulator.
79
79
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+
80| Modifier |
101| Modifier |
81| --- |
102| --- |
82−| `bold` |
103+| `line` |
83−| `dim` |
104+| `curl` |
84−| `italic` |
105+| `dashed` |
85−| `underlined` |
106+| `dot` |
86−| `slow_blink` |
107+| `double_line` |
87−| `rapid_blink` |
108+
88−| `reversed` |
89−| `hidden` |
90−| `crossed_out` |
91
109
92### Inheritance
110### Inheritance
93
111
helix-tui/Cargo.toml+1
@@ -20,6 +20,7 @@ bitflags = "1.3"
20cassowary = "0.3"
20cassowary = "0.3"
21unicode-segmentation = "1.10"
21unicode-segmentation = "1.10"
22crossterm = { version = "0.25", optional = true }
22crossterm = { version = "0.25", optional = true }
23+termini = "0.1"
23serde = { version = "1", "optional" = true, features = ["derive"]}
24serde = { version = "1", "optional" = true, features = ["derive"]}
24helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
25helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
25helix-core = { version = "0.6", path = "../helix-core" }
26helix-core = { version = "0.6", path = "../helix-core" }
helix-tui/src/backend/crossterm.rs+117−9
@@ -7,12 +7,45 @@ use crossterm::{
7 SetForegroundColor,
7 SetForegroundColor,
8 },
8 },
9 terminal::{self, Clear, ClearType},
9 terminal::{self, Clear, ClearType},
10+ Command,
10};
11};
11−use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
12+use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
13+use std::{
14+ fmt,
12−use std::io::{self, Write};
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+}
13
45
14pub struct CrosstermBackend<W: Write> {
46pub struct CrosstermBackend<W: Write> {
15 buffer: W,
47 buffer: W,
48+ capabilities: Capabilities,
16}
49}
17
50
18impl<W> CrosstermBackend<W>
51impl<W> CrosstermBackend<W>
@@ -20,7 +53,10 @@ where
20 W: Write,
53 W: Write,
21{
54{
22 pub fn new(buffer: W) -> CrosstermBackend<W> {
55 pub fn new(buffer: W) -> CrosstermBackend<W> {
23− CrosstermBackend { buffer }
56+ CrosstermBackend {
57+ buffer,
58+ capabilities: Capabilities::from_env_or_default(),
59+ }
24 }
60 }
25}
61}
26
62
@@ -47,6 +83,8 @@ where
47 {
83 {
48 let mut fg = Color::Reset;
84 let mut fg = Color::Reset;
49 let mut bg = Color::Reset;
85 let mut bg = Color::Reset;
86+ let mut underline_color = Color::Reset;
87+ let mut underline_style = UnderlineStyle::Reset;
50 let mut modifier = Modifier::empty();
88 let mut modifier = Modifier::empty();
51 let mut last_pos: Option<(u16, u16)> = None;
89 let mut last_pos: Option<(u16, u16)> = None;
52 for (x, y, cell) in content {
90 for (x, y, cell) in content {
@@ -74,11 +112,32 @@ where
74 bg = cell.bg;
112 bg = cell.bg;
75 }
113 }
76
114
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+
77 map_error(queue!(self.buffer, Print(&cell.symbol)))?;
135 map_error(queue!(self.buffer, Print(&cell.symbol)))?;
78 }
136 }
79
137
80 map_error(queue!(
138 map_error(queue!(
81 self.buffer,
139 self.buffer,
140+ SetUnderlineColor(CColor::Reset),
82 SetForegroundColor(CColor::Reset),
141 SetForegroundColor(CColor::Reset),
83 SetBackgroundColor(CColor::Reset),
142 SetBackgroundColor(CColor::Reset),
84 SetAttribute(CAttribute::Reset)
143 SetAttribute(CAttribute::Reset)
@@ -153,9 +212,6 @@ impl ModifierDiff {
153 if removed.contains(Modifier::ITALIC) {
212 if removed.contains(Modifier::ITALIC) {
154 map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
213 map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
155 }
214 }
156− if removed.contains(Modifier::UNDERLINED) {
157− map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
158− }
159 if removed.contains(Modifier::DIM) {
215 if removed.contains(Modifier::DIM) {
160 map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
216 map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
161 }
217 }
@@ -176,9 +232,6 @@ impl ModifierDiff {
176 if added.contains(Modifier::ITALIC) {
232 if added.contains(Modifier::ITALIC) {
177 map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
233 map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
178 }
234 }
179− if added.contains(Modifier::UNDERLINED) {
180− map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
181− }
182 if added.contains(Modifier::DIM) {
235 if added.contains(Modifier::DIM) {
183 map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
236 map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
184 }
237 }
@@ -195,3 +248,58 @@ impl ModifierDiff {
195 Ok(())
248 Ok(())
196 }
249 }
197}
250}
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+20−3
@@ -3,7 +3,7 @@ use helix_core::unicode::width::UnicodeWidthStr;
3use std::cmp::min;
3use std::cmp::min;
4use unicode_segmentation::UnicodeSegmentation;
4use unicode_segmentation::UnicodeSegmentation;
5
5
6−use helix_view::graphics::{Color, Modifier, Rect, Style};
6+use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle};
7
7
8/// A buffer cell
8/// A buffer cell
9#[derive(Debug, Clone, PartialEq)]
9#[derive(Debug, Clone, PartialEq)]
@@ -11,6 +11,8 @@ pub struct Cell {
11 pub symbol: String,
11 pub symbol: String,
12 pub fg: Color,
12 pub fg: Color,
13 pub bg: Color,
13 pub bg: Color,
14+ pub underline_color: Color,
15+ pub underline_style: UnderlineStyle,
14 pub modifier: Modifier,
16 pub modifier: Modifier,
15}
17}
16
18
@@ -44,6 +46,13 @@ impl Cell {
44 if let Some(c) = style.bg {
46 if let Some(c) = style.bg {
45 self.bg = c;
47 self.bg = c;
46 }
48 }
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+
47 self.modifier.insert(style.add_modifier);
56 self.modifier.insert(style.add_modifier);
48 self.modifier.remove(style.sub_modifier);
57 self.modifier.remove(style.sub_modifier);
49 self
58 self
@@ -53,6 +62,8 @@ impl Cell {
53 Style::default()
62 Style::default()
54 .fg(self.fg)
63 .fg(self.fg)
55 .bg(self.bg)
64 .bg(self.bg)
65+ .underline_color(self.underline_color)
66+ .underline_style(self.underline_style)
56 .add_modifier(self.modifier)
67 .add_modifier(self.modifier)
57 }
68 }
58
69
@@ -61,6 +72,8 @@ impl Cell {
61 self.symbol.push(' ');
72 self.symbol.push(' ');
62 self.fg = Color::Reset;
73 self.fg = Color::Reset;
63 self.bg = Color::Reset;
74 self.bg = Color::Reset;
75+ self.underline_color = Color::Reset;
76+ self.underline_style = UnderlineStyle::Reset;
64 self.modifier = Modifier::empty();
77 self.modifier = Modifier::empty();
65 }
78 }
66}
79}
@@ -71,6 +84,8 @@ impl Default for Cell {
71 symbol: " ".into(),
84 symbol: " ".into(),
72 fg: Color::Reset,
85 fg: Color::Reset,
73 bg: Color::Reset,
86 bg: Color::Reset,
87+ underline_color: Color::Reset,
88+ underline_style: UnderlineStyle::Reset,
74 modifier: Modifier::empty(),
89 modifier: Modifier::empty(),
75 }
90 }
76 }
91 }
@@ -87,7 +102,7 @@ impl Default for Cell {
87///
102///
88/// ```
103/// ```
89/// use helix_tui::buffer::{Buffer, Cell};
104/// 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};
91///
106///
92/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
107/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
93/// buf[(0, 2)].set_symbol("x");
108/// buf[(0, 2)].set_symbol("x");
@@ -97,7 +112,9 @@ impl Default for Cell {
97/// symbol: String::from("r"),
112/// symbol: String::from("r"),
98/// fg: Color::Red,
113/// fg: Color::Red,
99/// bg: Color::White,
114/// bg: Color::White,
115+/// underline_color: Color::Reset,
116+/// underline_style: UnderlineStyle::Reset,
100−/// modifier: Modifier::empty()
117+/// modifier: Modifier::empty(),
101/// });
118/// });
102/// buf[(5, 0)].set_char('x');
119/// buf[(5, 0)].set_char('x');
103/// assert_eq!(buf[(5, 0)].symbol, "x");
120/// assert_eq!(buf[(5, 0)].symbol, "x");
helix-tui/src/text.rs+8
@@ -134,6 +134,8 @@ impl<'a> Span<'a> {
134 /// style: Style {
134 /// style: Style {
135 /// fg: Some(Color::Yellow),
135 /// fg: Some(Color::Yellow),
136 /// bg: Some(Color::Black),
136 /// bg: Some(Color::Black),
137+ /// underline_color: None,
138+ /// underline_style: None,
137 /// add_modifier: Modifier::empty(),
139 /// add_modifier: Modifier::empty(),
138 /// sub_modifier: Modifier::empty(),
140 /// sub_modifier: Modifier::empty(),
139 /// },
141 /// },
@@ -143,6 +145,8 @@ impl<'a> Span<'a> {
143 /// style: Style {
145 /// style: Style {
144 /// fg: Some(Color::Yellow),
146 /// fg: Some(Color::Yellow),
145 /// bg: Some(Color::Black),
147 /// bg: Some(Color::Black),
148+ /// underline_color: None,
149+ /// underline_style: None,
146 /// add_modifier: Modifier::empty(),
150 /// add_modifier: Modifier::empty(),
147 /// sub_modifier: Modifier::empty(),
151 /// sub_modifier: Modifier::empty(),
148 /// },
152 /// },
@@ -152,6 +156,8 @@ impl<'a> Span<'a> {
152 /// style: Style {
156 /// style: Style {
153 /// fg: Some(Color::Yellow),
157 /// fg: Some(Color::Yellow),
154 /// bg: Some(Color::Black),
158 /// bg: Some(Color::Black),
159+ /// underline_color: None,
160+ /// underline_style: None,
155 /// add_modifier: Modifier::empty(),
161 /// add_modifier: Modifier::empty(),
156 /// sub_modifier: Modifier::empty(),
162 /// sub_modifier: Modifier::empty(),
157 /// },
163 /// },
@@ -161,6 +167,8 @@ impl<'a> Span<'a> {
161 /// style: Style {
167 /// style: Style {
162 /// fg: Some(Color::Yellow),
168 /// fg: Some(Color::Yellow),
163 /// bg: Some(Color::Black),
169 /// bg: Some(Color::Black),
170+ /// underline_color: None,
171+ /// underline_style: None,
164 /// add_modifier: Modifier::empty(),
172 /// add_modifier: Modifier::empty(),
165 /// sub_modifier: Modifier::empty(),
173 /// sub_modifier: Modifier::empty(),
166 /// },
174 /// },
helix-view/src/graphics.rs+82−4
@@ -315,6 +315,44 @@ impl From<Color> for crossterm::style::Color {
315 }
315 }
316}
316}
317
317
318+#[derive(Debug, Clone, Copy, PartialEq)]
319+pub enum UnderlineStyle {
320+ Reset,
321+ Line,
322+ Curl,
323+ Dotted,
324+ Dashed,
325+ DoubleLine,
326+}
327+
328+impl FromStr for UnderlineStyle {
329+ type Err = &'static str;
330+
331+ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
332+ match modifier {
333+ "line" => Ok(Self::Line),
334+ "curl" => Ok(Self::Curl),
335+ "dotted" => Ok(Self::Dotted),
336+ "dashed" => Ok(Self::Dashed),
337+ "double_line" => Ok(Self::DoubleLine),
338+ _ => Err("Invalid underline style"),
339+ }
340+ }
341+}
342+
343+impl From<UnderlineStyle> for crossterm::style::Attribute {
344+ fn from(style: UnderlineStyle) -> Self {
345+ match style {
346+ UnderlineStyle::Line => crossterm::style::Attribute::Underlined,
347+ UnderlineStyle::Curl => crossterm::style::Attribute::Undercurled,
348+ UnderlineStyle::Dotted => crossterm::style::Attribute::Underdotted,
349+ UnderlineStyle::Dashed => crossterm::style::Attribute::Underdashed,
350+ UnderlineStyle::DoubleLine => crossterm::style::Attribute::DoubleUnderlined,
351+ UnderlineStyle::Reset => crossterm::style::Attribute::NoUnderline,
352+ }
353+ }
354+}
355+
318bitflags! {
356bitflags! {
319 /// Modifier changes the way a piece of text is displayed.
357 /// Modifier changes the way a piece of text is displayed.
320 ///
358 ///
@@ -332,7 +370,6 @@ bitflags! {
332 const BOLD = 0b0000_0000_0001;
370 const BOLD = 0b0000_0000_0001;
333 const DIM = 0b0000_0000_0010;
371 const DIM = 0b0000_0000_0010;
334 const ITALIC = 0b0000_0000_0100;
372 const ITALIC = 0b0000_0000_0100;
335− const UNDERLINED = 0b0000_0000_1000;
336 const SLOW_BLINK = 0b0000_0001_0000;
373 const SLOW_BLINK = 0b0000_0001_0000;
337 const RAPID_BLINK = 0b0000_0010_0000;
374 const RAPID_BLINK = 0b0000_0010_0000;
338 const REVERSED = 0b0000_0100_0000;
375 const REVERSED = 0b0000_0100_0000;
@@ -349,7 +386,6 @@ impl FromStr for Modifier {
349 "bold" => Ok(Self::BOLD),
386 "bold" => Ok(Self::BOLD),
350 "dim" => Ok(Self::DIM),
387 "dim" => Ok(Self::DIM),
351 "italic" => Ok(Self::ITALIC),
388 "italic" => Ok(Self::ITALIC),
352− "underlined" => Ok(Self::UNDERLINED),
353 "slow_blink" => Ok(Self::SLOW_BLINK),
389 "slow_blink" => Ok(Self::SLOW_BLINK),
354 "rapid_blink" => Ok(Self::RAPID_BLINK),
390 "rapid_blink" => Ok(Self::RAPID_BLINK),
355 "reversed" => Ok(Self::REVERSED),
391 "reversed" => Ok(Self::REVERSED),
@@ -375,7 +411,7 @@ impl FromStr for Modifier {
375/// just S3.
411/// just S3.
376///
412///
377/// ```rust
413/// ```rust
378−/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
414+/// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style};
379/// # use helix_tui::buffer::Buffer;
415/// # use helix_tui::buffer::Buffer;
380/// let styles = [
416/// let styles = [
381/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
417/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
@@ -391,6 +427,8 @@ impl FromStr for Modifier {
391/// fg: Some(Color::Yellow),
427/// fg: Some(Color::Yellow),
392/// bg: Some(Color::Red),
428/// bg: Some(Color::Red),
393/// add_modifier: Modifier::BOLD,
429/// add_modifier: Modifier::BOLD,
430+/// underline_color: Some(Color::Reset),
431+/// underline_style: Some(UnderlineStyle::Reset),
394/// sub_modifier: Modifier::empty(),
432/// sub_modifier: Modifier::empty(),
395/// },
433/// },
396/// buffer[(0, 0)].style(),
434/// buffer[(0, 0)].style(),
@@ -401,7 +439,7 @@ impl FromStr for Modifier {
401/// reset all properties until that point use [`Style::reset`].
439/// reset all properties until that point use [`Style::reset`].
402///
440///
403/// ```
441/// ```
404−/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
442+/// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style};
405/// # use helix_tui::buffer::Buffer;
443/// # use helix_tui::buffer::Buffer;
406/// let styles = [
444/// let styles = [
407/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
445/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
@@ -415,6 +453,8 @@ impl FromStr for Modifier {
415/// Style {
453/// Style {
416/// fg: Some(Color::Yellow),
454/// fg: Some(Color::Yellow),
417/// bg: Some(Color::Reset),
455/// bg: Some(Color::Reset),
456+/// underline_color: Some(Color::Reset),
457+/// underline_style: Some(UnderlineStyle::Reset),
418/// add_modifier: Modifier::empty(),
458/// add_modifier: Modifier::empty(),
419/// sub_modifier: Modifier::empty(),
459/// sub_modifier: Modifier::empty(),
420/// },
460/// },
@@ -426,6 +466,8 @@ impl FromStr for Modifier {
426pub struct Style {
466pub struct Style {
427 pub fg: Option<Color>,
467 pub fg: Option<Color>,
428 pub bg: Option<Color>,
468 pub bg: Option<Color>,
469+ pub underline_color: Option<Color>,
470+ pub underline_style: Option<UnderlineStyle>,
429 pub add_modifier: Modifier,
471 pub add_modifier: Modifier,
430 pub sub_modifier: Modifier,
472 pub sub_modifier: Modifier,
431}
473}
@@ -435,6 +477,8 @@ impl Default for Style {
435 Style {
477 Style {
436 fg: None,
478 fg: None,
437 bg: None,
479 bg: None,
480+ underline_color: None,
481+ underline_style: None,
438 add_modifier: Modifier::empty(),
482 add_modifier: Modifier::empty(),
439 sub_modifier: Modifier::empty(),
483 sub_modifier: Modifier::empty(),
440 }
484 }
@@ -447,6 +491,8 @@ impl Style {
447 Style {
491 Style {
448 fg: Some(Color::Reset),
492 fg: Some(Color::Reset),
449 bg: Some(Color::Reset),
493 bg: Some(Color::Reset),
494+ underline_color: None,
495+ underline_style: None,
450 add_modifier: Modifier::empty(),
496 add_modifier: Modifier::empty(),
451 sub_modifier: Modifier::all(),
497 sub_modifier: Modifier::all(),
452 }
498 }
@@ -482,6 +528,36 @@ impl Style {
482 self
528 self
483 }
529 }
484
530
531+ /// Changes the underline color.
532+ ///
533+ /// ## Examples
534+ ///
535+ /// ```rust
536+ /// # use helix_view::graphics::{Color, Style};
537+ /// let style = Style::default().underline_color(Color::Blue);
538+ /// let diff = Style::default().underline_color(Color::Red);
539+ /// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red));
540+ /// ```
541+ pub fn underline_color(mut self, color: Color) -> Style {
542+ self.underline_color = Some(color);
543+ self
544+ }
545+
546+ /// Changes the underline style.
547+ ///
548+ /// ## Examples
549+ ///
550+ /// ```rust
551+ /// # use helix_view::graphics::{UnderlineStyle, Style};
552+ /// let style = Style::default().underline_style(UnderlineStyle::Line);
553+ /// let diff = Style::default().underline_style(UnderlineStyle::Curl);
554+ /// assert_eq!(style.patch(diff), Style::default().underline_style(UnderlineStyle::Curl));
555+ /// ```
556+ pub fn underline_style(mut self, style: UnderlineStyle) -> Style {
557+ self.underline_style = Some(style);
558+ self
559+ }
560+
485 /// Changes the text emphasis.
561 /// Changes the text emphasis.
486 ///
562 ///
487 /// When applied, it adds the given modifier to the `Style` modifiers.
563 /// When applied, it adds the given modifier to the `Style` modifiers.
@@ -538,6 +614,8 @@ impl Style {
538 pub fn patch(mut self, other: Style) -> Style {
614 pub fn patch(mut self, other: Style) -> Style {
539 self.fg = other.fg.or(self.fg);
615 self.fg = other.fg.or(self.fg);
540 self.bg = other.bg.or(self.bg);
616 self.bg = other.bg.or(self.bg);
617+ self.underline_color = other.underline_color.or(self.underline_color);
618+ self.underline_style = other.underline_style.or(self.underline_style);
541
619
542 self.add_modifier.remove(other.sub_modifier);
620 self.add_modifier.remove(other.sub_modifier);
543 self.add_modifier.insert(other.add_modifier);
621 self.add_modifier.insert(other.add_modifier);
helix-view/src/gutter.rs+2−2
@@ -1,7 +1,7 @@
1use std::fmt::Write;
1use std::fmt::Write;
2
2
3use crate::{
3use crate::{
4− graphics::{Color, Modifier, Style},
4+ graphics::{Color, Style, UnderlineStyle},
5 Document, Editor, Theme, View,
5 Document, Editor, Theme, View,
6};
6};
7
7
@@ -147,7 +147,7 @@ pub fn breakpoints<'doc>(
147 .find(|breakpoint| breakpoint.line == line)?;
147 .find(|breakpoint| breakpoint.line == line)?;
148
148
149 let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
149 let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
150− error.add_modifier(Modifier::UNDERLINED)
150+ error.underline_style(UnderlineStyle::Line)
151 } else if breakpoint.condition.is_some() {
151 } else if breakpoint.condition.is_some() {
152 error
152 error
153 } else if breakpoint.log_message.is_some() {
153 } else if breakpoint.log_message.is_some() {
helix-view/src/theme.rs+32−2
@@ -11,6 +11,7 @@ use once_cell::sync::Lazy;
11use serde::{Deserialize, Deserializer};
11use serde::{Deserialize, Deserializer};
12use toml::{map::Map, Value};
12use toml::{map::Map, Value};
13
13
14+use crate::graphics::UnderlineStyle;
14pub use crate::graphics::{Color, Modifier, Style};
15pub use crate::graphics::{Color, Modifier, Style};
15
16
16pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
17pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
@@ -378,19 +379,48 @@ impl ThemePalette {
378 .ok_or(format!("Theme: invalid modifier: {}", value))
379 .ok_or(format!("Theme: invalid modifier: {}", value))
379 }
380 }
380
381
382+ pub fn parse_underline_style(value: &Value) -> Result<UnderlineStyle, String> {
383+ value
384+ .as_str()
385+ .and_then(|s| s.parse().ok())
386+ .ok_or(format!("Theme: invalid underline style: {}", value))
387+ }
388+
381 pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
389 pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
382 if let Value::Table(entries) = value {
390 if let Value::Table(entries) = value {
383− for (name, value) in entries {
391+ for (name, mut value) in entries {
384 match name.as_str() {
392 match name.as_str() {
385 "fg" => *style = style.fg(self.parse_color(value)?),
393 "fg" => *style = style.fg(self.parse_color(value)?),
386 "bg" => *style = style.bg(self.parse_color(value)?),
394 "bg" => *style = style.bg(self.parse_color(value)?),
395+ "underline" => {
396+ let table = value
397+ .as_table_mut()
398+ .ok_or("Theme: underline must be table")?;
399+ if let Some(value) = table.remove("color") {
400+ *style = style.underline_color(self.parse_color(value)?);
401+ }
402+ if let Some(value) = table.remove("style") {
403+ *style = style.underline_style(Self::parse_underline_style(&value)?);
404+ }
405+
406+ if let Some(attr) = table.keys().next() {
407+ return Err(format!("Theme: invalid underline attribute: {attr}"));
408+ }
409+ }
387 "modifiers" => {
410 "modifiers" => {
388 let modifiers = value
411 let modifiers = value
389 .as_array()
412 .as_array()
390 .ok_or("Theme: modifiers should be an array")?;
413 .ok_or("Theme: modifiers should be an array")?;
391
414
392 for modifier in modifiers {
415 for modifier in modifiers {
416+ if modifier
417+ .as_str()
418+ .map_or(false, |modifier| modifier == "underlined")
419+ {
420+ *style = style.underline_style(UnderlineStyle::Line);
421+ } else {
393− *style = style.add_modifier(Self::parse_modifier(modifier)?);
422+ *style = style.add_modifier(Self::parse_modifier(modifier)?);
423+ }
394 }
424 }
395 }
425 }
396 _ => return Err(format!("Theme: invalid style attribute: {}", name)),
426 _ => return Err(format!("Theme: invalid style attribute: {}", name)),
runtime/themes/dark_plus.toml+2−1
@@ -92,7 +92,8 @@
92"info" = { fg = "light_blue" }
92"info" = { fg = "light_blue" }
93"hint" = { fg = "light_gray3" }
93"hint" = { fg = "light_gray3" }
94
94
95−diagnostic = { modifiers = ["underlined"] }
95+"diagnostic.error".underline = { color = "red", style = "curl" }
96+"diagnostic".underline = { color = "gold", style = "curl" }
96
97
97[palette]
98[palette]
98white = "#ffffff"
99white = "#ffffff"
runtime/themes/onedark.toml+4−1
@@ -39,7 +39,10 @@
39"diff.delta" = "gold"
39"diff.delta" = "gold"
40"diff.minus" = "red"
40"diff.minus" = "red"
41
41
42−diagnostic = { modifiers = ["underlined"] }
42+"diagnostic.info".underline = { color = "blue", style = "curl" }
43+"diagnostic.hint".underline = { color = "green", style = "curl" }
44+"diagnostic.warning".underline = { color = "yellow", style = "curl" }
45+"diagnostic.error".underline = { color = "red", style = "curl" }
43"info" = { fg = "blue", modifiers = ["bold"] }
46"info" = { fg = "blue", modifiers = ["bold"] }
44"hint" = { fg = "green", modifiers = ["bold"] }
47"hint" = { fg = "green", modifiers = ["bold"] }
45"warning" = { fg = "yellow", modifiers = ["bold"] }
48"warning" = { fg = "yellow", modifiers = ["bold"] }