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}