Skip to content

Commit

Permalink
Support different kinds of underline rendering
Browse files Browse the repository at this point in the history
Adds four new  modifiers that can be used in themes:

- undercurled
- underdashed
- underdotted
- double-underline
  • Loading branch information
sudormrfbin authored and the-mikedavis committed Sep 7, 2022
1 parent ed29f2c commit 92c83ae
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 30 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 17 additions & 13 deletions book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions helix-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
69 changes: 64 additions & 5 deletions helix-tui/src/backend/crossterm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,56 @@ 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<usize> {
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<W: Write> {
buffer: W,
capabilities: Capabilities,
}

impl<W> CrosstermBackend<W>
where
W: Write,
{
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
CrosstermBackend {
buffer,
capabilities: Capabilities::from_env_or_default(),
}
}
}

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)))?;
}
Expand Down Expand Up @@ -135,7 +174,7 @@ struct ModifierDiff {
}

impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
fn queue<W>(&self, mut w: W, caps: Capabilities) -> io::Result<()>
where
W: io::Write,
{
Expand All @@ -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) {
Expand All @@ -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)))?;
Expand All @@ -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)))?;
}
Expand Down
10 changes: 9 additions & 1 deletion helix-tui/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
pub underline: Color,
pub modifier: Modifier,
}

Expand Down Expand Up @@ -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
Expand All @@ -53,6 +57,7 @@ impl Cell {
Style::default()
.fg(self.fg)
.bg(self.bg)
.underline(self.underline)
.add_modifier(self.modifier)
}

Expand All @@ -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();
}
}
Expand All @@ -71,6 +77,7 @@ impl Default for Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
underline: Color::Reset,
modifier: Modifier::empty(),
}
}
Expand All @@ -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");
Expand Down
4 changes: 4 additions & 0 deletions helix-tui/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
/// },
Expand All @@ -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(),
/// },
Expand All @@ -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(),
/// },
Expand All @@ -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(),
/// },
Expand Down
51 changes: 42 additions & 9 deletions helix-view/src/graphics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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"),
}
}
Expand Down Expand Up @@ -426,6 +440,7 @@ impl FromStr for Modifier {
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub underline: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
Expand All @@ -435,6 +450,7 @@ impl Default for Style {
Style {
fg: None,
bg: None,
underline: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
Expand All @@ -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(),
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 92c83ae

Please sign in to comment.