Skip to main content

pr_bro/tui/
theme.rs

1//! Centralized theme module for TUI color constants and styles
2
3use ratatui::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Theme {
7    Light,
8    Dark,
9}
10
11/// Complete color palette for the TUI
12#[derive(Debug, Clone)]
13pub struct ThemeColors {
14    // Score-based colors (traffic light pattern)
15    pub score_high: Color,
16    pub score_mid: Color,
17    pub score_low: Color,
18
19    // Score bar colors
20    pub bar_filled_high: Color,
21    pub bar_filled_mid: Color,
22    pub bar_filled_low: Color,
23    pub bar_empty: Color,
24
25    // Table colors
26    pub row_alt_bg: Color,
27    pub index_color: Color,
28
29    // Styles
30    pub title_style: Style,
31    pub header_style: Style,
32    pub tab_active: Style,
33    pub row_selected: Style,
34
35    // General colors
36    pub muted: Color,
37    pub title_color: Color,
38
39    // Tab colors
40    pub tab_active_style: Style,
41    pub tab_inactive_style: Style,
42
43    // Status bar colors
44    pub status_bar_bg: Color,
45    pub status_key_color: Color,
46    pub flash_success: Color,
47    pub flash_error: Color,
48
49    // Divider and separator colors
50    pub divider_color: Color,
51
52    // Popup overlay colors
53    pub popup_border: Color,
54    pub popup_title: Style,
55    pub popup_bg: Color,
56
57    // Scrollbar colors
58    pub scrollbar_thumb: Color,
59    pub scrollbar_track: Color,
60
61    // Update banner colors
62    pub banner_bg: Color,
63    pub banner_fg: Color,
64    pub banner_key: Color,
65}
66
67impl ThemeColors {
68    /// Create a ThemeColors palette for the given theme
69    pub fn new(theme: Theme) -> Self {
70        match theme {
71            Theme::Dark => Self::dark(),
72            Theme::Light => Self::light(),
73        }
74    }
75
76    /// Dark theme palette (reproduces original constants exactly)
77    pub fn dark() -> Self {
78        Self {
79            score_high: Color::Red,
80            score_mid: Color::Yellow,
81            score_low: Color::Green,
82            bar_filled_high: Color::Red,
83            bar_filled_mid: Color::Yellow,
84            bar_filled_low: Color::Green,
85            bar_empty: Color::DarkGray,
86            row_alt_bg: Color::Indexed(235),
87            index_color: Color::DarkGray,
88            title_style: Style::new().bold(),
89            header_style: Style::new().bold(),
90            tab_active: Style::new().reversed(),
91            row_selected: Style::new().reversed(),
92            muted: Color::Gray,
93            title_color: Color::Cyan,
94            tab_active_style: Style::new().fg(Color::Cyan).bold(),
95            tab_inactive_style: Style::new().fg(Color::DarkGray),
96            status_bar_bg: Color::Indexed(236),
97            status_key_color: Color::Cyan,
98            flash_success: Color::Green,
99            flash_error: Color::Red,
100            divider_color: Color::Indexed(238),
101            popup_border: Color::Cyan,
102            popup_title: Style::new().fg(Color::Cyan).bold(),
103            popup_bg: Color::Indexed(234),
104            scrollbar_thumb: Color::Indexed(244),
105            scrollbar_track: Color::Indexed(236),
106            banner_bg: Color::Rgb(50, 50, 120),
107            banner_fg: Color::White,
108            banner_key: Color::Yellow,
109        }
110    }
111
112    /// Light theme palette (optimized for light terminal backgrounds)
113    fn light() -> Self {
114        Self {
115            // Score colors stay the same (traffic light colors work on both)
116            score_high: Color::Red,
117            score_mid: Color::Yellow,
118            score_low: Color::Green,
119            bar_filled_high: Color::Red,
120            bar_filled_mid: Color::Yellow,
121            bar_filled_low: Color::Green,
122            bar_empty: Color::Indexed(250), // Lighter empty bar
123            // Table colors adjusted for light background
124            row_alt_bg: Color::Indexed(254),  // Light gray
125            index_color: Color::Indexed(240), // Medium gray for contrast
126            // Styles stay the same (bold/reversed work universally)
127            title_style: Style::new().bold(),
128            header_style: Style::new().bold(),
129            tab_active: Style::new().reversed(),
130            row_selected: Style::new().reversed(),
131            // General colors adjusted
132            muted: Color::Indexed(244), // Darker muted for readability
133            title_color: Color::Blue,   // Blue instead of cyan
134            // Tab colors adjusted
135            tab_active_style: Style::new().fg(Color::Blue).bold(),
136            tab_inactive_style: Style::new().fg(Color::Indexed(240)),
137            // Status bar adjusted
138            status_bar_bg: Color::Indexed(253), // Light background
139            status_key_color: Color::Blue,
140            // Flash colors stay the same
141            flash_success: Color::Green,
142            flash_error: Color::Red,
143            // Divider adjusted
144            divider_color: Color::Indexed(250), // Lighter divider
145            // Popup adjusted
146            popup_border: Color::Blue,
147            popup_title: Style::new().fg(Color::Blue).bold(),
148            popup_bg: Color::Indexed(255), // Near-white background
149            // Scrollbar adjusted
150            scrollbar_thumb: Color::Indexed(240),
151            scrollbar_track: Color::Indexed(253),
152            // Banner adjusted
153            banner_bg: Color::Rgb(180, 180, 230), // Lighter blue-purple
154            banner_fg: Color::Black,              // Dark text on light banner
155            banner_key: Color::Indexed(88),       // Dark red for highlight
156        }
157    }
158
159    /// Returns the appropriate color for a score based on its percentage of max score
160    pub fn score_color(&self, score: f64, max_score: f64) -> Color {
161        let percentage = if max_score > 0.0 {
162            (score / max_score) * 100.0
163        } else {
164            0.0
165        };
166
167        if percentage >= 70.0 {
168            self.score_high
169        } else if percentage >= 40.0 {
170            self.score_mid
171        } else {
172            self.score_low
173        }
174    }
175}
176
177/// Resolve theme from config string ("dark", "light", "auto")
178pub fn resolve_theme(config_theme: &str) -> Theme {
179    match config_theme {
180        "light" => Theme::Light,
181        "dark" => Theme::Dark,
182        "auto" => detect_terminal_theme(),
183        _ => Theme::Dark, // Unknown value defaults to dark
184    }
185}
186
187/// Detect terminal theme from background luminance
188fn detect_terminal_theme() -> Theme {
189    match terminal_light::luma() {
190        Ok(luma) if luma > 0.5 => Theme::Light,
191        _ => Theme::Dark, // Detection failed or dark background -> default dark
192    }
193}