Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support different kinds of underline rendering (updated) #4061

Merged
merged 14 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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` |


### Scopes

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