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