fix(backend): add feature flag for underline-color (#570)

Windows 7 doesn't support the underline color attribute, so we need to
make it optional. This commit adds a feature flag for the underline
color attribute - it is enabled by default, but can be disabled by
passing `--no-default-features` to cargo.

We could specically check for Windows 7 and disable the feature flag
automatically, but I think it's better for this check to be done by the
crossterm crate, since it's the one that actually knows about the
underlying terminal.

To disable the feature flag in an application that supports Windows 7,
add the following to your Cargo.toml:

```toml
ratatui = { version = "0.24.0", default-features = false, features = ["crossterm"] }
```

Fixes https://github.com/ratatui-org/ratatui/issues/555
This commit is contained in:
Josh McKinney 2023-10-18 13:52:43 -07:00 committed by GitHub
parent b61f65bc20
commit 8d507c43fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 39 deletions

View file

@ -23,14 +23,8 @@ rust-version = "1.67.0"
[badges] [badges]
[dependencies] [dependencies]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = { version = "0.27", optional = true } crossterm = { version = "0.27", optional = true }
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = { version = "2.0", optional = true } termion = { version = "2.0", optional = true }
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = { version = "0.20.0", optional = true } termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
@ -60,7 +54,20 @@ palette = "0.7.3"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
[features] [features]
default = ["crossterm"] #! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"]
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends: #! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [Serde crate]. ## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file. ## This is useful if you want to save themes to a file.
@ -77,6 +84,11 @@ all-widgets = ["widget-calendar"]
## enables the [`calendar`] widget module and adds a dependency on the [Time crate]. ## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"] widget-calendar = ["dep:time"]
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## enables the backend code that sets the underline color.
underline-color = ["dep:crossterm"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html # see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html

View file

@ -9,9 +9,9 @@ ALL_FEATURES = "all-widgets,macros,serde"
# Windows does not support building termion, so this avoids the build failure by providing two # Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms. # sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz # Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --all-features # Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--all-features", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } } ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
[tasks.default] [tasks.default]
alias = "ci" alias = "ci"

View file

@ -4,12 +4,14 @@
//! [Crossterm]: https://crates.io/crates/crossterm //! [Crossterm]: https://crates.io/crates/crossterm
use std::io::{self, Write}; use std::io::{self, Write};
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crossterm::{ use crossterm::{
cursor::{Hide, MoveTo, Show}, cursor::{Hide, MoveTo, Show},
execute, queue, execute, queue,
style::{ style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor, SetUnderlineColor, SetForegroundColor,
}, },
terminal::{self, Clear}, terminal::{self, Clear},
}; };
@ -124,6 +126,7 @@ where
{ {
let mut fg = Color::Reset; let mut fg = Color::Reset;
let mut bg = Color::Reset; let mut bg = Color::Reset;
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset; let mut underline_color = Color::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;
@ -151,6 +154,7 @@ where
queue!(self.writer, SetBackgroundColor(color))?; queue!(self.writer, SetBackgroundColor(color))?;
bg = cell.bg; bg = cell.bg;
} }
#[cfg(feature = "underline-color")]
if cell.underline_color != underline_color { if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color); let color = CColor::from(cell.underline_color);
queue!(self.writer, SetUnderlineColor(color))?; queue!(self.writer, SetUnderlineColor(color))?;
@ -160,13 +164,21 @@ where
queue!(self.writer, Print(&cell.symbol))?; queue!(self.writer, Print(&cell.symbol))?;
} }
queue!( #[cfg(feature = "underline-color")]
return queue!(
self.writer, self.writer,
SetForegroundColor(CColor::Reset), SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset), SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset) SetAttribute(CAttribute::Reset),
) );
#[cfg(not(feature = "underline-color"))]
return queue!(
self.writer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset),
);
} }
fn hide_cursor(&mut self) -> io::Result<()> { fn hide_cursor(&mut self) -> io::Result<()> {

View file

@ -19,7 +19,7 @@ pub struct Cell {
pub symbol: String, pub symbol: String,
pub fg: Color, pub fg: Color,
pub bg: Color, pub bg: Color,
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
pub underline_color: Color, pub underline_color: Color,
pub modifier: Modifier, pub modifier: Modifier,
pub skip: bool, pub skip: bool,
@ -55,7 +55,7 @@ impl Cell {
if let Some(c) = style.bg { if let Some(c) = style.bg {
self.bg = c; self.bg = c;
} }
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
if let Some(c) = style.underline_color { if let Some(c) = style.underline_color {
self.underline_color = c; self.underline_color = c;
} }
@ -64,7 +64,7 @@ impl Cell {
self self
} }
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
pub fn style(&self) -> Style { pub fn style(&self) -> Style {
Style::default() Style::default()
.fg(self.fg) .fg(self.fg)
@ -73,7 +73,7 @@ impl Cell {
.add_modifier(self.modifier) .add_modifier(self.modifier)
} }
#[cfg(not(feature = "crossterm"))] #[cfg(not(feature = "underline-color"))]
pub fn style(&self) -> Style { pub fn style(&self) -> Style {
Style::default() Style::default()
.fg(self.fg) .fg(self.fg)
@ -95,7 +95,7 @@ impl Cell {
self.symbol.push(' '); self.symbol.push(' ');
self.fg = Color::Reset; self.fg = Color::Reset;
self.bg = Color::Reset; self.bg = Color::Reset;
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
{ {
self.underline_color = Color::Reset; self.underline_color = Color::Reset;
} }
@ -110,7 +110,7 @@ impl Default for Cell {
symbol: " ".into(), symbol: " ".into(),
fg: Color::Reset, fg: Color::Reset,
bg: Color::Reset, bg: Color::Reset,
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
underline_color: Color::Reset, underline_color: Color::Reset,
modifier: Modifier::empty(), modifier: Modifier::empty(),
skip: false, skip: false,
@ -138,7 +138,7 @@ impl Default for Cell {
/// symbol: String::from("r"), /// symbol: String::from("r"),
/// fg: Color::Red, /// fg: Color::Red,
/// bg: Color::White, /// bg: Color::White,
/// #[cfg(feature = "crossterm")] /// #[cfg(feature = "underline-color")]
/// underline_color: Color::Reset, /// underline_color: Color::Reset,
/// modifier: Modifier::empty(), /// modifier: Modifier::empty(),
/// skip: false /// skip: false
@ -560,7 +560,7 @@ impl Debug for Buffer {
overwritten.push((x, &c.symbol)); overwritten.push((x, &c.symbol));
} }
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1); skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
{ {
let style = (c.fg, c.bg, c.underline_color, c.modifier); let style = (c.fg, c.bg, c.underline_color, c.modifier);
if last_style != Some(style) { if last_style != Some(style) {
@ -568,7 +568,7 @@ impl Debug for Buffer {
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier)); styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
} }
} }
#[cfg(not(feature = "crossterm"))] #[cfg(not(feature = "underline-color"))]
{ {
let style = (c.fg, c.bg, c.modifier); let style = (c.fg, c.bg, c.modifier);
if last_style != Some(style) { if last_style != Some(style) {
@ -586,12 +586,12 @@ impl Debug for Buffer {
} }
f.write_str(" ],\n styles: [\n")?; f.write_str(" ],\n styles: [\n")?;
for s in styles { for s in styles {
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
f.write_fmt(format_args!( f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n", " x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4, s.5 s.0, s.1, s.2, s.3, s.4, s.5
))?; ))?;
#[cfg(not(feature = "crossterm"))] #[cfg(not(feature = "underline-color"))]
f.write_fmt(format_args!( f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n", " x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4 s.0, s.1, s.2, s.3, s.4
@ -625,7 +625,7 @@ mod tests {
.bg(Color::Yellow) .bg(Color::Yellow)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
); );
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
assert_eq!( assert_eq!(
format!("{buf:?}"), format!("{buf:?}"),
indoc::indoc!( indoc::indoc!(
@ -643,7 +643,7 @@ mod tests {
}" }"
) )
); );
#[cfg(not(feature = "crossterm"))] #[cfg(not(feature = "underline-color"))]
assert_eq!( assert_eq!(
format!("{buf:?}"), format!("{buf:?}"),
indoc::indoc!( indoc::indoc!(

View file

@ -140,7 +140,7 @@ impl fmt::Debug for Modifier {
/// 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),
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED), /// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
/// #[cfg(feature = "crossterm")] /// #[cfg(feature = "underline-color")]
/// Style::default().underline_color(Color::Green), /// Style::default().underline_color(Color::Green),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC), /// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// ]; /// ];
@ -152,7 +152,7 @@ impl fmt::Debug for Modifier {
/// Style { /// Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Red), /// bg: Some(Color::Red),
/// #[cfg(feature = "crossterm")] /// #[cfg(feature = "underline-color")]
/// underline_color: Some(Color::Green), /// underline_color: Some(Color::Green),
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED, /// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
@ -179,7 +179,7 @@ impl fmt::Debug for Modifier {
/// Style { /// Style {
/// fg: Some(Color::Yellow), /// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset), /// bg: Some(Color::Reset),
/// #[cfg(feature = "crossterm")] /// #[cfg(feature = "underline-color")]
/// underline_color: Some(Color::Reset), /// underline_color: Some(Color::Reset),
/// add_modifier: Modifier::empty(), /// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(), /// sub_modifier: Modifier::empty(),
@ -192,7 +192,7 @@ impl fmt::Debug for Modifier {
pub struct Style { pub struct Style {
pub fg: Option<Color>, pub fg: Option<Color>,
pub bg: Option<Color>, pub bg: Option<Color>,
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
pub underline_color: Option<Color>, pub underline_color: Option<Color>,
pub add_modifier: Modifier, pub add_modifier: Modifier,
pub sub_modifier: Modifier, pub sub_modifier: Modifier,
@ -220,7 +220,7 @@ impl Style {
Style { Style {
fg: None, fg: None,
bg: None, bg: None,
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
underline_color: None, underline_color: None,
add_modifier: Modifier::empty(), add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(), sub_modifier: Modifier::empty(),
@ -232,7 +232,7 @@ impl Style {
Style { Style {
fg: Some(Color::Reset), fg: Some(Color::Reset),
bg: Some(Color::Reset), bg: Some(Color::Reset),
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset), underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(), add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(), sub_modifier: Modifier::all(),
@ -272,9 +272,12 @@ impl Style {
/// Changes the underline color. The text must be underlined with a modifier for this to work. /// Changes the underline color. The text must be underlined with a modifier for this to work.
/// ///
/// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators, /// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators,
/// but is only implemented in the crossterm backend. /// but is only implemented in the crossterm backend and enabled by the `underline-color`
/// feature flag.
/// ///
/// See [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) code `58` and `59` for more information. /// See
/// [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters)
/// code `58` and `59` for more information.
/// ///
/// ## Examples /// ## Examples
/// ///
@ -284,7 +287,7 @@ impl Style {
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED); /// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED)); /// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
/// ``` /// ```
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
pub const fn underline_color(mut self, color: Color) -> Style { pub const fn underline_color(mut self, color: Color) -> Style {
self.underline_color = Some(color); self.underline_color = Some(color);
self self
@ -347,7 +350,7 @@ impl 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);
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
{ {
self.underline_color = other.underline_color.or(self.underline_color); self.underline_color = other.underline_color.or(self.underline_color);
} }

View file

@ -366,7 +366,7 @@ impl<'a> Widget for LineGauge<'a> {
.set_style(Style { .set_style(Style {
fg: self.gauge_style.fg, fg: self.gauge_style.fg,
bg: None, bg: None,
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
underline_color: self.gauge_style.underline_color, underline_color: self.gauge_style.underline_color,
add_modifier: self.gauge_style.add_modifier, add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier, sub_modifier: self.gauge_style.sub_modifier,
@ -378,7 +378,7 @@ impl<'a> Widget for LineGauge<'a> {
.set_style(Style { .set_style(Style {
fg: self.gauge_style.bg, fg: self.gauge_style.bg,
bg: None, bg: None,
#[cfg(feature = "crossterm")] #[cfg(feature = "underline-color")]
underline_color: self.gauge_style.underline_color, underline_color: self.gauge_style.underline_color,
add_modifier: self.gauge_style.add_modifier, add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier, sub_modifier: self.gauge_style.sub_modifier,