1use std::collections::HashMap;
12
13use crate::errors::Result;
14use crate::style::StyleType;
15
16use crate::style::Style;
17
18pub const DEFAULT_STYLES: &[(&str, &str)] = &[
26 ("none", "none"),
27 (
28 "reset",
29 "not bold not dim not italic not underline not blink not blink2 \
30 not reverse not conceal not strike default on default",
31 ),
32 ("dim", "dim"),
33 ("bright", "not dim"),
34 ("bold", "bold"),
35 ("strong", "bold"),
36 ("code", "bold reverse"),
37 ("italic", "italic"),
38 ("emphasize", "italic"),
39 ("underline", "underline"),
40 ("blink", "blink"),
41 ("blink2", "blink2"),
42 ("reverse", "reverse"),
43 ("strike", "strike"),
44 ("black", "black"),
45 ("red", "red"),
46 ("green", "green"),
47 ("yellow", "yellow"),
48 ("magenta", "magenta"),
49 ("cyan", "cyan"),
50 ("white", "white"),
51 ("inspect.attr", "italic yellow"),
52 ("inspect.attr.dunder", "dim italic yellow"),
53 ("inspect.callable", "bold red"),
54 ("inspect.async_def", "italic bright_cyan"),
55 ("inspect.def", "italic bright_cyan"),
56 ("inspect.class", "italic bright_cyan"),
57 ("inspect.error", "bold red"),
58 ("inspect.equals", "none"),
59 ("inspect.help", "cyan"),
60 ("inspect.doc", "dim"),
61 ("inspect.value.border", "green"),
62 ("live.ellipsis", "bold red"),
63 ("layout.tree.row", "not dim red"),
64 ("layout.tree.column", "not dim blue"),
65 ("logging.keyword", "bold yellow"),
66 ("logging.level.notset", "dim"),
67 ("logging.level.debug", "green"),
68 ("logging.level.info", "blue"),
69 ("logging.level.warning", "yellow"),
70 ("logging.level.error", "bold red"),
71 ("logging.level.critical", "bold reverse red"),
72 ("log.level", "none"),
73 ("log.time", "dim cyan"),
74 ("log.message", "none"),
75 ("log.path", "dim"),
76 ("repr.ellipsis", "yellow"),
77 ("repr.indent", "dim green"),
78 ("repr.error", "bold red"),
79 ("repr.str", "not bold not italic green"),
80 ("repr.brace", "bold"),
81 ("repr.comma", "bold"),
82 ("repr.ipv4", "bold bright_green"),
83 ("repr.ipv6", "bold bright_green"),
84 ("repr.eui48", "bold bright_green"),
85 ("repr.eui64", "bold bright_green"),
86 ("repr.tag_start", "bold"),
87 ("repr.tag_name", "bold bright_magenta"),
88 ("repr.tag_contents", "default"),
89 ("repr.tag_end", "bold"),
90 ("repr.attrib_name", "not italic yellow"),
91 ("repr.attrib_equal", "bold"),
92 ("repr.attrib_value", "not italic magenta"),
93 ("repr.number", "bold not italic cyan"),
94 ("repr.number_complex", "bold not italic cyan"),
95 ("repr.bool_true", "italic bright_green"),
96 ("repr.bool_false", "italic bright_red"),
97 ("repr.none", "italic magenta"),
98 ("repr.url", "not bold not italic underline bright_blue"),
99 ("repr.uuid", "not bold bright_yellow"),
100 ("repr.call", "bold magenta"),
101 ("repr.path", "magenta"),
102 ("repr.filename", "bright_magenta"),
103 ("rule.line", "bright_green"),
104 ("rule.text", "none"),
105 ("json.brace", "bold"),
106 ("json.bool_true", "italic bright_green"),
107 ("json.bool_false", "italic bright_red"),
108 ("json.null", "italic magenta"),
109 ("json.number", "bold not italic cyan"),
110 ("json.str", "not bold not italic green"),
111 ("json.key", "bold blue"),
112 ("prompt", "none"),
113 ("prompt.choices", "bold magenta"),
114 ("prompt.default", "bold cyan"),
115 ("prompt.invalid", "red"),
116 ("prompt.invalid.choice", "red"),
117 ("pretty", "none"),
118 ("scope.border", "blue"),
119 ("scope.key", "italic yellow"),
120 ("scope.key.special", "dim italic yellow"),
121 ("scope.equals", "red"),
122 ("table.header", "bold"),
123 ("table.footer", "bold"),
124 ("table.cell", "none"),
125 ("table.title", "italic"),
126 ("table.caption", "dim italic"),
127 ("traceback.error", "italic red"),
128 ("traceback.border.syntax_error", "bright_red"),
129 ("traceback.border", "red"),
130 ("traceback.text", "none"),
131 ("traceback.title", "bold red"),
132 ("traceback.exc_type", "bold bright_red"),
133 ("traceback.exc_value", "none"),
134 ("traceback.offset", "bold bright_red"),
135 ("traceback.error_range", "bold underline"),
136 ("traceback.note", "bold green"),
137 ("traceback.group.border", "magenta"),
138 ("bar.back", "grey23"),
139 ("bar.complete", "rgb(249,38,114)"),
140 ("bar.finished", "rgb(114,156,31)"),
141 ("bar.pulse", "rgb(249,38,114)"),
142 ("progress.description", "none"),
143 ("progress.filesize", "green"),
144 ("progress.filesize.total", "green"),
145 ("progress.download", "green"),
146 ("progress.elapsed", "yellow"),
147 ("progress.percentage", "magenta"),
148 ("progress.remaining", "cyan"),
149 ("progress.data.speed", "red"),
150 ("progress.spinner", "green"),
151 ("status.spinner", "green"),
152 ("tree", "none"),
153 ("tree.line", "none"),
154 ("markdown.paragraph", "none"),
155 ("markdown.text", "none"),
156 ("markdown.em", "italic"),
157 ("markdown.emph", "italic"),
158 ("markdown.strong", "bold"),
159 ("markdown.code", "bold cyan on black"),
160 ("markdown.code_block", "cyan on black"),
161 ("markdown.block_quote", "magenta"),
162 ("markdown.list", "cyan"),
163 ("markdown.item", "none"),
164 ("markdown.item.bullet", "bold"),
165 ("markdown.item.number", "cyan"),
166 ("markdown.hr", "dim"),
167 ("markdown.h1.border", "none"),
168 ("markdown.h1", "bold underline"),
169 ("markdown.h2", "underline magenta"),
170 ("markdown.h3", "bold magenta"),
171 ("markdown.h4", "italic magenta"),
172 ("markdown.h5", "italic"),
173 ("markdown.h6", "dim"),
174 ("markdown.h7", "dim italic"),
175 ("markdown.link", "bright_blue"),
176 ("markdown.link_url", "underline blue"),
177 ("markdown.s", "strike"),
178 ("markdown.table.border", "cyan"),
179 ("markdown.table.header", "not bold cyan"),
180 ("markdown.kbd", "bold bright_yellow"),
181 ("iso8601.date", "blue"),
182 ("iso8601.time", "magenta"),
183 ("iso8601.timezone", "yellow"),
184];
185
186#[derive(Debug, Clone, Default)]
188pub struct Theme {
189 styles: HashMap<String, Style>,
190}
191
192impl Theme {
193 pub fn new() -> Self {
194 Theme::default()
195 }
196
197 pub fn get(&self, name: &str) -> Option<&Style> {
199 self.styles.get(name)
200 }
201
202 pub fn insert(&mut self, name: impl Into<String>, style: Style) {
204 self.styles.insert(name.into(), style);
205 }
206
207 pub fn get_style(&self, style: &StyleType) -> Result<Style> {
220 match style {
221 StyleType::Style(style) => Ok(style.clone()),
222 StyleType::Name(name) => match self.styles.get(name) {
223 Some(style) => Ok(style.clone()),
224 None => Style::parse(name),
225 },
226 }
227 }
228
229 pub fn get_style_or_null(&self, style: &StyleType) -> Style {
234 self.get_style(style).unwrap_or_default()
235 }
236
237 pub fn len(&self) -> usize {
239 self.styles.len()
240 }
241
242 pub fn is_empty(&self) -> bool {
244 self.styles.is_empty()
245 }
246
247 pub fn default_theme() -> Self {
249 let mut theme = Theme::new();
250 for (name, spec) in DEFAULT_STYLES {
251 match Style::parse(spec) {
252 Ok(style) => theme.insert(*name, style),
253 Err(_) => continue,
257 }
258 }
259 theme
260 }
261
262 pub fn default_shared() -> &'static Theme {
265 static DEFAULT: std::sync::OnceLock<Theme> = std::sync::OnceLock::new();
266 DEFAULT.get_or_init(Theme::default_theme)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
281 fn theme_lookup_beats_the_style_parser() {
282 let mut theme = Theme::default_theme();
283 theme.insert("red", Style::parse("blue").unwrap());
284 assert_eq!(
285 theme.get_style(&StyleType::Name("red".into())).unwrap(),
286 Style::parse("blue").unwrap()
287 );
288 }
289
290 #[test]
295 fn lookup_is_case_sensitive_but_parsing_is_not() {
296 let mut theme = Theme::new();
297 theme.insert("Danger", Style::parse("bold red").unwrap());
298
299 assert_eq!(
300 theme.get_style(&StyleType::Name("BOLD".into())).unwrap(),
301 Style::parse("bold").unwrap()
302 );
303 assert!(theme.get_style(&StyleType::Name("danger".into())).is_err());
305 assert_eq!(
306 theme.get_style(&StyleType::Name("Danger".into())).unwrap(),
307 Style::parse("bold red").unwrap()
308 );
309 }
310
311 #[test]
313 fn parse_fallback_understands_aliases() {
314 let theme = Theme::new();
315 assert_eq!(
316 theme.get_style(&StyleType::Name("b".into())).unwrap(),
317 Style::parse("bold").unwrap()
318 );
319 }
320
321 #[test]
325 fn unknown_names_error_but_render_null() {
326 let theme = Theme::default_theme();
327 let unknown = StyleType::Name("repr.nope".into());
328 assert!(theme.get_style(&unknown).is_err());
329 assert!(theme.get_style_or_null(&unknown).is_null());
330 }
331
332 #[test]
334 fn resolved_styles_pass_through() {
335 let mut theme = Theme::new();
336 theme.insert("bold", Style::parse("red").unwrap());
337 let style = Style::parse("bold").unwrap();
338 assert_eq!(
339 theme.get_style(&StyleType::Style(style.clone())).unwrap(),
340 style
341 );
342 }
343
344 #[test]
345 fn theme_covers_upstream() {
346 assert_eq!(DEFAULT_STYLES.len(), 154);
348 let mut names: Vec<&str> = DEFAULT_STYLES.iter().map(|(n, _)| *n).collect();
350 names.sort_unstable();
351 let before = names.len();
352 names.dedup();
353 assert_eq!(
354 names.len(),
355 before,
356 "duplicate style names in DEFAULT_STYLES"
357 );
358 }
359
360 #[test]
364 fn every_default_style_parses() {
365 let unparsed: Vec<&str> = DEFAULT_STYLES
366 .iter()
367 .filter(|(_, spec)| Style::parse(spec).is_err())
368 .map(|(name, _)| *name)
369 .collect();
370 assert!(
371 unparsed.is_empty(),
372 "specs that failed to parse: {unparsed:?}"
373 );
374 assert_eq!(Theme::default_theme().len(), DEFAULT_STYLES.len());
375 }
376
377 #[test]
378 fn resolves_a_few_known_styles() {
379 let theme = Theme::default_theme();
380 assert_eq!(
381 theme.get("repr.number"),
382 Style::parse("bold not italic cyan").ok().as_ref()
383 );
384 assert_eq!(
385 theme.get("markdown.table.header"),
386 Style::parse("not bold cyan").ok().as_ref()
387 );
388 assert!(theme.get("no.such.style").is_none());
389 }
390}