perspt_tui/
theme.rs

1//! Theme module for consistent styling across the TUI
2//!
3//! Provides semantic colors optimized for dark terminal backgrounds.
4//! Inspired by Codex CLI's beautiful and responsive design.
5
6use ratatui::style::{Color, Modifier, Style};
7use ratatui::symbols;
8use ratatui::widgets::BorderType;
9
10/// Semantic color palette for consistent theming
11#[derive(Debug, Clone, Copy)]
12pub struct Palette {
13    /// Primary brand color
14    pub primary: Color,
15    /// Secondary accent color
16    pub secondary: Color,
17    /// Surface/background tint
18    pub surface: Color,
19    /// Text on surface
20    pub on_surface: Color,
21    /// Muted text
22    pub on_surface_muted: Color,
23    /// Border color
24    pub border: Color,
25    /// Border color when focused
26    pub border_focused: Color,
27    /// Success/green
28    pub success: Color,
29    /// Warning/amber
30    pub warning: Color,
31    /// Error/red
32    pub error: Color,
33}
34
35impl Default for Palette {
36    fn default() -> Self {
37        Self::dark()
38    }
39}
40
41impl Palette {
42    /// Dark theme palette
43    pub fn dark() -> Self {
44        Self {
45            primary: Color::Rgb(129, 199, 132),          // Soft green
46            secondary: Color::Rgb(144, 202, 249),        // Soft blue
47            surface: Color::Rgb(40, 42, 54),             // Dark background
48            on_surface: Color::Rgb(248, 248, 242),       // Off-white
49            on_surface_muted: Color::Rgb(120, 144, 156), // Dim gray
50            border: Color::Rgb(96, 125, 139),            // Blue-gray
51            border_focused: Color::Rgb(129, 199, 132),   // Green when focused
52            success: Color::Rgb(102, 187, 106),          // Green
53            warning: Color::Rgb(255, 183, 77),           // Amber
54            error: Color::Rgb(239, 83, 80),              // Red
55        }
56    }
57}
58
59/// Rounded border set for modern appearance
60pub const ROUNDED_BORDERS: symbols::border::Set = symbols::border::ROUNDED;
61
62/// Get rounded border type
63pub const fn rounded_border_type() -> BorderType {
64    BorderType::Rounded
65}
66
67/// Theme configuration for the TUI
68#[derive(Debug, Clone)]
69pub struct Theme {
70    /// Color palette
71    pub palette: Palette,
72    /// User message styling
73    pub user_message: Style,
74    /// Assistant message styling
75    pub assistant_message: Style,
76    /// System/info message styling
77    pub system_message: Style,
78    /// Code block background
79    pub code_block: Style,
80    /// Success/stable state
81    pub success: Style,
82    /// Warning state
83    pub warning: Style,
84    /// Error/failure state
85    pub error: Style,
86    /// High energy (unstable)
87    pub energy_high: Style,
88    /// Medium energy (converging)
89    pub energy_medium: Style,
90    /// Low energy (stable)
91    pub energy_low: Style,
92    /// Border styling
93    pub border: Style,
94    /// Highlight/selected item
95    pub highlight: Style,
96    /// Muted/secondary text
97    pub muted: Style,
98    /// Streaming cursor
99    pub cursor: Style,
100    /// Tab active style
101    pub tab_active: Style,
102    /// Tab inactive style
103    pub tab_inactive: Style,
104    /// User message background tint
105    pub user_message_bg: Style,
106}
107
108impl Default for Theme {
109    fn default() -> Self {
110        Self::dark()
111    }
112}
113
114impl Theme {
115    /// Dark theme optimized for modern terminals (Ghostty, iTerm2, etc.)
116    pub fn dark() -> Self {
117        let palette = Palette::dark();
118
119        Self {
120            palette,
121            // Messages
122            user_message: Style::default()
123                .fg(palette.primary)
124                .add_modifier(Modifier::BOLD),
125            assistant_message: Style::default().fg(palette.secondary),
126            system_message: Style::default().fg(Color::Rgb(176, 190, 197)),
127
128            // Code
129            code_block: Style::default().fg(palette.on_surface).bg(palette.surface),
130
131            // Status
132            success: Style::default().fg(palette.success),
133            warning: Style::default().fg(palette.warning),
134            error: Style::default().fg(palette.error),
135
136            // Energy levels (Lyapunov)
137            energy_high: Style::default()
138                .fg(palette.error)
139                .add_modifier(Modifier::BOLD),
140            energy_medium: Style::default().fg(palette.warning),
141            energy_low: Style::default()
142                .fg(palette.success)
143                .add_modifier(Modifier::BOLD),
144
145            // UI elements
146            border: Style::default().fg(palette.border),
147            highlight: Style::default()
148                .fg(Color::Rgb(224, 247, 250))
149                .bg(Color::Rgb(55, 71, 79))
150                .add_modifier(Modifier::BOLD),
151            muted: Style::default().fg(palette.on_surface_muted),
152
153            // Cursor for streaming
154            cursor: Style::default()
155                .fg(Color::Rgb(129, 212, 250))
156                .add_modifier(Modifier::SLOW_BLINK),
157
158            // Tab styling
159            tab_active: Style::default()
160                .fg(palette.primary)
161                .add_modifier(Modifier::BOLD),
162            tab_inactive: Style::default().fg(palette.on_surface_muted),
163
164            // User message background (subtle tint)
165            user_message_bg: Style::default().bg(Color::Rgb(30, 35, 40)), // Slightly lighter than terminal bg
166        }
167    }
168
169    /// Get style for energy value (Lyapunov)
170    pub fn energy_style(&self, energy: f32) -> Style {
171        if energy < 0.1 {
172            self.energy_low
173        } else if energy < 0.5 {
174            self.energy_medium
175        } else {
176            self.energy_high
177        }
178    }
179
180    /// Get style for task status
181    pub fn status_style(&self, status: &str) -> Style {
182        match status.to_lowercase().as_str() {
183            "completed" | "stable" | "ok" => self.success,
184            "running" | "pending" | "converging" => self.warning,
185            "failed" | "error" | "escalated" => self.error,
186            _ => self.muted,
187        }
188    }
189}
190
191/// Unicode icons for terminal display
192pub mod icons {
193    pub const USER: &str = "🧑";
194    pub const ASSISTANT: &str = "🤖";
195    pub const SYSTEM: &str = "â„šī¸";
196    pub const SUCCESS: &str = "✓";
197    pub const FAILURE: &str = "✗";
198    pub const WARNING: &str = "⚠";
199    pub const PENDING: &str = "○";
200    pub const RUNNING: &str = "◐";
201    pub const COMPLETED: &str = "●";
202    pub const ROCKET: &str = "🚀";
203    pub const TREE: &str = "đŸŒŗ";
204    pub const FILE: &str = "📄";
205    pub const FOLDER: &str = "📁";
206    pub const ENERGY: &str = "⚡";
207    pub const STABLE: &str = "🔒";
208    pub const CURSOR: &str = "▌";
209    /// Tree guide characters
210    pub const TREE_BRANCH: &str = "├─";
211    pub const TREE_LAST: &str = "└─";
212    pub const TREE_LINE: &str = "│ ";
213    pub const TREE_SPACE: &str = "  ";
214}