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