1use ratatui::{
7 layout::{Constraint, Layout, Rect},
8 text::{Line, Span},
9 widgets::{Block, Borders, Clear, Paragraph},
10 Frame,
11};
12
13use super::theme::{set_theme, Theme};
14use super::themes::{get_theme, THEMES};
15
16pub struct ThemePickerState {
18 pub active: bool,
20 pub selected_index: usize,
22 original_theme_name: String,
24 original_theme: Option<Theme>,
26}
27
28impl ThemePickerState {
29 pub fn new() -> Self {
30 Self {
31 active: false,
32 selected_index: 0,
33 original_theme_name: String::new(),
34 original_theme: None,
35 }
36 }
37
38 pub fn activate(&mut self, current_theme_name: &str, current_theme: Theme) {
40 self.active = true;
41 self.original_theme_name = current_theme_name.to_string();
42 self.original_theme = Some(current_theme);
43
44 self.selected_index = THEMES
46 .iter()
47 .position(|t| t.name == current_theme_name)
48 .unwrap_or(0);
49
50 self.apply_preview();
52 }
53
54 pub fn cancel(&mut self) {
56 if let Some(theme) = self.original_theme.take() {
57 set_theme(&self.original_theme_name, theme);
58 }
59 self.active = false;
60 }
61
62 pub fn confirm(&mut self) {
64 self.active = false;
65 self.original_theme = None;
66 }
67
68 pub fn select_previous(&mut self) {
70 if THEMES.is_empty() {
71 return;
72 }
73 if self.selected_index == 0 {
74 self.selected_index = THEMES.len() - 1;
75 } else {
76 self.selected_index -= 1;
77 }
78 self.apply_preview();
79 }
80
81 pub fn select_next(&mut self) {
83 if THEMES.is_empty() {
84 return;
85 }
86 self.selected_index = (self.selected_index + 1) % THEMES.len();
87 self.apply_preview();
88 }
89
90 fn apply_preview(&self) {
92 if let Some(info) = THEMES.get(self.selected_index) {
93 if let Some(theme) = get_theme(info.name) {
94 set_theme(info.name, theme);
95 }
96 }
97 }
98
99 pub fn selected_theme_name(&self) -> Option<&'static str> {
101 THEMES.get(self.selected_index).map(|t| t.name)
102 }
103}
104
105impl Default for ThemePickerState {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111use std::any::Any;
114use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
115use crate::tui::widgets::{widget_ids, Widget, WidgetAction, WidgetKeyResult};
116
117#[derive(Debug, Clone, PartialEq)]
119pub enum ThemeKeyAction {
120 None,
122 Navigated,
124 Confirmed,
126 Cancelled,
128}
129
130impl ThemePickerState {
131 pub fn process_key(&mut self, key: KeyEvent) -> ThemeKeyAction {
133 if !self.active {
134 return ThemeKeyAction::None;
135 }
136
137 match key.code {
138 KeyCode::Up => {
139 self.select_previous();
140 ThemeKeyAction::Navigated
141 }
142 KeyCode::Down => {
143 self.select_next();
144 ThemeKeyAction::Navigated
145 }
146 KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
147 self.select_previous();
148 ThemeKeyAction::Navigated
149 }
150 KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
151 self.select_next();
152 ThemeKeyAction::Navigated
153 }
154 KeyCode::Enter => {
155 self.confirm();
156 ThemeKeyAction::Confirmed
157 }
158 KeyCode::Esc => {
159 self.cancel();
160 ThemeKeyAction::Cancelled
161 }
162 _ => ThemeKeyAction::None,
163 }
164 }
165}
166
167impl Widget for ThemePickerState {
168 fn id(&self) -> &'static str {
169 widget_ids::THEME_PICKER
170 }
171
172 fn priority(&self) -> u8 {
173 250 }
175
176 fn is_active(&self) -> bool {
177 self.active
178 }
179
180 fn handle_key(&mut self, key: KeyEvent, _theme: &Theme) -> WidgetKeyResult {
181 if !self.active {
182 return WidgetKeyResult::NotHandled;
183 }
184
185 match self.process_key(key) {
186 ThemeKeyAction::Confirmed | ThemeKeyAction::Cancelled => {
187 WidgetKeyResult::Action(WidgetAction::Close)
188 }
189 ThemeKeyAction::Navigated => WidgetKeyResult::Handled,
190 ThemeKeyAction::None => WidgetKeyResult::Handled,
191 }
192 }
193
194 fn render(&self, frame: &mut Frame, area: Rect, _theme: &Theme) {
195 render_theme_picker(self, frame, area);
196 }
197
198 fn required_height(&self, _available: u16) -> u16 {
199 0 }
201
202 fn blocks_input(&self) -> bool {
203 self.active
204 }
205
206 fn is_overlay(&self) -> bool {
207 true
208 }
209
210 fn as_any(&self) -> &dyn Any {
211 self
212 }
213
214 fn as_any_mut(&mut self) -> &mut dyn Any {
215 self
216 }
217
218 fn into_any(self: Box<Self>) -> Box<dyn Any> {
219 self
220 }
221}
222
223pub fn render_theme_picker(state: &ThemePickerState, frame: &mut Frame, area: Rect) {
225 if !state.active {
226 return;
227 }
228
229 let theme = super::theme::theme();
230
231 frame.render_widget(Clear, area);
233
234 let main_chunks = Layout::vertical([Constraint::Min(0), Constraint::Length(2)])
236 .split(area);
237
238 let chunks = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
240 .split(main_chunks[0]);
241
242 render_theme_list(state, frame, chunks[0], &theme);
244
245 render_preview(frame, chunks[1], &theme);
247
248 render_help_bar(frame, main_chunks[1], &theme);
250}
251
252fn render_theme_list(state: &ThemePickerState, frame: &mut Frame, area: Rect, theme: &Theme) {
254 let mut lines = Vec::new();
255
256 lines.push(Line::from(""));
258
259 for (idx, info) in THEMES.iter().enumerate() {
260 let is_selected = idx == state.selected_index;
261 let is_current = info.name == state.original_theme_name;
262
263 let marker = if is_current { "* " } else { " " };
264 let prefix = if is_selected { " > " } else { " " };
265 let text = format!("{}{}{}", prefix, marker, info.display_name);
266
267 let style = if is_selected {
268 theme.popup_selected_bg.patch(theme.popup_item_selected)
269 } else {
270 theme.popup_item
271 };
272
273 let inner_width = area.width.saturating_sub(2) as usize;
275 let padded = format!("{:<width$}", text, width = inner_width);
276 lines.push(Line::from(Span::styled(padded, style)));
277 }
278
279 let block = Block::default()
280 .title(" Select Theme ")
281 .borders(Borders::ALL)
282 .border_style(theme.popup_border);
283
284 let list = Paragraph::new(lines)
285 .block(block)
286 .style(theme.background.patch(theme.text));
287
288 frame.render_widget(list, area);
289}
290
291fn render_preview(frame: &mut Frame, area: Rect, theme: &Theme) {
293 let mut lines = Vec::new();
294
295 lines.push(Line::from(""));
297 lines.push(Line::from(Span::styled(
298 " # Preview",
299 theme.heading_1,
300 )));
301 lines.push(Line::from(""));
302
303 lines.push(Line::from(Span::styled(" > User message example", theme.user_prefix)));
305 lines.push(Line::from(Span::styled(" - 10:30:00 AM", theme.timestamp)));
306 lines.push(Line::from(""));
307
308 lines.push(Line::from(vec![
310 Span::raw(" This is "),
311 Span::styled("bold", theme.text.add_modifier(theme.bold)),
312 Span::raw(" and "),
313 Span::styled("italic", theme.text.add_modifier(theme.italic)),
314 Span::raw(" text."),
315 ]));
316 lines.push(Line::from(vec![
317 Span::raw(" Here is "),
318 Span::styled("inline code", theme.inline_code),
319 Span::raw(" and a "),
320 Span::styled("link", theme.link_text),
321 Span::raw("."),
322 ]));
323 lines.push(Line::from(""));
324
325 lines.push(Line::from(Span::styled(" ## Heading 2", theme.heading_2)));
327 lines.push(Line::from(Span::styled(" ### Heading 3", theme.heading_3)));
328 lines.push(Line::from(""));
329
330 lines.push(Line::from(Span::styled(" ```rust", theme.code_block)));
332 lines.push(Line::from(Span::styled(" fn main() { }", theme.code_block)));
333 lines.push(Line::from(Span::styled(" ```", theme.code_block)));
334 lines.push(Line::from(""));
335
336 lines.push(Line::from(vec![
338 Span::styled(" ", theme.table_border),
339 Span::styled("Col1", theme.table_header),
340 Span::styled(" | ", theme.table_border),
341 Span::styled("Col2", theme.table_header),
342 ]));
343 lines.push(Line::from(Span::styled(" -----|-----", theme.table_border)));
344 lines.push(Line::from(vec![
345 Span::styled(" ", theme.table_border),
346 Span::styled("A", theme.table_cell),
347 Span::styled(" | ", theme.table_border),
348 Span::styled("B", theme.table_cell),
349 ]));
350 lines.push(Line::from(""));
351
352 lines.push(Line::from(Span::styled(" Tool executing...", theme.tool_executing)));
354 lines.push(Line::from(Span::styled(" Tool completed", theme.tool_completed)));
355 lines.push(Line::from(Span::styled(" Tool failed", theme.tool_failed)));
356 lines.push(Line::from(""));
357
358 let block = Block::default()
359 .title(" Preview ")
360 .borders(Borders::ALL)
361 .border_style(theme.popup_border);
362
363 let preview = Paragraph::new(lines)
364 .block(block)
365 .style(theme.background.patch(theme.text));
366
367 frame.render_widget(preview, area);
368}
369
370fn render_help_bar(frame: &mut Frame, area: Rect, theme: &Theme) {
372 let help_text = " Arrow keys to navigate | Enter to accept | Esc to cancel | * = current theme";
373 let help = Paragraph::new(help_text)
374 .style(theme.status_help);
375 frame.render_widget(help, area);
376}