Merge pull request #4061 from pascalkuthe/undercurl-modifier

Support different kinds of underline rendering (updated)
This commit is contained in:
Blaž Hrastnik 2022-10-19 13:51:13 +09:00 committed by GitHub
commit 418a622db9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 307 additions and 33 deletions

10
Cargo.lock generated
View File

@ -508,6 +508,7 @@ dependencies = [
"helix-core", "helix-core",
"helix-view", "helix-view",
"serde", "serde",
"termini",
"unicode-segmentation", "unicode-segmentation",
] ]
@ -1100,6 +1101,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "termini"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "394766021ef3dae8077f080518cdf5360831990f77f5708d5e3594c9b3efa2f9"
dependencies = [
"dirs-next",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.15.1" version = "0.15.1"

View File

@ -13,10 +13,10 @@ ## Creating a theme
Each line in the theme file is specified as below: Each line in the theme file is specified as below:
```toml ```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: To specify only the foreground color:
@ -77,17 +77,35 @@ ### Modifiers
Less common modifiers might not be supported by your terminal emulator. 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 | | Modifier |
| --- | | --- |
| `bold` | | `line` |
| `dim` | | `curl` |
| `italic` | | `dashed` |
| `underlined` | | `dot` |
| `slow_blink` | | `double_line` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
### Inheritance ### Inheritance

View File

@ -20,6 +20,7 @@ bitflags = "1.3"
cassowary = "0.3" cassowary = "0.3"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
crossterm = { version = "0.25", optional = true } crossterm = { version = "0.25", optional = true }
termini = "0.1"
serde = { version = "1", "optional" = true, features = ["derive"]} serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }

View File

@ -7,12 +7,45 @@
SetForegroundColor, SetForegroundColor,
}, },
terminal::{self, Clear, ClearType}, terminal::{self, Clear, ClearType},
Command,
}; };
use helix_view::graphics::{Color, CursorKind, Modifier, Rect}; use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
use std::io::{self, Write}; 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> { pub struct CrosstermBackend<W: Write> {
buffer: W, buffer: W,
capabilities: Capabilities,
} }
impl<W> CrosstermBackend<W> impl<W> CrosstermBackend<W>
@ -20,7 +53,10 @@ impl<W> CrosstermBackend<W>
W: Write, W: Write,
{ {
pub fn new(buffer: W) -> CrosstermBackend<W> { pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer } CrosstermBackend {
buffer,
capabilities: Capabilities::from_env_or_default(),
}
} }
} }
@ -47,6 +83,8 @@ fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
{ {
let mut fg = Color::Reset; let mut fg = Color::Reset;
let mut bg = 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 modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None; let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content { for (x, y, cell) in content {
@ -74,11 +112,32 @@ fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
bg = cell.bg; 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, Print(&cell.symbol)))?;
} }
map_error(queue!( map_error(queue!(
self.buffer, self.buffer,
SetUnderlineColor(CColor::Reset),
SetForegroundColor(CColor::Reset), SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset) SetAttribute(CAttribute::Reset)
@ -153,9 +212,6 @@ fn queue<W>(&self, mut w: W) -> io::Result<()>
if removed.contains(Modifier::ITALIC) { if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
} }
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
if removed.contains(Modifier::DIM) { if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
} }
@ -176,9 +232,6 @@ fn queue<W>(&self, mut w: W) -> io::Result<()>
if added.contains(Modifier::ITALIC) { if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::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) { if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
} }
@ -195,3 +248,58 @@ fn queue<W>(&self, mut w: W) -> io::Result<()>
Ok(()) 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.",
))
}
}

View File

