From 92c83aec70f396616cd79d9b71cdaa26ebcd4072 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 9 Jul 2022 03:35:06 +0530 Subject: [PATCH] Support different kinds of underline rendering Adds four new modifiers that can be used in themes: - undercurled - underdashed - underdotted - double-underline --- Cargo.lock | 7 +++ book/src/themes.md | 30 +++++++------ helix-tui/Cargo.toml | 1 + helix-tui/src/backend/crossterm.rs | 69 +++++++++++++++++++++++++++--- helix-tui/src/buffer.rs | 10 ++++- helix-tui/src/text.rs | 4 ++ helix-view/src/graphics.rs | 51 ++++++++++++++++++---- helix-view/src/theme.rs | 1 + runtime/themes/dark_plus.toml | 3 +- runtime/themes/onedark.toml | 2 +- 10 files changed, 148 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2225f8874152..f2c58b2902366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,6 +176,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "cxterminfo" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da92c5e3aaf2cc1fea346d9b3bac0c59c6ffc1d1d46f18d991d449912a3e6f07" + [[package]] name = "dirs-next" version = "2.0.0" @@ -506,6 +512,7 @@ dependencies = [ "bitflags", "cassowary", "crossterm", + "cxterminfo", "helix-core", "helix-view", "serde", diff --git a/book/src/themes.md b/book/src/themes.md index b7598febe5983..17a64f9685a67 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -13,10 +13,10 @@ The default theme.toml can be found [here](https://github.com/helix-editor/helix Each line in the theme file is specified as below: ```toml -key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] } +key = { fg = "#ffffff", bg = "#000000", underline = "#ff0000", modifiers = ["bold", "italic", "undercurled"] } ``` -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. +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. To specify only the foreground color: @@ -77,17 +77,21 @@ The following values may be used as modifiers. Less common modifiers might not be supported by your terminal emulator. -| Modifier | -| --- | -| `bold` | -| `dim` | -| `italic` | -| `underlined` | -| `slow_blink` | -| `rapid_blink` | -| `reversed` | -| `hidden` | -| `crossed_out` | +| Modifier | +| --- | +| `bold` | +| `dim` | +| `italic` | +| `underlined` | +| `undercurled` | +| `underdashed` | +| `underdotted` | +| `double-underlined` | +| `slow_blink` | +| `rapid_blink` | +| `reversed` | +| `hidden` | +| `crossed_out` | ### Rainbow diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index effad198d73b9..36bb6801a81f7 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -20,6 +20,7 @@ bitflags = "1.3" cassowary = "0.3" unicode-segmentation = "1.9" crossterm = { version = "0.25", optional = true } +cxterminfo = "0.2" serde = { version = "1", "optional" = true, features = ["derive"]} helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-core = { version = "0.6", path = "../helix-core" } diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index eff098b35c8c9..3a50074ede122 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -4,15 +4,45 @@ use crossterm::{ execute, queue, style::{ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, - SetForegroundColor, + SetForegroundColor, SetUnderlineColor, }, terminal::{self, Clear, ClearType}, }; use helix_view::graphics::{Color, CursorKind, Modifier, Rect}; use std::io::{self, Write}; +fn vte_version() -> Option { + std::env::var("VTE_VERSION").ok()?.parse().ok() +} + +/// Describes terminal capabilities like extended underline, truecolor, etc. +#[derive(Copy, Clone, Debug, Default)] +struct Capabilities { + /// Support for undercurled, underdashed, etc. + has_extended_underlines: bool, +} + +impl Capabilities { + /// Detect capabilities from the terminfo database located based + /// on the $TERM environment variable. If detection fails, returns + /// a default value where no capability is supported. + pub fn from_env_or_default() -> Self { + match cxterminfo::terminfo::TermInfo::from_env() { + Err(_) => Capabilities::default(), + Ok(t) => Capabilities { + // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284 + // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines + has_extended_underlines: t.get_ext_string("Smulx").is_some() + || *t.get_ext_bool("Su").unwrap_or(&false) + || vte_version() >= Some(5102), + }, + } + } +} + pub struct CrosstermBackend { buffer: W, + capabilities: Capabilities, } impl CrosstermBackend @@ -20,7 +50,10 @@ where W: Write, { pub fn new(buffer: W) -> CrosstermBackend { - CrosstermBackend { buffer } + CrosstermBackend { + buffer, + capabilities: Capabilities::from_env_or_default(), + } } } @@ -47,6 +80,7 @@ where { let mut fg = Color::Reset; let mut bg = Color::Reset; + let mut underline = Color::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<(u16, u16)> = None; for (x, y, cell) in content { @@ -60,7 +94,7 @@ where from: modifier, to: cell.modifier, }; - diff.queue(&mut self.buffer)?; + diff.queue(&mut self.buffer, self.capabilities)?; modifier = cell.modifier; } if cell.fg != fg { @@ -73,6 +107,11 @@ where map_error(queue!(self.buffer, SetBackgroundColor(color)))?; bg = cell.bg; } + if cell.underline != underline { + let color = CColor::from(cell.underline); + map_error(queue!(self.buffer, SetUnderlineColor(color)))?; + underline = cell.underline; + } map_error(queue!(self.buffer, Print(&cell.symbol)))?; } @@ -135,7 +174,7 @@ struct ModifierDiff { } impl ModifierDiff { - fn queue(&self, mut w: W) -> io::Result<()> + fn queue(&self, mut w: W, caps: Capabilities) -> io::Result<()> where W: io::Write, { @@ -153,7 +192,7 @@ impl ModifierDiff { if removed.contains(Modifier::ITALIC) { map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; } - if removed.contains(Modifier::UNDERLINED) { + if removed.intersects(Modifier::ANY_UNDERLINE) { map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?; } if removed.contains(Modifier::DIM) { @@ -166,6 +205,14 @@ impl ModifierDiff { map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; } + let queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> { + let underline = match caps.has_extended_underlines { + true => styled_underline, + false => CAttribute::Underlined, + }; + map_error(queue!(w, SetAttribute(underline))) + }; + let added = self.to - self.from; if added.contains(Modifier::REVERSED) { map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; @@ -179,6 +226,18 @@ impl ModifierDiff { if added.contains(Modifier::UNDERLINED) { map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; } + if added.contains(Modifier::UNDERCURLED) { + queue_styled_underline(CAttribute::Undercurled, &mut w)?; + } + if added.contains(Modifier::UNDERDOTTED) { + queue_styled_underline(CAttribute::Underdotted, &mut w)?; + } + if added.contains(Modifier::UNDERDASHED) { + queue_styled_underline(CAttribute::Underdashed, &mut w)?; + } + if added.contains(Modifier::DOUBLE_UNDERLINED) { + queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?; + } if added.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 21c53aadf9bc6..3036608d5e5cc 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -11,6 +11,7 @@ pub struct Cell { pub symbol: String, pub fg: Color, pub bg: Color, + pub underline: Color, pub modifier: Modifier, } @@ -44,6 +45,9 @@ impl Cell { if let Some(c) = style.bg { self.bg = c; } + if let Some(c) = style.underline { + self.underline = c; + } self.modifier.insert(style.add_modifier); self.modifier.remove(style.sub_modifier); self @@ -53,6 +57,7 @@ impl Cell { Style::default() .fg(self.fg) .bg(self.bg) + .underline(self.underline) .add_modifier(self.modifier) } @@ -61,6 +66,7 @@ impl Cell { self.symbol.push(' '); self.fg = Color::Reset; self.bg = Color::Reset; + self.underline = Color::Reset; self.modifier = Modifier::empty(); } } @@ -71,6 +77,7 @@ impl Default for Cell { symbol: " ".into(), fg: Color::Reset, bg: Color::Reset, + underline: Color::Reset, modifier: Modifier::empty(), } } @@ -97,7 +104,8 @@ impl Default for Cell { /// symbol: String::from("r"), /// fg: Color::Red, /// bg: Color::White, -/// modifier: Modifier::empty() +/// underline: Color::Reset, +/// modifier: Modifier::empty(), /// }); /// buf[(5, 0)].set_char('x'); /// assert_eq!(buf[(5, 0)].symbol, "x"); diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 602090e557efe..73d5880399f02 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -134,6 +134,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -143,6 +144,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -152,6 +154,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, @@ -161,6 +164,7 @@ impl<'a> Span<'a> { /// style: Style { /// fg: Some(Color::Yellow), /// bg: Some(Color::Black), + /// underline: None, /// add_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(), /// }, diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index fb3c8b3f99426..34a3bb1ce26e0 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -329,15 +329,25 @@ bitflags! { /// ``` #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Modifier: u16 { - const BOLD = 0b0000_0000_0001; - const DIM = 0b0000_0000_0010; - const ITALIC = 0b0000_0000_0100; - const UNDERLINED = 0b0000_0000_1000; - const SLOW_BLINK = 0b0000_0001_0000; - const RAPID_BLINK = 0b0000_0010_0000; - const REVERSED = 0b0000_0100_0000; - const HIDDEN = 0b0000_1000_0000; - const CROSSED_OUT = 0b0001_0000_0000; + const BOLD = 0b0000_0000_0000_0001; + const DIM = 0b0000_0000_0000_0010; + const ITALIC = 0b0000_0000_0000_0100; + const UNDERLINED = 0b0000_0000_0000_1000; + const SLOW_BLINK = 0b0000_0000_0001_0000; + const RAPID_BLINK = 0b0000_0000_0010_0000; + const REVERSED = 0b0000_0000_0100_0000; + const HIDDEN = 0b0000_0000_1000_0000; + const CROSSED_OUT = 0b0000_0001_0000_0000; + const UNDERCURLED = 0b0000_0010_0000_0000; + const UNDERDOTTED = 0b0000_0100_0000_0000; + const UNDERDASHED = 0b0000_1000_0000_0000; + const DOUBLE_UNDERLINED = 0b0001_0000_0000_0000; + + const ANY_UNDERLINE = Self::UNDERLINED.bits + | Self::UNDERCURLED.bits + | Self::UNDERDOTTED.bits + | Self::UNDERDASHED.bits + | Self::DOUBLE_UNDERLINED.bits; } } @@ -355,6 +365,10 @@ impl FromStr for Modifier { "reversed" => Ok(Self::REVERSED), "hidden" => Ok(Self::HIDDEN), "crossed_out" => Ok(Self::CROSSED_OUT), + "undercurled" => Ok(Self::UNDERCURLED), + "underdotted" => Ok(Self::UNDERDOTTED), + "underdashed" => Ok(Self::UNDERDASHED), + "double_underlined" => Ok(Self::DOUBLE_UNDERLINED), _ => Err("Invalid modifier"), } } @@ -426,6 +440,7 @@ impl FromStr for Modifier { pub struct Style { pub fg: Option, pub bg: Option, + pub underline: Option, pub add_modifier: Modifier, pub sub_modifier: Modifier, } @@ -435,6 +450,7 @@ impl Default for Style { Style { fg: None, bg: None, + underline: None, add_modifier: Modifier::empty(), sub_modifier: Modifier::empty(), } @@ -447,6 +463,7 @@ impl Style { Style { fg: Some(Color::Reset), bg: Some(Color::Reset), + underline: Some(Color::Reset), add_modifier: Modifier::empty(), sub_modifier: Modifier::all(), } @@ -482,6 +499,21 @@ impl Style { self } + /// Changes the underline color. + /// + /// ## Examples + /// + /// ```rust + /// # use helix_view::graphics::{Color, Style}; + /// let style = Style::default().underline(Color::Blue); + /// let diff = Style::default().underline(Color::Red); + /// assert_eq!(style.patch(diff), Style::default().underline(Color::Red)); + /// ``` + pub fn underline(mut self, color: Color) -> Style { + self.underline = Some(color); + self + } + /// Changes the text emphasis. /// /// When applied, it adds the given modifier to the `Style` modifiers. @@ -538,6 +570,7 @@ impl Style { pub fn patch(mut self, other: Style) -> Style { self.fg = other.fg.or(self.fg); self.bg = other.bg.or(self.bg); + self.underline = other.underline.or(self.underline); self.add_modifier.remove(other.sub_modifier); self.add_modifier.insert(other.add_modifier); diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 8472ec64265db..5e1401cc94b17 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -307,6 +307,7 @@ impl ThemePalette { match name.as_str() { "fg" => *style = style.fg(self.parse_color(value)?), "bg" => *style = style.bg(self.parse_color(value)?), + "underline" => *style = style.underline(self.parse_color(value)?), "modifiers" => { let modifiers = value .as_array() diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 29a552815bbc7..c853180978452 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -88,7 +88,8 @@ "info" = { fg = "light_blue" } "hint" = { fg = "light_gray3" } -diagnostic = { modifiers = ["underlined"] } +"diagnostic.error" = {underline = "red", modifiers = ["undercurled"] } +"diagnostic" = {underline = "gold", modifiers = ["undercurled"] } [palette] white = "#ffffff" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 1e7d9af1f19a6..a4cc12eb9ef2a 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -39,7 +39,7 @@ "diff.delta" = "gold" "diff.minus" = "red" -diagnostic = { modifiers = ["underlined"] } +diagnostic = { modifiers = ["undercurled"] } "info" = { fg = "blue", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] }