Skip to main content

scope/display/theme/
mod.rs

1//! # Display theme
2//!
3//! The one source of every color and text style that scope shows in a
4//! terminal: CLI output, the TUI and the monitor. Callers name a semantic
5//! [`Role`]; [`tone`] is the single mapping from a role to its look.
6//!
7//! This module is presentation policy. It holds no framework type: the
8//! adapters translate a [`Tone`] to their medium (the text painter in
9//! `text` for CLI output; ratatui styles in the CLI crate).
10//!
11//! Only the 16 named ANSI colors are used, so the terminal's own palette
12//! decides the exact shade on dark and light themes.
13
14mod decision;
15mod text;
16
17pub use decision::{color_enabled, color_enabled_stderr, color_enabled_with, set_color_override};
18pub use text::{Painter, paint};
19
20/// The monitor's color schemes. The configuration type is the scheme, so
21/// one enum names the schemes everywhere.
22pub use crate::config::ColorScheme as Scheme;
23
24/// What a piece of text means. Callers pick a role; never a color.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Role {
27    /// Structure and titles: box rules, section and table headers.
28    Accent,
29    /// Key names in hints and help.
30    Key,
31    /// A passed check, a gain, a healthy state.
32    Ok,
33    /// A warning: outside its target, but not a failure.
34    Warn,
35    /// A failure, a loss, an unhealthy state.
36    Err,
37    /// The glyph of an informational note.
38    Info,
39    /// Labels, descriptions and notes that sit behind the main text.
40    Secondary,
41    /// Plain text in the terminal's default style.
42    Neutral,
43    /// The selected row of a list.
44    Selected,
45    /// A search match.
46    Match,
47    /// A URL.
48    Link,
49}
50
51impl Role {
52    /// Every role, for adapters and tests that walk the whole theme.
53    pub const ALL: [Role; 11] = [
54        Role::Accent,
55        Role::Key,
56        Role::Ok,
57        Role::Warn,
58        Role::Err,
59        Role::Info,
60        Role::Secondary,
61        Role::Neutral,
62        Role::Selected,
63        Role::Match,
64        Role::Link,
65    ];
66}
67
68/// The 16 named ANSI foreground colors.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum AnsiColor {
71    Black,
72    Red,
73    Green,
74    Yellow,
75    Blue,
76    Magenta,
77    Cyan,
78    White,
79    BrightBlack,
80    BrightRed,
81    BrightGreen,
82    BrightYellow,
83    BrightBlue,
84    BrightMagenta,
85    BrightCyan,
86    BrightWhite,
87}
88
89/// How a role looks: a foreground color (`None` keeps the terminal's
90/// default) and text effects.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct Tone {
93    /// The foreground color; `None` keeps the terminal default.
94    pub color: Option<AnsiColor>,
95    /// Bold (heavier) text.
96    pub bold: bool,
97    /// Dim (fainter) text.
98    pub dim: bool,
99    /// Underlined text.
100    pub underline: bool,
101    /// Foreground and background swapped.
102    pub reverse: bool,
103}
104
105impl Tone {
106    /// The terminal's default style: no color, no effect.
107    pub const PLAIN: Tone = Tone {
108        color: None,
109        bold: false,
110        dim: false,
111        underline: false,
112        reverse: false,
113    };
114
115    /// A tone with only a foreground color.
116    const fn colored(color: AnsiColor) -> Tone {
117        Tone {
118            color: Some(color),
119            ..Tone::PLAIN
120        }
121    }
122
123    /// The same tone in bold: the strong form of a role, for verdicts
124    /// (`HEALTHY`, `Critical`) and headings.
125    pub const fn strong(self) -> Tone {
126        Tone { bold: true, ..self }
127    }
128}
129
130/// The single mapping from a role to its tone. The role table in
131/// `docs/design/2026-10-13-tui-color-theme.md` is its specification.
132pub const fn tone(role: Role) -> Tone {
133    match role {
134        Role::Accent => Tone::colored(AnsiColor::Cyan),
135        Role::Key => Tone::colored(AnsiColor::Magenta).strong(),
136        Role::Ok => Tone::colored(AnsiColor::Green),
137        Role::Warn => Tone::colored(AnsiColor::Yellow),
138        Role::Err => Tone::colored(AnsiColor::Red),
139        Role::Info => Tone::colored(AnsiColor::Blue),
140        Role::Secondary => Tone {
141            dim: true,
142            ..Tone::PLAIN
143        },
144        Role::Neutral => Tone::PLAIN,
145        Role::Selected => Tone {
146            reverse: true,
147            ..Tone::colored(AnsiColor::Cyan)
148        },
149        Role::Match => Tone {
150            reverse: true,
151            ..Tone::colored(AnsiColor::Yellow)
152        },
153        Role::Link => Tone {
154            underline: true,
155            ..Tone::PLAIN
156        },
157    }
158}
159
160/// Which way a monitored value moved.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum Direction {
163    Up,
164    Down,
165    Flat,
166}
167
168/// The tone of a value that moved in `direction` under a monitor scheme.
169/// A flat value is always plain; the monochrome scheme is always plain.
170pub const fn scheme_tone(scheme: Scheme, direction: Direction) -> Tone {
171    match (scheme, direction) {
172        (_, Direction::Flat) | (Scheme::Monochrome, _) => Tone::PLAIN,
173        (Scheme::GreenRed, Direction::Up) => Tone::colored(AnsiColor::Green),
174        (Scheme::GreenRed, Direction::Down) => Tone::colored(AnsiColor::Red),
175        (Scheme::BlueOrange, Direction::Up) => Tone::colored(AnsiColor::Blue),
176        // Orange is not one of the 16 named colors; yellow is the nearest.
177        (Scheme::BlueOrange, Direction::Down) => Tone::colored(AnsiColor::Yellow),
178    }
179}
180
181/// The tone of `role` in a monitor under `scheme`. The monochrome scheme
182/// shows no color, so it keeps only the role's effects (bold, dim, ...).
183pub const fn scheme_role_tone(scheme: Scheme, role: Role) -> Tone {
184    let t = tone(role);
185    match scheme {
186        Scheme::Monochrome => Tone { color: None, ..t },
187        Scheme::GreenRed | Scheme::BlueOrange => t,
188    }
189}
190
191#[cfg(test)]
192mod tests;