Skip to main content

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