on master help sign in
Detect extended underline support using terminfo

The cxterminfo crate has been used over popular alternatives like `term` since it supports querying for extended capabilities and also for it's small codebase size (which will make it easy to inline it into helix in the future if required).

commit79d3d44c3db48365597eefd274e868bc1e15de57 authorGokul Soumya <gokulps15@gmail.com> dateWed 7 Sep 2022, 17:28 (4 years ago) parentde72b9c
4 files changed+60−8
Cargo.lock+7 helix-tui/Cargo.toml+1 helix-tui/src/backend/crossterm.rs+48−7 runtime/themes/onedark.toml+4−1
Cargo.lock+7
@@ -176,6 +176,12 @@ dependencies = [
176176 "winapi",
177177]
178178
179+[[package]]
180+name = "cxterminfo"
181+version = "0.2.0"
182+source = "registry+https://github.com/rust-lang/crates.io-index"
183+checksum = "da92c5e3aaf2cc1fea346d9b3bac0c59c6ffc1d1d46f18d991d449912a3e6f07"
184+
179185[[package]]
180186name = "dirs-next"
181187version = "2.0.0"
@@ -504,6 +510,7 @@ dependencies = [
504510 "bitflags",
505511 "cassowary",
506512 "crossterm",
513+ "cxterminfo",
507514 "helix-core",
508515 "helix-view",
509516 "serde",
helix-tui/Cargo.toml+1
@@ -20,6 +20,7 @@ bitflags = "1.3"
2020cassowary = "0.3"
2121unicode-segmentation = "1.10"
2222crossterm = { version = "0.25", optional = true }
23+cxterminfo = "0.2"
2324serde = { version = "1", "optional" = true, features = ["derive"]}
2425helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
2526helix-core = { version = "0.6", path = "../helix-core" }
helix-tui/src/backend/crossterm.rs+48−7
@@ -11,8 +11,38 @@ use crossterm::{
1111use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
1212use std::io::{self, Write};
1313
14+fn vte_version() -> Option<usize> {
15+ std::env::var("VTE_VERSION").ok()?.parse().ok()
16+}
17+
18+/// Describes terminal capabilities like extended underline, truecolor, etc.
19+#[derive(Copy, Clone, Debug, Default)]
20+struct Capabilities {
21+ /// Support for undercurled, underdashed, etc.
22+ has_extended_underlines: bool,
23+}
24+
25+impl Capabilities {
26+ /// Detect capabilities from the terminfo database located based
27+ /// on the $TERM environment variable. If detection fails, returns
28+ /// a default value where no capability is supported.
29+ pub fn from_env_or_default() -> Self {
30+ match cxterminfo::terminfo::TermInfo::from_env() {
31+ Err(_) => Capabilities::default(),
32+ Ok(t) => Capabilities {
33+ // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
34+ // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
35+ has_extended_underlines: t.get_ext_string("Smulx").is_some()
36+ || *t.get_ext_bool("Su").unwrap_or(&false)
37+ || vte_version() >= Some(5102),
38+ },
39+ }
40+ }
41+}
42+
1443pub struct CrosstermBackend<W: Write> {
1544 buffer: W,
45+ capabilities: Capabilities,
1646}
1747
1848impl<W> CrosstermBackend<W>
@@ -20,7 +50,10 @@ where
2050 W: Write,
2151{
2252 pub fn new(buffer: W) -> CrosstermBackend<W> {
23− CrosstermBackend { buffer }
53+ CrosstermBackend {
54+ buffer,
55+ capabilities: Capabilities::from_env_or_default(),
56+ }
2457 }
2558}
2659
@@ -61,7 +94,7 @@ where
6194 from: modifier,
6295 to: cell.modifier,
6396 };
64− diff.queue(&mut self.buffer)?;
97+ diff.queue(&mut self.buffer, self.capabilities)?;
6598 modifier = cell.modifier;
6699 }
67100 if cell.fg != fg {
@@ -141,7 +174,7 @@ struct ModifierDiff {
141174}
142175
143176impl ModifierDiff {
144− fn queue<W>(&self, mut w: W) -> io::Result<()>
177+ fn queue<W>(&self, mut w: W, caps: Capabilities) -> io::Result<()>
145178 where
146179 W: io::Write,
147180 {
@@ -172,6 +205,14 @@ impl ModifierDiff {
172205 map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
173206 }
174207
208+ let queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> {
209+ let underline = match caps.has_extended_underlines {
210+ true => styled_underline,
211+ false => CAttribute::Underlined,
212+ };
213+ map_error(queue!(w, SetAttribute(underline)))
214+ };
215+
175216 let added = self.to - self.from;
176217 if added.contains(Modifier::REVERSED) {
177218 map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
@@ -186,16 +227,16 @@ impl ModifierDiff {
186227 map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
187228 }
188229 if added.contains(Modifier::UNDERCURLED) {
189− map_error(queue!(w, SetAttribute(CAttribute::Undercurled)))?;
230+ queue_styled_underline(CAttribute::Undercurled, &mut w)?;
190231 }
191232 if added.contains(Modifier::UNDERDOTTED) {
192− map_error(queue!(w, SetAttribute(CAttribute::Underdotted)))?;
233+ queue_styled_underline(CAttribute::Underdotted, &mut w)?;
193234 }
194235 if added.contains(Modifier::UNDERDASHED) {
195− map_error(queue!(w, SetAttribute(CAttribute::Underdashed)))?;
236+ queue_styled_underline(CAttribute::Underdashed, &mut w)?;
196237 }
197238 if added.contains(Modifier::DOUBLE_UNDERLINED) {
198− map_error(queue!(w, SetAttribute(CAttribute::DoubleUnderlined)))?;
239+ queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?;
199240 }
200241 if added.contains(Modifier::DIM) {
201242 map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
runtime/themes/onedark.toml+4−1
@@ -39,7 +39,10 @@
3939"diff.delta" = "gold"
4040"diff.minus" = "red"
4141
42−diagnostic = { modifiers = ["undercurled"] }
42+"diagnostic.info" = { underline = "blue", modifiers = ["undercurled"] }
43+"diagnostic.hint" = { underline = "green", modifiers = ["undercurled"] }
44+"diagnostic.warning" = { underline = "yellow", modifiers = ["undercurled"] }
45+"diagnostic.error" = { underline = "red", modifiers = ["undercurled"] }
4346"info" = { fg = "blue", modifiers = ["bold"] }
4447"hint" = { fg = "green", modifiers = ["bold"] }
4548"warning" = { fg = "yellow", modifiers = ["bold"] }