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;