Skip to main content

evault_tui/
theme.rs

1//! Semantic colour palette used by every view.
2//!
3//! Views never pick colours directly — they ask the [`Theme`] for a
4//! named role (`ok`, `error`, `warning`, `accent`, …). Centralising
5//! the mapping makes it trivial to swap palettes or add a light theme
6//! without touching individual widgets.
7
8use ratatui::style::{Color, Modifier, Style};
9
10/// Semantic palette.
11///
12/// Field names describe the *role* a colour plays, not the colour
13/// itself. The [`Self::dark`] constructor wires a default dark-theme
14/// palette tuned for legibility on a typical terminal background.
15///
16/// # Examples
17/// ```
18/// use evault_tui::Theme;
19/// let theme = Theme::dark();
20/// let _header_style = theme.header();
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Theme {
24    /// Success — green.
25    pub ok: Color,
26    /// Failure / validation error — red.
27    pub error: Color,
28    /// Soft warning (missing link, unset secret) — yellow.
29    pub warning: Color,
30    /// Highlight for rows added during this session — cyan.
31    pub new: Color,
32    /// Highlight for rows modified during this session — magenta.
33    pub modified: Color,
34    /// Indicator colour for masked / secret values — dark gray.
35    pub secret: Color,
36    /// Background colour applied to the selected row.
37    pub selected: Color,
38    /// Secondary text — timestamps, hints, metadata.
39    pub dim: Color,
40    /// Accent — titles, separators, key chord hints.
41    pub accent: Color,
42}
43
44impl Theme {
45    /// Default dark-theme palette.
46    #[must_use]
47    pub const fn dark() -> Self {
48        Self {
49            ok: Color::Green,
50            error: Color::Red,
51            warning: Color::Yellow,
52            new: Color::Cyan,
53            modified: Color::Magenta,
54            secret: Color::DarkGray,
55            selected: Color::Indexed(238),
56            dim: Color::Gray,
57            accent: Color::Blue,
58        }
59    }
60
61    /// Style applied to the dashboard header row.
62    #[must_use]
63    pub const fn header(&self) -> Style {
64        Style::new().fg(self.accent).add_modifier(Modifier::BOLD)
65    }
66
67    /// Style applied to the currently selected table row.
68    #[must_use]
69    pub const fn selected_row(&self) -> Style {
70        Style::new().bg(self.selected).add_modifier(Modifier::BOLD)
71    }
72
73    /// Style for a value that is hidden because it is a secret.
74    #[must_use]
75    pub const fn secret_cell(&self) -> Style {
76        Style::new().fg(self.secret).add_modifier(Modifier::DIM)
77    }
78
79    /// Style for a dim / metadata cell (timestamps, counts).
80    #[must_use]
81    pub const fn dim_cell(&self) -> Style {
82        Style::new().fg(self.dim)
83    }
84
85    /// Style for an error toast (red foreground on default background).
86    #[must_use]
87    pub const fn error_toast(&self) -> Style {
88        Style::new().fg(self.error).add_modifier(Modifier::BOLD)
89    }
90
91    /// Style for an informational toast.
92    #[must_use]
93    pub const fn info_toast(&self) -> Style {
94        Style::new().fg(self.ok)
95    }
96}
97
98impl Default for Theme {
99    fn default() -> Self {
100        Self::dark()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn default_equals_dark() {
110        assert_eq!(Theme::default(), Theme::dark());
111    }
112
113    #[test]
114    fn dark_palette_distinguishes_ok_from_error() {
115        let t = Theme::dark();
116        assert_ne!(t.ok, t.error);
117        assert_ne!(t.warning, t.error);
118    }
119}