Skip to content

Commit

Permalink
Merge pull request #4061 from pascalkuthe/undercurl-modifier
Browse files Browse the repository at this point in the history
Support different kinds of underline rendering (updated)
  • Loading branch information
archseer authored Oct 19, 2022
2 parents faf0c52 + 66a4908 commit 418a622
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 33 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

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

40 changes: 29 additions & 11 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 = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] }
```

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 `style`/`color`, 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,35 @@ 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` |

> Note: The `underlined` modifier is deprecated and only available for backwards compatibility.
> Its behavior is equivalent to setting `underline.style="line"`.
### Underline Style

One of the following values may be used as a value for `underline.style`.

Some styles might not be supported by your terminal emulator.

| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
| `line` |
| `curl` |
| `dashed` |
| `dot` |
| `double_line` |


### Inheritance

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.10"
crossterm = { version = "0.25", optional = true }
termini = "0.1"
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" }
126 changes: 117 additions & 9 deletions helix-tui/src/backend/crossterm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,56 @@ use crossterm::{
SetForegroundColor,
},
terminal::{self, Clear, ClearType},
Command,
};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
use std::io::{self, Write};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
use std::{
fmt,
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 termini::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.extended_cap("Smulx").is_some()
|| t.extended_cap("Su").is_some()
|| 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 +83,8 @@ where
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut underline_color = Color::Reset;
let mut underline_style = UnderlineStyle::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
Expand Down Expand Up @@ -74,11 +112,32 @@ where
bg = cell.bg;
}

let mut new_underline_style = cell.underline_style;
if self.capabilities.has_extended_underlines {
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
underline_color = cell.underline_color;
}
} else {
match new_underline_style {
UnderlineStyle::Reset | UnderlineStyle::Line => (),
_ => new_underline_style = UnderlineStyle::Line,
}
}

if new_underline_style != underline_style {
let attr = CAttribute::from(new_underline_style);
map_error(queue!(self.buffer, SetAttribute(attr)))?;
underline_style = new_underline_style;
}

map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}

map_error(queue!(
self.buffer,
SetUnderlineColor(CColor::Reset),
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
Expand Down Expand Up @@ -153,9 +212,6 @@ impl ModifierDiff {
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
Expand All @@ -176,9 +232,6 @@ impl ModifierDiff {
if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
}
Expand All @@ -195,3 +248,58 @@ impl ModifierDiff {
Ok(())
}
}

/// Crossterm uses semicolon as a seperator for colors
/// this is actually not spec compliant (altough commonly supported)
/// However the correct approach is to use colons as a seperator.
/// This usually doesn't make a difference for emulators that do support colored underlines.
/// However terminals that do not support colored underlines will ignore underlines colors with colons
/// while escape sequences with semicolons are always processed which leads to weird visual artifacts.
/// See [this nvim issue](https://github.com/neovim/neovim/issues/9270) for details
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SetUnderlineColor(pub CColor);

impl Command for SetUnderlineColor {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
let color = self.0;

if color == CColor::Reset {
write!(f, "\x1b[59m")?;
return Ok(());
}
f.write_str("\x1b[58:")?;

let res = match color {
CColor::Black => f.write_str("5:0"),
CColor::DarkGrey => f.write_str("5:8"),
CColor::Red => f.write_str("5:9"),
CColor::DarkRed => f.write_str("5:1"),
CColor::Green => f.write_str("5:10"),
CColor::DarkGreen => f.write_str("5:2"),
CColor::Yellow => f.write_str("5:11"),
CColor::DarkYellow => f.write_str("5:3"),
CColor::Blue => f.write_str("5:12"),
CColor::DarkBlue => f.write_str("5:4"),
CColor::Magenta => f.write_str("5:13"),
CColor::DarkMagenta => f.write_str("5:5"),
CColor::Cyan => f.write_str("5:14"),
CColor::DarkCyan => f.write_str("5:6"),
CColor::White => f.write_str("5:15"),
CColor::Grey => f.write_str("5:7"),
CColor::Rgb { r, g, b } => write!(f, "2::{}:{}:{}", r, g, b),
CColor::AnsiValue(val) => write!(f, "5:{}", val),
_ => Ok(()),
};
res?;
write!(f, "m")?;
Ok(())
}

#[cfg(windows)]
fn execute_winapi(&self) -> crossterm::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"SetUnderlineColor not supported by winapi.",
))
}
}
23 changes: 20 additions & 3 deletions helix-tui/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ use helix_core::unicode::width::UnicodeWidthStr;
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;

use helix_view::graphics::{Color, Modifier, Rect, Style};
use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle};

/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
pub bg: Color,
pub underline_color: Color,
pub underline_style: UnderlineStyle,
pub modifier: Modifier,
}

Expand Down Expand Up @@ -44,6 +46,13 @@ impl Cell {
if let Some(c) = style.bg {
self.bg = c;
}
if let Some(c) = style.underline_color {
self.underline_color = c;
}
if let Some(style) = style.underline_style {
self.underline_style = style;
}

self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
Expand All @@ -53,6 +62,8 @@ impl Cell {
Style::default()
.fg(self.fg)
.bg(self.bg)
.underline_color(self.underline_color)
.underline_style(self.underline_style)
.add_modifier(self.modifier)
}

Expand All @@ -61,6 +72,8 @@ impl Cell {
self.symbol.push(' ');
self.fg = Color::Reset;
self.bg = Color::Reset;
self.underline_color = Color::Reset;
self.underline_style = UnderlineStyle::Reset;
self.modifier = Modifier::empty();
}
}
Expand All @@ -71,6 +84,8 @@ impl Default for Cell {
symbol: " ".into(),
fg: Color::Reset,
bg: Color::Reset,
underline_color: Color::Reset,
underline_style: UnderlineStyle::Reset,
modifier: Modifier::empty(),
}
}
Expand All @@ -87,7 +102,7 @@ impl Default for Cell {
///
/// ```
/// use helix_tui::buffer::{Buffer, Cell};
/// use helix_view::graphics::{Rect, Color, Style, Modifier};
/// use helix_view::graphics::{Rect, Color, UnderlineStyle, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf[(0, 2)].set_symbol("x");
Expand All @@ -97,7 +112,9 @@ impl Default for Cell {
/// symbol: String::from("r"),
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// underline_color: Color::Reset,
/// underline_style: UnderlineStyle::Reset,
/// modifier: Modifier::empty(),
/// });
/// buf[(5, 0)].set_char('x');
/// assert_eq!(buf[(5, 0)].symbol, "x");
Expand Down
8 changes: 8 additions & 0 deletions helix-tui/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand All @@ -143,6 +145,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand All @@ -152,6 +156,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand All @@ -161,6 +167,8 @@ impl<'a> Span<'a> {
/// style: Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
Expand Down
Loading

0 comments on commit 418a622

Please sign in to comment.