Skip to main content

rusty_rich/
theme.rs

1//! Theme system — equivalent to Rich's `theme.py`.
2//!
3//! A Theme maps style names (like "repr.number", "repr.str") to Style values,
4//! allowing customizable color schemes for different renderable types.
5
6use std::collections::HashMap;
7
8use crate::style::Style;
9
10// ---------------------------------------------------------------------------
11// Theme
12// ---------------------------------------------------------------------------
13
14/// A set of named styles that can be applied to themed output.
15#[derive(Debug, Clone)]
16pub struct Theme {
17    /// Mapping from style name → Style.
18    pub styles: HashMap<String, Style>,
19    /// Optional inherited base theme.
20    pub inherit: Option<Box<Theme>>,
21}
22
23impl Theme {
24    /// Create a new empty theme.
25    pub fn new() -> Self {
26        Self {
27            styles: HashMap::new(),
28            inherit: None,
29        }
30    }
31
32    /// Create a theme that inherits from another.
33    pub fn with_inherit(inherit: Theme) -> Self {
34        Self {
35            styles: HashMap::new(),
36            inherit: Some(Box::new(inherit)),
37        }
38    }
39
40    /// Look up a style by name. Falls back to the inherited theme.
41    pub fn get(&self, name: &str) -> Option<&Style> {
42        self.styles
43            .get(name)
44            .or_else(|| self.inherit.as_ref().and_then(|i| i.get(name)))
45    }
46
47    /// Set a named style.
48    pub fn set(&mut self, name: impl Into<String>, style: Style) {
49        self.styles.insert(name.into(), style);
50    }
51
52    /// Merge another theme's styles into this one.
53    pub fn merge(&mut self, other: &Theme) {
54        for (name, style) in &other.styles {
55            self.styles
56                .entry(name.clone())
57                .or_insert_with(|| style.clone());
58        }
59    }
60}
61
62impl Default for Theme {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68// ---------------------------------------------------------------------------
69// ThemeStack
70// ---------------------------------------------------------------------------
71
72/// A stack of themes — when looking up a style, themes are checked from
73/// top to bottom until a match is found.
74#[derive(Debug, Clone)]
75pub struct ThemeStack {
76    themes: Vec<Theme>,
77}
78
79impl Default for ThemeStack {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl ThemeStack {
86    pub fn new() -> Self {
87        Self { themes: Vec::new() }
88    }
89
90    pub fn push(&mut self, theme: Theme) {
91        self.themes.push(theme);
92    }
93
94    pub fn pop(&mut self) -> Option<Theme> {
95        self.themes.pop()
96    }
97
98    /// Look up a style name across the stack (top-down).
99    pub fn get(&self, name: &str) -> Option<Style> {
100        for theme in self.themes.iter().rev() {
101            if let Some(s) = theme.get(name) {
102                return Some(s.clone());
103            }
104        }
105        None
106    }
107}
108
109// ---------------------------------------------------------------------------
110// Default themes
111// ---------------------------------------------------------------------------
112
113/// Built-in theme style names used by various Rich renderables.
114pub mod names {
115    // repr / pretty
116    pub const REPR_NUMBER: &str = "repr.number";
117    pub const REPR_STR: &str = "repr.str";
118    pub const REPR_BOOL_TRUE: &str = "repr.bool_true";
119    pub const REPR_BOOL_FALSE: &str = "repr.bool_false";
120    pub const REPR_NONE: &str = "repr.none";
121    pub const REPR_URL: &str = "repr.url";
122    pub const REPR_PATH: &str = "repr.path";
123    pub const REPR_IPV4: &str = "repr.ipv4";
124    pub const REPR_IPV6: &str = "repr.ipv6";
125    pub const REPR_ELLIPSIS: &str = "repr.ellipsis";
126    pub const REPR_ATTRIB_NAME: &str = "repr.attrib_name";
127    pub const REPR_ATTRIB_VALUE: &str = "repr.attrib_value";
128    pub const REPR_TAG_NAME: &str = "repr.tag_name";
129    pub const REPR_TAG_CONTENTS: &str = "repr.tag_contents";
130    pub const REPR_TAG_PUNCTUATION: &str = "repr.tag_punctuation";
131    pub const REPR_BRACE: &str = "repr.brace";
132    pub const REPR_CALL: &str = "repr.call";
133    pub const REPR_COMMA: &str = "repr.comma";
134    pub const REPR_BOOL: &str = "repr.bool";
135    pub const REPR_ERROR: &str = "repr.error";
136
137    // table
138    pub const TABLE_HEADER: &str = "table.header";
139    pub const TABLE_CELL: &str = "table.cell";
140    pub const TABLE_ROW: &str = "table.row";
141    pub const TABLE_FOOTER: &str = "table.footer";
142    pub const TABLE_TITLE: &str = "table.title";
143    pub const TABLE_CAPTION: &str = "table.caption";
144    pub const TABLE_BORDER: &str = "table.border";
145
146    // logging
147    pub const LOGGING_KEYWORD: &str = "logging.keyword";
148    pub const LOGGING_LEVEL_DEBUG: &str = "logging.level.debug";
149    pub const LOGGING_LEVEL_INFO: &str = "logging.level.info";
150    pub const LOGGING_LEVEL_WARNING: &str = "logging.level.warning";
151    pub const LOGGING_LEVEL_ERROR: &str = "logging.level.error";
152    pub const LOGGING_LEVEL_CRITICAL: &str = "logging.level.critical";
153    pub const LOGGING_LEVEL_NOTSET: &str = "logging.level.notset";
154
155    // traceback
156    pub const TRACEBACK_BORDER: &str = "traceback.border";
157    pub const TRACEBACK_TITLE: &str = "traceback.title";
158    pub const TRACEBACK_ERROR: &str = "traceback.error";
159    pub const TRACEBACK_ERROR_MARK: &str = "traceback.error_mark";
160    pub const TRACEBACK_FILENAME: &str = "traceback.filename";
161    pub const TRACEBACK_LINE_NO: &str = "traceback.line_no";
162    pub const TRACEBACK_LOCALS_HEADER: &str = "traceback.locals_header";
163    pub const TRACEBACK_LOCALS_KEY: &str = "traceback.locals_key";
164    pub const TRACEBACK_LOCALS_VALUE: &str = "traceback.locals_value";
165    pub const TRACEBACK_EXC_TYPE: &str = "traceback.exc_type";
166    pub const TRACEBACK_EXC_VALUE: &str = "traceback.exc_value";
167
168    // general
169    pub const RULE_LINE: &str = "rule.line";
170    pub const RULE_TEXT: &str = "rule.text";
171    pub const BAR_COMPLETE: &str = "bar.complete";
172    pub const BAR_FINISHED: &str = "bar.finished";
173    pub const BAR_PULSE: &str = "bar.pulse";
174    pub const BAR_REMAINING: &str = "bar.remaining";
175    pub const PROGRESS_DESCRIPTION: &str = "progress.description";
176    pub const PROGRESS_PERCENTAGE: &str = "progress.percentage";
177    pub const PROGRESS_REMAINING: &str = "progress.remaining";
178    pub const PROGRESS_ELAPSED: &str = "progress.elapsed";
179    pub const PROGRESS_DATA: &str = "progress.data";
180    pub const PROGRESS_DOWNLOAD: &str = "progress.download";
181    pub const PROGRESS_TRANSFER: &str = "progress.transfer";
182    pub const PROGRESS_FILESIZE: &str = "progress.filesize";
183    pub const PROGRESS_TOTAL: &str = "progress.total";
184    pub const TREE: &str = "tree";
185    pub const TREE_LINE: &str = "tree.line";
186    pub const MARKDOWN_H1: &str = "markdown.h1";
187    pub const MARKDOWN_H2: &str = "markdown.h2";
188    pub const MARKDOWN_CODE: &str = "markdown.code";
189    pub const MARKDOWN_LINK: &str = "markdown.link";
190    pub const MARKDOWN_ITEM: &str = "markdown.item";
191    pub const MARKDOWN_H3: &str = "markdown.h3";
192    pub const MARKDOWN_H4: &str = "markdown.h4";
193    pub const MARKDOWN_H5: &str = "markdown.h5";
194    pub const MARKDOWN_H6: &str = "markdown.h6";
195    pub const MARKDOWN_H7: &str = "markdown.h7";
196    pub const MARKDOWN_PARAGRAPH: &str = "markdown.paragraph";
197    pub const MARKDOWN_BLOCKQUOTE: &str = "markdown.blockquote";
198    pub const MARKDOWN_LIST: &str = "markdown.list";
199    pub const MARKDOWN_ITEM_BULLET: &str = "markdown.item.bullet";
200    pub const MARKDOWN_ITEM_NUMBER: &str = "markdown.item.number";
201    pub const MARKDOWN_TABLE: &str = "markdown.table";
202    pub const MARKDOWN_TABLE_HEADER: &str = "markdown.table.header";
203    pub const MARKDOWN_CODE_BLOCK: &str = "markdown.code.block";
204    pub const MARKDOWN_CODE_INLINE: &str = "markdown.code.inline";
205    pub const MARKDOWN_HR: &str = "markdown.hr";
206    pub const JSON_KEY: &str = "json.key";
207    pub const JSON_STR: &str = "json.str";
208    pub const JSON_NUMBER: &str = "json.number";
209    pub const JSON_BOOL: &str = "json.bool";
210    pub const JSON_NULL: &str = "json.null";
211    pub const JSON_BOOL_TRUE: &str = "json.bool_true";
212    pub const JSON_BOOL_FALSE: &str = "json.bool_false";
213    pub const JSON_BRACE: &str = "json.brace";
214
215    // status
216    pub const STATUS_SPINNER: &str = "status.spinner";
217    pub const STATUS_MESSAGE: &str = "status.message";
218
219    // prompt
220    pub const PROMPT: &str = "prompt";
221    pub const PROMPT_CHOICES: &str = "prompt.choices";
222    pub const PROMPT_DEFAULT: &str = "prompt.default";
223
224    // syntax highlighting (via syntect)
225    pub const SYNTAX_COMMENT: &str = "syntax.comment";
226    pub const SYNTAX_KEYWORD: &str = "syntax.keyword";
227    pub const SYNTAX_STRING: &str = "syntax.string";
228    pub const SYNTAX_NUMBER: &str = "syntax.number";
229    pub const SYNTAX_FUNCTION: &str = "syntax.function";
230    pub const SYNTAX_TYPE: &str = "syntax.type";
231}
232
233/// Create the default Rich-like theme.
234pub fn default_theme() -> Theme {
235    let mut t = Theme::new();
236    use crate::color::Color;
237    use crate::style::Style;
238
239    // repr styles
240    t.set(
241        names::REPR_NUMBER,
242        Style::new().color(Color::parse("cyan").unwrap()).bold(true),
243    );
244    t.set(
245        names::REPR_STR,
246        Style::new().color(Color::parse("green").unwrap()),
247    );
248    t.set(
249        names::REPR_BOOL_TRUE,
250        Style::new()
251            .color(Color::parse("bright_green").unwrap())
252            .bold(true),
253    );
254    t.set(
255        names::REPR_BOOL_FALSE,
256        Style::new()
257            .color(Color::parse("bright_red").unwrap())
258            .bold(true),
259    );
260    t.set(
261        names::REPR_NONE,
262        Style::new()
263            .color(Color::parse("bright_yellow").unwrap())
264            .dim(true),
265    );
266    t.set(
267        names::REPR_URL,
268        Style::new()
269            .color(Color::parse("bright_blue").unwrap())
270            .underline(true),
271    );
272    t.set(
273        names::REPR_PATH,
274        Style::new().color(Color::parse("magenta").unwrap()),
275    );
276    t.set(
277        names::REPR_ATTRIB_NAME,
278        Style::new().color(Color::parse("bright_cyan").unwrap()),
279    );
280    t.set(
281        names::REPR_ATTRIB_VALUE,
282        Style::new().color(Color::parse("white").unwrap()),
283    );
284    t.set(
285        names::REPR_ELLIPSIS,
286        Style::new().dim(true).color(Color::parse("white").unwrap()),
287    );
288    t.set(
289        names::REPR_IPV4,
290        Style::new()
291            .color(Color::parse("bright_blue").unwrap())
292            .bold(true),
293    );
294    t.set(
295        names::REPR_IPV6,
296        Style::new()
297            .color(Color::parse("bright_magenta").unwrap())
298            .bold(true),
299    );
300    t.set(
301        names::REPR_TAG_NAME,
302        Style::new()
303            .color(Color::parse("bright_blue").unwrap())
304            .bold(true),
305    );
306    t.set(
307        names::REPR_TAG_CONTENTS,
308        Style::new().color(Color::parse("white").unwrap()),
309    );
310    t.set(
311        names::REPR_TAG_PUNCTUATION,
312        Style::new().color(Color::parse("bright_black").unwrap()),
313    );
314    t.set(
315        names::REPR_BRACE,
316        Style::new().color(Color::parse("bright_black").unwrap()),
317    );
318    t.set(
319        names::REPR_CALL,
320        Style::new().color(Color::parse("bright_cyan").unwrap()),
321    );
322    t.set(
323        names::REPR_COMMA,
324        Style::new().color(Color::parse("bright_black").unwrap()),
325    );
326    t.set(
327        names::REPR_BOOL,
328        Style::new()
329            .color(Color::parse("bright_yellow").unwrap())
330            .bold(true),
331    );
332    t.set(
333        names::REPR_ERROR,
334        Style::new()
335            .color(Color::parse("bright_red").unwrap())
336            .bold(true),
337    );
338
339    // table styles
340    t.set(
341        names::TABLE_HEADER,
342        Style::new()
343            .bold(true)
344            .color(Color::parse("white").unwrap()),
345    );
346    t.set(names::TABLE_FOOTER, Style::new().bold(true));
347    t.set(names::TABLE_TITLE, Style::new().bold(true));
348    t.set(names::TABLE_CAPTION, Style::new().dim(true));
349    t.set(
350        names::TABLE_BORDER,
351        Style::new().color(Color::parse("bright_black").unwrap()),
352    );
353    t.set(
354        names::TABLE_CELL,
355        Style::new().color(Color::parse("white").unwrap()),
356    );
357    t.set(
358        names::TABLE_ROW,
359        Style::new().color(Color::parse("white").unwrap()),
360    );
361
362    // rule
363    t.set(
364        names::RULE_LINE,
365        Style::new().color(Color::parse("bright_black").unwrap()),
366    );
367    t.set(names::RULE_TEXT, Style::new().bold(true));
368
369    // tree
370    t.set(
371        names::TREE,
372        Style::new().color(Color::parse("white").unwrap()),
373    );
374    t.set(
375        names::TREE_LINE,
376        Style::new().color(Color::parse("bright_black").unwrap()),
377    );
378
379    // progress
380    t.set(
381        names::BAR_COMPLETE,
382        Style::new().color(Color::parse("green").unwrap()),
383    );
384    t.set(
385        names::BAR_FINISHED,
386        Style::new().color(Color::parse("bright_green").unwrap()),
387    );
388    t.set(
389        names::BAR_PULSE,
390        Style::new().color(Color::parse("bright_cyan").unwrap()),
391    );
392    t.set(
393        names::PROGRESS_DESCRIPTION,
394        Style::new().color(Color::parse("white").unwrap()),
395    );
396    t.set(
397        names::PROGRESS_PERCENTAGE,
398        Style::new().color(Color::parse("cyan").unwrap()),
399    );
400    t.set(
401        names::PROGRESS_REMAINING,
402        Style::new().color(Color::parse("bright_black").unwrap()),
403    );
404    t.set(
405        names::PROGRESS_ELAPSED,
406        Style::new().color(Color::parse("cyan").unwrap()),
407    );
408    t.set(
409        names::PROGRESS_DATA,
410        Style::new().color(Color::parse("bright_blue").unwrap()),
411    );
412    t.set(
413        names::PROGRESS_DOWNLOAD,
414        Style::new().color(Color::parse("bright_cyan").unwrap()),
415    );
416    t.set(
417        names::PROGRESS_TRANSFER,
418        Style::new().color(Color::parse("bright_magenta").unwrap()),
419    );
420    t.set(
421        names::PROGRESS_FILESIZE,
422        Style::new().color(Color::parse("bright_green").unwrap()),
423    );
424    t.set(
425        names::PROGRESS_TOTAL,
426        Style::new().color(Color::parse("bright_blue").unwrap()),
427    );
428    t.set(
429        names::BAR_REMAINING,
430        Style::new().color(Color::parse("bright_black").unwrap()),
431    );
432
433    // markdown
434    t.set(
435        names::MARKDOWN_H1,
436        Style::new()
437            .bold(true)
438            .color(Color::parse("bright_cyan").unwrap()),
439    );
440    t.set(
441        names::MARKDOWN_H2,
442        Style::new().bold(true).color(Color::parse("cyan").unwrap()),
443    );
444    t.set(
445        names::MARKDOWN_CODE,
446        Style::new()
447            .color(Color::parse("bright_yellow").unwrap())
448            .bgcolor(Color::parse("black").unwrap()),
449    );
450    t.set(
451        names::MARKDOWN_LINK,
452        Style::new()
453            .color(Color::parse("bright_blue").unwrap())
454            .underline(true),
455    );
456    t.set(
457        names::MARKDOWN_H3,
458        Style::new()
459            .bold(true)
460            .color(Color::parse("yellow").unwrap()),
461    );
462    t.set(
463        names::MARKDOWN_H4,
464        Style::new()
465            .bold(true)
466            .color(Color::parse("green").unwrap()),
467    );
468    t.set(
469        names::MARKDOWN_H5,
470        Style::new().bold(true).color(Color::parse("blue").unwrap()),
471    );
472    t.set(
473        names::MARKDOWN_H6,
474        Style::new()
475            .bold(true)
476            .color(Color::parse("bright_black").unwrap()),
477    );
478    t.set(
479        names::MARKDOWN_H7,
480        Style::new()
481            .dim(true)
482            .color(Color::parse("bright_black").unwrap()),
483    );
484    t.set(
485        names::MARKDOWN_PARAGRAPH,
486        Style::new().color(Color::parse("white").unwrap()),
487    );
488    t.set(
489        names::MARKDOWN_BLOCKQUOTE,
490        Style::new().color(Color::parse("bright_black").unwrap()),
491    );
492    t.set(
493        names::MARKDOWN_LIST,
494        Style::new().color(Color::parse("white").unwrap()),
495    );
496    t.set(
497        names::MARKDOWN_ITEM,
498        Style::new().color(Color::parse("cyan").unwrap()),
499    );
500    t.set(
501        names::MARKDOWN_ITEM_BULLET,
502        Style::new().color(Color::parse("cyan").unwrap()),
503    );
504    t.set(
505        names::MARKDOWN_ITEM_NUMBER,
506        Style::new().color(Color::parse("cyan").unwrap()),
507    );
508    t.set(
509        names::MARKDOWN_TABLE,
510        Style::new().color(Color::parse("white").unwrap()),
511    );
512    t.set(
513        names::MARKDOWN_TABLE_HEADER,
514        Style::new()
515            .bold(true)
516            .color(Color::parse("white").unwrap()),
517    );
518    t.set(
519        names::MARKDOWN_CODE_BLOCK,
520        Style::new()
521            .color(Color::parse("bright_yellow").unwrap())
522            .bgcolor(Color::parse("black").unwrap()),
523    );
524    t.set(
525        names::MARKDOWN_CODE_INLINE,
526        Style::new().color(Color::parse("bright_yellow").unwrap()),
527    );
528    t.set(
529        names::MARKDOWN_HR,
530        Style::new().color(Color::parse("bright_black").unwrap()),
531    );
532
533    // json
534    t.set(
535        names::JSON_KEY,
536        Style::new().color(Color::parse("cyan").unwrap()),
537    );
538    t.set(
539        names::JSON_STR,
540        Style::new().color(Color::parse("green").unwrap()),
541    );
542    t.set(
543        names::JSON_NUMBER,
544        Style::new()
545            .color(Color::parse("bright_blue").unwrap())
546            .bold(true),
547    );
548    t.set(
549        names::JSON_BOOL,
550        Style::new()
551            .color(Color::parse("bright_yellow").unwrap())
552            .bold(true),
553    );
554    t.set(
555        names::JSON_NULL,
556        Style::new()
557            .color(Color::parse("bright_red").unwrap())
558            .dim(true),
559    );
560    t.set(
561        names::JSON_BOOL_TRUE,
562        Style::new()
563            .color(Color::parse("bright_green").unwrap())
564            .bold(true),
565    );
566    t.set(
567        names::JSON_BOOL_FALSE,
568        Style::new()
569            .color(Color::parse("bright_red").unwrap())
570            .bold(true),
571    );
572    t.set(
573        names::JSON_BRACE,
574        Style::new().color(Color::parse("bright_black").unwrap()),
575    );
576
577    // traceback
578    t.set(
579        names::TRACEBACK_BORDER,
580        Style::new().color(Color::parse("red").unwrap()),
581    );
582    t.set(names::TRACEBACK_TITLE, Style::new().bold(true));
583    t.set(
584        names::TRACEBACK_ERROR,
585        Style::new()
586            .color(Color::parse("bright_red").unwrap())
587            .bold(true),
588    );
589    t.set(
590        names::TRACEBACK_ERROR_MARK,
591        Style::new()
592            .color(Color::parse("bright_red").unwrap())
593            .bold(true),
594    );
595    t.set(
596        names::TRACEBACK_FILENAME,
597        Style::new().color(Color::parse("cyan").unwrap()),
598    );
599    t.set(
600        names::TRACEBACK_LINE_NO,
601        Style::new().color(Color::parse("bright_black").unwrap()),
602    );
603    t.set(names::TRACEBACK_LOCALS_HEADER, Style::new().bold(true));
604    t.set(
605        names::TRACEBACK_LOCALS_KEY,
606        Style::new().color(Color::parse("bright_cyan").unwrap()),
607    );
608    t.set(
609        names::TRACEBACK_LOCALS_VALUE,
610        Style::new().color(Color::parse("white").unwrap()),
611    );
612    t.set(
613        names::TRACEBACK_EXC_TYPE,
614        Style::new()
615            .color(Color::parse("bright_red").unwrap())
616            .bold(true),
617    );
618    t.set(
619        names::TRACEBACK_EXC_VALUE,
620        Style::new().color(Color::parse("white").unwrap()),
621    );
622
623    // logging extras
624    t.set(
625        names::LOGGING_KEYWORD,
626        Style::new()
627            .color(Color::parse("bright_yellow").unwrap())
628            .bold(true),
629    );
630    t.set(
631        names::LOGGING_LEVEL_NOTSET,
632        Style::new().dim(true).color(Color::parse("white").unwrap()),
633    );
634
635    // status & prompt
636    t.set(
637        names::STATUS_SPINNER,
638        Style::new().color(Color::parse("bright_cyan").unwrap()),
639    );
640    t.set(
641        names::STATUS_MESSAGE,
642        Style::new().color(Color::parse("white").unwrap()),
643    );
644    t.set(
645        names::PROMPT,
646        Style::new().color(Color::parse("bright_cyan").unwrap()),
647    );
648    t.set(
649        names::PROMPT_CHOICES,
650        Style::new().color(Color::parse("cyan").unwrap()),
651    );
652    t.set(
653        names::PROMPT_DEFAULT,
654        Style::new().color(Color::parse("bright_black").unwrap()),
655    );
656
657    t
658}