Skip to content

1.0 #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft

1.0 #24

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/benchmark/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};

Expand All @@ -54,7 +54,7 @@ fn main() -> Result<()> {
fn bench() -> Result<Duration> {
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),
}
}
Expand Down
14 changes: 6 additions & 8 deletions crates/pycolorsaurus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand All @@ -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()
}

Expand All @@ -221,7 +219,7 @@ impl Color {

#[pyo3(name = "__repr__")]
fn repr(&self, python: Python<'_>) -> PyResult<String> {
let (r, g, b) = self.0.scale_to_8bit();
let (r, g, b, _) = self.0.scale_to_8bit();
let ty = type_name::<Self>(&python)?;
Ok(format!("<{ty} #{r:02x}{g:02x}{b:02x}>"))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/terminal-colorsaurus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion crates/terminal-colorsaurus/examples/bg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use terminal_colorsaurus::{background_color, Error, QueryOptions};
fn main() -> Result<(), display::DisplayAsDebug<Error>> {
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(())
}
Expand Down
2 changes: 1 addition & 1 deletion crates/terminal-colorsaurus/examples/fg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use terminal_colorsaurus::{foreground_color, Error, QueryOptions};
fn main() -> Result<(), display::DisplayAsDebug<Error>> {
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(())
}
Expand Down
108 changes: 74 additions & 34 deletions crates/terminal-colorsaurus/src/color.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}

Expand All @@ -56,36 +74,57 @@ fn scale_to_u8(channel: u16) -> u8 {
impl From<Color> 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<Color> for rgb::RGBA16 {
fn from(value: Color) -> Self {
rgb::RGBA16 {
r: value.red,
g: value.green,
b: value.blue,
a: value.alpha,
}
}
}

#[cfg(feature = "rgb")]
impl From<Color> 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<Color> 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<rgb::RGB16> 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,
}
}
}

#[cfg(feature = "anstyle")]
impl From<Color> 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)
}
}
Expand All @@ -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())
}
}
44 changes: 12 additions & 32 deletions crates/terminal-colorsaurus/src/color_scheme_tests.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
);

Expand Down
27 changes: 22 additions & 5 deletions crates/terminal-colorsaurus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ 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 {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::Io(source) => Some(source),
Error::NotATerminal(source) => Some(source),
Error::UnsupportedTerminal(source) => Some(source),
_ => None,
}
}
Expand All @@ -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),
}
}
}
Expand All @@ -60,6 +59,12 @@ impl From<io::Error> for Error {
}
}

impl Error {
pub(crate) fn unsupported() -> Self {
Error::UnsupportedTerminal(UnsupportedTerminalError)
}
}

#[derive(Debug)]
#[non_exhaustive]
pub struct NotATerminalError;
Expand All @@ -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")
}
}
Loading
Loading