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}
247
248#[derive(Debug, Clone)]
250pub enum SettingControl {
251 Toggle(ToggleState),
252 Number(NumberInputState),
253 Dropdown(DropdownState),
254 Text(TextInputState),
255 TextList(TextListState),
256 Map(MapState),
258 ObjectArray(KeybindingListState),
260 Json(JsonEditState),
262 Complex {
264 type_name: String,
265 },
266}
267
268impl SettingControl {
269 pub fn control_height(&self) -> u16 {
271 match self {
272 Self::TextList(state) => {
274 (state.items.len() + 2) as u16
276 }
277 Self::Map(state) => {
279 let header_row = if state.display_field.is_some() { 1 } else { 0 };
280 let add_new_row = if state.no_add { 0 } else { 1 };
281 let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
284 .expanded
285 .iter()
286 .filter_map(|&idx| state.entries.get(idx))
287 .map(|(_, v)| {
288 if let Some(obj) = v.as_object() {
289 obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
290 } else {
291 0
292 }
293 })
294 .sum();
295 (base + expanded_height) as u16
296 }
297 Self::Dropdown(state) => {
299 if state.open {
300 1 + state.options.len().min(8) as u16
302 } else {
303 1
304 }
305 }
306 Self::ObjectArray(state) => {
308 (state.bindings.len() + 2) as u16
310 }
311 Self::Json(state) => {
313 1 + state.display_height() as u16
315 }
316 _ => 1,
318 }
319 }
320}
321
322pub const SECTION_HEADER_HEIGHT: u16 = 2;
324
325impl SettingItem {
326 pub fn item_height(&self) -> u16 {
328 let section_height = if self.is_section_start {
330 SECTION_HEADER_HEIGHT
331 } else {
332 0
333 };
334 let description_height = if self.description.is_some() { 1 } else { 0 };
335 section_height + self.control.control_height() + description_height + 1
336 }
337
338 pub fn item_height_expanded(&self, width: u16) -> u16 {
340 let section_height = if self.is_section_start {
341 SECTION_HEADER_HEIGHT
342 } else {
343 0
344 };
345 let description_height = self.description_height_expanded(width);
346 section_height + self.control.control_height() + description_height + 1
347 }
348
349 pub fn description_height_expanded(&self, width: u16) -> u16 {
351 if let Some(ref desc) = self.description {
352 if desc.is_empty() || width == 0 {
353 return 0;
354 }
355 let chars_per_line = width.saturating_sub(2) as usize; if chars_per_line == 0 {
358 return 1;
359 }
360 desc.len().div_ceil(chars_per_line) as u16
361 } else {
362 0
363 }
364 }
365
366 pub fn content_height(&self) -> u16 {
368 let description_height = if self.description.is_some() { 1 } else { 0 };
369 self.control.control_height() + description_height
370 }
371
372 pub fn content_height_expanded(&self, width: u16) -> u16 {
374 let description_height = self.description_height_expanded(width);
375 self.control.control_height() + description_height
376 }
377}
378
379pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
382 let desc = description?;
383 if desc.is_empty() {
384 return None;
385 }
386
387 let name_words: HashSet<String> = name
389 .to_lowercase()
390 .split(|c: char| !c.is_alphanumeric())
391 .filter(|w| !w.is_empty() && w.len() > 2)
392 .map(String::from)
393 .collect();
394
395 let filler_words: HashSet<&str> = [
397 "the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
398 "whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
399 "disable", "set", "use", "allow", "default", "true", "false",
400 ]
401 .into_iter()
402 .collect();
403
404 let desc_words: Vec<&str> = desc
406 .split(|c: char| !c.is_alphanumeric())
407 .filter(|w| !w.is_empty())
408 .collect();
409
410 let has_new_info = desc_words.iter().any(|word| {
412 let lower = word.to_lowercase();
413 lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
414 });
415
416 if !has_new_info {
417 return None;
418 }
419
420 Some(desc.to_string())
421}
422
423impl ScrollItem for SettingItem {
424 fn height(&self) -> u16 {
425 self.item_height()
426 }
427
428 fn focus_regions(&self) -> Vec<FocusRegion> {
429 match &self.control {
430 SettingControl::TextList(state) => {
432 let mut regions = Vec::new();
433 regions.push(FocusRegion {
435 id: 0,
436 y_offset: 0,
437 height: 1,
438 });
439 for i in 0..state.items.len() {
441 regions.push(FocusRegion {
442 id: 1 + i,
443 y_offset: 1 + i as u16,
444 height: 1,
445 });
446 }
447 regions.push(FocusRegion {
449 id: 1 + state.items.len(),
450 y_offset: 1 + state.items.len() as u16,
451 height: 1,
452 });
453 regions
454 }
455 SettingControl::Map(state) => {
457 let mut regions = Vec::new();
458 let mut y = 0u16;
459
460 regions.push(FocusRegion {
462 id: 0,
463 y_offset: y,
464 height: 1,
465 });
466 y += 1;
467
468 if state.display_field.is_some() {
470 y += 1;
471 }
472
473 for (i, (_, v)) in state.entries.iter().enumerate() {
475 let mut entry_height = 1u16;
476 if state.expanded.contains(&i) {
478 if let Some(obj) = v.as_object() {
479 entry_height += obj.len().min(5) as u16;
480 if obj.len() > 5 {
481 entry_height += 1;
482 }
483 }
484 }
485 regions.push(FocusRegion {
486 id: 1 + i,
487 y_offset: y,
488 height: entry_height,
489 });
490 y += entry_height;
491 }
492
493 regions.push(FocusRegion {
495 id: 1 + state.entries.len(),
496 y_offset: y,
497 height: 1,
498 });
499 regions
500 }
501 SettingControl::ObjectArray(state) => {
503 let mut regions = Vec::new();
504 regions.push(FocusRegion {
506 id: 0,
507 y_offset: 0,
508 height: 1,
509 });
510 for i in 0..state.bindings.len() {
512 regions.push(FocusRegion {
513 id: 1 + i,
514 y_offset: 1 + i as u16,
515 height: 1,
516 });
517 }
518 regions.push(FocusRegion {
520 id: 1 + state.bindings.len(),
521 y_offset: 1 + state.bindings.len() as u16,
522 height: 1,
523 });
524 regions
525 }
526 _ => {
528 vec![FocusRegion {
529 id: 0,
530 y_offset: 0,
531 height: self.item_height().saturating_sub(1), }]
533 }
534 }
535 }
536}
537
538#[derive(Debug, Clone)]
540pub struct SettingsPage {
541 pub name: String,
543 pub path: String,
545 pub description: Option<String>,
547 pub items: Vec<SettingItem>,
549 pub subpages: Vec<SettingsPage>,
551}
552
553pub struct BuildContext<'a> {
555 pub config_value: &'a serde_json::Value,
557 pub layer_sources: &'a HashMap<String, ConfigLayer>,
559 pub target_layer: ConfigLayer,
561}
562
563pub fn build_pages(
565 categories: &[SettingCategory],
566 config_value: &serde_json::Value,
567 layer_sources: &HashMap<String, ConfigLayer>,
568 target_layer: ConfigLayer,
569) -> Vec<SettingsPage> {
570 let ctx = BuildContext {
571 config_value,
572 layer_sources,
573 target_layer,
574 };
575 categories.iter().map(|cat| build_page(cat, &ctx)).collect()
576}
577
578fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
580 let mut items: Vec<SettingItem> = category
581 .settings
582 .iter()
583 .map(|s| build_item(s, ctx))
584 .collect();
585
586 items.sort_by(|a, b| match (&a.section, &b.section) {
588 (Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
589 (Some(_), None) => std::cmp::Ordering::Less,
590 (None, Some(_)) => std::cmp::Ordering::Greater,
591 (None, None) => a.name.cmp(&b.name),
592 });
593
594 let mut prev_section: Option<&String> = None;
596 for item in &mut items {
597 let is_new_section = match (&item.section, prev_section) {
598 (Some(sec), Some(prev)) => sec != prev,
599 (Some(_), None) => true,
600 (None, Some(_)) => false, (None, None) => false,
602 };
603 item.is_section_start = is_new_section;
604 prev_section = item.section.as_ref();
605 }
606
607 let subpages = category
608 .subcategories
609 .iter()
610 .map(|sub| build_page(sub, ctx))
611 .collect();
612
613 SettingsPage {
614 name: category.name.clone(),
615 path: category.path.clone(),
616 description: category.description.clone(),
617 items,
618 subpages,
619 }
620}
621
622pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
624 let current_value = ctx.config_value.pointer(&schema.path);
626
627 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
629
630 let control = match &schema.setting_type {
632 SettingType::Boolean => {
633 let checked = current_value
634 .and_then(|v| v.as_bool())
635 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
636 .unwrap_or(false);
637 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
638 }
639
640 SettingType::Integer { minimum, maximum } => {
641 let value = current_value
642 .and_then(|v| v.as_i64())
643 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
644 .unwrap_or(0);
645
646 let mut state = NumberInputState::new(value, &schema.name);
647 if let Some(min) = minimum {
648 state = state.with_min(*min);
649 }
650 if let Some(max) = maximum {
651 state = state.with_max(*max);
652 }
653 SettingControl::Number(state)
654 }
655
656 SettingType::Number { minimum, maximum } => {
657 let value = current_value
659 .and_then(|v| v.as_f64())
660 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
661 .unwrap_or(0.0);
662
663 let int_value = (value * 100.0).round() as i64;
665 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
666 if let Some(min) = minimum {
667 state = state.with_min((*min * 100.0) as i64);
668 }
669 if let Some(max) = maximum {
670 state = state.with_max((*max * 100.0) as i64);
671 }
672 SettingControl::Number(state)
673 }
674
675 SettingType::String => {
676 let value = current_value
677 .and_then(|v| v.as_str())
678 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
679 .unwrap_or("");
680
681 let state = TextInputState::new(&schema.name).with_value(value);
682 SettingControl::Text(state)
683 }
684
685 SettingType::Enum { options } => {
686 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
688 "" } else {
690 current_value
691 .and_then(|v| v.as_str())
692 .or_else(|| {
693 let default = schema.default.as_ref()?;
694 if default.is_null() {
695 Some("")
696 } else {
697 default.as_str()
698 }
699 })
700 .unwrap_or("")
701 };
702
703 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
704 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
705 let selected = values.iter().position(|v| v == current).unwrap_or(0);
706 let state = DropdownState::with_values(display_names, values, &schema.name)
707 .with_selected(selected);
708 SettingControl::Dropdown(state)
709 }
710
711 SettingType::StringArray => {
712 let items: Vec<String> = current_value
713 .and_then(|v| v.as_array())
714 .map(|arr| {
715 arr.iter()
716 .filter_map(|v| v.as_str().map(String::from))
717 .collect()
718 })
719 .or_else(|| {
720 schema.default.as_ref().and_then(|d| {
721 d.as_array().map(|arr| {
722 arr.iter()
723 .filter_map(|v| v.as_str().map(String::from))
724 .collect()
725 })
726 })
727 })
728 .unwrap_or_default();
729
730 let state = TextListState::new(&schema.name).with_items(items);
731 SettingControl::TextList(state)
732 }
733
734 SettingType::Object { .. } => {
735 json_control(&schema.name, current_value, schema.default.as_ref())
736 }
737
738 SettingType::Map {
739 value_schema,
740 display_field,
741 no_add,
742 } => {
743 let map_value = current_value
745 .cloned()
746 .or_else(|| schema.default.clone())
747 .unwrap_or_else(|| serde_json::json!({}));
748
749 let mut state = MapState::new(&schema.name).with_entries(&map_value);
750 state = state.with_value_schema((**value_schema).clone());
751 if let Some(field) = display_field {
752 state = state.with_display_field(field.clone());
753 }
754 if *no_add {
755 state = state.with_no_add(true);
756 }
757 SettingControl::Map(state)
758 }
759
760 SettingType::ObjectArray {
761 item_schema,
762 display_field,
763 } => {
764 let array_value = current_value
766 .cloned()
767 .or_else(|| schema.default.clone())
768 .unwrap_or_else(|| serde_json::json!([]));
769
770 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
771 state = state.with_item_schema((**item_schema).clone());
772 if let Some(field) = display_field {
773 state = state.with_display_field(field.clone());
774 }
775 SettingControl::ObjectArray(state)
776 }
777
778 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
779 };
780
781 let layer_source = ctx
783 .layer_sources
784 .get(&schema.path)
785 .copied()
786 .unwrap_or(ConfigLayer::System);
787
788 let modified = if is_auto_managed {
791 false } else {
793 layer_source == ctx.target_layer
794 };
795
796 let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
798
799 SettingItem {
800 path: schema.path.clone(),
801 name: schema.name.clone(),
802 description: cleaned_description,
803 control,
804 default: schema.default.clone(),
805 modified,
806 layer_source,
807 read_only: schema.read_only,
808 is_auto_managed,
809 section: schema.section.clone(),
810 is_section_start: false, }
812}
813
814pub fn build_item_from_value(
816 schema: &SettingSchema,
817 current_value: Option<&serde_json::Value>,
818) -> SettingItem {
819 let control = match &schema.setting_type {
821 SettingType::Boolean => {
822 let checked = current_value
823 .and_then(|v| v.as_bool())
824 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
825 .unwrap_or(false);
826 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
827 }
828
829 SettingType::Integer { minimum, maximum } => {
830 let value = current_value
831 .and_then(|v| v.as_i64())
832 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
833 .unwrap_or(0);
834
835 let mut state = NumberInputState::new(value, &schema.name);
836 if let Some(min) = minimum {
837 state = state.with_min(*min);
838 }
839 if let Some(max) = maximum {
840 state = state.with_max(*max);
841 }
842 SettingControl::Number(state)
843 }
844
845 SettingType::Number { minimum, maximum } => {
846 let value = current_value
847 .and_then(|v| v.as_f64())
848 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
849 .unwrap_or(0.0);
850
851 let int_value = (value * 100.0).round() as i64;
852 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
853 if let Some(min) = minimum {
854 state = state.with_min((*min * 100.0) as i64);
855 }
856 if let Some(max) = maximum {
857 state = state.with_max((*max * 100.0) as i64);
858 }
859 SettingControl::Number(state)
860 }
861
862 SettingType::String => {
863 let value = current_value
864 .and_then(|v| v.as_str())
865 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
866 .unwrap_or("");
867
868 let state = TextInputState::new(&schema.name).with_value(value);
869 SettingControl::Text(state)
870 }
871
872 SettingType::Enum { options } => {
873 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
875 "" } else {
877 current_value
878 .and_then(|v| v.as_str())
879 .or_else(|| {
880 let default = schema.default.as_ref()?;
881 if default.is_null() {
882 Some("")
883 } else {
884 default.as_str()
885 }
886 })
887 .unwrap_or("")
888 };
889
890 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
891 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
892 let selected = values.iter().position(|v| v == current).unwrap_or(0);
893 let state = DropdownState::with_values(display_names, values, &schema.name)
894 .with_selected(selected);
895 SettingControl::Dropdown(state)
896 }
897
898 SettingType::StringArray => {
899 let items: Vec<String> = current_value
900 .and_then(|v| v.as_array())
901 .map(|arr| {
902 arr.iter()
903 .filter_map(|v| v.as_str().map(String::from))
904 .collect()
905 })
906 .or_else(|| {
907 schema.default.as_ref().and_then(|d| {
908 d.as_array().map(|arr| {
909 arr.iter()
910 .filter_map(|v| v.as_str().map(String::from))
911 .collect()
912 })
913 })
914 })
915 .unwrap_or_default();
916
917 let state = TextListState::new(&schema.name).with_items(items);
918 SettingControl::TextList(state)
919 }
920
921 SettingType::Object { .. } => {
922 json_control(&schema.name, current_value, schema.default.as_ref())
923 }
924
925 SettingType::Map {
926 value_schema,
927 display_field,
928 no_add,
929 } => {
930 let map_value = current_value
931 .cloned()
932 .or_else(|| schema.default.clone())
933 .unwrap_or_else(|| serde_json::json!({}));
934
935 let mut state = MapState::new(&schema.name).with_entries(&map_value);
936 state = state.with_value_schema((**value_schema).clone());
937 if let Some(field) = display_field {
938 state = state.with_display_field(field.clone());
939 }
940 if *no_add {
941 state = state.with_no_add(true);
942 }
943 SettingControl::Map(state)
944 }
945
946 SettingType::ObjectArray {
947 item_schema,
948 display_field,
949 } => {
950 let array_value = current_value
951 .cloned()
952 .or_else(|| schema.default.clone())
953 .unwrap_or_else(|| serde_json::json!([]));
954
955 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
956 state = state.with_item_schema((**item_schema).clone());
957 if let Some(field) = display_field {
958 state = state.with_display_field(field.clone());
959 }
960 SettingControl::ObjectArray(state)
961 }
962
963 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
964 };
965
966 let modified = match (¤t_value, &schema.default) {
969 (Some(current), Some(default)) => *current != default,
970 (Some(_), None) => true,
971 _ => false,
972 };
973
974 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
976
977 SettingItem {
978 path: schema.path.clone(),
979 name: schema.name.clone(),
980 description: schema.description.clone(),
981 control,
982 default: schema.default.clone(),
983 modified,
984 layer_source: ConfigLayer::System,
986 read_only: schema.read_only,
987 is_auto_managed,
988 section: schema.section.clone(),
989 is_section_start: false, }
991}
992
993pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
995 match control {
996 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
997
998 SettingControl::Number(state) => {
999 if state.is_percentage {
1000 let float_value = state.value as f64 / 100.0;
1002 serde_json::Number::from_f64(float_value)
1003 .map(serde_json::Value::Number)
1004 .unwrap_or(serde_json::Value::Number(state.value.into()))
1005 } else {
1006 serde_json::Value::Number(state.value.into())
1007 }
1008 }
1009
1010 SettingControl::Dropdown(state) => state
1011 .selected_value()
1012 .map(|s| {
1013 if s.is_empty() {
1014 serde_json::Value::Null
1016 } else {
1017 serde_json::Value::String(s.to_string())
1018 }
1019 })
1020 .unwrap_or(serde_json::Value::Null),
1021
1022 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1023
1024 SettingControl::TextList(state) => {
1025 let arr: Vec<serde_json::Value> = state
1026 .items
1027 .iter()
1028 .map(|s| serde_json::Value::String(s.clone()))
1029 .collect();
1030 serde_json::Value::Array(arr)
1031 }
1032
1033 SettingControl::Map(state) => state.to_value(),
1034
1035 SettingControl::ObjectArray(state) => state.to_value(),
1036
1037 SettingControl::Json(state) => {
1038 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1040 }
1041
1042 SettingControl::Complex { .. } => serde_json::Value::Null,
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049
1050 fn sample_config() -> serde_json::Value {
1051 serde_json::json!({
1052 "theme": "monokai",
1053 "check_for_updates": false,
1054 "editor": {
1055 "tab_size": 2,
1056 "line_numbers": true
1057 }
1058 })
1059 }
1060
1061 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1063 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1065 std::sync::LazyLock::new(HashMap::new);
1066 BuildContext {
1067 config_value: config,
1068 layer_sources: &EMPTY_SOURCES,
1069 target_layer: ConfigLayer::User,
1070 }
1071 }
1072
1073 fn test_context_with_sources<'a>(
1075 config: &'a serde_json::Value,
1076 layer_sources: &'a HashMap<String, ConfigLayer>,
1077 target_layer: ConfigLayer,
1078 ) -> BuildContext<'a> {
1079 BuildContext {
1080 config_value: config,
1081 layer_sources,
1082 target_layer,
1083 }
1084 }
1085
1086 #[test]
1087 fn test_build_toggle_item() {
1088 let schema = SettingSchema {
1089 path: "/check_for_updates".to_string(),
1090 name: "Check For Updates".to_string(),
1091 description: Some("Check for updates".to_string()),
1092 setting_type: SettingType::Boolean,
1093 default: Some(serde_json::Value::Bool(true)),
1094 read_only: false,
1095 section: None,
1096 };
1097
1098 let config = sample_config();
1099 let ctx = test_context(&config);
1100 let item = build_item(&schema, &ctx);
1101
1102 assert_eq!(item.path, "/check_for_updates");
1103 assert!(!item.modified);
1106 assert_eq!(item.layer_source, ConfigLayer::System);
1107
1108 if let SettingControl::Toggle(state) = &item.control {
1109 assert!(!state.checked); } else {
1111 panic!("Expected toggle control");
1112 }
1113 }
1114
1115 #[test]
1116 fn test_build_toggle_item_modified_in_user_layer() {
1117 let schema = SettingSchema {
1118 path: "/check_for_updates".to_string(),
1119 name: "Check For Updates".to_string(),
1120 description: Some("Check for updates".to_string()),
1121 setting_type: SettingType::Boolean,
1122 default: Some(serde_json::Value::Bool(true)),
1123 read_only: false,
1124 section: None,
1125 };
1126
1127 let config = sample_config();
1128 let mut layer_sources = HashMap::new();
1129 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1130 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1131 let item = build_item(&schema, &ctx);
1132
1133 assert!(item.modified);
1136 assert_eq!(item.layer_source, ConfigLayer::User);
1137 }
1138
1139 #[test]
1140 fn test_build_number_item() {
1141 let schema = SettingSchema {
1142 path: "/editor/tab_size".to_string(),
1143 name: "Tab Size".to_string(),
1144 description: None,
1145 setting_type: SettingType::Integer {
1146 minimum: Some(1),
1147 maximum: Some(16),
1148 },
1149 default: Some(serde_json::Value::Number(4.into())),
1150 read_only: false,
1151 section: None,
1152 };
1153
1154 let config = sample_config();
1155 let ctx = test_context(&config);
1156 let item = build_item(&schema, &ctx);
1157
1158 assert!(!item.modified);
1160
1161 if let SettingControl::Number(state) = &item.control {
1162 assert_eq!(state.value, 2);
1163 assert_eq!(state.min, Some(1));
1164 assert_eq!(state.max, Some(16));
1165 } else {
1166 panic!("Expected number control");
1167 }
1168 }
1169
1170 #[test]
1171 fn test_build_text_item() {
1172 let schema = SettingSchema {
1173 path: "/theme".to_string(),
1174 name: "Theme".to_string(),
1175 description: None,
1176 setting_type: SettingType::String,
1177 default: Some(serde_json::Value::String("high-contrast".to_string())),
1178 read_only: false,
1179 section: None,
1180 };
1181
1182 let config = sample_config();
1183 let ctx = test_context(&config);
1184 let item = build_item(&schema, &ctx);
1185
1186 assert!(!item.modified);
1188
1189 if let SettingControl::Text(state) = &item.control {
1190 assert_eq!(state.value, "monokai");
1191 } else {
1192 panic!("Expected text control");
1193 }
1194 }
1195
1196 #[test]
1197 fn test_clean_description_keeps_full_desc_with_new_info() {
1198 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1200 assert!(result.is_some());
1201 let cleaned = result.unwrap();
1202 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1205 assert!(cleaned.contains("character"));
1206 }
1207
1208 #[test]
1209 fn test_clean_description_keeps_extra_info() {
1210 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1212 assert!(result.is_some());
1213 let cleaned = result.unwrap();
1214 assert!(cleaned.contains("gutter"));
1215 }
1216
1217 #[test]
1218 fn test_clean_description_returns_none_for_pure_redundancy() {
1219 let result = clean_description("Theme", Some("Theme"));
1221 assert!(result.is_none());
1222
1223 let result = clean_description("Theme", Some("The theme to use"));
1225 assert!(result.is_none());
1226 }
1227
1228 #[test]
1229 fn test_clean_description_returns_none_for_empty() {
1230 let result = clean_description("Theme", Some(""));
1231 assert!(result.is_none());
1232
1233 let result = clean_description("Theme", None);
1234 assert!(result.is_none());
1235 }
1236
1237 #[test]
1238 fn test_control_to_value() {
1239 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1240 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1241
1242 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1243 assert_eq!(control_to_value(&number), serde_json::json!(42));
1244
1245 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1246 assert_eq!(
1247 control_to_value(&text),
1248 serde_json::Value::String("hello".to_string())
1249 );
1250 }
1251}