1use std::any::Any;
9use std::sync::{Arc, Mutex};
10
11use crossterm::event::{KeyCode, KeyEvent};
12
13use super::component::Component;
14use super::fuzzy::fuzzy_filter;
15use crate::utils::{truncate_to_width, visible_width};
16
17#[derive(Debug, Clone)]
19pub struct SettingItem {
20 pub key: String,
22 pub label: String,
24 pub value: String,
26 pub description: Option<String>,
28 pub setting_type: SettingType,
30 pub values: Vec<String>,
33 pub has_submenu: bool,
36}
37
38impl SettingItem {
39 pub fn new(key: &str, label: &str, value: &str) -> Self {
41 Self {
42 key: key.to_string(),
43 label: label.to_string(),
44 value: value.to_string(),
45 description: None,
46 setting_type: SettingType::String,
47 values: Vec::new(),
48 has_submenu: false,
49 }
50 }
51
52 pub fn with_description(mut self, description: &str) -> Self {
54 self.description = Some(description.to_string());
55 self
56 }
57
58 pub fn with_type(mut self, setting_type: SettingType) -> Self {
60 self.setting_type = setting_type;
61 self
62 }
63
64 pub fn with_values(mut self, values: &[&str]) -> Self {
68 self.values = values.iter().map(|v| v.to_string()).collect();
69 self
70 }
71
72 pub fn with_submenu(mut self) -> Self {
75 self.has_submenu = true;
76 self
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
82pub enum SettingType {
83 String,
85 Boolean,
87 Number,
89 Enum,
91}
92
93#[derive(Debug, Clone)]
95pub struct SettingsListTheme {
96 pub key: fn(&str) -> String,
97 pub value: fn(&str) -> String,
98 pub description: fn(&str) -> String,
99 pub selected: fn(&str) -> String,
100 pub selected_prefix: fn(&str) -> String,
101}
102
103impl Default for SettingsListTheme {
104 fn default() -> Self {
105 Self {
106 key: |s| format!("\x1b[1m{}\x1b[0m", s),
107 value: |s| format!("\x1b[36m{}\x1b[0m", s),
108 description: |s| format!("\x1b[90m{}\x1b[0m", s),
109 selected: |s| format!("\x1b[7m{}\x1b[0m", s),
110 selected_prefix: |s| format!("\x1b[32m{}\x1b[0m", s),
111 }
112 }
113}
114
115pub struct SettingsList {
117 settings: Mutex<Vec<SettingItem>>,
118 filtered: Mutex<Vec<SettingItem>>,
119 selected_index: Mutex<usize>,
120 max_visible: usize,
121 theme: SettingsListTheme,
122 filter: Mutex<String>,
123 on_change: Mutex<Option<Arc<dyn Fn(&str, &str) + Send + Sync>>>,
124 on_select: Mutex<Option<Arc<dyn Fn(&SettingItem) + Send + Sync>>>,
125 on_cancel: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
126}
127
128impl SettingsList {
129 pub fn new(settings: Vec<SettingItem>) -> Self {
131 Self {
132 filtered: Mutex::new(settings.clone()),
133 settings: Mutex::new(settings),
134 selected_index: Mutex::new(0),
135 max_visible: 10,
136 theme: SettingsListTheme::default(),
137 filter: Mutex::new(String::new()),
138 on_change: Mutex::new(None),
139 on_select: Mutex::new(None),
140 on_cancel: Mutex::new(None),
141 }
142 }
143
144 pub fn with_theme(settings: Vec<SettingItem>, theme: SettingsListTheme) -> Self {
146 Self {
147 filtered: Mutex::new(settings.clone()),
148 settings: Mutex::new(settings),
149 selected_index: Mutex::new(0),
150 max_visible: 10,
151 theme,
152 filter: Mutex::new(String::new()),
153 on_change: Mutex::new(None),
154 on_select: Mutex::new(None),
155 on_cancel: Mutex::new(None),
156 }
157 }
158
159 pub fn set_filter(&self, filter: &str) {
161 if let Ok(mut f) = self.filter.lock() {
162 *f = filter.to_string();
163 }
164 self.refilter();
165 if let Ok(mut idx) = self.selected_index.lock() {
167 *idx = 0;
168 }
169 }
170
171 fn refilter(&self) {
174 let filter = self.filter.lock().map(|f| f.clone()).unwrap_or_default();
175 if let Ok(settings) = self.settings.lock() {
176 let filtered = fuzzy_filter(&*settings, &filter, |item| item.key.as_str());
177 if let Ok(mut f) = self.filtered.lock() {
178 *f = filtered;
179 }
180 }
181 }
182
183 pub fn get_selected(&self) -> Option<SettingItem> {
185 let idx = *self.selected_index.lock().ok()?;
186 let filtered = self.filtered.lock().ok()?;
187 filtered.get(idx).cloned()
188 }
189
190 pub fn set_selected(&self, index: usize) {
192 if let Ok(mut idx) = self.selected_index.lock() {
193 if let Ok(filtered) = self.filtered.lock() {
194 *idx = index.min(filtered.len().saturating_sub(1));
195 }
196 }
197 }
198
199 pub fn move_up(&self) {
201 if let Ok(mut idx) = self.selected_index.lock() {
202 if let Ok(filtered) = self.filtered.lock() {
203 if !filtered.is_empty() {
204 if *idx == 0 {
205 *idx = filtered.len() - 1;
206 } else {
207 *idx -= 1;
208 }
209 }
210 }
211 }
212 }
213
214 pub fn move_down(&self) {
216 if let Ok(mut idx) = self.selected_index.lock() {
217 if let Ok(filtered) = self.filtered.lock() {
218 if !filtered.is_empty() {
219 if *idx >= filtered.len() - 1 {
220 *idx = 0;
221 } else {
222 *idx += 1;
223 }
224 }
225 }
226 }
227 }
228
229 pub fn toggle(&self) {
231 if let Some(item) = self.get_selected() {
232 if item.setting_type == SettingType::Boolean {
233 let new_value = if item.value == "true" {
234 "false"
235 } else {
236 "true"
237 };
238 self.update_value(&item.key, new_value);
239
240 if let Ok(cb) = self.on_change.lock() {
241 if let Some(callback) = cb.as_ref() {
242 callback(&item.key, new_value);
243 }
244 }
245 }
246 }
247 }
248
249 pub fn update_value(&self, key: &str, value: &str) {
251 if let Ok(mut settings) = self.settings.lock() {
252 for setting in settings.iter_mut() {
253 if setting.key == key {
254 setting.value = value.to_string();
255 break;
256 }
257 }
258 }
259
260 self.refilter();
262 if let Ok(mut idx) = self.selected_index.lock() {
263 if let Ok(filtered) = self.filtered.lock() {
264 *idx = (*idx).min(filtered.len().saturating_sub(1));
265 }
266 }
267 }
268
269 pub fn on_change(&self, callback: Arc<dyn Fn(&str, &str) + Send + Sync>) {
271 if let Ok(mut cb) = self.on_change.lock() {
272 *cb = Some(callback);
273 }
274 }
275
276 pub fn on_select(&self, callback: Arc<dyn Fn(&SettingItem) + Send + Sync>) {
278 if let Ok(mut cb) = self.on_select.lock() {
279 *cb = Some(callback);
280 }
281 }
282
283 pub fn on_cancel(&self, callback: Arc<dyn Fn() + Send + Sync>) {
285 if let Ok(mut cb) = self.on_cancel.lock() {
286 *cb = Some(callback);
287 }
288 }
289
290 pub fn cancel(&self) {
292 if let Ok(cb) = self.on_cancel.lock() {
293 if let Some(callback) = cb.as_ref() {
294 callback();
295 }
296 }
297 }
298
299 pub fn activate(&self) -> Option<(String, String)> {
304 let item = self.get_selected()?;
305 if item.has_submenu {
306 if let Ok(cb) = self.on_select.lock() {
307 if let Some(callback) = cb.as_ref() {
308 callback(&item);
309 }
310 }
311 return None;
312 }
313 if item.values.is_empty() {
314 return None;
315 }
316 let current = item
317 .values
318 .iter()
319 .position(|value| value == &item.value)
320 .unwrap_or(item.values.len().saturating_sub(1));
321 let next = item.values[(current + 1) % item.values.len()].clone();
322 self.update_value(&item.key, &next);
323 if let Ok(cb) = self.on_change.lock() {
324 if let Some(callback) = cb.as_ref() {
325 callback(&item.key, &next);
326 }
327 }
328 Some((item.key, next))
329 }
330
331 pub fn handle_key(&self, key: KeyEvent) {
334 match key.code {
335 KeyCode::Up => self.move_up(),
336 KeyCode::Down => self.move_down(),
337 KeyCode::Enter => {
338 self.activate();
339 }
340 KeyCode::Char(' ') => {
341 self.activate();
342 }
343 KeyCode::Esc => self.cancel(),
344 _ => {}
345 }
346 }
347
348 pub fn value_of(&self, key: &str) -> Option<String> {
350 let settings = self.settings.lock().ok()?;
351 settings
352 .iter()
353 .find(|item| item.key == key)
354 .map(|item| item.value.clone())
355 }
356
357 pub fn set_items(&self, settings: Vec<SettingItem>) {
360 let filter = self.filter.lock().map(|f| f.clone()).unwrap_or_default();
361 if let Ok(mut guard) = self.settings.lock() {
362 *guard = settings;
363 }
364 self.set_filter(&filter);
365 }
366
367 pub fn confirm(&self) {
369 if let Some(item) = self.get_selected() {
370 if let Ok(cb) = self.on_select.lock() {
371 if let Some(callback) = cb.as_ref() {
372 callback(&item);
373 }
374 }
375 }
376 }
377
378 fn render_item(&self, item: &SettingItem, is_selected: bool, width: usize) -> String {
380 let prefix = if is_selected { "→ " } else { " " };
381
382 let key_width = 20;
383 let value_width = 15;
384
385 let key_display = truncate_to_width(&item.label, key_width, "");
386 let value_display = truncate_to_width(&item.value, value_width, "");
387
388 let main_line = if is_selected {
389 format!(
390 "{}{} {} {}",
391 (self.theme.selected_prefix)(prefix),
392 (self.theme.key)(&key_display),
393 (self.theme.selected)(&value_display),
394 ""
395 )
396 } else {
397 format!(
398 "{}{} {}",
399 prefix,
400 (self.theme.key)(&key_display),
401 (self.theme.value)(&value_display)
402 )
403 };
404
405 if let Some(ref desc) = item.description {
407 let desc_width = width.saturating_sub(visible_width(&main_line) + 2);
408 if desc_width > 10 {
409 let desc_display = truncate_to_width(desc, desc_width, "");
410 format!("{} {}", main_line, (self.theme.description)(&desc_display))
411 } else {
412 main_line
413 }
414 } else {
415 main_line
416 }
417 }
418}
419
420impl Component for SettingsList {
421 fn render(&self, width: usize) -> Vec<String> {
422 let filtered = if let Ok(f) = self.filtered.lock() {
423 f.clone()
424 } else {
425 return Vec::new();
426 };
427
428 let selected = *self.selected_index.lock().unwrap();
429
430 if filtered.is_empty() {
431 return vec![(self.theme.description)(" No settings found")];
432 }
433
434 let start = if filtered.len() <= self.max_visible {
436 0
437 } else {
438 selected
439 .saturating_sub(self.max_visible / 2)
440 .min(filtered.len().saturating_sub(self.max_visible))
441 };
442 let end = (start + self.max_visible).min(filtered.len());
443
444 let mut lines = Vec::new();
445
446 for i in start..end {
447 if let Some(item) = filtered.get(i) {
448 lines.push(self.render_item(item, i == selected, width));
449 }
450 }
451
452 if start > 0 || end < filtered.len() {
454 let info = format!(" ({}/{})", selected + 1, filtered.len());
455 lines.push((self.theme.description)(&info));
456 }
457
458 lines
459 }
460
461 fn invalidate(&self) {
462 }
464
465 fn as_any(&self) -> &dyn Any {
466 self
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_settings_list_new() {
476 let settings = vec![
477 SettingItem::new("theme", "Theme", "dark"),
478 SettingItem::new("font", "Font Size", "14"),
479 ];
480 let list = SettingsList::new(settings);
481
482 let lines = list.render(60);
483 assert!(!lines.is_empty());
484 }
485
486 #[test]
487 fn test_settings_navigation() {
488 let settings = vec![
489 SettingItem::new("a", "A", "1"),
490 SettingItem::new("b", "B", "2"),
491 ];
492 let list = SettingsList::new(settings);
493
494 assert_eq!(list.get_selected().unwrap().key, "a");
495 list.move_down();
496 assert_eq!(list.get_selected().unwrap().key, "b");
497 list.move_down(); assert_eq!(list.get_selected().unwrap().key, "a");
499 }
500
501 #[test]
502 fn test_settings_filter() {
503 let settings = vec![
504 SettingItem::new("theme", "Theme", "dark"),
505 SettingItem::new("font", "Font Size", "14"),
506 ];
507 let list = SettingsList::new(settings);
508
509 list.set_filter("theme");
510 let selected = list.get_selected();
511 assert!(selected.is_some());
512 assert_eq!(selected.unwrap().key, "theme");
513 }
514
515 #[test]
516 fn activate_cycles_values_and_fires_change() {
517 let settings = vec![SettingItem::new("hide-thinking", "Hide thinking", "false")
518 .with_values(&["true", "false"])];
519 let list = SettingsList::new(settings);
520
521 let changes: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
522 let changes_cb = changes.clone();
523 list.on_change(Arc::new(move |key, value| {
524 changes_cb
525 .lock()
526 .unwrap()
527 .push((key.to_string(), value.to_string()));
528 }));
529
530 assert_eq!(
532 list.activate(),
533 Some(("hide-thinking".to_string(), "true".to_string()))
534 );
535 assert_eq!(list.value_of("hide-thinking").as_deref(), Some("true"));
536 list.activate();
538 assert_eq!(list.value_of("hide-thinking").as_deref(), Some("false"));
539 assert_eq!(changes.lock().unwrap().len(), 2);
540 }
541
542 #[test]
543 fn activating_a_value_preserves_the_cursor() {
544 let settings = vec![
545 SettingItem::new("a", "Auto-compact", "true").with_values(&["true", "false"]),
546 SettingItem::new("b", "Steering mode", "all").with_values(&["all", "one-at-a-time"]),
547 SettingItem::new("c", "Quiet startup", "false").with_values(&["true", "false"]),
548 ];
549 let list = SettingsList::new(settings);
550 list.move_down();
551 assert_eq!(list.get_selected().unwrap().key, "b");
552 list.activate();
553 assert_eq!(list.get_selected().unwrap().key, "b");
555 assert_eq!(
556 list.get_selected().unwrap().value,
557 "one-at-a-time",
558 "value should have cycled in place"
559 );
560 }
561
562 #[test]
563 fn submenu_row_calls_select_instead_of_cycling() {
564 let settings = vec![SettingItem::new("theme", "Theme", "dark").with_submenu()];
565 let list = SettingsList::new(settings);
566 let opened: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
567 let opened_cb = opened.clone();
568 list.on_select(Arc::new(move |item| {
569 opened_cb.lock().unwrap().push(item.key.clone());
570 }));
571 assert_eq!(list.activate(), None);
572 assert_eq!(opened.lock().unwrap().as_slice(), ["theme"]);
573 assert_eq!(list.value_of("theme").as_deref(), Some("dark"));
575 }
576
577 #[test]
578 fn handle_key_routes_navigation_activation_and_cancel() {
579 use crossterm::event::{KeyCode, KeyEvent};
580 let settings = vec![
581 SettingItem::new("a", "Alpha", "1").with_values(&["1", "2"]),
582 SettingItem::new("b", "Beta", "x").with_values(&["x", "y"]),
583 ];
584 let list = SettingsList::new(settings);
585 let cancelled = Arc::new(Mutex::new(false));
586 let cancelled_cb = cancelled.clone();
587 list.on_cancel(Arc::new(move || {
588 *cancelled_cb.lock().unwrap() = true;
589 }));
590
591 list.handle_key(KeyEvent::new(
592 KeyCode::Down,
593 crossterm::event::KeyModifiers::NONE,
594 ));
595 assert_eq!(list.get_selected().unwrap().key, "b");
596 list.handle_key(KeyEvent::new(
597 KeyCode::Char(' '),
598 crossterm::event::KeyModifiers::NONE,
599 ));
600 assert_eq!(list.value_of("b").as_deref(), Some("y"));
601 list.handle_key(KeyEvent::new(
602 KeyCode::Esc,
603 crossterm::event::KeyModifiers::NONE,
604 ));
605 assert!(*cancelled.lock().unwrap());
606 }
607}