diff --git a/crates/benchmark/src/main.rs b/crates/benchmark/src/main.rs index a986900..6ff67bc 100644 --- a/crates/benchmark/src/main.rs +++ b/crates/benchmark/src/main.rs @@ -42,7 +42,7 @@ fn main() -> Result<()> { let supported = match color_palette(QueryOptions::default()) { Ok(_) => true, - Err(Error::UnsupportedTerminal) => false, + Err(Error::UnsupportedTerminal(_)) => false, Err(e) => return Err(e), }; @@ -54,7 +54,7 @@ fn main() -> Result<()> { fn bench() -> Result { let start = Instant::now(); match black_box(color_palette(QueryOptions::default())) { - Ok(_) | Err(Error::UnsupportedTerminal) => Ok(start.elapsed()), + Ok(_) | Err(Error::UnsupportedTerminal(_)) => Ok(start.elapsed()), Err(err) => Err(err), } } diff --git a/crates/pycolorsaurus/src/lib.rs b/crates/pycolorsaurus/src/lib.rs index ae5641f..27028b8 100644 --- a/crates/pycolorsaurus/src/lib.rs +++ b/crates/pycolorsaurus/src/lib.rs @@ -176,9 +176,10 @@ impl Color { #[new] fn new(red: u8, green: u8, blue: u8) -> Self { Self(imp::Color { - r: scale_to_u16(red), - g: scale_to_u16(green), - b: scale_to_u16(blue), + red: scale_to_u16(red), + green: scale_to_u16(green), + blue: scale_to_u16(blue), + alpha: u16::MAX, }) } @@ -197,10 +198,7 @@ impl Color { self.0.scale_to_8bit().2 } - /// The perceived lightness of the color - /// as a value between 0 (black) and 100 (white) - /// where 50 is the perceptual "middle grey". - fn perceived_lightness(&self) -> u8 { + fn perceived_lightness(&self) -> f32 { self.0.perceived_lightness() } @@ -221,7 +219,7 @@ impl Color { #[pyo3(name = "__repr__")] fn repr(&self, python: Python<'_>) -> PyResult { - let (r, g, b) = self.0.scale_to_8bit(); + let (r, g, b, _) = self.0.scale_to_8bit(); let ty = type_name::(&python)?; Ok(format!("<{ty} #{r:02x}{g:02x}{b:02x}>")) } diff --git a/crates/terminal-colorsaurus/Cargo.toml b/crates/terminal-colorsaurus/Cargo.toml index 4e0371c..a2e97a3 100644 --- a/crates/terminal-colorsaurus/Cargo.toml +++ b/crates/terminal-colorsaurus/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["terminal", "light", "dark", "color-scheme", "cli"] license = "MIT OR Apache-2.0" version.workspace = true edition = "2021" -rust-version = "1.70.0" # Search for `FIXME(msrv)` when bumping. +rust-version = "1.74.0" # Search for `FIXME(msrv)` when bumping. exclude = [".github", ".gitignore", "*.sh", "benchmark/**/*", "doc/issues.md", "deny.toml"] [dependencies] diff --git a/crates/terminal-colorsaurus/examples/bg.rs b/crates/terminal-colorsaurus/examples/bg.rs index 778e83f..511c152 100644 --- a/crates/terminal-colorsaurus/examples/bg.rs +++ b/crates/terminal-colorsaurus/examples/bg.rs @@ -5,7 +5,7 @@ use terminal_colorsaurus::{background_color, Error, QueryOptions}; fn main() -> Result<(), display::DisplayAsDebug> { let bg = background_color(QueryOptions::default())?; let bg_8bit = bg.scale_to_8bit(); - println!("rgb16({}, {}, {})", bg.r, bg.g, bg.b); + println!("rgb16({}, {}, {})", bg.red, bg.green, bg.blue); println!("rgb8({}, {}, {})", bg_8bit.0, bg_8bit.1, bg_8bit.2); Ok(()) } diff --git a/crates/terminal-colorsaurus/examples/fg.rs b/crates/terminal-colorsaurus/examples/fg.rs index 0e92cab..57190c7 100644 --- a/crates/terminal-colorsaurus/examples/fg.rs +++ b/crates/terminal-colorsaurus/examples/fg.rs @@ -5,7 +5,7 @@ use terminal_colorsaurus::{foreground_color, Error, QueryOptions}; fn main() -> Result<(), display::DisplayAsDebug> { let fg = foreground_color(QueryOptions::default())?; let fg_8bit = fg.scale_to_8bit(); - println!("rgb16({}, {}, {})", fg.r, fg.g, fg.b); + println!("rgb16({}, {}, {})", fg.red, fg.green, fg.blue); println!("rgb8({}, {}, {})", fg_8bit.0, fg_8bit.1, fg_8bit.2); Ok(()) } diff --git a/crates/terminal-colorsaurus/src/color.rs b/crates/terminal-colorsaurus/src/color.rs index 8336387..d6fae63 100644 --- a/crates/terminal-colorsaurus/src/color.rs +++ b/crates/terminal-colorsaurus/src/color.rs @@ -1,50 +1,68 @@ /// An RGB color with 16 bits per channel. /// You can use [`Color::scale_to_8bit`] to convert to an 8bit RGB color. -#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)] #[allow(clippy::exhaustive_structs)] pub struct Color { /// Red - pub r: u16, + pub red: u16, /// Green - pub g: u16, + pub green: u16, /// Blue - pub b: u16, + pub blue: u16, + /// Can almost always be ignored as it is rarely set to + /// something other than the default (`0xffff`). + pub alpha: u16, } impl Color { - /// The perceived lightness of the color - /// as a value between `0` (black) and `100` (white) - /// where `50` is the perceptual "middle grey". + /// Perceptual lightness (L*) as a value between 0.0 (black) and 1.0 (white) + /// where 0.5 is the perceptual middle gray. + /// + /// Note that the color's alpha is ignored. /// ``` /// # use terminal_colorsaurus::Color; /// # let color = Color::default(); - /// let is_dark = color.perceived_lightness() <= 50; + /// let is_dark = color.perceived_lightness() <= 0.5; /// ``` - pub fn perceived_lightness(&self) -> u8 { - (self.perceived_lightness_f32() * 100.) as u8 + pub fn perceived_lightness(&self) -> f32 { + let color = xterm_color::Color { + red: self.red, + green: self.green, + blue: self.blue, + alpha: self.alpha, + }; + color.perceived_lightness() } /// Converts the color to 8 bit precision per channel by scaling each channel. /// /// ``` /// # use terminal_colorsaurus::Color; - /// let white = Color { r: u16::MAX, g: u16::MAX, b: u16::MAX }; - /// assert_eq!((u8::MAX, u8::MAX, u8::MAX), white.scale_to_8bit()); + /// let white = Color { red: u16::MAX, green: u16::MAX, blue: u16::MAX, alpha: u16::MAX }; + /// assert_eq!((u8::MAX, u8::MAX, u8::MAX, u8::MAX), white.scale_to_8bit()); /// - /// let black = Color { r: 0, g: 0, b: 0 }; - /// assert_eq!((0, 0, 0), black.scale_to_8bit()); + /// let black = Color { red: 0, green: 0, blue: 0, alpha: u16::MAX }; + /// assert_eq!((0, 0, 0, u8::MAX), black.scale_to_8bit()); /// ``` - pub fn scale_to_8bit(&self) -> (u8, u8, u8) { + pub fn scale_to_8bit(&self) -> (u8, u8, u8, u8) { ( - scale_to_u8(self.r), - scale_to_u8(self.g), - scale_to_u8(self.b), + scale_to_u8(self.red), + scale_to_u8(self.green), + scale_to_u8(self.blue), + scale_to_u8(self.alpha), ) } +} - pub(crate) fn perceived_lightness_f32(&self) -> f32 { - let color = xterm_color::Color::rgb(self.r, self.g, self.b); - color.perceived_lightness() +#[cfg(test)] +impl Color { + pub(crate) const fn rgb(red: u16, green: u16, blue: u16) -> Self { + Self { + red, + green, + blue, + alpha: u16::MAX, + } } } @@ -56,9 +74,21 @@ fn scale_to_u8(channel: u16) -> u8 { impl From for rgb::RGB16 { fn from(value: Color) -> Self { rgb::RGB16 { - r: value.r, - g: value.g, - b: value.b, + r: value.red, + g: value.green, + b: value.blue, + } + } +} + +#[cfg(feature = "rgb")] +impl From for rgb::RGBA16 { + fn from(value: Color) -> Self { + rgb::RGBA16 { + r: value.red, + g: value.green, + b: value.blue, + a: value.alpha, } } } @@ -66,18 +96,27 @@ impl From for rgb::RGB16 { #[cfg(feature = "rgb")] impl From for rgb::RGB8 { fn from(value: Color) -> Self { - let (r, g, b) = value.scale_to_8bit(); + let (r, g, b, _) = value.scale_to_8bit(); rgb::RGB8 { r, g, b } } } +#[cfg(feature = "rgb")] +impl From for rgb::RGBA8 { + fn from(value: Color) -> Self { + let (r, g, b, a) = value.scale_to_8bit(); + rgb::RGBA8 { r, g, b, a } + } +} + #[cfg(feature = "rgb")] impl From for Color { fn from(value: rgb::RGB16) -> Self { Color { - r: value.r, - g: value.g, - b: value.b, + red: value.r, + green: value.g, + blue: value.b, + alpha: u16::MAX, } } } @@ -85,7 +124,7 @@ impl From for Color { #[cfg(feature = "anstyle")] impl From for anstyle::RgbColor { fn from(value: Color) -> Self { - let (r, g, b) = value.scale_to_8bit(); + let (r, g, b, _) = value.scale_to_8bit(); anstyle::RgbColor(r, g, b) } } @@ -97,16 +136,17 @@ mod tests { #[test] fn black_has_perceived_lightness_zero() { let black = Color::default(); - assert_eq!(0, black.perceived_lightness()) + assert_eq!(0.0, black.perceived_lightness()) } #[test] fn white_has_perceived_lightness_100() { let white = Color { - r: u16::MAX, - g: u16::MAX, - b: u16::MAX, + red: u16::MAX, + green: u16::MAX, + blue: u16::MAX, + alpha: u16::MAX, }; - assert_eq!(100, white.perceived_lightness()) + assert_eq!(1.0, white.perceived_lightness()) } } diff --git a/crates/terminal-colorsaurus/src/color_scheme_tests.rs b/crates/terminal-colorsaurus/src/color_scheme_tests.rs index fbdec39..3ebf0ab 100644 --- a/crates/terminal-colorsaurus/src/color_scheme_tests.rs +++ b/crates/terminal-colorsaurus/src/color_scheme_tests.rs @@ -1,32 +1,12 @@ use super::*; use ColorScheme::*; -const BLACK: Color = Color { r: 0, g: 0, b: 0 }; -const WHITE: Color = Color { - r: u16::MAX, - g: u16::MAX, - b: u16::MAX, -}; -const DARK_GRAY: Color = Color { - r: 0x44ff, - g: 0x44ff, - b: 0x44ff, -}; -const DARKER_GRAY: Color = Color { - r: 0x22ff, - g: 0x22ff, - b: 0x22ff, -}; -const LIGHT_GRAY: Color = Color { - r: 0xccff, - g: 0xccff, - b: 0xccff, -}; -const LIGHTER_GRAY: Color = Color { - r: 0xeeff, - g: 0xeeff, - b: 0xeeff, -}; +const BLACK: Color = Color::rgb(0, 0, 0); +const WHITE: Color = Color::rgb(u16::MAX, u16::MAX, u16::MAX); +const DARK_GRAY: Color = Color::rgb(0x44ff, 0x44ff, 0x44ff); +const DARKER_GRAY: Color = Color::rgb(0x22ff, 0x22ff, 0x22ff); +const LIGHT_GRAY: Color = Color::rgb(0xccff, 0xccff, 0xccff); +const LIGHTER_GRAY: Color = Color::rgb(0xeeff, 0xeeff, 0xeeff); mod dark { use super::*; @@ -54,9 +34,9 @@ mod dark { #[test] fn fg_and_bg_both_dark() { for (foreground, background) in [(DARK_GRAY, DARKER_GRAY), (DARKER_GRAY, BLACK)] { - assert!(foreground.perceived_lightness_f32() < 0.5); - assert!(background.perceived_lightness_f32() < 0.5); - assert!(foreground.perceived_lightness_f32() != background.perceived_lightness_f32()); + assert!(foreground.perceived_lightness() < 0.5); + assert!(background.perceived_lightness() < 0.5); + assert!(foreground.perceived_lightness() != background.perceived_lightness()); let palette = ColorPalette { foreground, @@ -93,10 +73,10 @@ mod light { #[test] fn fg_and_bg_both_light() { for (foreground, background) in [(LIGHT_GRAY, LIGHTER_GRAY), (LIGHTER_GRAY, WHITE)] { - assert!(foreground.perceived_lightness_f32() > 0.5); - assert!(background.perceived_lightness_f32() > 0.5); + assert!(foreground.perceived_lightness() > 0.5); + assert!(background.perceived_lightness() > 0.5); assert!( - (foreground.perceived_lightness_f32() - background.perceived_lightness_f32()).abs() + (foreground.perceived_lightness() - background.perceived_lightness()).abs() >= f32::EPSILON ); diff --git a/crates/terminal-colorsaurus/src/error.rs b/crates/terminal-colorsaurus/src/error.rs index 5defeed..8f9c5c6 100644 --- a/crates/terminal-colorsaurus/src/error.rs +++ b/crates/terminal-colorsaurus/src/error.rs @@ -19,7 +19,7 @@ pub enum Error { /// [`QueryOptions::require_terminal_on_stdout`]: `crate::QueryOptions::require_terminal_on_stdout` NotATerminal(NotATerminalError), /// The terminal does not support querying for the foreground or background color. - UnsupportedTerminal, + UnsupportedTerminal(UnsupportedTerminalError), } impl error::Error for Error { @@ -27,6 +27,7 @@ impl error::Error for Error { match self { Error::Io(source) => Some(source), Error::NotATerminal(source) => Some(source), + Error::UnsupportedTerminal(source) => Some(source), _ => None, } } @@ -47,9 +48,7 @@ impl fmt::Display for Error { write!(f, "operation did not complete within {timeout:?}") } Error::NotATerminal(e) => fmt::Display::fmt(e, f), - Error::UnsupportedTerminal {} => { - write!(f, "the terminal does not support querying for its colors") - } + Error::UnsupportedTerminal(e) => fmt::Display::fmt(e, f), } } } @@ -60,6 +59,12 @@ impl From for Error { } } +impl Error { + pub(crate) fn unsupported() -> Self { + Error::UnsupportedTerminal(UnsupportedTerminalError) + } +} + #[derive(Debug)] #[non_exhaustive] pub struct NotATerminalError; @@ -68,6 +73,18 @@ impl error::Error for NotATerminalError {} impl fmt::Display for NotATerminalError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "stdout is not connected to a terminal") + f.write_str("stdout is not connected to a terminal") + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct UnsupportedTerminalError; + +impl error::Error for UnsupportedTerminalError {} + +impl fmt::Display for UnsupportedTerminalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("the terminal does not support querying for its colors") } } diff --git a/crates/terminal-colorsaurus/src/lib.rs b/crates/terminal-colorsaurus/src/lib.rs index e1c113d..aeac09b 100644 --- a/crates/terminal-colorsaurus/src/lib.rs +++ b/crates/terminal-colorsaurus/src/lib.rs @@ -32,7 +32,7 @@ //! use terminal_colorsaurus::{foreground_color, QueryOptions}; //! //! let fg = foreground_color(QueryOptions::default()).unwrap(); -//! println!("rgb({}, {}, {})", fg.r, fg.g, fg.b); +//! println!("rgb({}, {}, {})", fg.red, fg.green, fg.blue); //! ``` //! //! ## Optional Dependencies @@ -85,7 +85,7 @@ pub use color::*; /// The color palette i.e. foreground and background colors of the terminal. /// Retrieved by calling [`color_palette`]. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub struct ColorPalette { /// The foreground color of the terminal. @@ -98,12 +98,11 @@ pub struct ColorPalette { /// /// The easiest way to retrieve the color scheme /// is by calling [`color_scheme`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[allow(clippy::exhaustive_enums)] #[doc(alias = "Theme")] pub enum ColorScheme { /// The terminal uses a dark background with light text. - #[default] Dark, /// The terminal uses a light background with dark text. Light, @@ -112,8 +111,8 @@ pub enum ColorScheme { impl ColorPalette { /// Determines if the terminal uses a dark or light background. pub fn color_scheme(&self) -> ColorScheme { - let fg = self.foreground.perceived_lightness_f32(); - let bg = self.background.perceived_lightness_f32(); + let fg = self.foreground.perceived_lightness(); + let bg = self.background.perceived_lightness(); if bg < fg { ColorScheme::Dark } else if bg > fg || bg > 0.5 { diff --git a/crates/terminal-colorsaurus/src/unsupported.rs b/crates/terminal-colorsaurus/src/unsupported.rs index 9ea7bad..fae034c 100644 --- a/crates/terminal-colorsaurus/src/unsupported.rs +++ b/crates/terminal-colorsaurus/src/unsupported.rs @@ -1,13 +1,13 @@ use crate::{Color, ColorPalette, Error, QueryOptions, Result}; pub(crate) fn color_palette(_options: QueryOptions) -> Result { - Err(Error::UnsupportedTerminal) + Err(Error::unsupported()) } pub(crate) fn foreground_color(_options: QueryOptions) -> Result { - Err(Error::UnsupportedTerminal) + Err(Error::unsupported()) } pub(crate) fn background_color(_options: QueryOptions) -> Result { - Err(Error::UnsupportedTerminal) + Err(Error::unsupported()) } diff --git a/crates/terminal-colorsaurus/src/xterm.rs b/crates/terminal-colorsaurus/src/xterm.rs index 4886a22..8c17a0a 100644 --- a/crates/terminal-colorsaurus/src/xterm.rs +++ b/crates/terminal-colorsaurus/src/xterm.rs @@ -79,13 +79,13 @@ fn parse_response(response: Vec, prefix: &[u8]) -> Result { } fn xparsecolor(input: &[u8]) -> Option { - let xterm_color::Color { - red: r, - green: g, - blue: b, - .. - } = xterm_color::Color::parse(input).ok()?; - Some(Color { r, g, b }) + let color = xterm_color::Color::parse(input).ok()?; + Some(Color { + red: color.red, + green: color.green, + blue: color.blue, + alpha: color.alpha, + }) } type Reader<'a> = BufReader>>; @@ -105,7 +105,7 @@ fn query( ensure_is_terminal(options.require_terminal_on_stdout)?; if quirks.is_known_unsupported() { - return Err(Error::UnsupportedTerminal); + return Err(Error::unsupported()); } let mut tty = terminal()?; @@ -143,7 +143,7 @@ fn read_color_response(r: &mut Reader<'_>) -> Result> { // the terminal does not recocgnize the color query. if !r.buffer().starts_with(b"]") { _ = consume_da1_response(r, false); - return Err(Error::UnsupportedTerminal); + return Err(Error::unsupported()); } // Some terminals always respond with BEL (see terminal survey). diff --git a/crates/termtheme/Cargo.toml b/crates/termtheme/Cargo.toml index 70af04a..783ba1f 100644 --- a/crates/termtheme/Cargo.toml +++ b/crates/termtheme/Cargo.toml @@ -3,7 +3,7 @@ name = "termtheme" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.70.0" # Search for `FIXME(msrv)` when bumping. +rust-version = "1.74.0" publish = false # Not ready yet [dependencies] diff --git a/readme.md b/readme.md index 1039b23..d1696e1 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,17 @@ match color_scheme(QueryOptions::default()).unwrap() { ## [Docs](https://docs.rs/terminal-colorsaurus) +## MSRV Policy + +This crate's Minimum Supported Rust Version (MSRV) is based +on the MSRVs of downstream users such as `delta` and `bat`. +Changes to the MSRV will be accompanied by a minor version bump. + +The following formula determines the MSRV: +```text +min(msrv(bat), msrv(delta)) +``` + ## Inspiration This crate borrows ideas from many other projects. This list is by no means exhaustive.