Skip to main content

demand/
theme.rs

1use std::sync::LazyLock;
2
3use termcolor::{Color, ColorSpec};
4
5pub(crate) static DEFAULT: LazyLock<Theme> = LazyLock::new(Theme::default);
6
7#[derive(Clone, Debug)]
8pub enum CursorShape {
9    Block,
10    Underline,
11}
12
13/// Theme for styling the UI.
14///
15/// # Example
16///
17/// ```
18/// use demand::Theme;
19///
20/// let custom_theme = Theme {
21///   selected_prefix: String::from(" •"),
22///   unselected_prefix: String::from("  "),
23/// ..Theme::default()
24/// };
25/// ```
26#[derive(Clone, Debug)]
27pub struct Theme {
28    /// Prompt title color
29    pub title: ColorSpec,
30    /// Prompt description color
31    pub description: ColorSpec,
32    /// Cursor color
33    pub cursor: ColorSpec,
34    /// Cursor string e.g. "❯ "
35    pub cursor_str: String,
36
37    /// Selected option color
38    pub selected_option: ColorSpec,
39    /// Selected option prefix color
40    pub selected_prefix: String,
41    /// Selected prefix foreground color
42    pub selected_prefix_fg: ColorSpec,
43    /// Unselected option color
44    pub unselected_option: ColorSpec,
45    /// Unselected option prefix color
46    pub unselected_prefix: String,
47    /// Unselected prefix foreground color
48    pub unselected_prefix_fg: ColorSpec,
49
50    /// Char to use for the cursor
51    pub cursor_shape: CursorShape,
52    /// the color when there isn't text to get color from
53    pub cursor_style: ColorSpec,
54    /// use cursor_style even when there is text to get color from
55    pub force_style: bool,
56
57    /// Input cursor color
58    pub input_cursor: ColorSpec,
59    /// Input placeholder color
60    pub input_placeholder: ColorSpec,
61    /// Input prompt color
62    pub input_prompt: ColorSpec,
63
64    /// Help item key color
65    pub help_key: ColorSpec,
66    /// Help item description color
67    pub help_desc: ColorSpec,
68    /// Help item separator color
69    pub help_sep: ColorSpec,
70
71    /// Focused button color
72    pub focused_button: ColorSpec,
73    /// Blurred button color
74    pub blurred_button: ColorSpec,
75
76    /// Error indicator color
77    pub error_indicator: ColorSpec,
78}
79
80impl Theme {
81    /// Create a new theme with no colors.
82    pub fn new() -> Self {
83        let placeholder = Color::Ansi256(8);
84
85        let mut focused_button = make_color(Color::Ansi256(0));
86        focused_button.set_bg(Some(Color::Ansi256(7)));
87
88        let mut blurred_button = make_color(Color::Ansi256(7));
89        blurred_button.set_bg(Some(Color::Ansi256(0)));
90
91        // TODO: theme them
92        let mut cursor_style = ColorSpec::new();
93        cursor_style
94            .set_fg(Some(Color::White))
95            .set_bg(Some(Color::Black));
96
97        Self {
98            title: ColorSpec::new(),
99            error_indicator: ColorSpec::new(),
100            description: ColorSpec::new(),
101            cursor: ColorSpec::new(),
102            cursor_str: String::from("❯"),
103            selected_prefix: String::from("[•]"),
104            selected_prefix_fg: ColorSpec::new(),
105            selected_option: ColorSpec::new(),
106            unselected_prefix: String::from("[ ]"),
107            unselected_prefix_fg: ColorSpec::new(),
108            unselected_option: ColorSpec::new(),
109            input_cursor: ColorSpec::new(),
110            input_placeholder: make_color(placeholder),
111            input_prompt: ColorSpec::new(),
112            help_key: ColorSpec::new(),
113            help_desc: ColorSpec::new(),
114            help_sep: ColorSpec::new(),
115            focused_button,
116            blurred_button,
117
118            // TODO: theme these
119            cursor_shape: CursorShape::Block,
120            cursor_style,
121            force_style: true,
122        }
123    }
124
125    pub fn real_cursor_color(&self, other: Option<&ColorSpec>) -> ColorSpec {
126        // let mut c = self.input_cursor.clone();
127        let other = if self.force_style {
128            &self.cursor_style
129        } else {
130            other.unwrap_or(&self.cursor_style)
131        };
132
133        let mut c = ColorSpec::new();
134        match self.cursor_shape {
135            CursorShape::Block => {
136                c.set_bg(other.fg().copied());
137                c.set_fg(other.bg().copied());
138            }
139            CursorShape::Underline => {
140                c.set_bg(other.bg().copied());
141                c.set_fg(other.fg().copied());
142                c.set_underline(true);
143            }
144        }
145        // c.set_fg(self.input_cursor.bg().copied())
146        //     .set_bg(self.input_cursor.bg().copied());
147        c
148    }
149
150    /// Create a new theme with the charm color scheme
151    pub fn charm() -> Self {
152        let normal = Color::Ansi256(252);
153        let indigo = Color::Rgb(117, 113, 249);
154        let red = Color::Rgb(255, 70, 114);
155        let fuchsia = Color::Rgb(247, 128, 226);
156        let green = Color::Rgb(2, 191, 135);
157        let cream = Color::Rgb(255, 253, 245);
158
159        let mut title = make_color(indigo);
160        title.set_bold(true);
161
162        let mut focused_button = make_color(cream);
163        focused_button.set_bg(Some(fuchsia));
164
165        let mut blurred_button = make_color(normal);
166        blurred_button.set_bg(Some(Color::Ansi256(238)));
167
168        // TODO: theme them
169        let mut cursor_style = ColorSpec::new();
170        cursor_style
171            .set_fg(Some(Color::White))
172            .set_bg(Some(Color::Black));
173
174        Self {
175            title,
176            error_indicator: make_color(red),
177            description: make_color(Color::Ansi256(243)),
178            cursor: make_color(fuchsia),
179            cursor_str: String::from("❯"),
180
181            selected_prefix: String::from(" ✓"),
182            selected_prefix_fg: make_color(Color::Rgb(2, 168, 119)),
183            selected_option: make_color(green),
184            unselected_prefix: String::from(" •"),
185            unselected_prefix_fg: make_color(Color::Ansi256(243)),
186            unselected_option: make_color(normal),
187
188            input_cursor: make_color(green),
189            input_placeholder: make_color(Color::Ansi256(238)),
190            input_prompt: make_color(fuchsia),
191
192            help_key: make_color(Color::Rgb(98, 98, 98)),
193            help_desc: make_color(Color::Rgb(74, 74, 74)),
194            help_sep: make_color(Color::Rgb(60, 60, 60)),
195
196            focused_button,
197            blurred_button,
198
199            // TODO: theme these
200            cursor_shape: CursorShape::Block,
201            cursor_style,
202            force_style: true,
203        }
204    }
205
206    /// Create a new theme with the dracula color scheme
207    pub fn dracula() -> Self {
208        let background = Color::Rgb(40, 42, 54); // #282a36
209        let foreground = Color::Rgb(248, 248, 242); // #f8f8f2
210        let comment = Color::Rgb(98, 114, 164); // #6272a4
211        let green = Color::Rgb(80, 250, 123); // #50fa7b
212        let purple = Color::Rgb(189, 147, 249); // #bd93f9
213        let red = Color::Rgb(255, 85, 85); // ff5555
214        let yellow = Color::Rgb(241, 250, 140); // f1fa8c
215
216        let mut title = make_color(purple);
217        title.set_bold(true);
218
219        let mut focused_button = make_color(yellow);
220        focused_button.set_bg(Some(purple));
221
222        let mut blurred_button = make_color(foreground);
223        blurred_button.set_bg(Some(background));
224
225        // TODO: theme them
226        let mut cursor_style = ColorSpec::new();
227        cursor_style
228            .set_fg(Some(Color::White))
229            .set_bg(Some(Color::Black));
230
231        Self {
232            title,
233            error_indicator: make_color(red),
234            description: make_color(comment),
235            cursor: make_color(yellow),
236            cursor_str: String::from("❯"),
237
238            selected_prefix: String::from(" [•]"),
239            selected_prefix_fg: make_color(green),
240            selected_option: make_color(green),
241            unselected_prefix: String::from(" [ ]"),
242            unselected_prefix_fg: make_color(comment),
243            unselected_option: make_color(foreground),
244
245            input_cursor: make_color(yellow),
246            input_placeholder: make_color(comment),
247            input_prompt: make_color(yellow),
248
249            help_key: make_color(Color::Rgb(98, 98, 98)),
250            help_desc: make_color(Color::Rgb(74, 74, 74)),
251            help_sep: make_color(Color::Rgb(60, 60, 60)),
252
253            focused_button,
254            blurred_button,
255
256            // TODO: theme these
257            cursor_shape: CursorShape::Block,
258            cursor_style,
259            force_style: true,
260        }
261    }
262
263    /// Create a new theme with the base16 color scheme
264    pub fn base16() -> Self {
265        let mut title = make_color(Color::Ansi256(6));
266        title.set_bold(true);
267
268        let mut focused_button = make_color(Color::Ansi256(7));
269        focused_button.set_bg(Some(Color::Ansi256(5)));
270
271        let mut blurred_button = make_color(Color::Ansi256(7));
272        blurred_button.set_bg(Some(Color::Ansi256(0)));
273
274        // TODO: theme them
275        let mut cursor_style = ColorSpec::new();
276        cursor_style
277            .set_fg(Some(Color::White))
278            .set_bg(Some(Color::Black));
279
280        Self {
281            title,
282            error_indicator: make_color(Color::Ansi256(9)),
283            description: make_color(Color::Ansi256(8)),
284            cursor: make_color(Color::Ansi256(3)),
285            cursor_str: String::from("❯"),
286
287            selected_prefix: String::from(" [•]"),
288            selected_prefix_fg: make_color(Color::Ansi256(2)),
289            selected_option: make_color(Color::Ansi256(2)),
290            unselected_prefix: String::from(" [ ]"),
291            unselected_prefix_fg: make_color(Color::Ansi256(7)),
292            unselected_option: make_color(Color::Ansi256(7)),
293
294            input_cursor: make_color(Color::Ansi256(5)),
295            input_placeholder: make_color(Color::Ansi256(8)),
296            input_prompt: make_color(Color::Ansi256(3)),
297
298            help_key: make_color(Color::Rgb(98, 98, 98)),
299            help_desc: make_color(Color::Rgb(74, 74, 74)),
300            help_sep: make_color(Color::Rgb(60, 60, 60)),
301
302            focused_button,
303            blurred_button,
304
305            // TODO: theme these
306            cursor_shape: CursorShape::Block,
307            cursor_style,
308            force_style: true,
309        }
310    }
311
312    /// Create a new theme with the catppuccin color scheme
313    pub fn catppuccin() -> Self {
314        let base = Color::Rgb(30, 30, 46);
315        let text = Color::Rgb(205, 214, 244);
316        let subtext0 = Color::Rgb(166, 173, 200);
317        let overlay0 = Color::Rgb(108, 112, 134);
318        let overlay1 = Color::Rgb(127, 132, 156);
319        let green = Color::Rgb(166, 227, 161);
320        let red = Color::Rgb(243, 139, 168);
321        let pink = Color::Rgb(245, 194, 231);
322        let mauve = Color::Rgb(203, 166, 247);
323        let cursor = Color::Rgb(245, 224, 220);
324
325        let mut title = make_color(mauve);
326        title.set_bold(true);
327
328        let mut focused_button = make_color(base);
329        focused_button.set_bg(Some(pink));
330
331        let mut blurred_button = make_color(text);
332        blurred_button.set_bg(Some(base));
333
334        // TODO: theme them
335        let mut cursor_style = ColorSpec::new();
336        cursor_style
337            .set_fg(Some(Color::White))
338            .set_bg(Some(Color::Black));
339
340        Self {
341            title,
342            error_indicator: make_color(red),
343            description: make_color(subtext0),
344            cursor: make_color(pink),
345            cursor_str: String::from("❯"),
346
347            selected_prefix: String::from(" [•]"),
348            selected_prefix_fg: make_color(green),
349            selected_option: make_color(green),
350            unselected_prefix: String::from(" [ ]"),
351            unselected_prefix_fg: make_color(text),
352            unselected_option: make_color(text),
353
354            input_cursor: make_color(cursor),
355            input_placeholder: make_color(overlay0),
356            input_prompt: make_color(pink),
357
358            help_key: make_color(subtext0),
359            help_desc: make_color(overlay1),
360            help_sep: make_color(subtext0),
361
362            focused_button,
363            blurred_button,
364
365            // TODO: theme these
366            cursor_shape: CursorShape::Block,
367            cursor_style,
368            force_style: true,
369        }
370    }
371
372    /// Create a new color with foreground color from an RGB value.
373    pub fn color_rgb(r: u8, g: u8, b: u8) -> ColorSpec {
374        make_color(Color::Rgb(r, g, b))
375    }
376
377    /// Create a new color with foreground color from an ANSI 256 color code.
378    pub fn color_ansi256(n: u8) -> ColorSpec {
379        make_color(Color::Ansi256(n))
380    }
381}
382
383impl Default for Theme {
384    fn default() -> Self {
385        if console::colors_enabled_stderr() {
386            Theme::charm()
387        } else {
388            Theme::new()
389        }
390    }
391}
392
393fn make_color(color: Color) -> ColorSpec {
394    let mut spec = ColorSpec::new();
395    spec.set_fg(Some(color));
396    spec
397}