-
Notifications
You must be signed in to change notification settings - Fork 0
feat: modernize color composition and crate internals #5
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
Merged
Merged
Changes from 6 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
bc19a88
feat: modernize color composition
seapagan 76bc32a
refactor: split crate internals into modules
seapagan afdf8c3
docs: improve api guidance
seapagan 8714bd9
docs: add todo tracker
seapagan a062ea6
chore: clean up clippy warnings
seapagan e4b395a
test: cover remaining runtime paths
seapagan 982fff8
docs: fix readme format example
seapagan 806c8eb
test: avoid implicit msrv bump in test override
seapagan 3313a60
test: snapshot terminal override state
seapagan 8561c33
docs: track shorthand hex follow-up
seapagan a15a7eb
docs: clarify no-color precedence
seapagan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # TODO | ||
|
|
||
| - Add a writer-aware rendering API so `ColorMode::Auto` can use the actual | ||
| output target instead of always consulting `stdout()`. | ||
| - Revisit whether to add `rust-version` to `Cargo.toml` once we want to commit | ||
| to an explicit MSRV policy. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /// Convert HSL color values to RGB. | ||
| /// | ||
| /// - `h`: Hue in degrees | ||
| /// - `s`: Saturation percentage | ||
| /// - `l`: Lightness percentage | ||
| pub(crate) fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) { | ||
| let h = h / 360.0; | ||
| let s = s / 100.0; | ||
| let l = l / 100.0; | ||
|
|
||
| let c = (1.0 - (2.0 * l - 1.0).abs()) * s; | ||
| let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs()); | ||
| let m = l - c / 2.0; | ||
|
|
||
| let (r, g, b) = match (h * 6.0) as i32 { | ||
| 0 => (c, x, 0.0), | ||
| 1 => (x, c, 0.0), | ||
| 2 => (0.0, c, x), | ||
| 3 => (0.0, x, c), | ||
| 4 => (x, 0.0, c), | ||
| _ => (c, 0.0, x), | ||
| }; | ||
|
|
||
| ( | ||
| ((r + m) * 255.0) as u8, | ||
| ((g + m) * 255.0) as u8, | ||
| ((b + m) * 255.0) as u8, | ||
| ) | ||
| } | ||
|
|
||
| pub(crate) fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> { | ||
| let hex = hex.trim_start_matches('#'); | ||
| if hex.len() != 6 { | ||
| return None; | ||
| } | ||
|
|
||
| let r = u8::from_str_radix(&hex[0..2], 16).ok()?; | ||
| let g = u8::from_str_radix(&hex[2..4], 16).ok()?; | ||
| let b = u8::from_str_radix(&hex[4..6], 16).ok()?; | ||
|
|
||
| Some((r, g, b)) | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| pub(crate) enum NamedColor { | ||
| Black, | ||
| Red, | ||
| Green, | ||
| Yellow, | ||
| Blue, | ||
| Magenta, | ||
| Cyan, | ||
| White, | ||
| BrightRed, | ||
| BrightGreen, | ||
| BrightYellow, | ||
| BrightBlue, | ||
| BrightMagenta, | ||
| BrightCyan, | ||
| BrightWhite, | ||
| } | ||
|
|
||
| impl NamedColor { | ||
| fn foreground_code(self) -> &'static str { | ||
| match self { | ||
| Self::Black => "30", | ||
| Self::Red => "31", | ||
| Self::Green => "32", | ||
| Self::Yellow => "33", | ||
| Self::Blue => "34", | ||
| Self::Magenta => "35", | ||
| Self::Cyan => "36", | ||
| Self::White => "37", | ||
| Self::BrightRed => "91", | ||
| Self::BrightGreen => "92", | ||
| Self::BrightYellow => "93", | ||
| Self::BrightBlue => "94", | ||
| Self::BrightMagenta => "95", | ||
| Self::BrightCyan => "96", | ||
| Self::BrightWhite => "97", | ||
| } | ||
| } | ||
|
|
||
| fn background_code(self) -> &'static str { | ||
| match self { | ||
| Self::Black => "40", | ||
| Self::Red => "41", | ||
| Self::Green => "42", | ||
| Self::Yellow => "43", | ||
| Self::Blue => "44", | ||
| Self::Magenta => "45", | ||
| Self::Cyan => "46", | ||
| Self::White => "47", | ||
| Self::BrightRed => "101", | ||
| Self::BrightGreen => "102", | ||
| Self::BrightYellow => "103", | ||
| Self::BrightBlue => "104", | ||
| Self::BrightMagenta => "105", | ||
| Self::BrightCyan => "106", | ||
| Self::BrightWhite => "107", | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||
| pub(crate) enum ColorSpec { | ||
| Named(NamedColor), | ||
| Rgb(u8, u8, u8), | ||
| } | ||
|
|
||
| impl ColorSpec { | ||
| pub(crate) fn foreground_code(&self) -> String { | ||
| match self { | ||
| Self::Named(color) => color.foreground_code().to_string(), | ||
| Self::Rgb(r, g, b) => format!("38;2;{};{};{}", r, g, b), | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn background_code(&self) -> String { | ||
| match self { | ||
| Self::Named(color) => color.background_code().to_string(), | ||
| Self::Rgb(r, g, b) => format!("48;2;{};{};{}", r, g, b), | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| use std::cell::RefCell; | ||
| use std::io::IsTerminal; | ||
|
|
||
| /// Runtime color policy for rendered output. | ||
| #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] | ||
| pub enum ColorMode { | ||
| /// Enable styling only when stdout is a terminal. | ||
| #[default] | ||
| Auto, | ||
| /// Always emit styling, even when stdout is not a terminal. | ||
| Always, | ||
| /// Never emit styling. | ||
| Never, | ||
| } | ||
|
|
||
| /// Configuration for controlling runtime color behavior. | ||
| /// | ||
| /// The active configuration is stored per thread. This makes it straightforward | ||
| /// to force a specific color mode in tests or narrow execution paths without | ||
| /// changing global process state. | ||
| #[derive(Clone, Debug)] | ||
| pub struct ColorizeConfig { | ||
| color_mode: ColorMode, | ||
| } | ||
|
|
||
| thread_local! { | ||
| static CONFIG: RefCell<ColorizeConfig> = RefCell::new(ColorizeConfig::default()); | ||
| #[cfg(test)] | ||
| static TERMINAL_OVERRIDE: RefCell<Option<bool>> = const { RefCell::new(None) }; | ||
| } | ||
|
|
||
| impl Default for ColorizeConfig { | ||
| fn default() -> Self { | ||
| Self { | ||
| color_mode: ColorMode::Auto, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl ColorizeConfig { | ||
| /// Set the runtime color policy for the current thread. | ||
| /// | ||
| /// In [`ColorMode::Auto`], styling is emitted only when stdout is a | ||
| /// terminal. In [`ColorMode::Always`], styling is emitted regardless of | ||
| /// terminal detection. In [`ColorMode::Never`], styling is disabled. | ||
| pub fn set_color_mode(mode: ColorMode) { | ||
| CONFIG.with(|config| config.borrow_mut().color_mode = mode); | ||
| } | ||
|
|
||
| /// Get the runtime color policy for the current thread. | ||
| pub fn color_mode() -> ColorMode { | ||
| CONFIG.with(|config| config.borrow().color_mode) | ||
| } | ||
|
|
||
| /// Compatibility shim for the previous API. | ||
| /// | ||
| /// `true` maps to [`ColorMode::Auto`], and `false` maps to | ||
| /// [`ColorMode::Always`]. | ||
| #[deprecated(note = "use ColorizeConfig::set_color_mode(ColorMode) instead")] | ||
| pub fn set_terminal_check(check: bool) { | ||
| let mode = if check { | ||
| ColorMode::Auto | ||
| } else { | ||
| ColorMode::Always | ||
| }; | ||
| Self::set_color_mode(mode); | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn should_colorize() -> bool { | ||
| match ColorizeConfig::color_mode() { | ||
| ColorMode::Never => false, | ||
| ColorMode::Always => std::env::var_os("NO_COLOR").is_none(), | ||
| ColorMode::Auto => std::env::var_os("NO_COLOR").is_none() && stdout_is_terminal(), | ||
| } | ||
| } | ||
|
|
||
| fn stdout_is_terminal() -> bool { | ||
| #[cfg(test)] | ||
| if let Some(value) = TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow()) { | ||
| return value; | ||
| } | ||
|
|
||
| std::io::stdout().is_terminal() | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| pub(crate) fn set_terminal_override_for_tests(value: Option<bool>) { | ||
| TERMINAL_OVERRIDE.with(|override_value| *override_value.borrow_mut() = value); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.