Skip to main content

pulldown_cmark_mdcat/
theme.rs

1// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Provide a colour theme for mdcat.
8
9use anstyle::{AnsiColor, Color, RgbColor, Style};
10
11/// A colour theme for mdcat.
12///
13/// All fields are public so themes can be fully customised, e.g. from a user config file.
14/// `h1_prefix_style` is a derived convenience (background-only padding matching `h1_text_style`'s
15/// background); use [`Theme::with_h1`] to keep the two in sync when changing the H1 background.
16#[derive(Debug, Clone, Hash, PartialEq, Eq)]
17pub struct Theme {
18    /// Style for HTML blocks.
19    pub html_block_style: Style,
20    /// Style for inline HTML.
21    pub inline_html_style: Style,
22    /// Style for code, unless the code is syntax-highlighted.
23    pub code_style: Style,
24    /// Style for links.
25    pub link_style: Style,
26    /// Color for image links (unless the image is rendered inline)
27    pub image_link_style: Style,
28    /// Color for rulers.
29    pub rule_color: Color,
30    /// Style for block quote borders (`│`).
31    pub quote_border_style: Style,
32    /// Style for H2 headings.
33    pub h2_style: Style,
34    /// Style for H3 headings.
35    pub h3_style: Style,
36    /// Style for H4 headings.
37    pub h4_style: Style,
38    /// Style for H5 headings.
39    pub h5_style: Style,
40    /// Style for H6 headings.
41    pub h6_style: Style,
42    /// Style for footnote references and definitions.
43    pub footnote_style: Style,
44    /// Style for math expressions.
45    pub math_style: Style,
46    /// Style for `[!NOTE]` alerts.
47    pub alert_note_style: Style,
48    /// Style for `[!TIP]` alerts.
49    pub alert_tip_style: Style,
50    /// Style for `[!IMPORTANT]` alerts.
51    pub alert_important_style: Style,
52    /// Style for `[!WARNING]` alerts.
53    pub alert_warning_style: Style,
54    /// Style for `[!CAUTION]` alerts.
55    pub alert_caution_style: Style,
56    /// Background-colored padding space written before H1 text.
57    pub h1_prefix_style: Style,
58    /// Style for H1 heading text (fg + bg color).
59    pub h1_text_style: Style,
60}
61
62impl Theme {
63    /// Set the H1 style, keeping `h1_prefix_style`'s background in sync with `text_style`'s.
64    pub fn with_h1(mut self, text_style: Style) -> Self {
65        let bg = text_style.get_bg_color();
66        self.h1_prefix_style = Style::new().bg_color(bg).fg_color(bg);
67        self.h1_text_style = text_style;
68        self
69    }
70}
71
72fn rgb(r: u8, g: u8, b: u8) -> Color {
73    RgbColor(r, g, b).into()
74}
75
76fn h1(bg: Color, fg: Color) -> (Style, Style) {
77    (
78        Style::new().bg_color(Some(bg)).fg_color(Some(bg)),
79        Style::new().bg_color(Some(bg)).fg_color(Some(fg)).bold(),
80    )
81}
82
83impl Theme {
84    /// A theme for dark terminal backgrounds (the default).
85    pub fn dark() -> Self {
86        let (h1_prefix_style, h1_text_style) = (
87            Style::new()
88                .bg_color(Some(AnsiColor::BrightBlue.into()))
89                .fg_color(Some(AnsiColor::BrightBlue.into())),
90            Style::new()
91                .bg_color(Some(AnsiColor::BrightBlue.into()))
92                .fg_color(Some(AnsiColor::BrightWhite.into()))
93                .bold(),
94        );
95        Self {
96            html_block_style: Style::new().fg_color(Some(AnsiColor::Green.into())),
97            inline_html_style: Style::new().fg_color(Some(AnsiColor::Green.into())),
98            code_style: Style::new().fg_color(Some(AnsiColor::Yellow.into())),
99            link_style: Style::new().fg_color(Some(AnsiColor::Blue.into())),
100            image_link_style: Style::new().fg_color(Some(AnsiColor::Magenta.into())),
101            rule_color: AnsiColor::Green.into(),
102            quote_border_style: Style::new().fg_color(Some(AnsiColor::Cyan.into())),
103            h2_style: Style::new().fg_color(Some(AnsiColor::Blue.into())).bold(),
104            h3_style: Style::new().fg_color(Some(AnsiColor::Cyan.into())).bold(),
105            h4_style: Style::new().fg_color(Some(AnsiColor::Green.into())).bold(),
106            h5_style: Style::new().fg_color(Some(AnsiColor::Yellow.into())).bold(),
107            h6_style: Style::new()
108                .fg_color(Some(AnsiColor::Magenta.into()))
109                .bold(),
110            footnote_style: Style::new().fg_color(Some(AnsiColor::Cyan.into())),
111            math_style: Style::new().fg_color(Some(AnsiColor::Yellow.into())),
112            alert_note_style: Style::new()
113                .fg_color(Some(AnsiColor::BrightBlue.into()))
114                .bold(),
115            alert_tip_style: Style::new()
116                .fg_color(Some(AnsiColor::BrightGreen.into()))
117                .bold(),
118            alert_important_style: Style::new()
119                .fg_color(Some(AnsiColor::BrightMagenta.into()))
120                .bold(),
121            alert_warning_style: Style::new()
122                .fg_color(Some(AnsiColor::BrightYellow.into()))
123                .bold(),
124            alert_caution_style: Style::new()
125                .fg_color(Some(AnsiColor::BrightRed.into()))
126                .bold(),
127            h1_prefix_style,
128            h1_text_style,
129        }
130    }
131
132    /// A theme for light terminal backgrounds.
133    pub fn light() -> Self {
134        let (h1_prefix_style, h1_text_style) = (
135            Style::new()
136                .bg_color(Some(AnsiColor::BrightCyan.into()))
137                .fg_color(Some(AnsiColor::BrightCyan.into())),
138            Style::new()
139                .bg_color(Some(AnsiColor::BrightCyan.into()))
140                .fg_color(Some(AnsiColor::Black.into()))
141                .bold(),
142        );
143        Self {
144            html_block_style: Style::new().fg_color(Some(AnsiColor::Green.into())),
145            inline_html_style: Style::new().fg_color(Some(AnsiColor::Green.into())),
146            code_style: Style::new().fg_color(Some(AnsiColor::Blue.into())),
147            link_style: Style::new().fg_color(Some(AnsiColor::Blue.into())),
148            image_link_style: Style::new().fg_color(Some(AnsiColor::Magenta.into())),
149            rule_color: AnsiColor::Green.into(),
150            quote_border_style: Style::new().fg_color(Some(AnsiColor::Cyan.into())),
151            h2_style: Style::new()
152                .fg_color(Some(AnsiColor::Magenta.into()))
153                .bold(),
154            h3_style: Style::new().fg_color(Some(AnsiColor::Blue.into())).bold(),
155            h4_style: Style::new().fg_color(Some(AnsiColor::Cyan.into())).bold(),
156            h5_style: Style::new().fg_color(Some(AnsiColor::Green.into())).bold(),
157            h6_style: Style::new().fg_color(Some(AnsiColor::Red.into())).bold(),
158            footnote_style: Style::new().fg_color(Some(AnsiColor::Cyan.into())),
159            math_style: Style::new().fg_color(Some(AnsiColor::Blue.into())),
160            alert_note_style: Style::new().fg_color(Some(AnsiColor::Blue.into())).bold(),
161            alert_tip_style: Style::new().fg_color(Some(AnsiColor::Green.into())).bold(),
162            alert_important_style: Style::new()
163                .fg_color(Some(AnsiColor::Magenta.into()))
164                .bold(),
165            alert_warning_style: Style::new().fg_color(Some(AnsiColor::Red.into())).bold(),
166            alert_caution_style: Style::new()
167                .fg_color(Some(AnsiColor::BrightRed.into()))
168                .bold(),
169            h1_prefix_style,
170            h1_text_style,
171        }
172    }
173
174    /// Catppuccin Mocha (dark).
175    pub fn catppuccin_mocha() -> Self {
176        let (h1_prefix_style, h1_text_style) = h1(rgb(203, 166, 247), rgb(17, 17, 27));
177        Self {
178            html_block_style: Style::new().fg_color(Some(rgb(250, 179, 135))),
179            inline_html_style: Style::new().fg_color(Some(rgb(250, 179, 135))),
180            code_style: Style::new().fg_color(Some(rgb(137, 220, 235))),
181            link_style: Style::new().fg_color(Some(rgb(137, 180, 250))),
182            image_link_style: Style::new().fg_color(Some(rgb(245, 194, 231))),
183            rule_color: rgb(148, 226, 213),
184            quote_border_style: Style::new().fg_color(Some(rgb(108, 112, 134))),
185            h2_style: Style::new().fg_color(Some(rgb(203, 166, 247))).bold(),
186            h3_style: Style::new().fg_color(Some(rgb(137, 180, 250))).bold(),
187            h4_style: Style::new().fg_color(Some(rgb(137, 220, 235))).bold(),
188            h5_style: Style::new().fg_color(Some(rgb(166, 227, 161))).bold(),
189            h6_style: Style::new().fg_color(Some(rgb(250, 179, 135))).bold(),
190            footnote_style: Style::new().fg_color(Some(rgb(108, 112, 134))),
191            math_style: Style::new().fg_color(Some(rgb(137, 220, 235))),
192            alert_note_style: Style::new().fg_color(Some(rgb(116, 199, 236))).bold(),
193            alert_tip_style: Style::new().fg_color(Some(rgb(166, 227, 161))).bold(),
194            alert_important_style: Style::new().fg_color(Some(rgb(203, 166, 247))).bold(),
195            alert_warning_style: Style::new().fg_color(Some(rgb(249, 226, 175))).bold(),
196            alert_caution_style: Style::new().fg_color(Some(rgb(243, 139, 168))).bold(),
197            h1_prefix_style,
198            h1_text_style,
199        }
200    }
201
202    /// Catppuccin Latte (light).
203    pub fn catppuccin_latte() -> Self {
204        let (h1_prefix_style, h1_text_style) = h1(rgb(136, 57, 239), rgb(239, 241, 245));
205        Self {
206            html_block_style: Style::new().fg_color(Some(rgb(254, 100, 11))),
207            inline_html_style: Style::new().fg_color(Some(rgb(254, 100, 11))),
208            code_style: Style::new().fg_color(Some(rgb(4, 165, 229))),
209            link_style: Style::new().fg_color(Some(rgb(30, 102, 245))),
210            image_link_style: Style::new().fg_color(Some(rgb(234, 118, 203))),
211            rule_color: rgb(23, 146, 153),
212            quote_border_style: Style::new().fg_color(Some(rgb(124, 127, 147))),
213            h2_style: Style::new().fg_color(Some(rgb(136, 57, 239))).bold(),
214            h3_style: Style::new().fg_color(Some(rgb(30, 102, 245))).bold(),
215            h4_style: Style::new().fg_color(Some(rgb(23, 146, 153))).bold(),
216            h5_style: Style::new().fg_color(Some(rgb(64, 160, 43))).bold(),
217            h6_style: Style::new().fg_color(Some(rgb(254, 100, 11))).bold(),
218            footnote_style: Style::new().fg_color(Some(rgb(124, 127, 147))),
219            math_style: Style::new().fg_color(Some(rgb(4, 165, 229))),
220            alert_note_style: Style::new().fg_color(Some(rgb(32, 159, 181))).bold(),
221            alert_tip_style: Style::new().fg_color(Some(rgb(64, 160, 43))).bold(),
222            alert_important_style: Style::new().fg_color(Some(rgb(136, 57, 239))).bold(),
223            alert_warning_style: Style::new().fg_color(Some(rgb(223, 142, 29))).bold(),
224            alert_caution_style: Style::new().fg_color(Some(rgb(210, 15, 57))).bold(),
225            h1_prefix_style,
226            h1_text_style,
227        }
228    }
229
230    /// Gruvbox Dark.
231    pub fn gruvbox_dark() -> Self {
232        let (h1_prefix_style, h1_text_style) = h1(rgb(250, 189, 47), rgb(40, 40, 40));
233        Self {
234            html_block_style: Style::new().fg_color(Some(rgb(214, 93, 14))),
235            inline_html_style: Style::new().fg_color(Some(rgb(214, 93, 14))),
236            code_style: Style::new().fg_color(Some(rgb(131, 165, 152))),
237            link_style: Style::new().fg_color(Some(rgb(131, 165, 152))),
238            image_link_style: Style::new().fg_color(Some(rgb(211, 134, 155))),
239            rule_color: rgb(184, 187, 38),
240            quote_border_style: Style::new().fg_color(Some(rgb(142, 192, 124))),
241            h2_style: Style::new().fg_color(Some(rgb(250, 189, 47))).bold(),
242            h3_style: Style::new().fg_color(Some(rgb(131, 165, 152))).bold(),
243            h4_style: Style::new().fg_color(Some(rgb(142, 192, 124))).bold(),
244            h5_style: Style::new().fg_color(Some(rgb(211, 134, 155))).bold(),
245            h6_style: Style::new().fg_color(Some(rgb(254, 128, 25))).bold(),
246            footnote_style: Style::new().fg_color(Some(rgb(146, 131, 116))),
247            math_style: Style::new().fg_color(Some(rgb(131, 165, 152))),
248            alert_note_style: Style::new().fg_color(Some(rgb(131, 165, 152))).bold(),
249            alert_tip_style: Style::new().fg_color(Some(rgb(184, 187, 38))).bold(),
250            alert_important_style: Style::new().fg_color(Some(rgb(211, 134, 155))).bold(),
251            alert_warning_style: Style::new().fg_color(Some(rgb(254, 128, 25))).bold(),
252            alert_caution_style: Style::new().fg_color(Some(rgb(251, 73, 52))).bold(),
253            h1_prefix_style,
254            h1_text_style,
255        }
256    }
257
258    /// Gruvbox Light.
259    pub fn gruvbox_light() -> Self {
260        let (h1_prefix_style, h1_text_style) = h1(rgb(181, 118, 20), rgb(251, 241, 199));
261        Self {
262            html_block_style: Style::new().fg_color(Some(rgb(175, 58, 3))),
263            inline_html_style: Style::new().fg_color(Some(rgb(175, 58, 3))),
264            code_style: Style::new().fg_color(Some(rgb(7, 102, 120))),
265            link_style: Style::new().fg_color(Some(rgb(7, 102, 120))),
266            image_link_style: Style::new().fg_color(Some(rgb(143, 63, 113))),
267            rule_color: rgb(66, 123, 88),
268            quote_border_style: Style::new().fg_color(Some(rgb(66, 123, 88))),
269            h2_style: Style::new().fg_color(Some(rgb(181, 118, 20))).bold(),
270            h3_style: Style::new().fg_color(Some(rgb(7, 102, 120))).bold(),
271            h4_style: Style::new().fg_color(Some(rgb(66, 123, 88))).bold(),
272            h5_style: Style::new().fg_color(Some(rgb(143, 63, 113))).bold(),
273            h6_style: Style::new().fg_color(Some(rgb(175, 58, 3))).bold(),
274            footnote_style: Style::new().fg_color(Some(rgb(124, 111, 100))),
275            math_style: Style::new().fg_color(Some(rgb(7, 102, 120))),
276            alert_note_style: Style::new().fg_color(Some(rgb(7, 102, 120))).bold(),
277            alert_tip_style: Style::new().fg_color(Some(rgb(121, 116, 14))).bold(),
278            alert_important_style: Style::new().fg_color(Some(rgb(143, 63, 113))).bold(),
279            alert_warning_style: Style::new().fg_color(Some(rgb(181, 118, 20))).bold(),
280            alert_caution_style: Style::new().fg_color(Some(rgb(157, 0, 6))).bold(),
281            h1_prefix_style,
282            h1_text_style,
283        }
284    }
285
286    /// Dracula.
287    pub fn dracula() -> Self {
288        let (h1_prefix_style, h1_text_style) = h1(rgb(189, 147, 249), rgb(40, 42, 54));
289        Self {
290            html_block_style: Style::new().fg_color(Some(rgb(255, 184, 108))),
291            inline_html_style: Style::new().fg_color(Some(rgb(255, 184, 108))),
292            code_style: Style::new().fg_color(Some(rgb(241, 250, 140))),
293            link_style: Style::new().fg_color(Some(rgb(139, 233, 253))),
294            image_link_style: Style::new().fg_color(Some(rgb(255, 121, 198))),
295            rule_color: rgb(80, 250, 123),
296            quote_border_style: Style::new().fg_color(Some(rgb(98, 114, 164))),
297            h2_style: Style::new().fg_color(Some(rgb(189, 147, 249))).bold(),
298            h3_style: Style::new().fg_color(Some(rgb(139, 233, 253))).bold(),
299            h4_style: Style::new().fg_color(Some(rgb(80, 250, 123))).bold(),
300            h5_style: Style::new().fg_color(Some(rgb(255, 121, 198))).bold(),
301            h6_style: Style::new().fg_color(Some(rgb(255, 184, 108))).bold(),
302            footnote_style: Style::new().fg_color(Some(rgb(98, 114, 164))),
303            math_style: Style::new().fg_color(Some(rgb(241, 250, 140))),
304            alert_note_style: Style::new().fg_color(Some(rgb(139, 233, 253))).bold(),
305            alert_tip_style: Style::new().fg_color(Some(rgb(80, 250, 123))).bold(),
306            alert_important_style: Style::new().fg_color(Some(rgb(255, 121, 198))).bold(),
307            alert_warning_style: Style::new().fg_color(Some(rgb(255, 184, 108))).bold(),
308            alert_caution_style: Style::new().fg_color(Some(rgb(255, 85, 85))).bold(),
309            h1_prefix_style,
310            h1_text_style,
311        }
312    }
313
314    /// Nord.
315    pub fn nord() -> Self {
316        let (h1_prefix_style, h1_text_style) = h1(rgb(94, 129, 172), rgb(236, 239, 244));
317        Self {
318            html_block_style: Style::new().fg_color(Some(rgb(208, 135, 112))),
319            inline_html_style: Style::new().fg_color(Some(rgb(208, 135, 112))),
320            code_style: Style::new().fg_color(Some(rgb(143, 188, 187))),
321            link_style: Style::new().fg_color(Some(rgb(136, 192, 208))),
322            image_link_style: Style::new().fg_color(Some(rgb(180, 142, 173))),
323            rule_color: rgb(136, 192, 208),
324            quote_border_style: Style::new().fg_color(Some(rgb(76, 86, 106))),
325            h2_style: Style::new().fg_color(Some(rgb(94, 129, 172))).bold(),
326            h3_style: Style::new().fg_color(Some(rgb(129, 161, 193))).bold(),
327            h4_style: Style::new().fg_color(Some(rgb(136, 192, 208))).bold(),
328            h5_style: Style::new().fg_color(Some(rgb(163, 190, 140))).bold(),
329            h6_style: Style::new().fg_color(Some(rgb(235, 203, 139))).bold(),
330            footnote_style: Style::new().fg_color(Some(rgb(76, 86, 106))),
331            math_style: Style::new().fg_color(Some(rgb(143, 188, 187))),
332            alert_note_style: Style::new().fg_color(Some(rgb(143, 188, 187))).bold(),
333            alert_tip_style: Style::new().fg_color(Some(rgb(163, 190, 140))).bold(),
334            alert_important_style: Style::new().fg_color(Some(rgb(180, 142, 173))).bold(),
335            alert_warning_style: Style::new().fg_color(Some(rgb(235, 203, 139))).bold(),
336            alert_caution_style: Style::new().fg_color(Some(rgb(191, 97, 106))).bold(),
337            h1_prefix_style,
338            h1_text_style,
339        }
340    }
341
342    /// Solarized Dark.
343    pub fn solarized_dark() -> Self {
344        let (h1_prefix_style, h1_text_style) = h1(rgb(38, 139, 210), rgb(0, 43, 54));
345        Self {
346            html_block_style: Style::new().fg_color(Some(rgb(203, 75, 22))),
347            inline_html_style: Style::new().fg_color(Some(rgb(203, 75, 22))),
348            code_style: Style::new().fg_color(Some(rgb(42, 161, 152))),
349            link_style: Style::new().fg_color(Some(rgb(38, 139, 210))),
350            image_link_style: Style::new().fg_color(Some(rgb(211, 54, 130))),
351            rule_color: rgb(42, 161, 152),
352            quote_border_style: Style::new().fg_color(Some(rgb(88, 110, 117))),
353            h2_style: Style::new().fg_color(Some(rgb(38, 139, 210))).bold(),
354            h3_style: Style::new().fg_color(Some(rgb(42, 161, 152))).bold(),
355            h4_style: Style::new().fg_color(Some(rgb(133, 153, 0))).bold(),
356            h5_style: Style::new().fg_color(Some(rgb(211, 54, 130))).bold(),
357            h6_style: Style::new().fg_color(Some(rgb(203, 75, 22))).bold(),
358            footnote_style: Style::new().fg_color(Some(rgb(88, 110, 117))),
359            math_style: Style::new().fg_color(Some(rgb(42, 161, 152))),
360            alert_note_style: Style::new().fg_color(Some(rgb(108, 113, 196))).bold(),
361            alert_tip_style: Style::new().fg_color(Some(rgb(133, 153, 0))).bold(),
362            alert_important_style: Style::new().fg_color(Some(rgb(211, 54, 130))).bold(),
363            alert_warning_style: Style::new().fg_color(Some(rgb(181, 137, 0))).bold(),
364            alert_caution_style: Style::new().fg_color(Some(rgb(220, 50, 47))).bold(),
365            h1_prefix_style,
366            h1_text_style,
367        }
368    }
369
370    /// Solarized Light.
371    pub fn solarized_light() -> Self {
372        let (h1_prefix_style, h1_text_style) = h1(rgb(38, 139, 210), rgb(253, 246, 227));
373        Self {
374            h1_prefix_style,
375            h1_text_style,
376            ..Self::solarized_dark()
377        }
378    }
379}
380
381impl Default for Theme {
382    fn default() -> Self {
383        Self::dark()
384    }
385}
386
387/// Combine styles.
388pub trait CombineStyle {
389    /// Put this style on top of the other style.
390    ///
391    /// Return a new style which falls back to the `other` style for all style attributes not
392    /// specified in this style.
393    fn on_top_of(self, other: &Self) -> Self;
394}
395
396impl CombineStyle for Style {
397    /// Put this style on top of the `other` style.
398    fn on_top_of(self, other: &Style) -> Style {
399        Style::new()
400            .fg_color(self.get_fg_color().or(other.get_fg_color()))
401            .bg_color(self.get_bg_color().or(other.get_bg_color()))
402            .effects(other.get_effects() | self.get_effects())
403            .underline_color(self.get_underline_color().or(other.get_underline_color()))
404    }
405}