@ -3,7 +3,7 @@
use std::cmp::min; use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use helix_view::graphics::{Color, Modifier, Rect, Style}; use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle};
/// A buffer cell /// A buffer cell
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -11,6 +11,8 @@ pub struct Cell {
pub symbol: String, pub symbol: String,
pub fg: Color, pub fg: Color,
pub bg: Color, pub bg: Color,
pub underline_color: Color,
pub underline_style: UnderlineStyle,
pub modifier: Modifier, pub modifier: Modifier,
} }
@ -44,6 +46,13 @@ pub fn set_style(&mut self, style: Style) -> &mut Cell {
if let Some(c) = style.bg { if let Some(c) = style.bg {
self.bg = c; 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.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier); self.modifier.remove(style.sub_modifier);
self self
@ -53,6 +62,8 @@ pub fn style(&self) -> Style {
Style::default() Style::default()
.fg(self.fg) .fg(self.fg)
.bg(self.bg) .bg(self.bg)
.underline_color(self.underline_color)
.underline_style(self.underline_style)
.add_modifier(self.modifier) .add_modifier(self.modifier)
} }
@ -61,6 +72,8 @@ pub fn reset(&mut self) {
self.symbol.push(' '); self.symbol.push(' ');
self.fg = Color::Reset; self.fg = Color::Reset;
self.bg = Color::Reset; self.bg = Color::Reset;
self.underline_color = Color::Reset;
self.underline_style = UnderlineStyle::Reset;
self.modifier = Modifier::empty(); self.modifier = Modifier::empty();
} }
} }
@ -71,6 +84,8 @@ fn default() -> Cell {
symbol: " ".into(), symbol: " ".into(),
fg: Color::Reset, fg: Color::Reset,
bg: Color::Reset, bg: Color::Reset,
underline_color: Color::Reset,
underline_style: UnderlineStyle::Reset,
modifier: Modifier::empty(), modifier: Modifier::empty(),
} }
} }
@ -87,7 +102,7 @@ fn default() -> Cell {
/// ///
/// ``` /// ```
/// use helix_tui::buffer::{Buffer, 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}); /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf[(0, 2)].set_symbol("x"); /// buf[(0, 2)].set_symbol("x");
@ -97,7 +112,9 @@ fn default() -> Cell {
/// symbol: String::from("r"), /// symbol: String::from("r"),
/// fg: Color::Red, /// fg: Color::Red,
/// bg: Color::White, /// bg: Color::White,
/// modifier: Modifier::empty() /// underline_color: Color::Reset,
/// underline_style: UnderlineStyle::Reset,
/// modifier: Modifier::empty(),
/// }); /// });
/// buf[(5, 0)].set_char('x'); /// buf[(5, 0)].set_char('x');
/// assert_eq!(buf[(5, 0)].symbol, "x"); /// assert_eq!(buf[(5, 0)].symbol, "x");

View File

@ -134,6 +134,8 @@ pub fn width(&self) -> usize {
/// style: Style { /// style: Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Black), /// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(), /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
/// }, /// },
@ -143,6 +145,8 @@ pub fn width(&self) -> usize {
/// style: Style { /// style: Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Black), /// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(), /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
/// }, /// },
@ -152,6 +156,8 @@ pub fn width(&self) -> usize {
/// style: Style { /// style: Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Black), /// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(), /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
/// }, /// },
@ -161,6 +167,8 @@ pub fn width(&self) -> usize {
/// style: Style { /// style: Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Black), /// bg: Some(Color::Black),
/// underline_color: None,
/// underline_style: None,
/// add_modifier: Modifier::empty(), /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
/// }, /// },

View File

@ -315,6 +315,44 @@ fn from(color: Color) -> Self {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UnderlineStyle {
Reset,
Line,
Curl,
Dotted,
Dashed,
DoubleLine,
}
impl FromStr for UnderlineStyle {
type Err = &'static str;
fn from_str(modifier: &str) -> Result<Self, Self::Err> {
match modifier {
"line" => Ok(Self::Line),
"curl" => Ok(Self::Curl),
"dotted" => Ok(Self::Dotted),
"dashed" => Ok(Self::Dashed),
"double_line" => Ok(Self::DoubleLine),
_ => Err("Invalid underline style"),
}
}
}
impl From<UnderlineStyle> for crossterm::style::Attribute {
fn from(style: UnderlineStyle) -> Self {
match style {
UnderlineStyle::Line => crossterm::style::Attribute::Underlined,
UnderlineStyle::Curl => crossterm::style::Attribute::Undercurled,
UnderlineStyle::Dotted => crossterm::style::Attribute::Underdotted,
UnderlineStyle::Dashed => crossterm::style::Attribute::Underdashed,
UnderlineStyle::DoubleLine => crossterm::style::Attribute::DoubleUnderlined,
UnderlineStyle::Reset => crossterm::style::Attribute::NoUnderline,
}
}
}
bitflags! { bitflags! {
/// Modifier changes the way a piece of text is displayed. /// Modifier changes the way a piece of text is displayed.
/// ///
@ -332,7 +370,6 @@ pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001; const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010; const DIM = 0b0000_0000_0010;
const ITALIC = 0b0000_0000_0100; const ITALIC = 0b0000_0000_0100;
const UNDERLINED = 0b0000_0000_1000;
const SLOW_BLINK = 0b0000_0001_0000; const SLOW_BLINK = 0b0000_0001_0000;
const RAPID_BLINK = 0b0000_0010_0000; const RAPID_BLINK = 0b0000_0010_0000;
const REVERSED = 0b0000_0100_0000; const REVERSED = 0b0000_0100_0000;
@ -349,7 +386,6 @@ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
"bold" => Ok(Self::BOLD), "bold" => Ok(Self::BOLD),
"dim" => Ok(Self::DIM), "dim" => Ok(Self::DIM),
"italic" => Ok(Self::ITALIC), "italic" => Ok(Self::ITALIC),
"underlined" => Ok(Self::UNDERLINED),
"slow_blink" => Ok(Self::SLOW_BLINK), "slow_blink" => Ok(Self::SLOW_BLINK),
"rapid_blink" => Ok(Self::RAPID_BLINK), "rapid_blink" => Ok(Self::RAPID_BLINK),
"reversed" => Ok(Self::REVERSED), "reversed" => Ok(Self::REVERSED),
@ -375,7 +411,7 @@ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
/// just S3. /// just S3.
/// ///
/// ```rust /// ```rust
/// # use helix_view::graphics::{Rect, Color, Modifier, Style}; /// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style};
/// # use helix_tui::buffer::Buffer; /// # use helix_tui::buffer::Buffer;
/// let styles = [ /// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), /// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
@ -391,6 +427,8 @@ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Red), /// bg: Some(Color::Red),
/// add_modifier: Modifier::BOLD, /// add_modifier: Modifier::BOLD,
/// underline_color: Some(Color::Reset),
/// underline_style: Some(UnderlineStyle::Reset),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
/// }, /// },
/// buffer[(0, 0)].style(), /// buffer[(0, 0)].style(),
@ -401,7 +439,7 @@ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
/// reset all properties until that point use [`Style::reset`]. /// reset all properties until that point use [`Style::reset`].
/// ///
/// ``` /// ```
/// # use helix_view::graphics::{Rect, Color, Modifier, Style}; /// # use helix_view::graphics::{Rect, Color, UnderlineStyle, Modifier, Style};
/// # use helix_tui::buffer::Buffer; /// # use helix_tui::buffer::Buffer;
/// let styles = [ /// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC), /// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
@ -415,6 +453,8 @@ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
/// Style { /// Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset), /// bg: Some(Color::Reset),
/// underline_color: Some(Color::Reset),
/// underline_style: Some(UnderlineStyle::Reset),
/// add_modifier: Modifier::empty(), /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
/// }, /// },
@ -426,6 +466,8 @@ fn from_str(modifier: &str) -> Result<Self, Self::Err> {
pub struct Style { pub struct Style {
pub fg: Option<Color>, pub fg: Option<Color>,
pub bg: Option<Color>, pub bg: Option<Color>,
pub underline_color: Option<Color>,
pub underline_style: Option<UnderlineStyle>,
pub add_modifier: Modifier, pub add_modifier: Modifier,
pub sub_modifier: Modifier, pub sub_modifier: Modifier,
} }
@ -435,6 +477,8 @@ fn default() -> Style {
Style { Style {
fg: None, fg: None,
bg: None, bg: None,
underline_color: None,
underline_style: None,
add_modifier: Modifier::empty(), add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(), sub_modifier: Modifier::empty(),
} }
@ -447,6 +491,8 @@ pub fn reset() -> Style {
Style { Style {
fg: Some(Color::Reset), fg: Some(Color::Reset),
bg: Some(Color::Reset), bg: Some(Color::Reset),
underline_color: None,
underline_style: None,
add_modifier: Modifier::empty(), add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(), sub_modifier: Modifier::all(),
} }
@ -482,6 +528,36 @@ pub fn bg(mut self, color: Color) -> Style {
self self
} }
/// Changes the underline color.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{Color, Style};
/// let style = Style::default().underline_color(Color::Blue);
/// let diff = Style::default().underline_color(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red));
/// ```
pub fn underline_color(mut self, color: Color) -> Style {
self.underline_color = Some(color);
self
}
/// Changes the underline style.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{UnderlineStyle, Style};
/// let style = Style::default().underline_style(UnderlineStyle::Line);
/// let diff = Style::default().underline_style(UnderlineStyle::Curl);
/// assert_eq!(style.patch(diff), Style::default().underline_style(UnderlineStyle::Curl));
/// ```
pub fn underline_style(mut self, style: UnderlineStyle) -> Style {
self.underline_style = Some(style);
self
}
/// Changes the text emphasis. /// Changes the text emphasis.
/// ///
/// When applied, it adds the given modifier to the `Style` modifiers. /// When applied, it adds the given modifier to the `Style` modifiers.
@ -538,6 +614,8 @@ pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
pub fn patch(mut self, other: Style) -> Style { pub fn patch(mut self, other: Style) -> Style {
self.fg = other.fg.or(self.fg); self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg); self.bg = other.bg.or(self.bg);
self.underline_color = other.underline_color.or(self.underline_color);
self.underline_style = other.underline_style.or(self.underline_style);
self.add_modifier.remove(other.sub_modifier); self.add_modifier.remove(other.sub_modifier);
self.add_modifier.insert(other.add_modifier); self.add_modifier.insert(other.add_modifier);

View File

@ -1,7 +1,7 @@
use std::fmt::Write; use std::fmt::Write;
use crate::{ use crate::{
graphics::{Color, Modifier, Style}, graphics::{Color, Style, UnderlineStyle},
Document, Editor, Theme, View, Document, Editor, Theme, View,
}; };
@ -147,7 +147,7 @@ pub fn breakpoints<'doc>(
.find(|breakpoint| breakpoint.line == line)?; .find(|breakpoint| breakpoint.line == line)?;
let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
error.add_modifier(Modifier::UNDERLINED) error.underline_style(UnderlineStyle::Line)
} else if breakpoint.condition.is_some() { } else if breakpoint.condition.is_some() {
error error
} else if breakpoint.log_message.is_some() { } else if breakpoint.log_message.is_some() {

View File

@ -11,6 +11,7 @@
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use toml::{map::Map, Value}; use toml::{map::Map, Value};
use crate::graphics::UnderlineStyle;
pub use crate::graphics::{Color, Modifier, Style}; pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| { pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
@ -378,19 +379,48 @@ pub fn parse_modifier(value: &Value) -> Result<Modifier, String> {
.ok_or(format!("Theme: invalid modifier: {}", value)) .ok_or(format!("Theme: invalid modifier: {}", value))
} }
pub fn parse_underline_style(value: &Value) -> Result<UnderlineStyle, String> {
value
.as_str()
.and_then(|s| s.parse().ok())
.ok_or(format!("Theme: invalid underline style: {}", value))
}
pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> { pub fn parse_style(&self, style: &mut Style, value: Value) -> Result<(), String> {
if let Value::Table(entries) = value { if let Value::Table(entries) = value {
for (name, value) in entries { for (name, mut value) in entries {
match name.as_str() { match name.as_str() {
"fg" => *style = style.fg(self.parse_color(value)?), "fg" => *style = style.fg(self.parse_color(value)?),
"bg" => *style = style.bg(self.parse_color(value)?), "bg" => *style = style.bg(self.parse_color(value)?),
"underline" => {
let table = value
.as_table_mut()
.ok_or("Theme: underline must be table")?;
if let Some(value) = table.remove("color") {
*style = style.underline_color(self.parse_color(value)?);
}
if let Some(value) = table.remove("style") {
*style = style.underline_style(Self::parse_underline_style(&value)?);
}
if let Some(attr) = table.keys().next() {
return Err(format!("Theme: invalid underline attribute: {attr}"));
}
}
"modifiers" => { "modifiers" => {
let modifiers = value let modifiers = value
.as_array() .as_array()
.ok_or("Theme: modifiers should be an array")?; .ok_or("Theme: modifiers should be an array")?;
for modifier in modifiers { for modifier in modifiers {
*style = style.add_modifier(Self::parse_modifier(modifier)?); if modifier
.as_str()
.map_or(false, |modifier| modifier == "underlined")
{
*style = style.underline_style(UnderlineStyle::Line);
} else {
*style = style.add_modifier(Self::parse_modifier(modifier)?);
}
} }
} }
_ => return Err(format!("Theme: invalid style attribute: {}", name)), _ => return Err(format!("Theme: invalid style attribute: {}", name)),

View File

@ -92,7 +92,8 @@
"info" = { fg = "light_blue" } "info" = { fg = "light_blue" }
"hint" = { fg = "light_gray3" } "hint" = { fg = "light_gray3" }
diagnostic = { modifiers = ["underlined"] } "diagnostic.error".underline = { color = "red", style = "curl" }
"diagnostic".underline = { color = "gold", style = "curl" }
[palette] [palette]
white = "#ffffff" white = "#ffffff"

View File

@ -39,7 +39,10 @@
"diff.delta" = "gold" "diff.delta" = "gold"
"diff.minus" = "red" "diff.minus" = "red"
diagnostic = { modifiers = ["underlined"] } "diagnostic.info".underline = { color = "blue", style = "curl" }
"diagnostic.hint".underline = { color = "green", style = "curl" }
"diagnostic.warning".underline = { color = "yellow", style = "curl" }
"diagnostic.error".underline = { color = "red", style = "curl" }
"info" = { fg = "blue", modifiers = ["bold"] } "info" = { fg = "blue", modifiers = ["bold"] }
"hint" = { fg = "green", modifiers = ["bold"] } "hint" = { fg = "green", modifiers = ["bold"] }
"warning" = { fg = "yellow", modifiers = ["bold"] } "warning" = { fg = "yellow", modifiers = ["bold"] }