Skip to main content

aranet_cli/tui/ui/
theme.rs

1//! Centralized theme system for the TUI.
2
3use aranet_types::Status;
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::widgets::BorderType;
6
7use crate::tui::app::ConnectionStatus;
8
9/// Application theme with all UI colors.
10///
11/// Colors are based on the Tailwind CSS palette for consistency.
12#[derive(Debug, Clone, Copy)]
13pub struct AppTheme {
14    // Primary colors
15    pub primary: Color,
16
17    // Status colors
18    pub success: Color,
19    pub warning: Color,
20    pub caution: Color,
21    pub danger: Color,
22    pub info: Color,
23
24    // Sensor/series colors
25    pub sensor_temperature: Color,
26    pub sensor_humidity: Color,
27    pub sensor_pressure: Color,
28    pub sensor_radiation: Color,
29    pub series_co2: Color,
30    pub series_radon: Color,
31    pub series_radiation: Color,
32
33    // Trend and signal colors
34    pub trend_rising: Color,
35    pub trend_falling: Color,
36    pub trend_stable: Color,
37    pub signal_excellent: Color,
38    pub signal_good: Color,
39    pub signal_fair: Color,
40    pub signal_weak: Color,
41    pub signal_offline: Color,
42
43    // Text colors
44    pub text_primary: Color,
45    pub text_secondary: Color,
46    pub text_muted: Color,
47
48    // Border colors
49    pub border_active: Color,
50    pub border_inactive: Color,
51
52    // Background colors
53    pub bg_selected: Color,
54    pub bg_header: Color,
55}
56
57impl Default for AppTheme {
58    fn default() -> Self {
59        Self::dark()
60    }
61}
62
63impl AppTheme {
64    /// Dark theme using Tailwind-inspired colors.
65    #[must_use]
66    pub const fn dark() -> Self {
67        Self {
68            // Primary: Cyan/Teal
69            primary: Color::Rgb(34, 211, 238), // cyan-400
70
71            // Status colors
72            success: Color::Rgb(74, 222, 128), // green-400
73            warning: Color::Rgb(251, 191, 36), // amber-400
74            caution: Color::Rgb(251, 146, 60), // orange-400
75            danger: Color::Rgb(248, 113, 113), // red-400
76            info: Color::Rgb(96, 165, 250),    // blue-400
77
78            // Sensor/series colors
79            sensor_temperature: Color::Rgb(251, 191, 36), // amber-400
80            sensor_humidity: Color::Rgb(96, 165, 250),    // blue-400
81            sensor_pressure: Color::Rgb(248, 250, 252),   // slate-50
82            sensor_radiation: Color::Rgb(217, 70, 239),   // fuchsia-500
83            series_co2: Color::Rgb(74, 222, 128),         // green-400
84            series_radon: Color::Rgb(34, 211, 238),       // cyan-400
85            series_radiation: Color::Rgb(217, 70, 239),   // fuchsia-500
86
87            // Trend and signal colors
88            trend_rising: Color::Rgb(248, 113, 113), // red-400
89            trend_falling: Color::Rgb(74, 222, 128), // green-400
90            trend_stable: Color::Rgb(100, 116, 139), // slate-500
91            signal_excellent: Color::Rgb(74, 222, 128), // green-400
92            signal_good: Color::Rgb(74, 222, 128),   // green-400
93            signal_fair: Color::Rgb(251, 191, 36),   // amber-400
94            signal_weak: Color::Rgb(248, 113, 113),  // red-400
95            signal_offline: Color::Rgb(100, 116, 139), // slate-500
96
97            // Text
98            text_primary: Color::Rgb(248, 250, 252), // slate-50
99            text_secondary: Color::Rgb(148, 163, 184), // slate-400
100            text_muted: Color::Rgb(100, 116, 139),   // slate-500
101
102            // Borders
103            border_active: Color::Rgb(34, 211, 238), // cyan-400
104            border_inactive: Color::Rgb(71, 85, 105), // slate-600
105
106            // Backgrounds
107            bg_selected: Color::Rgb(51, 65, 85), // slate-700
108            bg_header: Color::Rgb(30, 41, 59),   // slate-800
109        }
110    }
111
112    /// Light theme using Tailwind-inspired colors.
113    #[must_use]
114    pub const fn light() -> Self {
115        Self {
116            // Primary: Cyan/Teal (darker for light theme)
117            primary: Color::Rgb(6, 182, 212), // cyan-500
118
119            // Status colors (darker for readability)
120            success: Color::Rgb(22, 163, 74), // green-600
121            warning: Color::Rgb(217, 119, 6), // amber-600
122            caution: Color::Rgb(234, 88, 12), // orange-600
123            danger: Color::Rgb(220, 38, 38),  // red-600
124            info: Color::Rgb(37, 99, 235),    // blue-600
125
126            // Sensor/series colors
127            sensor_temperature: Color::Rgb(217, 119, 6), // amber-600
128            sensor_humidity: Color::Rgb(37, 99, 235),    // blue-600
129            sensor_pressure: Color::Rgb(15, 23, 42),     // slate-900
130            sensor_radiation: Color::Rgb(147, 51, 234),  // violet-600
131            series_co2: Color::Rgb(22, 163, 74),         // green-600
132            series_radon: Color::Rgb(8, 145, 178),       // cyan-600
133            series_radiation: Color::Rgb(147, 51, 234),  // violet-600
134
135            // Trend and signal colors
136            trend_rising: Color::Rgb(220, 38, 38),   // red-600
137            trend_falling: Color::Rgb(22, 163, 74),  // green-600
138            trend_stable: Color::Rgb(148, 163, 184), // slate-400
139            signal_excellent: Color::Rgb(22, 163, 74), // green-600
140            signal_good: Color::Rgb(22, 163, 74),    // green-600
141            signal_fair: Color::Rgb(217, 119, 6),    // amber-600
142            signal_weak: Color::Rgb(220, 38, 38),    // red-600
143            signal_offline: Color::Rgb(148, 163, 184), // slate-400
144
145            // Text (dark for light backgrounds)
146            text_primary: Color::Rgb(15, 23, 42),    // slate-900
147            text_secondary: Color::Rgb(71, 85, 105), // slate-600
148            text_muted: Color::Rgb(148, 163, 184),   // slate-400
149
150            // Borders
151            border_active: Color::Rgb(6, 182, 212), // cyan-500
152            border_inactive: Color::Rgb(203, 213, 225), // slate-300
153
154            // Backgrounds
155            bg_selected: Color::Rgb(226, 232, 240), // slate-200
156            bg_header: Color::Rgb(241, 245, 249),   // slate-100
157        }
158    }
159
160    // Style helpers
161
162    /// Style for active/focused borders.
163    #[inline]
164    #[must_use]
165    pub fn border_active_style(&self) -> Style {
166        Style::default().fg(self.border_active)
167    }
168
169    /// Style for inactive borders.
170    #[inline]
171    #[must_use]
172    pub fn border_inactive_style(&self) -> Style {
173        Style::default().fg(self.border_inactive)
174    }
175
176    /// Style for selected items (inverted/highlighted).
177    #[inline]
178    #[must_use]
179    pub fn selected_style(&self) -> Style {
180        Style::default()
181            .bg(self.bg_selected)
182            .fg(self.text_primary)
183            .add_modifier(Modifier::BOLD)
184    }
185
186    /// Style for titles.
187    #[inline]
188    #[must_use]
189    pub fn title_style(&self) -> Style {
190        Style::default()
191            .fg(self.primary)
192            .add_modifier(Modifier::BOLD)
193    }
194
195    /// Style for header/app bar.
196    #[inline]
197    #[must_use]
198    pub fn header_style(&self) -> Style {
199        Style::default().bg(self.bg_header)
200    }
201
202    /// Semantic color for CO2 levels.
203    #[must_use]
204    pub fn co2_level_color(&self, ppm: u16) -> Color {
205        match ppm {
206            0..=800 => self.success,
207            801..=1000 => self.warning,
208            1001..=1500 => self.caution,
209            _ => self.danger,
210        }
211    }
212
213    /// Semantic color for radon levels.
214    #[must_use]
215    pub fn radon_level_color(&self, bq_m3: u32) -> Color {
216        match bq_m3 {
217            0..=100 => self.success,
218            101..=150 => self.warning,
219            151..=300 => self.caution,
220            _ => self.danger,
221        }
222    }
223
224    /// Semantic color for battery levels.
225    #[must_use]
226    pub fn battery_level_color(&self, percent: u8) -> Color {
227        match percent {
228            0..=20 => self.danger,
229            21..=50 => self.warning,
230            _ => self.success,
231        }
232    }
233
234    /// Semantic color for a sensor-reported status.
235    #[must_use]
236    pub fn sensor_status_color(&self, status: &Status) -> Color {
237        match status {
238            Status::Green => self.success,
239            Status::Yellow => self.warning,
240            Status::Red => self.danger,
241            Status::Error => self.signal_offline,
242            _ => self.signal_offline,
243        }
244    }
245
246    /// Semantic color for connection state.
247    #[must_use]
248    pub fn connection_color(&self, status: &ConnectionStatus) -> Color {
249        match status {
250            ConnectionStatus::Disconnected => self.signal_offline,
251            ConnectionStatus::Connecting => self.warning,
252            ConnectionStatus::Connected => self.success,
253            ConnectionStatus::Error(_) => self.danger,
254        }
255    }
256
257    /// Signal bars and color for RSSI strength.
258    #[must_use]
259    pub fn signal_strength_display(&self, rssi: i16) -> (&'static str, Color) {
260        if rssi >= -50 {
261            ("▂▄▆█", self.signal_excellent)
262        } else if rssi >= -60 {
263            ("▂▄▆░", self.signal_good)
264        } else if rssi >= -70 {
265            ("▂▄░░", self.signal_fair)
266        } else if rssi >= -80 {
267            ("▂░░░", self.signal_weak)
268        } else {
269            ("░░░░", self.signal_offline)
270        }
271    }
272
273    /// Semantic color for trend direction.
274    #[must_use]
275    pub fn trend_color(&self, diff: i32, threshold: i32) -> Color {
276        if diff > threshold {
277            self.trend_rising
278        } else if diff < -threshold {
279            self.trend_falling
280        } else {
281            self.trend_stable
282        }
283    }
284}
285
286/// Default border type for all blocks (rounded for modern look).
287pub const BORDER_TYPE: BorderType = BorderType::Rounded;