on master help sign in
Detect extended underline support using terminfo
79d3d44Gokul Soumya
Cargo.lock+7
@@ -176,6 +176,12 @@ dependencies = [
176 "winapi",
176 "winapi",
177]
177]
178
178
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+
179[[package]]
185[[package]]
180name = "dirs-next"
186name = "dirs-next"
181version = "2.0.0"
187version = "2.0.0"
@@ -504,6 +510,7 @@ dependencies = [
504 "bitflags",
510 "bitflags",
505 "cassowary",
511 "cassowary",
506 "crossterm",
512 "crossterm",
513+ "cxterminfo",
507 "helix-core",
514 "helix-core",
508 "helix-view",
515 "helix-view",
509 "serde",
516 "serde",
helix-tui/Cargo.toml+1
@@ -20,6 +20,7 @@ bitflags = "1.3"
20cassowary = "0.3"
20cassowary = "0.3"
21unicode-segmentation = "1.10"
21unicode-segmentation = "1.10"
22crossterm = { version = "0.25", optional = true }
22crossterm = { version = "0.25", optional = true }
23+cxterminfo = "0.2"
23serde = { version = "1", "optional" = true, features = ["derive"]}
24serde = { version = "1", "optional" = true, features = ["derive"]}
24helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
25helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
25helix-core = { version = "0.6", path = "../helix-core" }
26helix-core = { version = "0.6", path = "../helix-core" }
helix-tui/src/backend/crossterm.rs+48−7
@@ -11,8 +11,38 @@ use crossterm::{
11use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
11use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
12use std::io::{self, Write};
12use std::io::{self, Write};
13
13
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+
14pub struct CrosstermBackend<W: Write> {
43pub struct CrosstermBackend<W: Write> {
15 buffer: W,
44 buffer: W,
45+ capabilities: Capabilities,
16}
46}
17
47
18impl<W> CrosstermBackend<W>
48impl<W> CrosstermBackend<W>
@@ -20,7 +50,10 @@ where
20 W: Write,
50 W: Write,
21{
51{
22 pub fn new(buffer: W) -> CrosstermBackend<W> {
52 pub fn new(buffer: W) -> CrosstermBackend<W> {
23− CrosstermBackend { buffer }
53+ CrosstermBackend {
54+ buffer,
55+ capabilities: Capabilities::from_env_or_default(),
56+ }
24 }
57 }
25}
58}
26
59
@@ -61,7 +94,7 @@ where
61 from: modifier,
94 from: modifier,
62 to: cell.modifier,
95 to: cell.modifier,
63 };
96 };
64− diff.queue(&mut self.buffer)?;
97+ diff.queue(&mut self.buffer, self.capabilities)?;
65 modifier = cell.modifier;
98 modifier = cell.modifier;
66 }
99 }
67 if cell.fg != fg {
100 if cell.fg != fg {
@@ -141,7 +174,7 @@ struct ModifierDiff {
141}
174}
142
175
143impl ModifierDiff {
176impl ModifierDiff {
144− fn queue<W>(&self, mut w: W) -> io::Result<()>
177+ fn queue<W>(&self, mut w: W, caps: Capabilities) -> io::Result<()>
145 where
178 where
146 W: io::Write,
179 W: io::Write,
147 {
180 {
@@ -172,6 +205,14 @@ impl ModifierDiff {
172 map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
205 map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
173 }
206 }
174
207
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+
175 let added = self.to - self.from;
216 let added = self.to - self.from;
176 if added.contains(Modifier::REVERSED) {
217 if added.contains(Modifier::REVERSED) {
177 map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
218 map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
@@ -186,16 +227,16 @@ impl ModifierDiff {
186 map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
227 map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
187 }
228 }
188 if added.contains(Modifier::UNDERCURLED) {
229 if added.contains(Modifier::UNDERCURLED) {
189− map_error(queue!(w, SetAttribute(CAttribute::Undercurled)))?;
230+ queue_styled_underline(CAttribute::Undercurled, &mut w)?;
190 }
231 }
191 if added.contains(Modifier::UNDERDOTTED) {
232 if added.contains(Modifier::UNDERDOTTED) {
192− map_error(queue!(w, SetAttribute(CAttribute::Underdotted)))?;
233+ queue_styled_underline(CAttribute::Underdotted, &mut w)?;
193 }
234 }
194 if added.contains(Modifier::UNDERDASHED) {
235 if added.contains(Modifier::UNDERDASHED) {
195− map_error(queue!(w, SetAttribute(CAttribute::Underdashed)))?;
236+ queue_styled_underline(CAttribute::Underdashed, &mut w)?;
196 }
237 }
197 if added.contains(Modifier::DOUBLE_UNDERLINED) {
238 if added.contains(Modifier::DOUBLE_UNDERLINED) {
198− map_error(queue!(w, SetAttribute(CAttribute::DoubleUnderlined)))?;
239+ queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?;
199 }
240 }
200 if added.contains(Modifier::DIM) {
241 if added.contains(Modifier::DIM) {
201 map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
242 map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
runtime/themes/onedark.toml+4−1
@@ -39,7 +39,10 @@
39"diff.delta" = "gold"
39"diff.delta" = "gold"
40"diff.minus" = "red"
40"diff.minus" = "red"
41
41
42+"diagnostic.info" = { underline = "blue", modifiers = ["undercurled"] }
43+"diagnostic.hint" = { underline = "green", modifiers = ["undercurled"] }
44+"diagnostic.warning" = { underline = "yellow", modifiers = ["undercurled"] }
42−diagnostic = { modifiers = ["undercurled"] }
45+"diagnostic.error" = { underline = "red", modifiers = ["undercurled"] }
43"info" = { fg = "blue", modifiers = ["bold"] }
46"info" = { fg = "blue", modifiers = ["bold"] }
44"hint" = { fg = "green", modifiers = ["bold"] }
47"hint" = { fg = "green", modifiers = ["bold"] }
45"warning" = { fg = "yellow", modifiers = ["bold"] }
48"warning" = { fg = "yellow", modifiers = ["bold"] }