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.entry(name.clone()).or_insert_with(|| style.clone());
56        }
57    }
58}
59
60impl Default for Theme {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66// ---------------------------------------------------------------------------
67// ThemeStack
68// ---------------------------------------------------------------------------
69
70/// A stack of themes — when looking up a style, themes are checked from
71/// top to bottom until a match is found.
72#[derive(Debug, Clone)]
73pub struct ThemeStack {
74    themes: Vec<Theme>,
75}
76
77impl ThemeStack {
78    pub fn new() -> Self {
79        Self { themes: Vec::new() }
80    }
81
82    pub fn push(&mut self, theme: Theme) {
83        self.themes.push(theme);
84    }
85
86    pub fn pop(&mut self) -> Option<Theme> {
87        self.themes.pop()
88    }
89
90    /// Look up a style name across the stack (top-down).
91    pub fn get(&self, name: &str) -> Option<Style> {
92        for theme in self.themes.iter().rev() {
93            if let Some(s) = theme.get(name) {
94                return Some(s.clone());
95            }
96        }
97        None
98    }
99}
100
101// ---------------------------------------------------------------------------
102// Default themes
103// ---------------------------------------------------------------------------
104
105/// Built-in theme style names used by various Rich renderables.
106pub mod names {
107    // repr / pretty
108    pub const REPR_NUMBER: &str = "repr.number";
109    pub const REPR_STR: &str = "repr.str";
110    pub const REPR_BOOL_TRUE: &str = "repr.bool_true";
111    pub const REPR_BOOL_FALSE: &str = "repr.bool_false";
112    pub const REPR_NONE: &str = "repr.none";
113    pub const REPR_URL: &str = "repr.url";
114    pub const REPR_PATH: &str = "repr.path";
115    pub const REPR_IPV4: &str = "repr.ipv4";
116    pub const REPR_IPV6: &str = "repr.ipv6";
117    pub const REPR_ELLIPSIS: &str = "repr.ellipsis";
118    pub const REPR_ATTRIB_NAME: &str = "repr.attrib_name";
119    pub const REPR_ATTRIB_VALUE: &str = "repr.attrib_value";
120    pub const REPR_TAG_NAME: &str = "repr.tag_name";
121    pub const REPR_TAG_CONTENTS: &str = "repr.tag_contents";
122    pub const REPR_TAG_PUNCTUATION: &str = "repr.tag_punctuation";
123
124    // table
125    pub const TABLE_HEADER: &str = "table.header";
126    pub const TABLE_FOOTER: &str = "table.footer";
127    pub const TABLE_TITLE: &str = "table.title";
128    pub const TABLE_CAPTION: &str = "table.caption";
129    pub const TABLE_BORDER: &str = "table.border";
130
131    // logging
132    pub const LOGGING_KEYWORD: &str = "logging.keyword";
133    pub const LOGGING_LEVEL_DEBUG: &str = "logging.level.debug";
134    pub const LOGGING_LEVEL_INFO: &str = "logging.level.info";
135    pub const LOGGING_LEVEL_WARNING: &str = "logging.level.warning";
136    pub const LOGGING_LEVEL_ERROR: &str = "logging.level.error";
137    pub const LOGGING_LEVEL_CRITICAL: &str = "logging.level.critical";
138
139    // traceback
140    pub const TRACEBACK_BORDER: &str = "traceback.border";
141    pub const TRACEBACK_TITLE: &str = "traceback.title";
142    pub const TRACEBACK_ERROR: &str = "traceback.error";
143    pub const TRACEBACK_ERROR_MARK: &str = "traceback.error_mark";
144    pub const TRACEBACK_FILENAME: &str = "traceback.filename";
145    pub const TRACEBACK_LINE_NO: &str = "traceback.line_no";
146    pub const TRACEBACK_LOCALS_HEADER: &str = "traceback.locals_header";
147
148    // general
149    pub const RULE_LINE: &str = "rule.line";
150    pub const RULE_TEXT: &str = "rule.text";
151    pub const BAR_COMPLETE: &str = "bar.complete";
152    pub const BAR_FINISHED: &str = "bar.finished";
153    pub const BAR_PULSE: &str = "bar.pulse";
154    pub const PROGRESS_DESCRIPTION: &str = "progress.description";
155    pub const PROGRESS_PERCENTAGE: &str = "progress.percentage";
156    pub const PROGRESS_REMAINING: &str = "progress.remaining";
157    pub const PROGRESS_ELAPSED: &str = "progress.elapsed";
158    pub const PROGRESS_DATA: &str = "progress.data";
159    pub const TREE: &str = "tree";
160    pub const TREE_LINE: &str = "tree.line";
161    pub const MARKDOWN_H1: &str = "markdown.h1";
162    pub const MARKDOWN_H2: &str = "markdown.h2";
163    pub const MARKDOWN_CODE: &str = "markdown.code";
164    pub const MARKDOWN_LINK: &str = "markdown.link";
165    pub const MARKDOWN_ITEM: &str = "markdown.item";
166    pub const MARKDOWN_BLOCKQUOTE: &str = "markdown.blockquote";
167    pub const JSON_KEY: &str = "json.key";
168    pub const JSON_STR: &str = "json.str";
169    pub const JSON_NUMBER: &str = "json.number";
170    pub const JSON_BOOL: &str = "json.bool";
171    pub const JSON_NULL: &str = "json.null";
172    pub const JSON_BRACE: &str = "json.brace";
173
174    // syntax highlighting (via syntect)
175    pub const SYNTAX_COMMENT: &str = "syntax.comment";
176    pub const SYNTAX_KEYWORD: &str = "syntax.keyword";
177    pub const SYNTAX_STRING: &str = "syntax.string";
178    pub const SYNTAX_NUMBER: &str = "syntax.number";
179    pub const SYNTAX_FUNCTION: &str = "syntax.function";
180    pub const SYNTAX_TYPE: &str = "syntax.type";
181}
182
183/// Create the default Rich-like theme.
184pub fn default_theme() -> Theme {
185    let mut t = Theme::new();
186    use crate::color::Color;
187    use crate::style::Style;
188
189    // repr styles
190    t.set(names::REPR_NUMBER, Style::new().color(Color::parse("cyan").unwrap()).bold(true));
191    t.set(names::REPR_STR, Style::new().color(Color::parse("green").unwrap()));
192    t.set(names::REPR_BOOL_TRUE, Style::new().color(Color::parse("bright_green").unwrap()).bold(true));
193    t.set(names::REPR_BOOL_FALSE, Style::new().color(Color::parse("bright_red").unwrap()).bold(true));
194    t.set(names::REPR_NONE, Style::new().color(Color::parse("bright_yellow").unwrap()).dim(true));
195    t.set(names::REPR_URL, Style::new().color(Color::parse("bright_blue").unwrap()).underline(true));
196    t.set(names::REPR_PATH, Style::new().color(Color::parse("magenta").unwrap()));
197    t.set(names::REPR_ATTRIB_NAME, Style::new().color(Color::parse("bright_cyan").unwrap()));
198    t.set(names::REPR_ATTRIB_VALUE, Style::new().color(Color::parse("white").unwrap()));
199
200    // table styles
201    t.set(names::TABLE_HEADER, Style::new().bold(true).color(Color::parse("white").unwrap()));
202    t.set(names::TABLE_FOOTER, Style::new().bold(true));
203    t.set(names::TABLE_TITLE, Style::new().bold(true));
204    t.set(names::TABLE_CAPTION, Style::new().dim(true));
205    t.set(names::TABLE_BORDER, Style::new().color(Color::parse("bright_black").unwrap()));
206
207    // rule
208    t.set(names::RULE_LINE, Style::new().color(Color::parse("bright_black").unwrap()));
209    t.set(names::RULE_TEXT, Style::new().bold(true));
210
211    // tree
212    t.set(names::TREE, Style::new().color(Color::parse("white").unwrap()));
213    t.set(names::TREE_LINE, Style::new().color(Color::parse("bright_black").unwrap()));
214
215    // progress
216    t.set(names::BAR_COMPLETE, Style::new().color(Color::parse("green").unwrap()));
217    t.set(names::BAR_FINISHED, Style::new().color(Color::parse("bright_green").unwrap()));
218    t.set(names::BAR_PULSE, Style::new().color(Color::parse("bright_cyan").unwrap()));
219    t.set(names::PROGRESS_DESCRIPTION, Style::new().color(Color::parse("white").unwrap()));
220    t.set(names::PROGRESS_PERCENTAGE, Style::new().color(Color::parse("cyan").unwrap()));
221
222    // markdown
223    t.set(names::MARKDOWN_H1, Style::new().bold(true).color(Color::parse("bright_cyan").unwrap()));
224    t.set(names::MARKDOWN_H2, Style::new().bold(true).color(Color::parse("cyan").unwrap()));
225    t.set(names::MARKDOWN_CODE, Style::new().color(Color::parse("bright_yellow").unwrap()).bgcolor(Color::parse("black").unwrap()));
226    t.set(names::MARKDOWN_LINK, Style::new().color(Color::parse("bright_blue").unwrap()).underline(true));
227
228    // json
229    t.set(names::JSON_KEY, Style::new().color(Color::parse("cyan").unwrap()));
230    t.set(names::JSON_STR, Style::new().color(Color::parse("green").unwrap()));
231    t.set(names::JSON_NUMBER, Style::new().color(Color::parse("bright_blue").unwrap()).bold(true));
232    t.set(names::JSON_BOOL, Style::new().color(Color::parse("bright_yellow").unwrap()).bold(true));
233    t.set(names::JSON_NULL, Style::new().color(Color::parse("bright_red").unwrap()).dim(true));
234    t.set(names::JSON_BRACE, Style::new().color(Color::parse("bright_black").unwrap()));
235
236    // traceback
237    t.set(names::TRACEBACK_BORDER, Style::new().color(Color::parse("red").unwrap()));
238    t.set(names::TRACEBACK_TITLE, Style::new().bold(true));
239    t.set(names::TRACEBACK_ERROR, Style::new().color(Color::parse("bright_red").unwrap()).bold(true));
240    t.set(names::TRACEBACK_ERROR_MARK, Style::new().color(Color::parse("bright_red").unwrap()).bold(true));
241    t.set(names::TRACEBACK_FILENAME, Style::new().color(Color::parse("cyan").unwrap()));
242    t.set(names::TRACEBACK_LINE_NO, Style::new().color(Color::parse("bright_black").unwrap()));
243    t.set(names::TRACEBACK_LOCALS_HEADER, Style::new().bold(true));
244
245    t
246}