1use super::schema::{SettingCategory, SettingSchema, SettingType};
6use crate::config_io::ConfigLayer;
7use crate::view::controls::{
8 DropdownState, DualListState, FocusState, KeybindingListState, MapState, NumberInputState,
9 TextInputState, 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
218fn value_as_string_array(
220 current: Option<&serde_json::Value>,
221 default: Option<&serde_json::Value>,
222) -> Vec<String> {
223 let from = |v: &serde_json::Value| -> Option<Vec<String>> {
224 v.as_array().map(|arr| {
225 arr.iter()
226 .filter_map(|v| v.as_str().map(String::from))
227 .collect()
228 })
229 };
230 current
231 .and_then(from)
232 .or_else(|| default.and_then(from))
233 .unwrap_or_default()
234}
235
236fn build_dual_list_state(
238 schema: &SettingSchema,
239 options: &[crate::view::settings::schema::EnumOption],
240 current_value: Option<&serde_json::Value>,
241 excluded: Vec<String>,
242) -> DualListState {
243 let all_options: Vec<(String, String)> = options
244 .iter()
245 .map(|o| (o.value.clone(), o.name.clone()))
246 .collect();
247 let included = value_as_string_array(current_value, schema.default.as_ref());
248 DualListState::new(&schema.name, all_options)
249 .with_included(included)
250 .with_excluded(excluded)
251}
252
253#[derive(Debug, Clone)]
255pub struct SettingItem {
256 pub path: String,
258 pub name: String,
260 pub description: Option<String>,
262 pub control: SettingControl,
264 pub default: Option<serde_json::Value>,
266 pub modified: bool,
270 pub layer_source: ConfigLayer,
273 pub read_only: bool,
275 pub is_auto_managed: bool,
277 pub nullable: bool,
279 pub is_null: bool,
281 pub section: Option<String>,
283 pub is_section_start: bool,
285 pub layout_width: u16,
287 pub dual_list_sibling: Option<String>,
289}
290
291#[derive(Debug, Clone)]
293pub enum SettingControl {
294 Toggle(ToggleState),
295 Number(NumberInputState),
296 Dropdown(DropdownState),
297 Text(TextInputState),
298 TextList(TextListState),
299 DualList(DualListState),
301 Map(MapState),
303 ObjectArray(KeybindingListState),
305 Json(JsonEditState),
307 Complex {
309 type_name: String,
310 },
311}
312
313impl SettingControl {
314 pub fn control_height(&self) -> u16 {
316 match self {
317 Self::TextList(state) => {
319 (state.items.len() + 2) as u16
321 }
322 Self::DualList(state) => 2 + state.body_rows() as u16,
324 Self::Map(state) => {
326 let header_row = if state.display_field.is_some() { 1 } else { 0 };
327 let add_new_row = if state.no_add { 0 } else { 1 };
328 let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
331 .expanded
332 .iter()
333 .filter_map(|&idx| state.entries.get(idx))
334 .map(|(_, v)| {
335 if let Some(obj) = v.as_object() {
336 obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
337 } else {
338 0
339 }
340 })
341 .sum();
342 (base + expanded_height) as u16
343 }
344 Self::Dropdown(state) => {
346 if state.open {
347 1 + state.options.len().min(8) as u16
349 } else {
350 1
351 }
352 }
353 Self::ObjectArray(state) => {
355 (state.bindings.len() + 2) as u16
357 }
358 Self::Json(state) => {
360 1 + state.display_height() as u16
362 }
363 _ => 1,
365 }
366 }
367
368 pub fn is_composite(&self) -> bool {
372 matches!(
373 self,
374 Self::TextList(_) | Self::DualList(_) | Self::Map(_) | Self::ObjectArray(_)
375 )
376 }
377
378 pub fn focused_sub_row(&self) -> u16 {
382 match self {
383 Self::TextList(state) => {
384 match state.focused_item {
386 Some(idx) => 1 + idx as u16, None => 1 + state.items.len() as u16, }
389 }
390 Self::DualList(state) => {
391 use crate::view::controls::DualListColumn;
393 let row = match state.active_column {
394 DualListColumn::Available => state.available_cursor,
395 DualListColumn::Included => state.included_cursor,
396 };
397 2 + row as u16
398 }
399 Self::ObjectArray(state) => {
400 match state.focused_index {
402 Some(idx) => 1 + idx as u16,
403 None => 1 + state.bindings.len() as u16,
404 }
405 }
406 Self::Map(state) => {
407 let header_offset = if state.display_field.is_some() { 1 } else { 0 };
409 match state.focused_entry {
410 Some(idx) => 1 + header_offset + idx as u16,
411 None => 1 + header_offset + state.entries.len() as u16,
412 }
413 }
414 _ => 0,
415 }
416 }
417}
418
419pub const SECTION_HEADER_HEIGHT: u16 = 2;
421
422impl SettingItem {
423 pub fn item_height(&self) -> u16 {
426 let section_height = if self.is_section_start {
428 SECTION_HEADER_HEIGHT
429 } else {
430 0
431 };
432 let description_height = self.description_height(self.layout_width);
433 section_height + self.control.control_height() + description_height + 1
434 }
435
436 pub fn description_height(&self, width: u16) -> u16 {
439 if let Some(ref desc) = self.description {
440 if desc.is_empty() {
441 return 0;
442 }
443 if width == 0 {
444 return 1;
445 }
446 let chars_per_line = width.saturating_sub(2) as usize; if chars_per_line == 0 {
449 return 1;
450 }
451 desc.len().div_ceil(chars_per_line) as u16
452 } else {
453 0
454 }
455 }
456
457 pub fn content_height(&self) -> u16 {
460 let description_height = self.description_height(self.layout_width);
461 self.control.control_height() + description_height
462 }
463}
464
465pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
468 let desc = description?;
469 if desc.is_empty() {
470 return None;
471 }
472
473 let name_words: HashSet<String> = name
475 .to_lowercase()
476 .split(|c: char| !c.is_alphanumeric())
477 .filter(|w| !w.is_empty() && w.len() > 2)
478 .map(String::from)
479 .collect();
480
481 let filler_words: HashSet<&str> = [
483 "the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
484 "whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
485 "disable", "set", "use", "allow", "default", "true", "false",
486 ]
487 .into_iter()
488 .collect();
489
490 let desc_words: Vec<&str> = desc
492 .split(|c: char| !c.is_alphanumeric())
493 .filter(|w| !w.is_empty())
494 .collect();
495
496 let has_new_info = desc_words.iter().any(|word| {
498 let lower = word.to_lowercase();
499 lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
500 });
501
502 if !has_new_info {
503 return None;
504 }
505
506 Some(desc.to_string())
507}
508
509impl ScrollItem for SettingItem {
510 fn height(&self) -> u16 {
511 self.item_height()
512 }
513
514 fn focus_regions(&self) -> Vec<FocusRegion> {
515 match &self.control {
516 SettingControl::TextList(state) => {
518 let mut regions = Vec::new();
519 regions.push(FocusRegion {
521 id: 0,
522 y_offset: 0,
523 height: 1,
524 });
525 for i in 0..state.items.len() {
527 regions.push(FocusRegion {
528 id: 1 + i,
529 y_offset: 1 + i as u16,
530 height: 1,
531 });
532 }
533 regions.push(FocusRegion {
535 id: 1 + state.items.len(),
536 y_offset: 1 + state.items.len() as u16,
537 height: 1,
538 });
539 regions
540 }
541 SettingControl::DualList(state) => {
543 let mut regions = Vec::new();
544 regions.push(FocusRegion {
546 id: 0,
547 y_offset: 0,
548 height: 1,
549 });
550 let body = state.body_rows();
553 for i in 0..body {
554 regions.push(FocusRegion {
555 id: 1 + i,
556 y_offset: 2 + i as u16, height: 1,
558 });
559 }
560 regions
561 }
562 SettingControl::Map(state) => {
564 let mut regions = Vec::new();
565 let mut y = 0u16;
566
567 regions.push(FocusRegion {
569 id: 0,
570 y_offset: y,
571 height: 1,
572 });
573 y += 1;
574
575 if state.display_field.is_some() {
577 y += 1;
578 }
579
580 for (i, (_, v)) in state.entries.iter().enumerate() {
582 let mut entry_height = 1u16;
583 if state.expanded.contains(&i) {
585 if let Some(obj) = v.as_object() {
586 entry_height += obj.len().min(5) as u16;
587 if obj.len() > 5 {
588 entry_height += 1;
589 }
590 }
591 }
592 regions.push(FocusRegion {
593 id: 1 + i,
594 y_offset: y,
595 height: entry_height,
596 });
597 y += entry_height;
598 }
599
600 regions.push(FocusRegion {
602 id: 1 + state.entries.len(),
603 y_offset: y,
604 height: 1,
605 });
606 regions
607 }
608 SettingControl::ObjectArray(state) => {
610 let mut regions = Vec::new();
611 regions.push(FocusRegion {
613 id: 0,
614 y_offset: 0,
615 height: 1,
616 });
617 for i in 0..state.bindings.len() {
619 regions.push(FocusRegion {
620 id: 1 + i,
621 y_offset: 1 + i as u16,
622 height: 1,
623 });
624 }
625 regions.push(FocusRegion {
627 id: 1 + state.bindings.len(),
628 y_offset: 1 + state.bindings.len() as u16,
629 height: 1,
630 });
631 regions
632 }
633 _ => {
635 vec![FocusRegion {
636 id: 0,
637 y_offset: 0,
638 height: self.item_height().saturating_sub(1), }]
640 }
641 }
642 }
643}
644
645#[derive(Debug, Clone)]
647pub struct SettingsPage {
648 pub name: String,
650 pub path: String,
652 pub description: Option<String>,
654 pub nullable: bool,
656 pub items: Vec<SettingItem>,
658 pub subpages: Vec<SettingsPage>,
660}
661
662pub struct BuildContext<'a> {
664 pub config_value: &'a serde_json::Value,
666 pub layer_sources: &'a HashMap<String, ConfigLayer>,
668 pub target_layer: ConfigLayer,
670}
671
672pub fn build_pages(
674 categories: &[SettingCategory],
675 config_value: &serde_json::Value,
676 layer_sources: &HashMap<String, ConfigLayer>,
677 target_layer: ConfigLayer,
678) -> Vec<SettingsPage> {
679 let ctx = BuildContext {
680 config_value,
681 layer_sources,
682 target_layer,
683 };
684 categories.iter().map(|cat| build_page(cat, &ctx)).collect()
685}
686
687fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
689 let mut items: Vec<SettingItem> = category
690 .settings
691 .iter()
692 .flat_map(|s| expand_or_build(s, ctx))
693 .collect();
694
695 items.sort_by(|a, b| match (&a.section, &b.section) {
697 (Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
698 (Some(_), None) => std::cmp::Ordering::Less,
699 (None, Some(_)) => std::cmp::Ordering::Greater,
700 (None, None) => a.name.cmp(&b.name),
701 });
702
703 let mut prev_section: Option<&String> = None;
705 for item in &mut items {
706 let is_new_section = match (&item.section, prev_section) {
707 (Some(sec), Some(prev)) => sec != prev,
708 (Some(_), None) => true,
709 (None, Some(_)) => false, (None, None) => false,
711 };
712 item.is_section_start = is_new_section;
713 prev_section = item.section.as_ref();
714 }
715
716 let subpages = category
717 .subcategories
718 .iter()
719 .map(|sub| build_page(sub, ctx))
720 .collect();
721
722 SettingsPage {
723 name: category.name.clone(),
724 path: category.path.clone(),
725 description: category.description.clone(),
726 nullable: category.nullable,
727 items,
728 subpages,
729 }
730}
731
732fn expand_or_build(schema: &SettingSchema, ctx: &BuildContext) -> Vec<SettingItem> {
738 if let SettingType::Object { properties } = &schema.setting_type {
739 let all_native = !properties.is_empty()
740 && properties.iter().all(|child| {
741 !matches!(
742 child.setting_type,
743 SettingType::Object { .. } | SettingType::Complex
744 )
745 });
746 if all_native {
747 return properties
751 .iter()
752 .map(|child| {
753 let mut child = child.clone();
754 if !child.path.starts_with(&schema.path) {
755 child.path = format!("{}{}", schema.path, child.path);
756 }
757 if let Some(ref mut sib) = child.dual_list_sibling {
758 if !sib.starts_with(&schema.path) {
759 *sib = format!("{}{}", schema.path, sib);
760 }
761 }
762 build_item(&child, ctx)
763 })
764 .collect();
765 }
766 }
767 vec![build_item(schema, ctx)]
768}
769
770pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
772 let current_value = ctx.config_value.pointer(&schema.path);
774
775 let is_null = schema.nullable
777 && current_value
778 .map(|v| v.is_null())
779 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
780
781 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
783
784 let control = match &schema.setting_type {
786 SettingType::Boolean => {
787 let checked = current_value
788 .and_then(|v| v.as_bool())
789 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
790 .unwrap_or(false);
791 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
792 }
793
794 SettingType::Integer { minimum, maximum } => {
795 let value = current_value
796 .and_then(|v| v.as_i64())
797 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
798 .unwrap_or(0);
799
800 let mut state = NumberInputState::new(value, &schema.name);
801 if let Some(min) = minimum {
802 state = state.with_min(*min);
803 }
804 if let Some(max) = maximum {
805 state = state.with_max(*max);
806 }
807 SettingControl::Number(state)
808 }
809
810 SettingType::Number { minimum, maximum } => {
811 let value = current_value
813 .and_then(|v| v.as_f64())
814 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
815 .unwrap_or(0.0);
816
817 let int_value = (value * 100.0).round() as i64;
819 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
820 if let Some(min) = minimum {
821 state = state.with_min((*min * 100.0) as i64);
822 }
823 if let Some(max) = maximum {
824 state = state.with_max((*max * 100.0) as i64);
825 }
826 SettingControl::Number(state)
827 }
828
829 SettingType::String => {
830 let value = current_value
831 .and_then(|v| v.as_str())
832 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
833 .unwrap_or("");
834
835 if let Some(ref source_path) = schema.enum_from {
837 let mut options: Vec<String> = ctx
838 .config_value
839 .pointer(source_path)
840 .and_then(|v| v.as_object())
841 .map(|obj| obj.keys().cloned().collect())
842 .unwrap_or_default();
843 options.sort();
844
845 let mut display_names = Vec::new();
847 let mut values = Vec::new();
848 if schema.nullable {
849 display_names.push("(none)".to_string());
850 values.push(String::new());
851 }
852 for key in &options {
853 display_names.push(key.clone());
854 values.push(key.clone());
855 }
856
857 let current = if is_null { "" } else { value };
858 let selected = values.iter().position(|v| v == current).unwrap_or(0);
859 let state = DropdownState::with_values(display_names, values, &schema.name)
860 .with_selected(selected);
861 SettingControl::Dropdown(state)
862 } else {
863 let state = TextInputState::new(&schema.name).with_value(value);
864 SettingControl::Text(state)
865 }
866 }
867
868 SettingType::Enum { options } => {
869 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
871 "" } else {
873 current_value
874 .and_then(|v| v.as_str())
875 .or_else(|| {
876 let default = schema.default.as_ref()?;
877 if default.is_null() {
878 Some("")
879 } else {
880 default.as_str()
881 }
882 })
883 .unwrap_or("")
884 };
885
886 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
887 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
888 let selected = values.iter().position(|v| v == current).unwrap_or(0);
889 let state = DropdownState::with_values(display_names, values, &schema.name)
890 .with_selected(selected);
891 SettingControl::Dropdown(state)
892 }
893
894 SettingType::DualList {
895 options,
896 sibling_path,
897 } => {
898 let excluded = sibling_path
899 .as_ref()
900 .and_then(|path| ctx.config_value.pointer(path))
901 .map(|v| value_as_string_array(Some(v), None))
902 .unwrap_or_default();
903 SettingControl::DualList(build_dual_list_state(
904 schema,
905 options,
906 current_value,
907 excluded,
908 ))
909 }
910
911 SettingType::StringArray => {
912 let items = value_as_string_array(current_value, schema.default.as_ref());
913 let state = TextListState::new(&schema.name).with_items(items);
914 SettingControl::TextList(state)
915 }
916
917 SettingType::IntegerArray => {
918 let items: Vec<String> = current_value
919 .and_then(|v| v.as_array())
920 .map(|arr| {
921 arr.iter()
922 .filter_map(|v| {
923 v.as_i64()
924 .map(|n| n.to_string())
925 .or_else(|| v.as_u64().map(|n| n.to_string()))
926 .or_else(|| v.as_f64().map(|n| n.to_string()))
927 })
928 .collect()
929 })
930 .or_else(|| {
931 schema.default.as_ref().and_then(|d| {
932 d.as_array().map(|arr| {
933 arr.iter()
934 .filter_map(|v| {
935 v.as_i64()
936 .map(|n| n.to_string())
937 .or_else(|| v.as_u64().map(|n| n.to_string()))
938 .or_else(|| v.as_f64().map(|n| n.to_string()))
939 })
940 .collect()
941 })
942 })
943 })
944 .unwrap_or_default();
945
946 let state = TextListState::new(&schema.name)
947 .with_items(items)
948 .with_integer_mode();
949 SettingControl::TextList(state)
950 }
951
952 SettingType::Object { .. } => {
953 json_control(&schema.name, current_value, schema.default.as_ref())
954 }
955
956 SettingType::Map {
957 value_schema,
958 display_field,
959 no_add,
960 } => {
961 let map_value = current_value
963 .cloned()
964 .or_else(|| schema.default.clone())
965 .unwrap_or_else(|| serde_json::json!({}));
966
967 let mut state = MapState::new(&schema.name).with_entries(&map_value);
968 state = state.with_value_schema((**value_schema).clone());
969 if let Some(field) = display_field {
970 state = state.with_display_field(field.clone());
971 }
972 if *no_add {
973 state = state.with_no_add(true);
974 }
975 SettingControl::Map(state)
976 }
977
978 SettingType::ObjectArray {
979 item_schema,
980 display_field,
981 } => {
982 let array_value = current_value
984 .cloned()
985 .or_else(|| schema.default.clone())
986 .unwrap_or_else(|| serde_json::json!([]));
987
988 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
989 state = state.with_item_schema((**item_schema).clone());
990 if let Some(field) = display_field {
991 state = state.with_display_field(field.clone());
992 }
993 SettingControl::ObjectArray(state)
994 }
995
996 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
997 };
998
999 let layer_source = ctx
1001 .layer_sources
1002 .get(&schema.path)
1003 .copied()
1004 .unwrap_or(ConfigLayer::System);
1005
1006 let modified = if is_auto_managed {
1009 false } else {
1011 layer_source == ctx.target_layer
1012 };
1013
1014 let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
1016
1017 SettingItem {
1018 path: schema.path.clone(),
1019 name: schema.name.clone(),
1020 description: cleaned_description,
1021 control,
1022 default: schema.default.clone(),
1023 modified,
1024 layer_source,
1025 read_only: schema.read_only,
1026 is_auto_managed,
1027 nullable: schema.nullable,
1028 is_null,
1029 section: schema.section.clone(),
1030 is_section_start: false, layout_width: 0,
1032 dual_list_sibling: schema.dual_list_sibling.clone(),
1033 }
1034}
1035
1036pub fn build_item_from_value(
1038 schema: &SettingSchema,
1039 current_value: Option<&serde_json::Value>,
1040) -> SettingItem {
1041 let control = match &schema.setting_type {
1043 SettingType::Boolean => {
1044 let checked = current_value
1045 .and_then(|v| v.as_bool())
1046 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
1047 .unwrap_or(false);
1048 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
1049 }
1050
1051 SettingType::Integer { minimum, maximum } => {
1052 let value = current_value
1053 .and_then(|v| v.as_i64())
1054 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
1055 .unwrap_or(0);
1056
1057 let mut state = NumberInputState::new(value, &schema.name);
1058 if let Some(min) = minimum {
1059 state = state.with_min(*min);
1060 }
1061 if let Some(max) = maximum {
1062 state = state.with_max(*max);
1063 }
1064 SettingControl::Number(state)
1065 }
1066
1067 SettingType::Number { minimum, maximum } => {
1068 let value = current_value
1069 .and_then(|v| v.as_f64())
1070 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
1071 .unwrap_or(0.0);
1072
1073 let int_value = (value * 100.0).round() as i64;
1074 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1075 if let Some(min) = minimum {
1076 state = state.with_min((*min * 100.0) as i64);
1077 }
1078 if let Some(max) = maximum {
1079 state = state.with_max((*max * 100.0) as i64);
1080 }
1081 SettingControl::Number(state)
1082 }
1083
1084 SettingType::String => {
1085 let value = current_value
1086 .and_then(|v| v.as_str())
1087 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1088 .unwrap_or("");
1089
1090 let state = TextInputState::new(&schema.name).with_value(value);
1091 SettingControl::Text(state)
1092 }
1093
1094 SettingType::Enum { options } => {
1095 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1097 "" } else {
1099 current_value
1100 .and_then(|v| v.as_str())
1101 .or_else(|| {
1102 let default = schema.default.as_ref()?;
1103 if default.is_null() {
1104 Some("")
1105 } else {
1106 default.as_str()
1107 }
1108 })
1109 .unwrap_or("")
1110 };
1111
1112 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1113 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1114 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1115 let state = DropdownState::with_values(display_names, values, &schema.name)
1116 .with_selected(selected);
1117 SettingControl::Dropdown(state)
1118 }
1119
1120 SettingType::DualList { options, .. } => {
1121 SettingControl::DualList(build_dual_list_state(
1123 schema,
1124 options,
1125 current_value,
1126 vec![],
1127 ))
1128 }
1129
1130 SettingType::StringArray => {
1131 let items: Vec<String> = current_value
1132 .and_then(|v| v.as_array())
1133 .map(|arr| {
1134 arr.iter()
1135 .filter_map(|v| v.as_str().map(String::from))
1136 .collect()
1137 })
1138 .or_else(|| {
1139 schema.default.as_ref().and_then(|d| {
1140 d.as_array().map(|arr| {
1141 arr.iter()
1142 .filter_map(|v| v.as_str().map(String::from))
1143 .collect()
1144 })
1145 })
1146 })
1147 .unwrap_or_default();
1148
1149 let state = TextListState::new(&schema.name).with_items(items);
1150 SettingControl::TextList(state)
1151 }
1152
1153 SettingType::IntegerArray => {
1154 let items: Vec<String> = current_value
1155 .and_then(|v| v.as_array())
1156 .map(|arr| {
1157 arr.iter()
1158 .filter_map(|v| {
1159 v.as_i64()
1160 .map(|n| n.to_string())
1161 .or_else(|| v.as_u64().map(|n| n.to_string()))
1162 .or_else(|| v.as_f64().map(|n| n.to_string()))
1163 })
1164 .collect()
1165 })
1166 .or_else(|| {
1167 schema.default.as_ref().and_then(|d| {
1168 d.as_array().map(|arr| {
1169 arr.iter()
1170 .filter_map(|v| {
1171 v.as_i64()
1172 .map(|n| n.to_string())
1173 .or_else(|| v.as_u64().map(|n| n.to_string()))
1174 .or_else(|| v.as_f64().map(|n| n.to_string()))
1175 })
1176 .collect()
1177 })
1178 })
1179 })
1180 .unwrap_or_default();
1181
1182 let state = TextListState::new(&schema.name)
1183 .with_items(items)
1184 .with_integer_mode();
1185 SettingControl::TextList(state)
1186 }
1187
1188 SettingType::Object { .. } => {
1189 json_control(&schema.name, current_value, schema.default.as_ref())
1190 }
1191
1192 SettingType::Map {
1193 value_schema,
1194 display_field,
1195 no_add,
1196 } => {
1197 let map_value = current_value
1198 .cloned()
1199 .or_else(|| schema.default.clone())
1200 .unwrap_or_else(|| serde_json::json!({}));
1201
1202 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1203 state = state.with_value_schema((**value_schema).clone());
1204 if let Some(field) = display_field {
1205 state = state.with_display_field(field.clone());
1206 }
1207 if *no_add {
1208 state = state.with_no_add(true);
1209 }
1210 SettingControl::Map(state)
1211 }
1212
1213 SettingType::ObjectArray {
1214 item_schema,
1215 display_field,
1216 } => {
1217 let array_value = current_value
1218 .cloned()
1219 .or_else(|| schema.default.clone())
1220 .unwrap_or_else(|| serde_json::json!([]));
1221
1222 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1223 state = state.with_item_schema((**item_schema).clone());
1224 if let Some(field) = display_field {
1225 state = state.with_display_field(field.clone());
1226 }
1227 SettingControl::ObjectArray(state)
1228 }
1229
1230 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1231 };
1232
1233 let modified = match (¤t_value, &schema.default) {
1236 (Some(current), Some(default)) => *current != default,
1237 (Some(_), None) => true,
1238 _ => false,
1239 };
1240
1241 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
1243
1244 let is_null = schema.nullable
1245 && current_value
1246 .map(|v| v.is_null())
1247 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
1248
1249 SettingItem {
1250 path: schema.path.clone(),
1251 name: schema.name.clone(),
1252 description: schema.description.clone(),
1253 control,
1254 default: schema.default.clone(),
1255 modified,
1256 layer_source: ConfigLayer::System,
1258 read_only: schema.read_only,
1259 is_auto_managed,
1260 nullable: schema.nullable,
1261 is_null,
1262 section: schema.section.clone(),
1263 is_section_start: false, layout_width: 0,
1265 dual_list_sibling: schema.dual_list_sibling.clone(),
1266 }
1267}
1268
1269pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
1271 match control {
1272 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
1273
1274 SettingControl::Number(state) => {
1275 if state.is_percentage {
1276 let float_value = state.value as f64 / 100.0;
1278 serde_json::Number::from_f64(float_value)
1279 .map(serde_json::Value::Number)
1280 .unwrap_or(serde_json::Value::Number(state.value.into()))
1281 } else {
1282 serde_json::Value::Number(state.value.into())
1283 }
1284 }
1285
1286 SettingControl::Dropdown(state) => state
1287 .selected_value()
1288 .map(|s| {
1289 if s.is_empty() {
1290 serde_json::Value::Null
1292 } else {
1293 serde_json::Value::String(s.to_string())
1294 }
1295 })
1296 .unwrap_or(serde_json::Value::Null),
1297
1298 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1299
1300 SettingControl::TextList(state) => {
1301 let arr: Vec<serde_json::Value> = state
1302 .items
1303 .iter()
1304 .filter_map(|s| {
1305 if state.is_integer {
1306 s.parse::<i64>()
1307 .ok()
1308 .map(|n| serde_json::Value::Number(n.into()))
1309 } else {
1310 Some(serde_json::Value::String(s.clone()))
1311 }
1312 })
1313 .collect();
1314 serde_json::Value::Array(arr)
1315 }
1316
1317 SettingControl::DualList(state) => {
1318 let arr: Vec<serde_json::Value> = state
1319 .included
1320 .iter()
1321 .map(|s| serde_json::Value::String(s.clone()))
1322 .collect();
1323 serde_json::Value::Array(arr)
1324 }
1325
1326 SettingControl::Map(state) => state.to_value(),
1327
1328 SettingControl::ObjectArray(state) => state.to_value(),
1329
1330 SettingControl::Json(state) => {
1331 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1333 }
1334
1335 SettingControl::Complex { .. } => serde_json::Value::Null,
1336 }
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341 use super::*;
1342
1343 fn sample_config() -> serde_json::Value {
1344 serde_json::json!({
1345 "theme": "monokai",
1346 "check_for_updates": false,
1347 "editor": {
1348 "tab_size": 2,
1349 "line_numbers": true
1350 }
1351 })
1352 }
1353
1354 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1356 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1358 std::sync::LazyLock::new(HashMap::new);
1359 BuildContext {
1360 config_value: config,
1361 layer_sources: &EMPTY_SOURCES,
1362 target_layer: ConfigLayer::User,
1363 }
1364 }
1365
1366 fn test_context_with_sources<'a>(
1368 config: &'a serde_json::Value,
1369 layer_sources: &'a HashMap<String, ConfigLayer>,
1370 target_layer: ConfigLayer,
1371 ) -> BuildContext<'a> {
1372 BuildContext {
1373 config_value: config,
1374 layer_sources,
1375 target_layer,
1376 }
1377 }
1378
1379 #[test]
1380 fn test_build_toggle_item() {
1381 let schema = SettingSchema {
1382 path: "/check_for_updates".to_string(),
1383 name: "Check For Updates".to_string(),
1384 description: Some("Check for updates".to_string()),
1385 setting_type: SettingType::Boolean,
1386 default: Some(serde_json::Value::Bool(true)),
1387 read_only: false,
1388 section: None,
1389 order: None,
1390 nullable: false,
1391 enum_from: None,
1392 dual_list_sibling: None,
1393 };
1394
1395 let config = sample_config();
1396 let ctx = test_context(&config);
1397 let item = build_item(&schema, &ctx);
1398
1399 assert_eq!(item.path, "/check_for_updates");
1400 assert!(!item.modified);
1403 assert_eq!(item.layer_source, ConfigLayer::System);
1404
1405 if let SettingControl::Toggle(state) = &item.control {
1406 assert!(!state.checked); } else {
1408 panic!("Expected toggle control");
1409 }
1410 }
1411
1412 #[test]
1413 fn test_build_toggle_item_modified_in_user_layer() {
1414 let schema = SettingSchema {
1415 path: "/check_for_updates".to_string(),
1416 name: "Check For Updates".to_string(),
1417 description: Some("Check for updates".to_string()),
1418 setting_type: SettingType::Boolean,
1419 default: Some(serde_json::Value::Bool(true)),
1420 read_only: false,
1421 section: None,
1422 order: None,
1423 nullable: false,
1424 enum_from: None,
1425 dual_list_sibling: None,
1426 };
1427
1428 let config = sample_config();
1429 let mut layer_sources = HashMap::new();
1430 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1431 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1432 let item = build_item(&schema, &ctx);
1433
1434 assert!(item.modified);
1437 assert_eq!(item.layer_source, ConfigLayer::User);
1438 }
1439
1440 #[test]
1441 fn test_build_number_item() {
1442 let schema = SettingSchema {
1443 path: "/editor/tab_size".to_string(),
1444 name: "Tab Size".to_string(),
1445 description: None,
1446 setting_type: SettingType::Integer {
1447 minimum: Some(1),
1448 maximum: Some(16),
1449 },
1450 default: Some(serde_json::Value::Number(4.into())),
1451 read_only: false,
1452 section: None,
1453 order: None,
1454 nullable: false,
1455 enum_from: None,
1456 dual_list_sibling: None,
1457 };
1458
1459 let config = sample_config();
1460 let ctx = test_context(&config);
1461 let item = build_item(&schema, &ctx);
1462
1463 assert!(!item.modified);
1465
1466 if let SettingControl::Number(state) = &item.control {
1467 assert_eq!(state.value, 2);
1468 assert_eq!(state.min, Some(1));
1469 assert_eq!(state.max, Some(16));
1470 } else {
1471 panic!("Expected number control");
1472 }
1473 }
1474
1475 #[test]
1476 fn test_build_text_item() {
1477 let schema = SettingSchema {
1478 path: "/theme".to_string(),
1479 name: "Theme".to_string(),
1480 description: None,
1481 setting_type: SettingType::String,
1482 default: Some(serde_json::Value::String("high-contrast".to_string())),
1483 read_only: false,
1484 section: None,
1485 order: None,
1486 nullable: false,
1487 enum_from: None,
1488 dual_list_sibling: None,
1489 };
1490
1491 let config = sample_config();
1492 let ctx = test_context(&config);
1493 let item = build_item(&schema, &ctx);
1494
1495 assert!(!item.modified);
1497
1498 if let SettingControl::Text(state) = &item.control {
1499 assert_eq!(state.value, "monokai");
1500 } else {
1501 panic!("Expected text control");
1502 }
1503 }
1504
1505 #[test]
1506 fn test_clean_description_keeps_full_desc_with_new_info() {
1507 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1509 assert!(result.is_some());
1510 let cleaned = result.unwrap();
1511 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1514 assert!(cleaned.contains("character"));
1515 }
1516
1517 #[test]
1518 fn test_clean_description_keeps_extra_info() {
1519 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1521 assert!(result.is_some());
1522 let cleaned = result.unwrap();
1523 assert!(cleaned.contains("gutter"));
1524 }
1525
1526 #[test]
1527 fn test_clean_description_returns_none_for_pure_redundancy() {
1528 let result = clean_description("Theme", Some("Theme"));
1530 assert!(result.is_none());
1531
1532 let result = clean_description("Theme", Some("The theme to use"));
1534 assert!(result.is_none());
1535 }
1536
1537 #[test]
1538 fn test_clean_description_returns_none_for_empty() {
1539 let result = clean_description("Theme", Some(""));
1540 assert!(result.is_none());
1541
1542 let result = clean_description("Theme", None);
1543 assert!(result.is_none());
1544 }
1545
1546 #[test]
1547 fn test_control_to_value() {
1548 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1549 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1550
1551 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1552 assert_eq!(control_to_value(&number), serde_json::json!(42));
1553
1554 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1555 assert_eq!(
1556 control_to_value(&text),
1557 serde_json::Value::String("hello".to_string())
1558 );
1559 }
1560}