Skip to main content

inspect_format/
theme.rs

1//! Themes mapping semantic roles to terminal styles.
2
3use crate::{
4    color_choice::ColorLevel,
5    role::StyleRole,
6    style::{AnsiColor, Style},
7};
8
9/// A collection of styles for each semantic role.
10///
11/// Themes map each [`StyleRole`] to a concrete [`Style`].
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Theme {
14    styles: [Style; StyleRole::COUNT],
15}
16
17impl Default for Theme {
18    fn default() -> Self {
19        Self::dark()
20    }
21}
22
23impl Theme {
24    /// Canonical list of all built-in theme names.
25    pub const NAMES: &'static [&'static str] = &[
26        "dark",
27        "light",
28        "monochrome",
29        "ansi16",
30        "gruvbox",
31        "nord",
32        "dracula",
33        "solarized-dark",
34        "solarized-light",
35        "one-dark",
36        "catppuccin",
37        "tokyo-night",
38        "monokai",
39        "ayu-dark",
40    ];
41
42    /// Look up a built-in theme by name (case-insensitive, kebab-case or snake_case).
43    pub fn by_name(name: &str) -> Option<Self> {
44        let normalized = name.trim().to_ascii_lowercase().replace('_', "-");
45        match normalized.as_str() {
46            "default" | "dark" => Some(Self::dark()),
47            "light" => Some(Self::light()),
48            "monochrome" | "mono" | "plain" => Some(Self::monochrome()),
49            "ansi16" | "ansi" => Some(Self::ansi16()),
50            "gruvbox" => Some(Self::gruvbox()),
51            "nord" => Some(Self::nord()),
52            "dracula" => Some(Self::dracula()),
53            "solarized-dark" => Some(Self::solarized_dark()),
54            "solarized-light" => Some(Self::solarized_light()),
55            "one-dark" | "onedark" => Some(Self::one_dark()),
56            "catppuccin" | "catppuccin-mocha" => Some(Self::catppuccin_mocha()),
57            "tokyo-night" | "tokyonight" => Some(Self::tokyo_night()),
58            "monokai" => Some(Self::monokai()),
59            "ayu" | "ayu-dark" => Some(Self::ayu_dark()),
60            _ => None,
61        }
62    }
63
64    /// Create a theme with empty (plain) styles for every role.
65    pub const fn empty() -> Self {
66        Self { styles: [Style::new(); StyleRole::COUNT] }
67    }
68
69    /// The default dark theme.
70    ///
71    /// Calibrated for modern dark terminal backgrounds with restrained,
72    /// warm developer syntax highlighting.
73    pub fn dark() -> Self {
74        Self::empty()
75            .with(StyleRole::Type, Style::new().bold().fg_rgb(224, 138, 90))
76            .with(StyleRole::Field, Style::new().fg_rgb(110, 180, 235))
77            .with(StyleRole::Variant, Style::new().bold().fg_rgb(180, 145, 225))
78            .with(StyleRole::Key, Style::new().fg_rgb(105, 205, 195))
79            .with(StyleRole::String, Style::new().fg_rgb(142, 198, 128))
80            .with(StyleRole::Number, Style::new().fg_rgb(235, 190, 95))
81            .with(StyleRole::Boolean, Style::new().fg_rgb(195, 135, 185))
82            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(140, 145, 155))
83            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(115, 120, 130))
84            .with(StyleRole::Index, Style::new().fg_rgb(115, 185, 195))
85            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(130, 135, 145))
86            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(235, 95, 95))
87            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(225, 180, 75))
88            .with(StyleRole::Error, Style::new().bold().fg_rgb(240, 85, 85))
89            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(120, 125, 135))
90    }
91
92    /// A light theme optimized for light / white background terminals.
93    pub fn light() -> Self {
94        Self::empty()
95            .with(StyleRole::Type, Style::new().bold().fg_rgb(175, 75, 25))
96            .with(StyleRole::Field, Style::new().fg_rgb(30, 95, 160))
97            .with(StyleRole::Variant, Style::new().bold().fg_rgb(115, 45, 150))
98            .with(StyleRole::Key, Style::new().fg_rgb(20, 115, 120))
99            .with(StyleRole::String, Style::new().fg_rgb(35, 125, 55))
100            .with(StyleRole::Number, Style::new().fg_rgb(165, 105, 15))
101            .with(StyleRole::Boolean, Style::new().fg_rgb(140, 35, 105))
102            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(110, 115, 125))
103            .with(StyleRole::Punctuation, Style::new().fg_rgb(120, 125, 135))
104            .with(StyleRole::Index, Style::new().fg_rgb(40, 105, 135))
105            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(105, 110, 120))
106            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(185, 30, 30))
107            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(155, 95, 20))
108            .with(StyleRole::Error, Style::new().bold().fg_rgb(195, 20, 20))
109            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(110, 115, 125))
110    }
111
112    /// A monochrome theme using clear terminal effects (bold, dim, italic, underline) without colors.
113    pub fn monochrome() -> Self {
114        Self::empty()
115            .with(StyleRole::Type, Style::new().bold())
116            .with(StyleRole::Field, Style::new())
117            .with(StyleRole::Variant, Style::new().bold().underline())
118            .with(StyleRole::Key, Style::new().underline())
119            .with(StyleRole::String, Style::new())
120            .with(StyleRole::Number, Style::new().bold())
121            .with(StyleRole::Boolean, Style::new().bold())
122            .with(StyleRole::Null, Style::new().dimmed().italic())
123            .with(StyleRole::Punctuation, Style::new().dimmed())
124            .with(StyleRole::Index, Style::new().dimmed())
125            .with(StyleRole::Metadata, Style::new().dimmed())
126            .with(StyleRole::Sensitive, Style::new().bold().underline())
127            .with(StyleRole::Truncated, Style::new().dimmed().italic())
128            .with(StyleRole::Error, Style::new().bold().underline())
129            .with(StyleRole::Muted, Style::new().dimmed())
130    }
131
132    /// A theme using only standard 16 ANSI colors for maximum compatibility.
133    pub fn ansi16() -> Self {
134        Self::empty()
135            .with(StyleRole::Type, Style::new().bold().fg_ansi(AnsiColor::Yellow))
136            .with(StyleRole::Field, Style::new().fg_ansi(AnsiColor::Cyan))
137            .with(StyleRole::Variant, Style::new().bold().fg_ansi(AnsiColor::Magenta))
138            .with(StyleRole::Key, Style::new().fg_ansi(AnsiColor::Cyan))
139            .with(StyleRole::String, Style::new().fg_ansi(AnsiColor::Green))
140            .with(StyleRole::Number, Style::new().fg_ansi(AnsiColor::Yellow))
141            .with(StyleRole::Boolean, Style::new().fg_ansi(AnsiColor::Magenta))
142            .with(StyleRole::Null, Style::new().fg_ansi(AnsiColor::BrightBlack))
143            .with(StyleRole::Punctuation, Style::new().fg_ansi(AnsiColor::BrightBlack))
144            .with(StyleRole::Index, Style::new().fg_ansi(AnsiColor::Cyan))
145            .with(StyleRole::Metadata, Style::new().fg_ansi(AnsiColor::BrightBlack))
146            .with(StyleRole::Sensitive, Style::new().bold().fg_ansi(AnsiColor::BrightRed))
147            .with(StyleRole::Truncated, Style::new().fg_ansi(AnsiColor::Yellow))
148            .with(StyleRole::Error, Style::new().bold().fg_ansi(AnsiColor::Red))
149            .with(StyleRole::Muted, Style::new().fg_ansi(AnsiColor::BrightBlack))
150    }
151
152    /// The Gruvbox theme (warm, earthy palette).
153    pub fn gruvbox() -> Self {
154        Self::empty()
155            .with(StyleRole::Type, Style::new().bold().fg_rgb(254, 128, 25))
156            .with(StyleRole::Field, Style::new().fg_rgb(131, 165, 152))
157            .with(StyleRole::Variant, Style::new().bold().fg_rgb(211, 134, 155))
158            .with(StyleRole::Key, Style::new().fg_rgb(142, 192, 124))
159            .with(StyleRole::String, Style::new().fg_rgb(184, 187, 38))
160            .with(StyleRole::Number, Style::new().fg_rgb(250, 189, 47))
161            .with(StyleRole::Boolean, Style::new().fg_rgb(211, 134, 155))
162            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(146, 131, 116))
163            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(146, 131, 116))
164            .with(StyleRole::Index, Style::new().fg_rgb(142, 192, 124))
165            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(146, 131, 116))
166            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(251, 73, 52))
167            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(250, 189, 47))
168            .with(StyleRole::Error, Style::new().bold().fg_rgb(251, 73, 52))
169            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(102, 92, 84))
170    }
171
172    /// The Nord theme (arctic, cool bluish-slate palette).
173    pub fn nord() -> Self {
174        Self::empty()
175            .with(StyleRole::Type, Style::new().bold().fg_rgb(208, 135, 112))
176            .with(StyleRole::Field, Style::new().fg_rgb(136, 192, 208))
177            .with(StyleRole::Variant, Style::new().bold().fg_rgb(180, 142, 173))
178            .with(StyleRole::Key, Style::new().fg_rgb(129, 161, 193))
179            .with(StyleRole::String, Style::new().fg_rgb(163, 190, 140))
180            .with(StyleRole::Number, Style::new().fg_rgb(235, 203, 139))
181            .with(StyleRole::Boolean, Style::new().fg_rgb(180, 142, 173))
182            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(147, 156, 172))
183            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(97, 110, 136))
184            .with(StyleRole::Index, Style::new().fg_rgb(136, 192, 208))
185            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(108, 120, 144))
186            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(191, 97, 106))
187            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(235, 203, 139))
188            .with(StyleRole::Error, Style::new().bold().fg_rgb(191, 97, 106))
189            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(76, 86, 106))
190    }
191
192    /// The Dracula theme (classic high-contrast dark theme).
193    pub fn dracula() -> Self {
194        Self::empty()
195            .with(StyleRole::Type, Style::new().bold().fg_rgb(255, 184, 108))
196            .with(StyleRole::Field, Style::new().fg_rgb(139, 233, 253))
197            .with(StyleRole::Variant, Style::new().bold().fg_rgb(189, 147, 249))
198            .with(StyleRole::Key, Style::new().fg_rgb(139, 233, 253))
199            .with(StyleRole::String, Style::new().fg_rgb(80, 250, 123))
200            .with(StyleRole::Number, Style::new().fg_rgb(241, 250, 140))
201            .with(StyleRole::Boolean, Style::new().fg_rgb(255, 121, 198))
202            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(98, 114, 164))
203            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(98, 114, 164))
204            .with(StyleRole::Index, Style::new().fg_rgb(139, 233, 253))
205            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(98, 114, 164))
206            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(255, 85, 85))
207            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(241, 250, 140))
208            .with(StyleRole::Error, Style::new().bold().fg_rgb(255, 85, 85))
209            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(98, 114, 164))
210    }
211
212    /// The Solarized Dark theme.
213    pub fn solarized_dark() -> Self {
214        Self::empty()
215            .with(StyleRole::Type, Style::new().bold().fg_rgb(203, 75, 22))
216            .with(StyleRole::Field, Style::new().fg_rgb(38, 139, 210))
217            .with(StyleRole::Variant, Style::new().bold().fg_rgb(108, 113, 196))
218            .with(StyleRole::Key, Style::new().fg_rgb(42, 161, 152))
219            .with(StyleRole::String, Style::new().fg_rgb(133, 153, 0))
220            .with(StyleRole::Number, Style::new().fg_rgb(181, 137, 0))
221            .with(StyleRole::Boolean, Style::new().fg_rgb(211, 54, 130))
222            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(101, 123, 131))
223            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(88, 110, 117))
224            .with(StyleRole::Index, Style::new().fg_rgb(42, 161, 152))
225            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(88, 110, 117))
226            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(220, 50, 47))
227            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(181, 137, 0))
228            .with(StyleRole::Error, Style::new().bold().fg_rgb(220, 50, 47))
229            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(88, 110, 117))
230    }
231
232    /// The Solarized Light theme.
233    pub fn solarized_light() -> Self {
234        Self::empty()
235            .with(StyleRole::Type, Style::new().bold().fg_rgb(203, 75, 22))
236            .with(StyleRole::Field, Style::new().fg_rgb(38, 139, 210))
237            .with(StyleRole::Variant, Style::new().bold().fg_rgb(108, 113, 196))
238            .with(StyleRole::Key, Style::new().fg_rgb(42, 161, 152))
239            .with(StyleRole::String, Style::new().fg_rgb(133, 153, 0))
240            .with(StyleRole::Number, Style::new().fg_rgb(181, 137, 0))
241            .with(StyleRole::Boolean, Style::new().fg_rgb(211, 54, 130))
242            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(147, 161, 161))
243            .with(StyleRole::Punctuation, Style::new().fg_rgb(101, 123, 131))
244            .with(StyleRole::Index, Style::new().fg_rgb(42, 161, 152))
245            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(131, 148, 150))
246            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(220, 50, 47))
247            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(181, 137, 0))
248            .with(StyleRole::Error, Style::new().bold().fg_rgb(220, 50, 47))
249            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(147, 161, 161))
250    }
251
252    /// The One Dark theme (iconic Atom/VS Code palette).
253    pub fn one_dark() -> Self {
254        Self::empty()
255            .with(StyleRole::Type, Style::new().bold().fg_rgb(229, 192, 123))
256            .with(StyleRole::Field, Style::new().fg_rgb(97, 175, 239))
257            .with(StyleRole::Variant, Style::new().bold().fg_rgb(198, 120, 221))
258            .with(StyleRole::Key, Style::new().fg_rgb(86, 182, 194))
259            .with(StyleRole::String, Style::new().fg_rgb(152, 195, 121))
260            .with(StyleRole::Number, Style::new().fg_rgb(209, 154, 102))
261            .with(StyleRole::Boolean, Style::new().fg_rgb(198, 120, 221))
262            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(120, 128, 142))
263            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(92, 99, 112))
264            .with(StyleRole::Index, Style::new().fg_rgb(86, 182, 194))
265            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(92, 99, 112))
266            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(224, 108, 117))
267            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(229, 192, 123))
268            .with(StyleRole::Error, Style::new().bold().fg_rgb(224, 108, 117))
269            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(92, 99, 112))
270    }
271
272    /// The Catppuccin Mocha theme (popular soft pastel comfort palette).
273    pub fn catppuccin_mocha() -> Self {
274        Self::empty()
275            .with(StyleRole::Type, Style::new().bold().fg_rgb(250, 179, 135))
276            .with(StyleRole::Field, Style::new().fg_rgb(137, 180, 250))
277            .with(StyleRole::Variant, Style::new().bold().fg_rgb(203, 166, 247))
278            .with(StyleRole::Key, Style::new().fg_rgb(148, 226, 213))
279            .with(StyleRole::String, Style::new().fg_rgb(166, 227, 161))
280            .with(StyleRole::Number, Style::new().fg_rgb(249, 226, 175))
281            .with(StyleRole::Boolean, Style::new().fg_rgb(245, 194, 231))
282            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(147, 153, 178))
283            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(108, 112, 134))
284            .with(StyleRole::Index, Style::new().fg_rgb(137, 220, 235))
285            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(166, 173, 200))
286            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(243, 139, 168))
287            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(249, 226, 175))
288            .with(StyleRole::Error, Style::new().bold().fg_rgb(243, 139, 168))
289            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(88, 91, 112))
290    }
291
292    /// Alias for [`Theme::catppuccin_mocha()`].
293    pub fn catppuccin() -> Self {
294        Self::catppuccin_mocha()
295    }
296
297    /// The Tokyo Night theme (neon city night aesthetic).
298    pub fn tokyo_night() -> Self {
299        Self::empty()
300            .with(StyleRole::Type, Style::new().bold().fg_rgb(255, 158, 100))
301            .with(StyleRole::Field, Style::new().fg_rgb(122, 162, 247))
302            .with(StyleRole::Variant, Style::new().bold().fg_rgb(187, 154, 247))
303            .with(StyleRole::Key, Style::new().fg_rgb(125, 207, 255))
304            .with(StyleRole::String, Style::new().fg_rgb(158, 206, 106))
305            .with(StyleRole::Number, Style::new().fg_rgb(224, 175, 104))
306            .with(StyleRole::Boolean, Style::new().fg_rgb(187, 154, 247))
307            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(120, 124, 153))
308            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(86, 95, 137))
309            .with(StyleRole::Index, Style::new().fg_rgb(125, 207, 255))
310            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(86, 95, 137))
311            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(247, 118, 142))
312            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(224, 175, 104))
313            .with(StyleRole::Error, Style::new().bold().fg_rgb(247, 118, 142))
314            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(86, 95, 137))
315    }
316
317    /// The Monokai theme (the classic Sublime Text / TextMate palette).
318    pub fn monokai() -> Self {
319        Self::empty()
320            .with(StyleRole::Type, Style::new().bold().fg_rgb(253, 151, 31))
321            .with(StyleRole::Field, Style::new().fg_rgb(102, 217, 239))
322            .with(StyleRole::Variant, Style::new().bold().fg_rgb(174, 129, 255))
323            .with(StyleRole::Key, Style::new().fg_rgb(102, 217, 239))
324            .with(StyleRole::String, Style::new().fg_rgb(166, 226, 46))
325            .with(StyleRole::Number, Style::new().fg_rgb(230, 219, 116))
326            .with(StyleRole::Boolean, Style::new().fg_rgb(249, 38, 114))
327            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(140, 138, 125))
328            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(117, 113, 94))
329            .with(StyleRole::Index, Style::new().fg_rgb(102, 217, 239))
330            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(117, 113, 94))
331            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(249, 38, 114))
332            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(230, 219, 116))
333            .with(StyleRole::Error, Style::new().bold().fg_rgb(249, 38, 114))
334            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(117, 113, 94))
335    }
336
337    /// The Ayu Dark theme.
338    pub fn ayu_dark() -> Self {
339        Self::empty()
340            .with(StyleRole::Type, Style::new().bold().fg_rgb(255, 153, 64))
341            .with(StyleRole::Field, Style::new().fg_rgb(54, 163, 217))
342            .with(StyleRole::Variant, Style::new().bold().fg_rgb(212, 191, 255))
343            .with(StyleRole::Key, Style::new().fg_rgb(149, 230, 203))
344            .with(StyleRole::String, Style::new().fg_rgb(184, 204, 82))
345            .with(StyleRole::Number, Style::new().fg_rgb(231, 197, 71))
346            .with(StyleRole::Boolean, Style::new().fg_rgb(212, 191, 255))
347            .with(StyleRole::Null, Style::new().dimmed().fg_rgb(120, 126, 135))
348            .with(StyleRole::Punctuation, Style::new().dimmed().fg_rgb(92, 103, 115))
349            .with(StyleRole::Index, Style::new().fg_rgb(95, 180, 210))
350            .with(StyleRole::Metadata, Style::new().dimmed().fg_rgb(92, 103, 115))
351            .with(StyleRole::Sensitive, Style::new().bold().fg_rgb(240, 113, 120))
352            .with(StyleRole::Truncated, Style::new().italic().fg_rgb(231, 197, 71))
353            .with(StyleRole::Error, Style::new().bold().fg_rgb(240, 113, 120))
354            .with(StyleRole::Muted, Style::new().dimmed().fg_rgb(92, 103, 115))
355    }
356
357    /// Set the style for a specific semantic role.
358    pub const fn with(mut self, role: StyleRole, style: Style) -> Self {
359        self.styles[role.index()] = style;
360        self
361    }
362
363    /// Get the style for a specific semantic role.
364    #[inline]
365    pub const fn style(&self, role: StyleRole) -> Style {
366        self.styles[role.index()]
367    }
368
369    /// Adapt theme colors to match the target color capability level.
370    pub fn adapt_to(self, level: ColorLevel) -> Self {
371        match level {
372            ColorLevel::Auto => {
373                let detected = ColorLevel::detect();
374                if detected == ColorLevel::Auto { self } else { self.adapt_to(detected) }
375            }
376            ColorLevel::Truecolor => self,
377            ColorLevel::Ansi256 => self.to_ansi256(),
378            ColorLevel::Ansi16 => self.to_ansi16(),
379            ColorLevel::None => self.to_monochrome(),
380        }
381    }
382
383    /// Convert all colors in the theme to 256-color palette.
384    pub fn to_ansi256(self) -> Self {
385        let mut new_theme = self;
386        let mut i = 0;
387        while i < StyleRole::COUNT {
388            new_theme.styles[i] = new_theme.styles[i].to_ansi256();
389            i += 1;
390        }
391        new_theme
392    }
393
394    /// Convert all colors in the theme to standard 16 ANSI colors.
395    pub fn to_ansi16(self) -> Self {
396        let mut new_theme = self;
397        let mut i = 0;
398        while i < StyleRole::COUNT {
399            new_theme.styles[i] = new_theme.styles[i].to_ansi16();
400            i += 1;
401        }
402        new_theme
403    }
404
405    /// Convert the theme to monochrome (preserve effects like bold/dim, strip colors).
406    pub fn to_monochrome(self) -> Self {
407        let mut new_theme = self;
408        let mut i = 0;
409        while i < StyleRole::COUNT {
410            new_theme.styles[i] = new_theme.styles[i].to_monochrome();
411            i += 1;
412        }
413        new_theme
414    }
415
416    /// True if all styles in the theme are plain/empty.
417    pub fn is_plain(&self) -> bool {
418        self.styles.iter().all(|s| s.is_plain())
419    }
420}