1use super::schema::{SettingCategory, SettingSchema, SettingType};
6use crate::config_io::ConfigLayer;
7use crate::view::controls::{
8 DropdownState, FocusState, KeybindingListState, MapState, NumberInputState, TextInputState,
9 TextListState, ToggleState,
10};
11use crate::view::ui::{FocusRegion, ScrollItem, TextEdit};
12use std::collections::{HashMap, HashSet};
13
14#[derive(Debug, Clone)]
16pub struct JsonEditState {
17 pub editor: TextEdit,
19 pub original_text: String,
21 pub label: String,
23 pub focus: FocusState,
25 pub scroll_offset: usize,
27 pub max_visible_lines: usize,
29}
30
31impl JsonEditState {
32 pub fn new(label: impl Into<String>, value: Option<&serde_json::Value>) -> Self {
34 let json_str = value
35 .map(|v| serde_json::to_string_pretty(v).unwrap_or_else(|_| "null".to_string()))
36 .unwrap_or_else(|| "null".to_string());
37
38 Self {
39 original_text: json_str.clone(),
40 editor: TextEdit::with_text(&json_str),
41 label: label.into(),
42 focus: FocusState::Normal,
43 scroll_offset: 0,
44 max_visible_lines: 6,
45 }
46 }
47
48 pub fn revert(&mut self) {
50 self.editor.set_value(&self.original_text);
51 self.scroll_offset = 0;
52 }
53
54 pub fn commit(&mut self) {
56 self.original_text = self.editor.value();
57 }
58
59 pub fn value(&self) -> String {
61 self.editor.value()
62 }
63
64 pub fn is_valid(&self) -> bool {
66 serde_json::from_str::<serde_json::Value>(&self.value()).is_ok()
67 }
68
69 pub fn display_height(&self) -> usize {
71 self.editor.line_count()
72 }
73
74 pub fn display_height_capped(&self) -> usize {
76 self.editor.line_count().min(self.max_visible_lines)
77 }
78
79 pub fn lines(&self) -> &[String] {
81 &self.editor.lines
82 }
83
84 pub fn cursor_pos(&self) -> (usize, usize) {
86 (self.editor.cursor_row, self.editor.cursor_col)
87 }
88
89 pub fn insert(&mut self, c: char) {
91 self.editor.insert_char(c);
92 }
93
94 pub fn insert_str(&mut self, s: &str) {
95 self.editor.insert_str(s);
96 }
97
98 pub fn backspace(&mut self) {
99 self.editor.backspace();
100 }
101
102 pub fn delete(&mut self) {
103 self.editor.delete();
104 }
105
106 pub fn move_left(&mut self) {
107 self.editor.move_left();
108 }
109
110 pub fn move_right(&mut self) {
111 self.editor.move_right();
112 }
113
114 pub fn move_up(&mut self) {
115 self.editor.move_up();
116 }
117
118 pub fn move_down(&mut self) {
119 self.editor.move_down();
120 }
121
122 pub fn move_home(&mut self) {
123 self.editor.move_home();
124 }
125
126 pub fn move_end(&mut self) {
127 self.editor.move_end();
128 }
129
130 pub fn move_word_left(&mut self) {
131 self.editor.move_word_left();
132 }
133
134 pub fn move_word_right(&mut self) {
135 self.editor.move_word_right();
136 }
137
138 pub fn has_selection(&self) -> bool {
140 self.editor.has_selection()
141 }
142
143 pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
144 self.editor.selection_range()
145 }
146
147 pub fn selected_text(&self) -> Option<String> {
148 self.editor.selected_text()
149 }
150
151 pub fn delete_selection(&mut self) -> Option<String> {
152 self.editor.delete_selection()
153 }
154
155 pub fn clear_selection(&mut self) {
156 self.editor.clear_selection();
157 }
158
159 pub fn move_left_selecting(&mut self) {
160 self.editor.move_left_selecting();
161 }
162
163 pub fn move_right_selecting(&mut self) {
164 self.editor.move_right_selecting();
165 }
166
167 pub fn move_up_selecting(&mut self) {
168 self.editor.move_up_selecting();
169 }
170
171 pub fn move_down_selecting(&mut self) {
172 self.editor.move_down_selecting();
173 }
174
175 pub fn move_home_selecting(&mut self) {
176 self.editor.move_home_selecting();
177 }
178
179 pub fn move_end_selecting(&mut self) {
180 self.editor.move_end_selecting();
181 }
182
183 pub fn move_word_left_selecting(&mut self) {
184 self.editor.move_word_left_selecting();
185 }
186
187 pub fn move_word_right_selecting(&mut self) {
188 self.editor.move_word_right_selecting();
189 }
190
191 pub fn select_all(&mut self) {
192 self.editor.select_all();
193 }
194
195 pub fn delete_word_forward(&mut self) {
196 self.editor.delete_word_forward();
197 }
198
199 pub fn delete_word_backward(&mut self) {
200 self.editor.delete_word_backward();
201 }
202
203 pub fn delete_to_end(&mut self) {
204 self.editor.delete_to_end();
205 }
206}
207
208fn json_control(
210 name: &str,
211 current_value: Option<&serde_json::Value>,
212 default: Option<&serde_json::Value>,
213) -> SettingControl {
214 let value = current_value.or(default);
215 SettingControl::Json(JsonEditState::new(name, value))
216}
217
218#[derive(Debug, Clone)]
220pub struct SettingItem {
221 pub path: String,
223 pub name: String,
225 pub description: Option<String>,
227 pub control: SettingControl,
229 pub default: Option<serde_json::Value>,
231 pub modified: bool,
235 pub layer_source: ConfigLayer,
238 pub read_only: bool,
240 pub is_auto_managed: bool,
242 pub section: Option<String>,
244 pub is_section_start: bool,
246 pub layout_width: u16,
248}
249
250#[derive(Debug, Clone)]
252pub enum SettingControl {
253 Toggle(ToggleState),
254 Number(NumberInputState),
255 Dropdown(DropdownState),
256 Text(TextInputState),
257 TextList(TextListState),
258 Map(MapState),
260 ObjectArray(KeybindingListState),
262 Json(JsonEditState),
264 Complex {
266 type_name: String,
267 },
268}
269
270impl SettingControl {
271 pub fn control_height(&self) -> u16 {
273 match self {
274 Self::TextList(state) => {
276 (state.items.len() + 2) as u16
278 }
279 Self::Map(state) => {
281 let header_row = if state.display_field.is_some() { 1 } else { 0 };
282 let add_new_row = if state.no_add { 0 } else { 1 };
283 let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
286 .expanded
287 .iter()
288 .filter_map(|&idx| state.entries.get(idx))
289 .map(|(_, v)| {
290 if let Some(obj) = v.as_object() {
291 obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
292 } else {
293 0
294 }
295 })
296 .sum();
297 (base + expanded_height) as u16
298 }
299 Self::Dropdown(state) => {
301 if state.open {
302 1 + state.options.len().min(8) as u16
304 } else {
305 1
306 }
307 }
308 Self::ObjectArray(state) => {
310 (state.bindings.len() + 2) as u16
312 }
313 Self::Json(state) => {
315 1 + state.display_height() as u16
317 }
318 _ => 1,
320 }
321 }
322
323 pub fn is_composite(&self) -> bool {
327 matches!(
328 self,
329 Self::TextList(_) | Self::Map(_) | Self::ObjectArray(_)
330 )
331 }
332
333 pub fn focused_sub_row(&self) -> u16 {
337 match self {
338 Self::TextList(state) => {
339 match state.focused_item {
341 Some(idx) => 1 + idx as u16, None => 1 + state.items.len() as u16, }
344 }
345 Self::ObjectArray(state) => {
346 match state.focused_index {
348 Some(idx) => 1 + idx as u16,
349 None => 1 + state.bindings.len() as u16,
350 }
351 }
352 Self::Map(state) => {
353 let header_offset = if state.display_field.is_some() { 1 } else { 0 };
355 match state.focused_entry {
356 Some(idx) => 1 + header_offset + idx as u16,
357 None => 1 + header_offset + state.entries.len() as u16,
358 }
359 }
360 _ => 0,
361 }
362 }
363}
364
365pub const SECTION_HEADER_HEIGHT: u16 = 2;
367
368impl SettingItem {
369 pub fn item_height(&self) -> u16 {
372 let section_height = if self.is_section_start {
374 SECTION_HEADER_HEIGHT
375 } else {
376 0
377 };
378 let description_height = self.description_height(self.layout_width);
379 section_height + self.control.control_height() + description_height + 1
380 }
381
382 pub fn description_height(&self, width: u16) -> u16 {
385 if let Some(ref desc) = self.description {
386 if desc.is_empty() {
387 return 0;
388 }
389 if width == 0 {
390 return 1;
391 }
392 let chars_per_line = width.saturating_sub(2) as usize; if chars_per_line == 0 {
395 return 1;
396 }
397 desc.len().div_ceil(chars_per_line) as u16
398 } else {
399 0
400 }
401 }
402
403 pub fn content_height(&self) -> u16 {
406 let description_height = self.description_height(self.layout_width);
407 self.control.control_height() + description_height
408 }
409}
410
411pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
414 let desc = description?;
415 if desc.is_empty() {
416 return None;
417 }
418
419 let name_words: HashSet<String> = name
421 .to_lowercase()
422 .split(|c: char| !c.is_alphanumeric())
423 .filter(|w| !w.is_empty() && w.len() > 2)
424 .map(String::from)
425 .collect();
426
427 let filler_words: HashSet<&str> = [
429 "the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
430 "whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
431 "disable", "set", "use", "allow", "default", "true", "false",
432 ]
433 .into_iter()
434 .collect();
435
436 let desc_words: Vec<&str> = desc
438 .split(|c: char| !c.is_alphanumeric())
439 .filter(|w| !w.is_empty())
440 .collect();
441
442 let has_new_info = desc_words.iter().any(|word| {
444 let lower = word.to_lowercase();
445 lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
446 });
447
448 if !has_new_info {
449 return None;
450 }
451
452 Some(desc.to_string())
453}
454
455impl ScrollItem for SettingItem {
456 fn height(&self) -> u16 {
457 self.item_height()
458 }
459
460 fn focus_regions(&self) -> Vec<FocusRegion> {
461 match &self.control {
462 SettingControl::TextList(state) => {
464 let mut regions = Vec::new();
465 regions.push(FocusRegion {
467 id: 0,
468 y_offset: 0,
469 height: 1,
470 });
471 for i in 0..state.items.len() {
473 regions.push(FocusRegion {
474 id: 1 + i,
475 y_offset: 1 + i as u16,
476 height: 1,
477 });
478 }
479 regions.push(FocusRegion {
481 id: 1 + state.items.len(),
482 y_offset: 1 + state.items.len() as u16,
483 height: 1,
484 });
485 regions
486 }
487 SettingControl::Map(state) => {
489 let mut regions = Vec::new();
490 let mut y = 0u16;
491
492 regions.push(FocusRegion {
494 id: 0,
495 y_offset: y,
496 height: 1,
497 });
498 y += 1;
499
500 if state.display_field.is_some() {
502 y += 1;
503 }
504
505 for (i, (_, v)) in state.entries.iter().enumerate() {
507 let mut entry_height = 1u16;
508 if state.expanded.contains(&i) {
510 if let Some(obj) = v.as_object() {
511 entry_height += obj.len().min(5) as u16;
512 if obj.len() > 5 {
513 entry_height += 1;
514 }
515 }
516 }
517 regions.push(FocusRegion {
518 id: 1 + i,
519 y_offset: y,
520 height: entry_height,
521 });
522 y += entry_height;
523 }
524
525 regions.push(FocusRegion {
527 id: 1 + state.entries.len(),
528 y_offset: y,
529 height: 1,
530 });
531 regions
532 }
533 SettingControl::ObjectArray(state) => {
535 let mut regions = Vec::new();
536 regions.push(FocusRegion {
538 id: 0,
539 y_offset: 0,
540 height: 1,
541 });
542 for i in 0..state.bindings.len() {
544 regions.push(FocusRegion {
545 id: 1 + i,
546 y_offset: 1 + i as u16,
547 height: 1,
548 });
549 }
550 regions.push(FocusRegion {
552 id: 1 + state.bindings.len(),
553 y_offset: 1 + state.bindings.len() as u16,
554 height: 1,
555 });
556 regions
557 }
558 _ => {
560 vec![FocusRegion {
561 id: 0,
562 y_offset: 0,
563 height: self.item_height().saturating_sub(1), }]
565 }
566 }
567 }
568}
569
570#[derive(Debug, Clone)]
572pub struct SettingsPage {
573 pub name: String,
575 pub path: String,
577 pub description: Option<String>,
579 pub items: Vec<SettingItem>,
581 pub subpages: Vec<SettingsPage>,
583}
584
585pub struct BuildContext<'a> {
587 pub config_value: &'a serde_json::Value,
589 pub layer_sources: &'a HashMap<String, ConfigLayer>,
591 pub target_layer: ConfigLayer,
593}
594
595pub fn build_pages(
597 categories: &[SettingCategory],
598 config_value: &serde_json::Value,
599 layer_sources: &HashMap<String, ConfigLayer>,
600 target_layer: ConfigLayer,
601) -> Vec<SettingsPage> {
602 let ctx = BuildContext {
603 config_value,
604 layer_sources,
605 target_layer,
606 };
607 categories.iter().map(|cat| build_page(cat, &ctx)).collect()
608}
609
610fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
612 let mut items: Vec<SettingItem> = category
613 .settings
614 .iter()
615 .map(|s| build_item(s, ctx))
616 .collect();
617
618 items.sort_by(|a, b| match (&a.section, &b.section) {
620 (Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
621 (Some(_), None) => std::cmp::Ordering::Less,
622 (None, Some(_)) => std::cmp::Ordering::Greater,
623 (None, None) => a.name.cmp(&b.name),
624 });
625
626 let mut prev_section: Option<&String> = None;
628 for item in &mut items {
629 let is_new_section = match (&item.section, prev_section) {
630 (Some(sec), Some(prev)) => sec != prev,
631 (Some(_), None) => true,
632 (None, Some(_)) => false, (None, None) => false,
634 };
635 item.is_section_start = is_new_section;
636 prev_section = item.section.as_ref();
637 }
638
639 let subpages = category
640 .subcategories
641 .iter()
642 .map(|sub| build_page(sub, ctx))
643 .collect();
644
645 SettingsPage {
646 name: category.name.clone(),
647 path: category.path.clone(),
648 description: category.description.clone(),
649 items,
650 subpages,
651 }
652}
653
654pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
656 let current_value = ctx.config_value.pointer(&schema.path);
658
659 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
661
662 let control = match &schema.setting_type {
664 SettingType::Boolean => {
665 let checked = current_value
666 .and_then(|v| v.as_bool())
667 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
668 .unwrap_or(false);
669 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
670 }
671
672 SettingType::Integer { minimum, maximum } => {
673 let value = current_value
674 .and_then(|v| v.as_i64())
675 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
676 .unwrap_or(0);
677
678 let mut state = NumberInputState::new(value, &schema.name);
679 if let Some(min) = minimum {
680 state = state.with_min(*min);
681 }
682 if let Some(max) = maximum {
683 state = state.with_max(*max);
684 }
685 SettingControl::Number(state)
686 }
687
688 SettingType::Number { minimum, maximum } => {
689 let value = current_value
691 .and_then(|v| v.as_f64())
692 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
693 .unwrap_or(0.0);
694
695 let int_value = (value * 100.0).round() as i64;
697 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
698 if let Some(min) = minimum {
699 state = state.with_min((*min * 100.0) as i64);
700 }
701 if let Some(max) = maximum {
702 state = state.with_max((*max * 100.0) as i64);
703 }
704 SettingControl::Number(state)
705 }
706
707 SettingType::String => {
708 let value = current_value
709 .and_then(|v| v.as_str())
710 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
711 .unwrap_or("");
712
713 let state = TextInputState::new(&schema.name).with_value(value);
714 SettingControl::Text(state)
715 }
716
717 SettingType::Enum { options } => {
718 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
720 "" } else {
722 current_value
723 .and_then(|v| v.as_str())
724 .or_else(|| {
725 let default = schema.default.as_ref()?;
726 if default.is_null() {
727 Some("")
728 } else {
729 default.as_str()
730 }
731 })
732 .unwrap_or("")
733 };
734
735 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
736 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
737 let selected = values.iter().position(|v| v == current).unwrap_or(0);
738 let state = DropdownState::with_values(display_names, values, &schema.name)
739 .with_selected(selected);
740 SettingControl::Dropdown(state)
741 }
742
743 SettingType::StringArray => {
744 let items: Vec<String> = current_value
745 .and_then(|v| v.as_array())
746 .map(|arr| {
747 arr.iter()
748 .filter_map(|v| v.as_str().map(String::from))
749 .collect()
750 })
751 .or_else(|| {
752 schema.default.as_ref().and_then(|d| {
753 d.as_array().map(|arr| {
754 arr.iter()
755 .filter_map(|v| v.as_str().map(String::from))
756 .collect()
757 })
758 })
759 })
760 .unwrap_or_default();
761
762 let state = TextListState::new(&schema.name).with_items(items);
763 SettingControl::TextList(state)
764 }
765
766 SettingType::IntegerArray => {
767 let items: Vec<String> = current_value
768 .and_then(|v| v.as_array())
769 .map(|arr| {
770 arr.iter()
771 .filter_map(|v| {
772 v.as_i64()
773 .map(|n| n.to_string())
774 .or_else(|| v.as_u64().map(|n| n.to_string()))
775 .or_else(|| v.as_f64().map(|n| n.to_string()))
776 })
777 .collect()
778 })
779 .or_else(|| {
780 schema.default.as_ref().and_then(|d| {
781 d.as_array().map(|arr| {
782 arr.iter()
783 .filter_map(|v| {
784 v.as_i64()
785 .map(|n| n.to_string())
786 .or_else(|| v.as_u64().map(|n| n.to_string()))
787 .or_else(|| v.as_f64().map(|n| n.to_string()))
788 })
789 .collect()
790 })
791 })
792 })
793 .unwrap_or_default();
794
795 let state = TextListState::new(&schema.name)
796 .with_items(items)
797 .with_integer_mode();
798 SettingControl::TextList(state)
799 }
800
801 SettingType::Object { .. } => {
802 json_control(&schema.name, current_value, schema.default.as_ref())
803 }
804
805 SettingType::Map {
806 value_schema,
807 display_field,
808 no_add,
809 } => {
810 let map_value = current_value
812 .cloned()
813 .or_else(|| schema.default.clone())
814 .unwrap_or_else(|| serde_json::json!({}));
815
816 let mut state = MapState::new(&schema.name).with_entries(&map_value);
817 state = state.with_value_schema((**value_schema).clone());
818 if let Some(field) = display_field {
819 state = state.with_display_field(field.clone());
820 }
821 if *no_add {
822 state = state.with_no_add(true);
823 }
824 SettingControl::Map(state)
825 }
826
827 SettingType::ObjectArray {
828 item_schema,
829 display_field,
830 } => {
831 let array_value = current_value
833 .cloned()
834 .or_else(|| schema.default.clone())
835 .unwrap_or_else(|| serde_json::json!([]));
836
837 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
838 state = state.with_item_schema((**item_schema).clone());
839 if let Some(field) = display_field {
840 state = state.with_display_field(field.clone());
841 }
842 SettingControl::ObjectArray(state)
843 }
844
845 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
846 };
847
848 let layer_source = ctx
850 .layer_sources
851 .get(&schema.path)
852 .copied()
853 .unwrap_or(ConfigLayer::System);
854
855 let modified = if is_auto_managed {
858 false } else {
860 layer_source == ctx.target_layer
861 };
862
863 let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
865
866 SettingItem {
867 path: schema.path.clone(),
868 name: schema.name.clone(),
869 description: cleaned_description,
870 control,
871 default: schema.default.clone(),
872 modified,
873 layer_source,
874 read_only: schema.read_only,
875 is_auto_managed,
876 section: schema.section.clone(),
877 is_section_start: false, layout_width: 0,
879 }
880}
881
882pub fn build_item_from_value(
884 schema: &SettingSchema,
885 current_value: Option<&serde_json::Value>,
886) -> SettingItem {
887 let control = match &schema.setting_type {
889 SettingType::Boolean => {
890 let checked = current_value
891 .and_then(|v| v.as_bool())
892 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
893 .unwrap_or(false);
894 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
895 }
896
897 SettingType::Integer { minimum, maximum } => {
898 let value = current_value
899 .and_then(|v| v.as_i64())
900 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
901 .unwrap_or(0);
902
903 let mut state = NumberInputState::new(value, &schema.name);
904 if let Some(min) = minimum {
905 state = state.with_min(*min);
906 }
907 if let Some(max) = maximum {
908 state = state.with_max(*max);
909 }
910 SettingControl::Number(state)
911 }
912
913 SettingType::Number { minimum, maximum } => {
914 let value = current_value
915 .and_then(|v| v.as_f64())
916 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
917 .unwrap_or(0.0);
918
919 let int_value = (value * 100.0).round() as i64;
920 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
921 if let Some(min) = minimum {
922 state = state.with_min((*min * 100.0) as i64);
923 }
924 if let Some(max) = maximum {
925 state = state.with_max((*max * 100.0) as i64);
926 }
927 SettingControl::Number(state)
928 }
929
930 SettingType::String => {
931 let value = current_value
932 .and_then(|v| v.as_str())
933 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
934 .unwrap_or("");
935
936 let state = TextInputState::new(&schema.name).with_value(value);
937 SettingControl::Text(state)
938 }
939
940 SettingType::Enum { options } => {
941 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
943 "" } else {
945 current_value
946 .and_then(|v| v.as_str())
947 .or_else(|| {
948 let default = schema.default.as_ref()?;
949 if default.is_null() {
950 Some("")
951 } else {
952 default.as_str()
953 }
954 })
955 .unwrap_or("")
956 };
957
958 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
959 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
960 let selected = values.iter().position(|v| v == current).unwrap_or(0);
961 let state = DropdownState::with_values(display_names, values, &schema.name)
962 .with_selected(selected);
963 SettingControl::Dropdown(state)
964 }
965
966 SettingType::StringArray => {
967 let items: Vec<String> = current_value
968 .and_then(|v| v.as_array())
969 .map(|arr| {
970 arr.iter()
971 .filter_map(|v| v.as_str().map(String::from))
972 .collect()
973 })
974 .or_else(|| {
975 schema.default.as_ref().and_then(|d| {
976 d.as_array().map(|arr| {
977 arr.iter()
978 .filter_map(|v| v.as_str().map(String::from))
979 .collect()
980 })
981 })
982 })
983 .unwrap_or_default();
984
985 let state = TextListState::new(&schema.name).with_items(items);
986 SettingControl::TextList(state)
987 }
988
989 SettingType::IntegerArray => {
990 let items: Vec<String> = current_value
991 .and_then(|v| v.as_array())
992 .map(|arr| {
993 arr.iter()
994 .filter_map(|v| {
995 v.as_i64()
996 .map(|n| n.to_string())
997 .or_else(|| v.as_u64().map(|n| n.to_string()))
998 .or_else(|| v.as_f64().map(|n| n.to_string()))
999 })
1000 .collect()
1001 })
1002 .or_else(|| {
1003 schema.default.as_ref().and_then(|d| {
1004 d.as_array().map(|arr| {
1005 arr.iter()
1006 .filter_map(|v| {
1007 v.as_i64()
1008 .map(|n| n.to_string())
1009 .or_else(|| v.as_u64().map(|n| n.to_string()))
1010 .or_else(|| v.as_f64().map(|n| n.to_string()))
1011 })
1012 .collect()
1013 })
1014 })
1015 })
1016 .unwrap_or_default();
1017
1018 let state = TextListState::new(&schema.name)
1019 .with_items(items)
1020 .with_integer_mode();
1021 SettingControl::TextList(state)
1022 }
1023
1024 SettingType::Object { .. } => {
1025 json_control(&schema.name, current_value, schema.default.as_ref())
1026 }
1027
1028 SettingType::Map {
1029 value_schema,
1030 display_field,
1031 no_add,
1032 } => {
1033 let map_value = current_value
1034 .cloned()
1035 .or_else(|| schema.default.clone())
1036 .unwrap_or_else(|| serde_json::json!({}));
1037
1038 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1039 state = state.with_value_schema((**value_schema).clone());
1040 if let Some(field) = display_field {
1041 state = state.with_display_field(field.clone());
1042 }
1043 if *no_add {
1044 state = state.with_no_add(true);
1045 }
1046 SettingControl::Map(state)
1047 }
1048
1049 SettingType::ObjectArray {
1050 item_schema,
1051 display_field,
1052 } => {
1053 let array_value = current_value
1054 .cloned()
1055 .or_else(|| schema.default.clone())
1056 .unwrap_or_else(|| serde_json::json!([]));
1057
1058 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1059 state = state.with_item_schema((**item_schema).clone());
1060 if let Some(field) = display_field {
1061 state = state.with_display_field(field.clone());
1062 }
1063 SettingControl::ObjectArray(state)
1064 }
1065
1066 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1067 };
1068
1069 let modified = match (¤t_value, &schema.default) {
1072 (Some(current), Some(default)) => *current != default,
1073 (Some(_), None) => true,
1074 _ => false,
1075 };
1076
1077 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
1079
1080 SettingItem {
1081 path: schema.path.clone(),
1082 name: schema.name.clone(),
1083 description: schema.description.clone(),
1084 control,
1085 default: schema.default.clone(),
1086 modified,
1087 layer_source: ConfigLayer::System,
1089 read_only: schema.read_only,
1090 is_auto_managed,
1091 section: schema.section.clone(),
1092 is_section_start: false, layout_width: 0,
1094 }
1095}
1096
1097pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
1099 match control {
1100 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
1101
1102 SettingControl::Number(state) => {
1103 if state.is_percentage {
1104 let float_value = state.value as f64 / 100.0;
1106 serde_json::Number::from_f64(float_value)
1107 .map(serde_json::Value::Number)
1108 .unwrap_or(serde_json::Value::Number(state.value.into()))
1109 } else {
1110 serde_json::Value::Number(state.value.into())
1111 }
1112 }
1113
1114 SettingControl::Dropdown(state) => state
1115 .selected_value()
1116 .map(|s| {
1117 if s.is_empty() {
1118 serde_json::Value::Null
1120 } else {
1121 serde_json::Value::String(s.to_string())
1122 }
1123 })
1124 .unwrap_or(serde_json::Value::Null),
1125
1126 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1127
1128 SettingControl::TextList(state) => {
1129 let arr: Vec<serde_json::Value> = state
1130 .items
1131 .iter()
1132 .filter_map(|s| {
1133 if state.is_integer {
1134 s.parse::<i64>()
1135 .ok()
1136 .map(|n| serde_json::Value::Number(n.into()))
1137 } else {
1138 Some(serde_json::Value::String(s.clone()))
1139 }
1140 })
1141 .collect();
1142 serde_json::Value::Array(arr)
1143 }
1144
1145 SettingControl::Map(state) => state.to_value(),
1146
1147 SettingControl::ObjectArray(state) => state.to_value(),
1148
1149 SettingControl::Json(state) => {
1150 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1152 }
1153
1154 SettingControl::Complex { .. } => serde_json::Value::Null,
1155 }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161
1162 fn sample_config() -> serde_json::Value {
1163 serde_json::json!({
1164 "theme": "monokai",
1165 "check_for_updates": false,
1166 "editor": {
1167 "tab_size": 2,
1168 "line_numbers": true
1169 }
1170 })
1171 }
1172
1173 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1175 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1177 std::sync::LazyLock::new(HashMap::new);
1178 BuildContext {
1179 config_value: config,
1180 layer_sources: &EMPTY_SOURCES,
1181 target_layer: ConfigLayer::User,
1182 }
1183 }
1184
1185 fn test_context_with_sources<'a>(
1187 config: &'a serde_json::Value,
1188 layer_sources: &'a HashMap<String, ConfigLayer>,
1189 target_layer: ConfigLayer,
1190 ) -> BuildContext<'a> {
1191 BuildContext {
1192 config_value: config,
1193 layer_sources,
1194 target_layer,
1195 }
1196 }
1197
1198 #[test]
1199 fn test_build_toggle_item() {
1200 let schema = SettingSchema {
1201 path: "/check_for_updates".to_string(),
1202 name: "Check For Updates".to_string(),
1203 description: Some("Check for updates".to_string()),
1204 setting_type: SettingType::Boolean,
1205 default: Some(serde_json::Value::Bool(true)),
1206 read_only: false,
1207 section: None,
1208 order: None,
1209 };
1210
1211 let config = sample_config();
1212 let ctx = test_context(&config);
1213 let item = build_item(&schema, &ctx);
1214
1215 assert_eq!(item.path, "/check_for_updates");
1216 assert!(!item.modified);
1219 assert_eq!(item.layer_source, ConfigLayer::System);
1220
1221 if let SettingControl::Toggle(state) = &item.control {
1222 assert!(!state.checked); } else {
1224 panic!("Expected toggle control");
1225 }
1226 }
1227
1228 #[test]
1229 fn test_build_toggle_item_modified_in_user_layer() {
1230 let schema = SettingSchema {
1231 path: "/check_for_updates".to_string(),
1232 name: "Check For Updates".to_string(),
1233 description: Some("Check for updates".to_string()),
1234 setting_type: SettingType::Boolean,
1235 default: Some(serde_json::Value::Bool(true)),
1236 read_only: false,
1237 section: None,
1238 order: None,
1239 };
1240
1241 let config = sample_config();
1242 let mut layer_sources = HashMap::new();
1243 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1244 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1245 let item = build_item(&schema, &ctx);
1246
1247 assert!(item.modified);
1250 assert_eq!(item.layer_source, ConfigLayer::User);
1251 }
1252
1253 #[test]
1254 fn test_build_number_item() {
1255 let schema = SettingSchema {
1256 path: "/editor/tab_size".to_string(),
1257 name: "Tab Size".to_string(),
1258 description: None,
1259 setting_type: SettingType::Integer {
1260 minimum: Some(1),
1261 maximum: Some(16),
1262 },
1263 default: Some(serde_json::Value::Number(4.into())),
1264 read_only: false,
1265 section: None,
1266 order: None,
1267 };
1268
1269 let config = sample_config();
1270 let ctx = test_context(&config);
1271 let item = build_item(&schema, &ctx);
1272
1273 assert!(!item.modified);
1275
1276 if let SettingControl::Number(state) = &item.control {
1277 assert_eq!(state.value, 2);
1278 assert_eq!(state.min, Some(1));
1279 assert_eq!(state.max, Some(16));
1280 } else {
1281 panic!("Expected number control");
1282 }
1283 }
1284
1285 #[test]
1286 fn test_build_text_item() {
1287 let schema = SettingSchema {
1288 path: "/theme".to_string(),
1289 name: "Theme".to_string(),
1290 description: None,
1291 setting_type: SettingType::String,
1292 default: Some(serde_json::Value::String("high-contrast".to_string())),
1293 read_only: false,
1294 section: None,
1295 order: None,
1296 };
1297
1298 let config = sample_config();
1299 let ctx = test_context(&config);
1300 let item = build_item(&schema, &ctx);
1301
1302 assert!(!item.modified);
1304
1305 if let SettingControl::Text(state) = &item.control {
1306 assert_eq!(state.value, "monokai");
1307 } else {
1308 panic!("Expected text control");
1309 }
1310 }
1311
1312 #[test]
1313 fn test_clean_description_keeps_full_desc_with_new_info() {
1314 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1316 assert!(result.is_some());
1317 let cleaned = result.unwrap();
1318 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1321 assert!(cleaned.contains("character"));
1322 }
1323
1324 #[test]
1325 fn test_clean_description_keeps_extra_info() {
1326 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1328 assert!(result.is_some());
1329 let cleaned = result.unwrap();
1330 assert!(cleaned.contains("gutter"));
1331 }
1332
1333 #[test]
1334 fn test_clean_description_returns_none_for_pure_redundancy() {
1335 let result = clean_description("Theme", Some("Theme"));
1337 assert!(result.is_none());
1338
1339 let result = clean_description("Theme", Some("The theme to use"));
1341 assert!(result.is_none());
1342 }
1343
1344 #[test]
1345 fn test_clean_description_returns_none_for_empty() {
1346 let result = clean_description("Theme", Some(""));
1347 assert!(result.is_none());
1348
1349 let result = clean_description("Theme", None);
1350 assert!(result.is_none());
1351 }
1352
1353 #[test]
1354 fn test_control_to_value() {
1355 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1356 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1357
1358 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1359 assert_eq!(control_to_value(&number), serde_json::json!(42));
1360
1361 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1362 assert_eq!(
1363 control_to_value(&text),
1364 serde_json::Value::String("hello".to_string())
1365 );
1366 }
1367}