1use crate::components::footer::Footer;
8use crate::components::header::Header;
9use crate::config::{Config, RepoMode};
10use crate::icons::Icons;
11use crate::keymap::{Action, KeymapPreset};
12use crate::screens::screen_trait::{RenderContext, Screen, ScreenAction, ScreenContext};
13use crate::styles::{init_theme, theme, ThemeType};
14use crate::ui::Screen as ScreenId;
15use crate::utils::{
16 create_split_layout, create_standard_layout, focused_border_style, unfocused_border_style,
17 MouseRegions,
18};
19use anyhow::Result;
20use crossterm::event::{Event, KeyEventKind, MouseButton, MouseEventKind};
21use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
22use ratatui::style::{Modifier, Style};
23use ratatui::text::{Line, Span, Text};
24use ratatui::widgets::{
25 Block, Borders, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Wrap,
26};
27use ratatui::Frame;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SettingItem {
32 Theme,
33 IconSet,
34 KeymapPreset,
35 Backups,
36 CheckForUpdates,
37 EmbedCredentials,
38}
39
40impl SettingItem {
41 #[must_use]
42 pub fn all(repo_mode: RepoMode) -> Vec<SettingItem> {
43 let mut items = vec![
44 SettingItem::Theme,
45 SettingItem::IconSet,
46 SettingItem::KeymapPreset,
47 SettingItem::Backups,
48 SettingItem::CheckForUpdates,
49 ];
50 if repo_mode == RepoMode::GitHub {
51 items.push(SettingItem::EmbedCredentials);
52 }
53 items
54 }
55
56 #[must_use]
57 pub fn name(&self) -> &'static str {
58 match self {
59 SettingItem::Theme => "Theme",
60 SettingItem::IconSet => "Icon Set",
61 SettingItem::KeymapPreset => "Keymap Preset",
62 SettingItem::Backups => "Backups",
63 SettingItem::CheckForUpdates => "Check for Updates",
64 SettingItem::EmbedCredentials => "Token in Remote URL",
65 }
66 }
67
68 #[must_use]
69 pub fn from_index(index: usize, repo_mode: RepoMode) -> Option<SettingItem> {
70 Self::all(repo_mode).get(index).copied()
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub enum SettingsFocus {
77 #[default]
78 List,
79 Options,
80}
81
82#[derive(Debug)]
84pub struct SettingsState {
85 pub list_state: ListState,
86 pub focus: SettingsFocus,
87 pub option_index: usize, }
89
90impl Default for SettingsState {
91 fn default() -> Self {
92 let mut list_state = ListState::default();
93 list_state.select(Some(0));
94 Self {
95 list_state,
96 focus: SettingsFocus::List,
97 option_index: 0,
98 }
99 }
100}
101
102pub struct SettingsScreen {
104 state: SettingsState,
105 settings_regions: MouseRegions<usize>,
107 option_regions: MouseRegions<usize>,
109 list_pane_area: Option<Rect>,
111 options_pane_area: Option<Rect>,
113}
114
115impl Default for SettingsScreen {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl SettingsScreen {
122 #[must_use]
123 pub fn new() -> Self {
124 Self {
125 state: SettingsState::default(),
126 settings_regions: MouseRegions::new(),
127 option_regions: MouseRegions::new(),
128 list_pane_area: None,
129 options_pane_area: None,
130 }
131 }
132
133 fn selected_setting(&self, repo_mode: RepoMode) -> Option<SettingItem> {
134 self.state
135 .list_state
136 .selected()
137 .and_then(|i| SettingItem::from_index(i, repo_mode))
138 }
139
140 fn get_options(&self, config: &Config) -> Vec<(String, bool)> {
142 match self.selected_setting(config.repo_mode) {
143 Some(SettingItem::Theme) => {
144 let current = &config.theme;
145 ThemeType::all()
146 .iter()
147 .map(|t| (t.name().to_string(), current == t.to_config_string()))
148 .collect()
149 }
150 Some(SettingItem::IconSet) => {
151 use crate::icons::IconSet;
152 let current = &config.icon_set;
153 vec![
154 ("auto".to_string(), current == "auto"),
155 (IconSet::NerdFonts.name().to_string(), current == "nerd"),
156 (IconSet::Unicode.name().to_string(), current == "unicode"),
157 (IconSet::Emoji.name().to_string(), current == "emoji"),
158 (IconSet::Ascii.name().to_string(), current == "ascii"),
159 ]
160 }
161 Some(SettingItem::KeymapPreset) => {
162 let current = config.keymap.preset;
163 vec![
164 ("Standard".to_string(), current == KeymapPreset::Standard),
165 ("Vim".to_string(), current == KeymapPreset::Vim),
166 ("Emacs".to_string(), current == KeymapPreset::Emacs),
167 ]
168 }
169 Some(SettingItem::Backups) => {
170 vec![
171 ("Enabled".to_string(), config.backup_enabled),
172 ("Disabled".to_string(), !config.backup_enabled),
173 ]
174 }
175 Some(SettingItem::CheckForUpdates) => {
176 vec![
177 ("Enabled".to_string(), config.updates.check_enabled),
178 ("Disabled".to_string(), !config.updates.check_enabled),
179 ]
180 }
181 Some(SettingItem::EmbedCredentials) => {
182 vec![
183 ("Enabled".to_string(), config.embed_credentials_in_url),
184 ("Disabled".to_string(), !config.embed_credentials_in_url),
185 ]
186 }
187 None => vec![],
188 }
189 }
190
191 fn get_explanation(&self, config: &Config) -> Text<'static> {
193 let t = theme();
194 let icons = Icons::from_config(config);
195
196 match self.selected_setting(config.repo_mode) {
197 Some(SettingItem::Theme) => {
198 let lines = vec![
199 Line::from(Span::styled("Color Theme", t.title_style())),
200 Line::from(""),
201 Line::from(Span::styled(
202 "Choose how DotState looks. The theme affects all colors in the UI.",
203 t.text_style(),
204 )),
205 Line::from(""),
206 Line::from(vec![
207 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
208 Span::styled(" Current: ", t.muted_style()),
209 Span::styled(config.theme.clone(), t.emphasis_style()),
210 ]),
211 ];
212 Text::from(lines)
213 }
214 Some(SettingItem::IconSet) => {
215 let icons_preview = Icons::from_config(config);
216 let lines = vec![
217 Line::from(Span::styled("Icon Set", t.title_style())),
218 Line::from(""),
219 Line::from(Span::styled(
220 "Choose which icon set to use in the interface.",
221 t.text_style(),
222 )),
223 Line::from(""),
224 Line::from(Span::styled("Preview:", t.muted_style())),
225 Line::from(vec![
226 Span::styled(
227 format!(" {} Folder ", icons_preview.folder()),
228 t.text_style(),
229 ),
230 Span::styled(format!("{} File ", icons_preview.file()), t.text_style()),
231 Span::styled(format!("{} Sync", icons_preview.sync()), t.text_style()),
232 ]),
233 Line::from(""),
234 Line::from(vec![
235 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
236 Span::styled(" Tip: ", Style::default().fg(t.secondary)),
237 Span::styled(
238 "Use 'nerd' if you have a NerdFont installed",
239 t.text_style(),
240 ),
241 ]),
242 ];
243 Text::from(lines)
244 }
245 Some(SettingItem::KeymapPreset) => {
246 let lines = vec![
247 Line::from(Span::styled("Keymap Preset", t.title_style())),
248 Line::from(""),
249 Line::from(Span::styled(
250 "Choose keyboard bindings that feel natural to you.",
251 t.text_style(),
252 )),
253 Line::from(""),
254 Line::from(vec![
255 Span::styled(" • ", t.muted_style()),
256 Span::styled("Standard", t.emphasis_style()),
257 Span::styled(": Arrow keys, Enter, Escape", t.text_style()),
258 ]),
259 Line::from(vec![
260 Span::styled(" • ", t.muted_style()),
261 Span::styled("Vim", t.emphasis_style()),
262 Span::styled(": hjkl navigation, Esc to cancel", t.text_style()),
263 ]),
264 Line::from(vec![
265 Span::styled(" • ", t.muted_style()),
266 Span::styled("Emacs", t.emphasis_style()),
267 Span::styled(": Ctrl+n/p/f/b navigation", t.text_style()),
268 ]),
269 Line::from(""),
270 Line::from(vec![
271 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
272 Span::styled(" Override bindings in config:", t.muted_style()),
273 ]),
274 Line::from(Span::styled(" [keymap.overrides]", t.emphasis_style())),
275 Line::from(Span::styled(" confirm = \"ctrl+s\"", t.emphasis_style())),
276 ];
277 Text::from(lines)
278 }
279 Some(SettingItem::Backups) => {
280 let lines = vec![
281 Line::from(Span::styled("Automatic Backups", t.title_style())),
282 Line::from(""),
283 Line::from(Span::styled(
284 "When enabled, DotState creates .bak files before overwriting existing files during sync operations.",
285 t.text_style(),
286 )),
287 Line::from(""),
288 Line::from(vec![
289 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
290 Span::styled(" Current: ", t.muted_style()),
291 Span::styled(
292 if config.backup_enabled { "Enabled" } else { "Disabled" },
293 t.emphasis_style(),
294 ),
295 ]),
296 ];
297 Text::from(lines)
298 }
299 Some(SettingItem::CheckForUpdates) => {
300 let lines = vec![
301 Line::from(Span::styled("Update Checks", t.title_style())),
302 Line::from(""),
303 Line::from(Span::styled(
304 "When enabled, DotState periodically checks for new versions and shows a notification in the main menu.",
305 t.text_style(),
306 )),
307 Line::from(""),
308 Line::from(Span::styled(
309 "You can always manually check for updates using:",
310 t.text_style(),
311 )),
312 Line::from(Span::styled(" dotstate upgrade", t.emphasis_style())),
313 Line::from(""),
314 Line::from(vec![
315 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
316 Span::styled(" Current: ", t.muted_style()),
317 Span::styled(
318 if config.updates.check_enabled { "Enabled" } else { "Disabled" },
319 t.emphasis_style(),
320 ),
321 ]),
322 ];
323 Text::from(lines)
324 }
325 Some(SettingItem::EmbedCredentials) => {
326 let lines = vec![
327 Line::from(Span::styled("Token in Remote URL", t.title_style())),
328 Line::from(""),
329 Line::from(Span::styled(
330 "Controls how DotState authenticates with GitHub when syncing your dotfiles.",
331 t.text_style(),
332 )),
333 Line::from(""),
334 Line::from(vec![
335 Span::styled(" • ", t.muted_style()),
336 Span::styled("Enabled", t.emphasis_style()),
337 Span::styled(
338 ": Token is stored in the remote URL",
339 t.text_style(),
340 ),
341 ]),
342 Line::from(vec![
343 Span::styled(" • ", t.muted_style()),
344 Span::styled("Disabled", t.emphasis_style()),
345 Span::styled(
346 ": Uses your system's git credential manager",
347 t.text_style(),
348 ),
349 ]),
350 Line::from(""),
351 Line::from(vec![
352 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
353 Span::styled(
354 " Disable if your environment blocks URLs with embedded tokens.",
355 t.muted_style(),
356 ),
357 ]),
358 Line::from(""),
359 Line::from(vec![
360 Span::styled(icons.lightbulb(), Style::default().fg(t.secondary)),
361 Span::styled(" Current: ", t.muted_style()),
362 Span::styled(
363 if config.embed_credentials_in_url { "Enabled" } else { "Disabled" },
364 t.emphasis_style(),
365 ),
366 ]),
367 ];
368 Text::from(lines)
369 }
370 None => Text::from(""),
371 }
372 }
373
374 pub fn apply_setting_to_config(
376 &self,
377 config: &mut Config,
378 setting_name: &str,
379 option_index: usize,
380 ) -> bool {
381 Self::apply_setting_by_name(config, setting_name, option_index)
382 }
383
384 fn apply_setting_by_name(config: &mut Config, setting_name: &str, option_index: usize) -> bool {
386 match setting_name {
387 "Theme" => {
388 let themes = ThemeType::all();
389 if option_index < themes.len() {
390 let selected_theme = themes[option_index];
391 config.theme = selected_theme.to_config_string().to_string();
392 init_theme(selected_theme);
394 return true;
395 }
396 }
397 "Icon Set" => {
398 let sets = ["auto", "nerd", "unicode", "emoji", "ascii"];
399 if option_index < sets.len() {
400 config.icon_set = sets[option_index].to_string();
401 return true;
402 }
403 }
404 "Keymap Preset" => {
405 let presets = [
406 KeymapPreset::Standard,
407 KeymapPreset::Vim,
408 KeymapPreset::Emacs,
409 ];
410 if option_index < presets.len() {
411 config.keymap.preset = presets[option_index];
412 config.keymap.overrides.clear();
414 return true;
415 }
416 }
417 "Backups" => {
418 config.backup_enabled = option_index == 0;
419 return true;
420 }
421 "Check for Updates" => {
422 config.updates.check_enabled = option_index == 0;
423 return true;
424 }
425 "Token in Remote URL" => {
426 config.embed_credentials_in_url = option_index == 0;
427 return true;
428 }
429 _ => {}
430 }
431 false
432 }
433
434 fn current_option_index(&self, config: &Config) -> usize {
436 let options = self.get_options(config);
437 options
438 .iter()
439 .position(|(_, selected)| *selected)
440 .unwrap_or(0)
441 }
442
443 fn render_settings_list(&mut self, frame: &mut Frame, area: Rect, config: &Config) {
444 let t = theme();
445 let icons = Icons::from_config(config);
446 let is_focused = self.state.focus == SettingsFocus::List;
447
448 self.list_pane_area = Some(area);
450 self.settings_regions.clear();
451 let inner = Block::default().borders(Borders::ALL).inner(area);
452 let item_count = SettingItem::all(config.repo_mode).len();
453 let scroll_offset = self.state.list_state.offset();
454 for i in 0..item_count {
455 let visible_idx = i.saturating_sub(scroll_offset);
456 if i >= scroll_offset && (visible_idx as u16) < inner.height {
457 let row = Rect::new(inner.x, inner.y + visible_idx as u16, inner.width, 1);
458 self.settings_regions.add(row, i);
459 }
460 }
461
462 let items: Vec<ListItem> = SettingItem::all(config.repo_mode)
463 .iter()
464 .map(|item| {
465 let current_value = match item {
466 SettingItem::Theme => config.theme.clone(),
467 SettingItem::IconSet => config.icon_set.clone(),
468 SettingItem::KeymapPreset => format!("{:?}", config.keymap.preset),
469 SettingItem::Backups => {
470 if config.backup_enabled {
471 "On".to_string()
472 } else {
473 "Off".to_string()
474 }
475 }
476 SettingItem::CheckForUpdates => {
477 if config.updates.check_enabled {
478 "On".to_string()
479 } else {
480 "Off".to_string()
481 }
482 }
483 SettingItem::EmbedCredentials => {
484 if config.embed_credentials_in_url {
485 "On".to_string()
486 } else {
487 "Off".to_string()
488 }
489 }
490 };
491
492 let line = Line::from(vec![
493 Span::styled(
494 format!("{} ", icons.cog()),
495 Style::default().fg(t.secondary),
496 ),
497 Span::styled(item.name(), t.text_style()),
498 Span::styled(format!(" ({current_value})"), t.muted_style()),
499 ]);
500 ListItem::new(line)
501 })
502 .collect();
503
504 let border_style = if is_focused {
505 focused_border_style()
506 } else {
507 unfocused_border_style()
508 };
509
510 let list = List::new(items)
511 .block(
512 Block::default()
513 .borders(Borders::ALL)
514 .title(" Settings ")
515 .title_alignment(Alignment::Center)
516 .border_type(t.border_type(is_focused))
517 .border_style(border_style)
518 .style(t.background_style()),
519 )
520 .highlight_style(t.highlight_style())
521 .highlight_symbol(crate::styles::LIST_HIGHLIGHT_SYMBOL);
522
523 StatefulWidget::render(list, area, frame.buffer_mut(), &mut self.state.list_state);
524 }
525
526 fn render_options_pane(&mut self, frame: &mut Frame, area: Rect, config: &Config) {
527 let t = theme();
528 let is_focused = self.state.focus == SettingsFocus::Options;
529
530 let chunks = Layout::default()
532 .direction(Direction::Vertical)
533 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
534 .split(area);
535
536 self.options_pane_area = Some(chunks[0]);
538 self.option_regions.clear();
539
540 let options = self.get_options(config);
542 let icons = Icons::from_config(config);
543
544 let options_inner = Block::default().borders(Borders::ALL).inner(chunks[0]);
546 for i in 0..options.len() {
547 if (i as u16) < options_inner.height {
548 let row = Rect::new(
549 options_inner.x,
550 options_inner.y + i as u16,
551 options_inner.width,
552 1,
553 );
554 self.option_regions.add(row, i);
555 }
556 }
557
558 let option_lines: Vec<Line> = options
559 .iter()
560 .enumerate()
561 .map(|(i, (name, selected))| {
562 let marker = if *selected {
563 icons.circle_filled()
564 } else {
565 icons.circle_empty()
566 };
567 let style = if *selected {
568 Style::default().fg(t.success).add_modifier(Modifier::BOLD)
569 } else if is_focused && i == self.state.option_index {
570 t.highlight_style()
571 } else {
572 t.text_style()
573 };
574 Line::from(vec![
575 Span::styled(format!(" {marker} "), style),
576 Span::styled(name.clone(), style),
577 ])
578 })
579 .collect();
580
581 let border_style = if is_focused {
582 focused_border_style()
583 } else {
584 unfocused_border_style()
585 };
586
587 let options_block = Paragraph::new(option_lines)
588 .block(
589 Block::default()
590 .borders(Borders::ALL)
591 .title(" Options ")
592 .title_alignment(Alignment::Center)
593 .border_type(t.border_type(is_focused))
594 .border_style(border_style)
595 .style(t.background_style()),
596 )
597 .wrap(Wrap { trim: false });
598 frame.render_widget(options_block, chunks[0]);
599
600 let explanation = self.get_explanation(config);
602 let explanation_block = Paragraph::new(explanation)
603 .block(
604 Block::default()
605 .borders(Borders::ALL)
606 .title(" Details ")
607 .title_alignment(Alignment::Center)
608 .border_type(t.border_type(false))
609 .border_style(unfocused_border_style())
610 .padding(Padding::proportional(1))
611 .style(t.background_style()),
612 )
613 .wrap(Wrap { trim: false });
614 frame.render_widget(explanation_block, chunks[1]);
615 }
616}
617
618impl Screen for SettingsScreen {
619 fn render(&mut self, frame: &mut Frame, area: Rect, ctx: &RenderContext) -> Result<()> {
620 let (header_chunk, content_chunk, footer_chunk) = create_standard_layout(area, 5, 3);
622
623 Header::render(
625 frame,
626 header_chunk,
627 "DotState - Settings",
628 "Configure your preferences. Changes are applied instantly.",
629 )?;
630
631 let panes = create_split_layout(content_chunk, &[40, 60]);
633
634 self.render_settings_list(frame, panes[0], ctx.config);
636
637 self.render_options_pane(frame, panes[1], ctx.config);
639
640 let k = |a| ctx.config.keymap.get_key_display_for_action(a);
642 let footer_text = format!(
643 "{}: Navigate | {}: Switch Focus | {}: Select | {}: Back",
644 ctx.config.keymap.navigation_display(),
645 k(Action::NextTab),
646 k(Action::Confirm),
647 k(Action::Cancel),
648 );
649 Footer::render(frame, footer_chunk, &footer_text)?;
650
651 Ok(())
652 }
653
654 fn handle_event(&mut self, event: Event, ctx: &ScreenContext) -> Result<ScreenAction> {
655 match event {
656 Event::Key(key) if key.kind == KeyEventKind::Press => {
657 let action = ctx.config.keymap.get_action(key.code, key.modifiers);
658
659 if let Some(action) = action {
660 match self.state.focus {
661 SettingsFocus::List => match action {
662 Action::MoveUp => {
663 self.state.list_state.select_previous();
664 self.state.option_index = self.current_option_index(ctx.config);
665 }
666 Action::MoveDown => {
667 self.state.list_state.select_next();
668 self.state.option_index = self.current_option_index(ctx.config);
669 }
670 Action::Confirm | Action::NextTab | Action::MoveRight => {
671 self.state.focus = SettingsFocus::Options;
672 self.state.option_index = self.current_option_index(ctx.config);
673 }
674 Action::Cancel | Action::Quit => {
675 return Ok(ScreenAction::Navigate(ScreenId::MainMenu));
676 }
677 _ => {}
678 },
679 SettingsFocus::Options => {
680 let options = self.get_options(ctx.config);
681 match action {
682 Action::MoveUp if self.state.option_index > 0 => {
683 self.state.option_index -= 1;
684 }
685 Action::MoveDown
686 if self.state.option_index
687 < options.len().saturating_sub(1) =>
688 {
689 self.state.option_index += 1;
690 }
691 Action::Confirm => {
692 return Ok(ScreenAction::UpdateSetting {
693 setting: self
694 .selected_setting(ctx.config.repo_mode)
695 .map(|s| s.name().to_string())
696 .unwrap_or_default(),
697 option_index: self.state.option_index,
698 });
699 }
700 Action::NextTab | Action::MoveLeft | Action::Cancel => {
701 self.state.focus = SettingsFocus::List;
702 }
703 Action::Quit => {
704 return Ok(ScreenAction::Navigate(ScreenId::MainMenu));
705 }
706 _ => {}
707 }
708 }
709 }
710 }
711 }
712 Event::Mouse(mouse) => {
713 return self.handle_mouse_event(mouse, ctx);
714 }
715 _ => {}
716 }
717
718 Ok(ScreenAction::None)
719 }
720
721 fn is_input_focused(&self) -> bool {
722 false
723 }
724
725 fn on_enter(&mut self, _ctx: &ScreenContext) -> Result<()> {
726 self.state.list_state.select(Some(0));
728 self.state.focus = SettingsFocus::List;
729 self.state.option_index = 0;
730 Ok(())
731 }
732}
733
734impl SettingsScreen {
735 fn handle_mouse_event(
736 &mut self,
737 mouse: crossterm::event::MouseEvent,
738 ctx: &ScreenContext,
739 ) -> Result<ScreenAction> {
740 match mouse.kind {
741 MouseEventKind::Down(MouseButton::Left) => {
742 if let Some(&idx) = self.settings_regions.hit_test(mouse.column, mouse.row) {
744 self.state.list_state.select(Some(idx));
745 self.state.focus = SettingsFocus::List;
746 self.state.option_index = self.current_option_index(ctx.config);
747 return Ok(ScreenAction::Refresh);
748 }
749 if let Some(&idx) = self.option_regions.hit_test(mouse.column, mouse.row) {
751 self.state.focus = SettingsFocus::Options;
752 self.state.option_index = idx;
753 return Ok(ScreenAction::UpdateSetting {
755 setting: self
756 .selected_setting(ctx.config.repo_mode)
757 .map(|s| s.name().to_string())
758 .unwrap_or_default(),
759 option_index: idx,
760 });
761 }
762 }
763 MouseEventKind::ScrollUp => {
764 if let Some(area) = self.list_pane_area {
765 if area.contains(ratatui::layout::Position::new(mouse.column, mouse.row)) {
766 for _ in 0..3 {
767 self.state.list_state.select_previous();
768 }
769 self.state.option_index = self.current_option_index(ctx.config);
770 return Ok(ScreenAction::Refresh);
771 }
772 }
773 if let Some(area) = self.options_pane_area {
774 if area.contains(ratatui::layout::Position::new(mouse.column, mouse.row)) {
775 self.state.focus = SettingsFocus::Options;
776 self.state.option_index = self.state.option_index.saturating_sub(3);
777 return Ok(ScreenAction::Refresh);
778 }
779 }
780 }
781 MouseEventKind::ScrollDown => {
782 if let Some(area) = self.list_pane_area {
783 if area.contains(ratatui::layout::Position::new(mouse.column, mouse.row)) {
784 for _ in 0..3 {
785 self.state.list_state.select_next();
786 }
787 self.state.option_index = self.current_option_index(ctx.config);
788 return Ok(ScreenAction::Refresh);
789 }
790 }
791 if let Some(area) = self.options_pane_area {
792 if area.contains(ratatui::layout::Position::new(mouse.column, mouse.row)) {
793 self.state.focus = SettingsFocus::Options;
794 let max = self.get_options(ctx.config).len().saturating_sub(1);
795 self.state.option_index = (self.state.option_index + 3).min(max);
796 return Ok(ScreenAction::Refresh);
797 }
798 }
799 }
800 _ => {}
801 }
802 Ok(ScreenAction::None)
803 }
804}