terminal_colorsaurus/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Determines the background and foreground color of the terminal
4//! using the `OSC 10` and `OSC 11` terminal sequence.
5//!
6//! This crate helps answer the question *"Is this terminal dark or light?"*.
7//!
8//! ## Features
9//! * Background and foreground color detection.
10//! * Uses a fast and reliable heuristic to detect if the terminal supports color querying.
11//! * *Correct* perceived lightness calculation.
12//! * Works on Windows (starting with Windows Terminal v1.22).
13//! * Safely restores the terminal from raw mode even if the library errors or panicks.
14//! * Does not send any escape sequences if `TERM=dumb`.
15//! * Works even if all of stderr, stdout and stdin are redirected.
16//! * Supports a timeout (for situations with high latency such as an SSH connection).
17//!
18//! ## Terminal Support
19//! `terminal-colorsaurus` works with most modern terminals and has been [tested extensively](`terminal_survey`).
20//! It's also really good at [detecting](`feature_detection`) when querying for the terminal's colors is not supported.
21//!
22//! ## Example 1: Test If the Terminal Uses a Dark Background
23//! ```no_run
24//! use terminal_colorsaurus::{theme_mode, ThemeMode, QueryOptions};
25//!
26//! let theme_mode = theme_mode(QueryOptions::default()).unwrap();
27//! dbg!(theme_mode == ThemeMode::Dark);
28//! ```
29//!
30//! ## Example 2: Get the Terminal's Foreground Color
31//! ```no_run
32//! use terminal_colorsaurus::{foreground_color, QueryOptions};
33//!
34//! let fg = foreground_color(QueryOptions::default()).unwrap();
35//! println!("rgb({}, {}, {})", fg.r, fg.g, fg.b);
36//! ```
37//!
38//! ## Optional Dependencies
39//! * [`rgb`] — Enable this feature to convert between [`Color`] and [`rgb::RGB16`] / [`rgb::RGB8`].
40//! * [`anstyle`] — Enable this feature to convert [`Color`] to [`anstyle::RgbColor`].
41
42use cfg_if::cfg_if;
43
44mod color;
45mod error;
46mod fmt;
47
48cfg_if! {
49    if #[cfg(all(any(unix, windows), not(terminal_colorsaurus_test_unsupported)))] {
50        mod io;
51        mod quirks;
52        mod xterm;
53        use xterm as imp;
54    } else {
55        mod unsupported;
56        use unsupported as imp;
57    }
58}
59
60cfg_if! {
61    if #[cfg(docsrs)] {
62        #[doc(cfg(docsrs))]
63        #[doc = include_str!("../doc/terminal-survey.md")]
64        pub mod terminal_survey {}
65
66        #[doc(cfg(docsrs))]
67        #[doc = include_str!("../doc/latency-rustdoc.md")]
68        pub mod latency {}
69
70        #[doc(cfg(docsrs))]
71        #[doc = include_str!("../doc/feature-detection.md")]
72        pub mod feature_detection {}
73
74        #[doc(cfg(docsrs))]
75        #[doc = include_str!("../doc/comparison.md")]
76        pub mod comparison {}
77    }
78}
79
80#[cfg(doctest)]
81#[doc = include_str!("../readme.md")]
82pub mod readme_doctests {}
83
84pub use color::*;
85
86/// The subset of the terminal's color palette needed for
87/// deriving the [`ThemeMode`], namely: the foreground and background color.
88/// Retrieved by calling [`color_palette`].
89#[derive(Debug, Clone, PartialEq, Eq, Hash)]
90#[non_exhaustive]
91pub struct ColorPalette {
92    /// The foreground color of the terminal.
93    pub foreground: Color,
94    /// The background color of the terminal.
95    pub background: Color,
96}
97
98/// The terminal's theme mode (i.e. dark or light).
99///
100/// You can retrieve it using [`theme_mode`].
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[allow(clippy::exhaustive_enums)]
103#[doc(alias = "color scheme")]
104pub enum ThemeMode {
105    /// The terminal uses a dark background with light text.
106    Dark,
107    /// The terminal uses a light background with dark text.
108    Light,
109}
110
111impl ColorPalette {
112    /// Determines if the terminal uses a dark or light background.
113    pub fn theme_mode(&self) -> ThemeMode {
114        let fg = self.foreground.perceived_lightness();
115        let bg = self.background.perceived_lightness();
116        if bg < fg {
117            ThemeMode::Dark
118        } else if bg > fg || bg > 0.5 {
119            ThemeMode::Light
120        } else {
121            ThemeMode::Dark
122        }
123    }
124}
125
126/// Result used by this library.
127pub type Result<T> = std::result::Result<T, Error>;
128pub use error::Error;
129
130/// Options to be used with [`foreground_color`] and [`background_color`].
131/// You should almost always use the unchanged [`QueryOptions::default`] value.
132#[derive(Debug, Clone, PartialEq, Eq)]
133#[non_exhaustive]
134pub struct QueryOptions {
135    /// The maximum time spent waiting for a response from the terminal. Defaults to 1 s.
136    ///
137    /// Consider leaving this on a high value as there might be a lot of latency \
138    /// between you and the terminal (e.g. when you're connected via SSH).
139    ///
140    /// Terminals that don't support querying for colors will
141    /// almost always be detected as such before this timeout elapses.
142    ///
143    /// See [Feature Detection](`feature_detection`) for details on how this works.
144    pub timeout: std::time::Duration,
145}
146
147impl Default for QueryOptions {
148    fn default() -> Self {
149        Self {
150            timeout: std::time::Duration::from_secs(1),
151        }
152    }
153}
154
155/// Detects if the terminal is dark or light.
156#[doc = include_str!("../doc/caveats.md")]
157#[doc(alias = "theme")]
158pub fn theme_mode(options: QueryOptions) -> Result<ThemeMode> {
159    color_palette(options).map(|p| p.theme_mode())
160}
161
162/// Queries the terminal for it's color palette (foreground and background color).
163#[doc = include_str!("../doc/caveats.md")]
164pub fn color_palette(options: QueryOptions) -> Result<ColorPalette> {
165    imp::color_palette(options)
166}
167
168/// Queries the terminal for it's foreground color. \
169/// If you also need the foreground color it is more efficient to use [`color_palette`] instead.
170#[doc = include_str!("../doc/caveats.md")]
171#[doc(alias = "fg")]
172pub fn foreground_color(options: QueryOptions) -> Result<Color> {
173    imp::foreground_color(options)
174}
175
176/// Queries the terminal for it's background color. \
177/// If you also need the foreground color it is more efficient to use [`color_palette`] instead.
178#[doc = include_str!("../doc/caveats.md")]
179#[doc(alias = "bg")]
180pub fn background_color(options: QueryOptions) -> Result<Color> {
181    imp::background_color(options)
182}
183
184#[cfg(test)]
185#[path = "theme_mode_tests.rs"]
186mod tests;