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 style: ItemBoxStyle,
290 pub dual_list_sibling: Option<String>,
292}
293
294#[derive(Debug, Clone)]
296pub enum SettingControl {
297 Toggle(ToggleState),
298 Number(NumberInputState),
299 Dropdown(DropdownState),
300 Text(TextInputState),
301 TextList(TextListState),
302 DualList(DualListState),
304 Map(MapState),
306 ObjectArray(KeybindingListState),
308 Json(JsonEditState),
310 Complex {
312 type_name: String,
313 },
314}
315
316impl SettingControl {
317 pub fn control_height(&self) -> u16 {
319 match self {
320 Self::TextList(state) => {
322 (state.items.len() + 2) as u16
324 }
325 Self::DualList(state) => 2 + state.body_rows() as u16,
327 Self::Map(state) => {
329 let header_row = if state.display_field.is_some() { 1 } else { 0 };
330 let add_new_row = if state.no_add { 0 } else { 1 };
331 let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
334 .expanded
335 .iter()
336 .filter_map(|&idx| state.entries.get(idx))
337 .map(|(_, v)| {
338 if let Some(obj) = v.as_object() {
339 obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
340 } else {
341 0
342 }
343 })
344 .sum();
345 (base + expanded_height) as u16
346 }
347 Self::Dropdown(state) => {
349 if state.open {
350 1 + state.options.len().min(8) as u16
352 } else {
353 1
354 }
355 }
356 Self::ObjectArray(state) => {
358 (state.bindings.len() + 2) as u16
360 }
361 Self::Json(state) => {
363 1 + state.display_height() as u16
365 }
366 _ => 1,
368 }
369 }
370
371 pub fn is_composite(&self) -> bool {
375 matches!(
376 self,
377 Self::TextList(_) | Self::DualList(_) | Self::Map(_) | Self::ObjectArray(_)
378 )
379 }
380
381 pub fn focused_sub_row(&self) -> u16 {
385 match self {
386 Self::TextList(state) => {
387 match state.focused_item {
389 Some(idx) => 1 + idx as u16, None => 1 + state.items.len() as u16, }
392 }
393 Self::DualList(state) => {
394 use crate::view::controls::DualListColumn;
396 let row = match state.active_column {
397 DualListColumn::Available => state.available_cursor,
398 DualListColumn::Included => state.included_cursor,
399 };
400 2 + row as u16
401 }
402 Self::ObjectArray(state) => {
403 match state.focused_index {
405 Some(idx) => 1 + idx as u16,
406 None => 1 + state.bindings.len() as u16,
407 }
408 }
409 Self::Map(state) => {
410 let header_offset = if state.display_field.is_some() { 1 } else { 0 };
412 match state.focused_entry {
413 Some(idx) => 1 + header_offset + idx as u16,
414 None => 1 + header_offset + state.entries.len() as u16,
415 }
416 }
417 _ => 0,
418 }
419 }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub struct ItemBoxStyle {
438 pub section_header_rows: u16,
441 pub card_border_rows: u16,
443 pub card_border_cols: u16,
445 pub focus_indicator_cols: u16,
448 pub description_right_padding_cols: u16,
451}
452
453impl ItemBoxStyle {
454 pub const fn cards() -> Self {
457 Self {
458 section_header_rows: 2,
459 card_border_rows: 1,
460 card_border_cols: 1,
461 focus_indicator_cols: 3,
462 description_right_padding_cols: 2,
463 }
464 }
465
466 pub const fn flat() -> Self {
469 Self {
470 section_header_rows: 2,
471 card_border_rows: 0,
472 card_border_cols: 0,
473 focus_indicator_cols: 3,
474 description_right_padding_cols: 2,
475 }
476 }
477
478 pub fn inner_text_width(&self, card_outer_width: u16) -> u16 {
482 card_outer_width
483 .saturating_sub(2 * self.card_border_cols)
484 .saturating_sub(self.focus_indicator_cols)
485 .saturating_sub(self.description_right_padding_cols)
486 }
487}
488
489impl Default for ItemBoxStyle {
490 fn default() -> Self {
491 Self::cards()
492 }
493}
494
495#[derive(Debug, Clone, Copy, Default)]
503pub struct ItemBox {
504 pub section_header_rows: u16,
506 pub top_border_rows: u16,
508 pub control_rows: u16,
510 pub description_rows: u16,
512 pub bottom_border_rows: u16,
514}
515
516impl ItemBox {
517 pub fn total_rows(&self) -> u16 {
518 self.section_header_rows
519 + self.top_border_rows
520 + self.control_rows
521 + self.description_rows
522 + self.bottom_border_rows
523 }
524
525 pub fn card_top_y(&self) -> u16 {
527 self.section_header_rows
528 }
529
530 pub fn control_y(&self) -> u16 {
532 self.card_top_y() + self.top_border_rows
533 }
534
535 pub fn description_y(&self) -> u16 {
537 self.control_y() + self.control_rows
538 }
539
540 pub fn bottom_border_y(&self) -> u16 {
542 self.description_y() + self.description_rows
543 }
544
545 pub fn card_height(&self) -> u16 {
547 self.top_border_rows + self.control_rows + self.description_rows + self.bottom_border_rows
548 }
549
550 pub fn content_rows(&self) -> u16 {
552 self.control_rows + self.description_rows
553 }
554}
555
556impl SettingItem {
557 pub fn layout_box(&self, width: u16, style: &ItemBoxStyle) -> ItemBox {
561 ItemBox {
562 section_header_rows: if self.is_section_start {
563 style.section_header_rows
564 } else {
565 0
566 },
567 top_border_rows: style.card_border_rows,
568 control_rows: self.control.control_height(),
569 description_rows: self.description_rows_for(style.inner_text_width(width)),
570 bottom_border_rows: style.card_border_rows,
571 }
572 }
573
574 pub fn description_rows_for(&self, inner_width: u16) -> u16 {
581 let Some(desc) = self.description.as_deref() else {
582 return 0;
583 };
584 if desc.is_empty() {
585 return 0;
586 }
587 if inner_width == 0 {
588 return 1;
589 }
590 desc.len().div_ceil(inner_width as usize) as u16
591 }
592}
593
594pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
597 let desc = description?;
598 if desc.is_empty() {
599 return None;
600 }
601
602 let name_words: HashSet<String> = name
604 .to_lowercase()
605 .split(|c: char| !c.is_alphanumeric())
606 .filter(|w| !w.is_empty() && w.len() > 2)
607 .map(String::from)
608 .collect();
609
610 let filler_words: HashSet<&str> = [
612 "the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
613 "whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
614 "disable", "set", "use", "allow", "default", "true", "false",
615 ]
616 .into_iter()
617 .collect();
618
619 let desc_words: Vec<&str> = desc
621 .split(|c: char| !c.is_alphanumeric())
622 .filter(|w| !w.is_empty())
623 .collect();
624
625 let has_new_info = desc_words.iter().any(|word| {
627 let lower = word.to_lowercase();
628 lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
629 });
630
631 if !has_new_info {
632 return None;
633 }
634
635 Some(desc.to_string())
636}
637
638impl ScrollItem for SettingItem {
639 fn height(&self, width: u16) -> u16 {
640 self.layout_box(width, &self.style).total_rows()
641 }
642
643 fn focus_regions(&self, width: u16) -> Vec<FocusRegion> {
644 let plan = self.layout_box(width, &self.style);
653 let label_y = plan.control_y();
654
655 match &self.control {
656 SettingControl::TextList(state) => {
658 let mut regions = Vec::new();
659 regions.push(FocusRegion {
661 id: 0,
662 y_offset: label_y,
663 height: 1,
664 });
665 for i in 0..state.items.len() {
667 regions.push(FocusRegion {
668 id: 1 + i,
669 y_offset: label_y + 1 + i as u16,
670 height: 1,
671 });
672 }
673 regions.push(FocusRegion {
675 id: 1 + state.items.len(),
676 y_offset: label_y + 1 + state.items.len() as u16,
677 height: 1,
678 });
679 regions
680 }
681 SettingControl::DualList(state) => {
683 let mut regions = Vec::new();
684 regions.push(FocusRegion {
686 id: 0,
687 y_offset: label_y,
688 height: 1,
689 });
690 let body = state.body_rows();
693 for i in 0..body {
694 regions.push(FocusRegion {
695 id: 1 + i,
696 y_offset: label_y + 2 + i as u16, height: 1,
698 });
699 }
700 regions
701 }
702 SettingControl::Map(state) => {
704 let mut regions = Vec::new();
705 let mut y = label_y;
706
707 regions.push(FocusRegion {
709 id: 0,
710 y_offset: y,
711 height: 1,
712 });
713 y += 1;
714
715 if state.display_field.is_some() {
717 y += 1;
718 }
719
720 for (i, (_, v)) in state.entries.iter().enumerate() {
722 let mut entry_height = 1u16;
723 if state.expanded.contains(&i) {
725 if let Some(obj) = v.as_object() {
726 entry_height += obj.len().min(5) as u16;
727 if obj.len() > 5 {
728 entry_height += 1;
729 }
730 }
731 }
732 regions.push(FocusRegion {
733 id: 1 + i,
734 y_offset: y,
735 height: entry_height,
736 });
737 y += entry_height;
738 }
739
740 regions.push(FocusRegion {
742 id: 1 + state.entries.len(),
743 y_offset: y,
744 height: 1,
745 });
746 regions
747 }
748 SettingControl::ObjectArray(state) => {
750 let mut regions = Vec::new();
751 regions.push(FocusRegion {
753 id: 0,
754 y_offset: label_y,
755 height: 1,
756 });
757 for i in 0..state.bindings.len() {
759 regions.push(FocusRegion {
760 id: 1 + i,
761 y_offset: label_y + 1 + i as u16,
762 height: 1,
763 });
764 }
765 regions.push(FocusRegion {
767 id: 1 + state.bindings.len(),
768 y_offset: label_y + 1 + state.bindings.len() as u16,
769 height: 1,
770 });
771 regions
772 }
773 _ => {
775 vec![FocusRegion {
776 id: 0,
777 y_offset: label_y,
778 height: plan.content_rows(),
779 }]
780 }
781 }
782 }
783}
784
785#[derive(Debug, Clone)]
787pub struct SettingsPage {
788 pub name: String,
790 pub path: String,
792 pub description: Option<String>,
794 pub nullable: bool,
796 pub items: Vec<SettingItem>,
798 pub subpages: Vec<SettingsPage>,
800 pub sections: Vec<SectionInfo>,
803}
804
805#[derive(Debug, Clone)]
808pub struct SectionInfo {
809 pub name: String,
810 pub first_item_index: usize,
811}
812
813pub struct BuildContext<'a> {
815 pub config_value: &'a serde_json::Value,
817 pub layer_sources: &'a HashMap<String, ConfigLayer>,
819 pub target_layer: ConfigLayer,
821}
822
823pub fn build_pages(
825 categories: &[SettingCategory],
826 config_value: &serde_json::Value,
827 layer_sources: &HashMap<String, ConfigLayer>,
828 target_layer: ConfigLayer,
829) -> Vec<SettingsPage> {
830 let ctx = BuildContext {
831 config_value,
832 layer_sources,
833 target_layer,
834 };
835 categories.iter().map(|cat| build_page(cat, &ctx)).collect()
836}
837
838fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
840 let mut items: Vec<SettingItem> = category
841 .settings
842 .iter()
843 .flat_map(|s| expand_or_build(s, ctx))
844 .collect();
845
846 items.sort_by(|a, b| match (&a.section, &b.section) {
848 (Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
849 (Some(_), None) => std::cmp::Ordering::Less,
850 (None, Some(_)) => std::cmp::Ordering::Greater,
851 (None, None) => a.name.cmp(&b.name),
852 });
853
854 let mut sections: Vec<SectionInfo> = Vec::new();
857 let mut prev_section: Option<&String> = None;
858 for (idx, item) in items.iter_mut().enumerate() {
859 let is_new_section = match (&item.section, prev_section) {
860 (Some(sec), Some(prev)) => sec != prev,
861 (Some(_), None) => true,
862 (None, Some(_)) => false, (None, None) => false,
864 };
865 item.is_section_start = is_new_section;
866 if is_new_section {
867 if let Some(name) = item.section.clone() {
868 sections.push(SectionInfo {
869 name,
870 first_item_index: idx,
871 });
872 }
873 }
874 prev_section = item.section.as_ref();
875 }
876
877 let subpages = category
878 .subcategories
879 .iter()
880 .map(|sub| build_page(sub, ctx))
881 .collect();
882
883 SettingsPage {
884 name: category.name.clone(),
885 path: category.path.clone(),
886 description: category.description.clone(),
887 nullable: category.nullable,
888 items,
889 subpages,
890 sections,
891 }
892}
893
894fn expand_or_build(schema: &SettingSchema, ctx: &BuildContext) -> Vec<SettingItem> {
900 if let SettingType::Object { properties } = &schema.setting_type {
901 let all_native = !properties.is_empty()
902 && properties.iter().all(|child| {
903 !matches!(
904 child.setting_type,
905 SettingType::Object { .. } | SettingType::Complex
906 )
907 });
908 if all_native {
909 return properties
913 .iter()
914 .map(|child| {
915 let mut child = child.clone();
916 if !child.path.starts_with(&schema.path) {
917 child.path = format!("{}{}", schema.path, child.path);
918 }
919 if let Some(ref mut sib) = child.dual_list_sibling {
920 if !sib.starts_with(&schema.path) {
921 *sib = format!("{}{}", schema.path, sib);
922 }
923 }
924 build_item(&child, ctx)
925 })
926 .collect();
927 }
928 }
929 vec![build_item(schema, ctx)]
930}
931
932pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
934 let current_value = ctx.config_value.pointer(&schema.path);
936
937 let is_null = schema.nullable
939 && current_value
940 .map(|v| v.is_null())
941 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
942
943 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
945
946 let control = match &schema.setting_type {
948 SettingType::Boolean => {
949 let checked = current_value
950 .and_then(|v| v.as_bool())
951 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
952 .unwrap_or(false);
953 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
954 }
955
956 SettingType::Integer { minimum, maximum } => {
957 let value = current_value
958 .and_then(|v| v.as_i64())
959 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
960 .unwrap_or(0);
961
962 let mut state = NumberInputState::new(value, &schema.name);
963 if let Some(min) = minimum {
964 state = state.with_min(*min);
965 }
966 if let Some(max) = maximum {
967 state = state.with_max(*max);
968 }
969 SettingControl::Number(state)
970 }
971
972 SettingType::Number { minimum, maximum } => {
973 let value = current_value
975 .and_then(|v| v.as_f64())
976 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
977 .unwrap_or(0.0);
978
979 let int_value = (value * 100.0).round() as i64;
981 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
982 if let Some(min) = minimum {
983 state = state.with_min((*min * 100.0) as i64);
984 }
985 if let Some(max) = maximum {
986 state = state.with_max((*max * 100.0) as i64);
987 }
988 SettingControl::Number(state)
989 }
990
991 SettingType::String => {
992 let value = current_value
993 .and_then(|v| v.as_str())
994 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
995 .unwrap_or("");
996
997 if let Some(ref source_path) = schema.enum_from {
999 let mut options: Vec<String> = ctx
1000 .config_value
1001 .pointer(source_path)
1002 .and_then(|v| v.as_object())
1003 .map(|obj| obj.keys().cloned().collect())
1004 .unwrap_or_default();
1005 options.sort();
1006
1007 let mut display_names = Vec::new();
1009 let mut values = Vec::new();
1010 if schema.nullable {
1011 display_names.push("(none)".to_string());
1012 values.push(String::new());
1013 }
1014 for key in &options {
1015 display_names.push(key.clone());
1016 values.push(key.clone());
1017 }
1018
1019 let current = if is_null { "" } else { value };
1020 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1021 let state = DropdownState::with_values(display_names, values, &schema.name)
1022 .with_selected(selected);
1023 SettingControl::Dropdown(state)
1024 } else {
1025 let state = TextInputState::new(&schema.name).with_value(value);
1026 SettingControl::Text(state)
1027 }
1028 }
1029
1030 SettingType::Enum { options } => {
1031 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1033 "" } else {
1035 current_value
1036 .and_then(|v| v.as_str())
1037 .or_else(|| {
1038 let default = schema.default.as_ref()?;
1039 if default.is_null() {
1040 Some("")
1041 } else {
1042 default.as_str()
1043 }
1044 })
1045 .unwrap_or("")
1046 };
1047
1048 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1049 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1050 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1051 let state = DropdownState::with_values(display_names, values, &schema.name)
1052 .with_selected(selected);
1053 SettingControl::Dropdown(state)
1054 }
1055
1056 SettingType::DualList {
1057 options,
1058 sibling_path,
1059 } => {
1060 let excluded = sibling_path
1061 .as_ref()
1062 .and_then(|path| ctx.config_value.pointer(path))
1063 .map(|v| value_as_string_array(Some(v), None))
1064 .unwrap_or_default();
1065 SettingControl::DualList(build_dual_list_state(
1066 schema,
1067 options,
1068 current_value,
1069 excluded,
1070 ))
1071 }
1072
1073 SettingType::StringArray => {
1074 let items = value_as_string_array(current_value, schema.default.as_ref());
1075 let state = TextListState::new(&schema.name).with_items(items);
1076 SettingControl::TextList(state)
1077 }
1078
1079 SettingType::IntegerArray => {
1080 let items: Vec<String> = current_value
1081 .and_then(|v| v.as_array())
1082 .map(|arr| {
1083 arr.iter()
1084 .filter_map(|v| {
1085 v.as_i64()
1086 .map(|n| n.to_string())
1087 .or_else(|| v.as_u64().map(|n| n.to_string()))
1088 .or_else(|| v.as_f64().map(|n| n.to_string()))
1089 })
1090 .collect()
1091 })
1092 .or_else(|| {
1093 schema.default.as_ref().and_then(|d| {
1094 d.as_array().map(|arr| {
1095 arr.iter()
1096 .filter_map(|v| {
1097 v.as_i64()
1098 .map(|n| n.to_string())
1099 .or_else(|| v.as_u64().map(|n| n.to_string()))
1100 .or_else(|| v.as_f64().map(|n| n.to_string()))
1101 })
1102 .collect()
1103 })
1104 })
1105 })
1106 .unwrap_or_default();
1107
1108 let state = TextListState::new(&schema.name)
1109 .with_items(items)
1110 .with_integer_mode();
1111 SettingControl::TextList(state)
1112 }
1113
1114 SettingType::Object { .. } => {
1115 json_control(&schema.name, current_value, schema.default.as_ref())
1116 }
1117
1118 SettingType::Map {
1119 value_schema,
1120 display_field,
1121 no_add,
1122 } => {
1123 let map_value = current_value
1125 .cloned()
1126 .or_else(|| schema.default.clone())
1127 .unwrap_or_else(|| serde_json::json!({}));
1128
1129 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1130 state = state.with_value_schema((**value_schema).clone());
1131 if let Some(field) = display_field {
1132 state = state.with_display_field(field.clone());
1133 }
1134 if *no_add {
1135 state = state.with_no_add(true);
1136 }
1137 SettingControl::Map(state)
1138 }
1139
1140 SettingType::ObjectArray {
1141 item_schema,
1142 display_field,
1143 } => {
1144 let array_value = current_value
1146 .cloned()
1147 .or_else(|| schema.default.clone())
1148 .unwrap_or_else(|| serde_json::json!([]));
1149
1150 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1151 state = state.with_item_schema((**item_schema).clone());
1152 if let Some(field) = display_field {
1153 state = state.with_display_field(field.clone());
1154 }
1155 SettingControl::ObjectArray(state)
1156 }
1157
1158 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1159 };
1160
1161 let layer_source = ctx
1163 .layer_sources
1164 .get(&schema.path)
1165 .copied()
1166 .unwrap_or(ConfigLayer::System);
1167
1168 let modified = if is_auto_managed {
1171 false } else {
1173 layer_source == ctx.target_layer
1174 };
1175
1176 let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
1178
1179 SettingItem {
1180 path: schema.path.clone(),
1181 name: schema.name.clone(),
1182 description: cleaned_description,
1183 control,
1184 default: schema.default.clone(),
1185 modified,
1186 layer_source,
1187 read_only: schema.read_only,
1188 is_auto_managed,
1189 nullable: schema.nullable,
1190 is_null,
1191 section: schema.section.clone(),
1192 is_section_start: false, style: ItemBoxStyle::default(),
1194 dual_list_sibling: schema.dual_list_sibling.clone(),
1195 }
1196}
1197
1198pub fn build_item_from_value(
1200 schema: &SettingSchema,
1201 current_value: Option<&serde_json::Value>,
1202) -> SettingItem {
1203 let control = match &schema.setting_type {
1205 SettingType::Boolean => {
1206 let checked = current_value
1207 .and_then(|v| v.as_bool())
1208 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
1209 .unwrap_or(false);
1210 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
1211 }
1212
1213 SettingType::Integer { minimum, maximum } => {
1214 let value = current_value
1215 .and_then(|v| v.as_i64())
1216 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
1217 .unwrap_or(0);
1218
1219 let mut state = NumberInputState::new(value, &schema.name);
1220 if let Some(min) = minimum {
1221 state = state.with_min(*min);
1222 }
1223 if let Some(max) = maximum {
1224 state = state.with_max(*max);
1225 }
1226 SettingControl::Number(state)
1227 }
1228
1229 SettingType::Number { minimum, maximum } => {
1230 let value = current_value
1231 .and_then(|v| v.as_f64())
1232 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
1233 .unwrap_or(0.0);
1234
1235 let int_value = (value * 100.0).round() as i64;
1236 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1237 if let Some(min) = minimum {
1238 state = state.with_min((*min * 100.0) as i64);
1239 }
1240 if let Some(max) = maximum {
1241 state = state.with_max((*max * 100.0) as i64);
1242 }
1243 SettingControl::Number(state)
1244 }
1245
1246 SettingType::String => {
1247 let value = current_value
1248 .and_then(|v| v.as_str())
1249 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1250 .unwrap_or("");
1251
1252 let state = TextInputState::new(&schema.name).with_value(value);
1253 SettingControl::Text(state)
1254 }
1255
1256 SettingType::Enum { options } => {
1257 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1259 "" } else {
1261 current_value
1262 .and_then(|v| v.as_str())
1263 .or_else(|| {
1264 let default = schema.default.as_ref()?;
1265 if default.is_null() {
1266 Some("")
1267 } else {
1268 default.as_str()
1269 }
1270 })
1271 .unwrap_or("")
1272 };
1273
1274 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1275 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1276 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1277 let state = DropdownState::with_values(display_names, values, &schema.name)
1278 .with_selected(selected);
1279 SettingControl::Dropdown(state)
1280 }
1281
1282 SettingType::DualList { options, .. } => {
1283 SettingControl::DualList(build_dual_list_state(
1285 schema,
1286 options,
1287 current_value,
1288 vec![],
1289 ))
1290 }
1291
1292 SettingType::StringArray => {
1293 let items: Vec<String> = current_value
1294 .and_then(|v| v.as_array())
1295 .map(|arr| {
1296 arr.iter()
1297 .filter_map(|v| v.as_str().map(String::from))
1298 .collect()
1299 })
1300 .or_else(|| {
1301 schema.default.as_ref().and_then(|d| {
1302 d.as_array().map(|arr| {
1303 arr.iter()
1304 .filter_map(|v| v.as_str().map(String::from))
1305 .collect()
1306 })
1307 })
1308 })
1309 .unwrap_or_default();
1310
1311 let state = TextListState::new(&schema.name).with_items(items);
1312 SettingControl::TextList(state)
1313 }
1314
1315 SettingType::IntegerArray => {
1316 let items: Vec<String> = current_value
1317 .and_then(|v| v.as_array())
1318 .map(|arr| {
1319 arr.iter()
1320 .filter_map(|v| {
1321 v.as_i64()
1322 .map(|n| n.to_string())
1323 .or_else(|| v.as_u64().map(|n| n.to_string()))
1324 .or_else(|| v.as_f64().map(|n| n.to_string()))
1325 })
1326 .collect()
1327 })
1328 .or_else(|| {
1329 schema.default.as_ref().and_then(|d| {
1330 d.as_array().map(|arr| {
1331 arr.iter()
1332 .filter_map(|v| {
1333 v.as_i64()
1334 .map(|n| n.to_string())
1335 .or_else(|| v.as_u64().map(|n| n.to_string()))
1336 .or_else(|| v.as_f64().map(|n| n.to_string()))
1337 })
1338 .collect()
1339 })
1340 })
1341 })
1342 .unwrap_or_default();
1343
1344 let state = TextListState::new(&schema.name)
1345 .with_items(items)
1346 .with_integer_mode();
1347 SettingControl::TextList(state)
1348 }
1349
1350 SettingType::Object { .. } => {
1351 json_control(&schema.name, current_value, schema.default.as_ref())
1352 }
1353
1354 SettingType::Map {
1355 value_schema,
1356 display_field,
1357 no_add,
1358 } => {
1359 let map_value = current_value
1360 .cloned()
1361 .or_else(|| schema.default.clone())
1362 .unwrap_or_else(|| serde_json::json!({}));
1363
1364 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1365 state = state.with_value_schema((**value_schema).clone());
1366 if let Some(field) = display_field {
1367 state = state.with_display_field(field.clone());
1368 }
1369 if *no_add {
1370 state = state.with_no_add(true);
1371 }
1372 SettingControl::Map(state)
1373 }
1374
1375 SettingType::ObjectArray {
1376 item_schema,
1377 display_field,
1378 } => {
1379 let array_value = current_value
1380 .cloned()
1381 .or_else(|| schema.default.clone())
1382 .unwrap_or_else(|| serde_json::json!([]));
1383
1384 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1385 state = state.with_item_schema((**item_schema).clone());
1386 if let Some(field) = display_field {
1387 state = state.with_display_field(field.clone());
1388 }
1389 SettingControl::ObjectArray(state)
1390 }
1391
1392 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1393 };
1394
1395 let modified = match (¤t_value, &schema.default) {
1398 (Some(current), Some(default)) => *current != default,
1399 (Some(_), None) => true,
1400 _ => false,
1401 };
1402
1403 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
1405
1406 let is_null = schema.nullable
1407 && current_value
1408 .map(|v| v.is_null())
1409 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
1410
1411 SettingItem {
1412 path: schema.path.clone(),
1413 name: schema.name.clone(),
1414 description: schema.description.clone(),
1415 control,
1416 default: schema.default.clone(),
1417 modified,
1418 layer_source: ConfigLayer::System,
1420 read_only: schema.read_only,
1421 is_auto_managed,
1422 nullable: schema.nullable,
1423 is_null,
1424 section: schema.section.clone(),
1425 is_section_start: false, style: ItemBoxStyle::default(),
1427 dual_list_sibling: schema.dual_list_sibling.clone(),
1428 }
1429}
1430
1431pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
1433 match control {
1434 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
1435
1436 SettingControl::Number(state) => {
1437 if state.is_percentage {
1438 let float_value = state.value as f64 / 100.0;
1440 serde_json::Number::from_f64(float_value)
1441 .map(serde_json::Value::Number)
1442 .unwrap_or(serde_json::Value::Number(state.value.into()))
1443 } else {
1444 serde_json::Value::Number(state.value.into())
1445 }
1446 }
1447
1448 SettingControl::Dropdown(state) => state
1449 .selected_value()
1450 .map(|s| {
1451 if s.is_empty() {
1452 serde_json::Value::Null
1454 } else {
1455 serde_json::Value::String(s.to_string())
1456 }
1457 })
1458 .unwrap_or(serde_json::Value::Null),
1459
1460 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1461
1462 SettingControl::TextList(state) => {
1463 let arr: Vec<serde_json::Value> = state
1464 .items
1465 .iter()
1466 .filter_map(|s| {
1467 if state.is_integer {
1468 s.parse::<i64>()
1469 .ok()
1470 .map(|n| serde_json::Value::Number(n.into()))
1471 } else {
1472 Some(serde_json::Value::String(s.clone()))
1473 }
1474 })
1475 .collect();
1476 serde_json::Value::Array(arr)
1477 }
1478
1479 SettingControl::DualList(state) => {
1480 let arr: Vec<serde_json::Value> = state
1481 .included
1482 .iter()
1483 .map(|s| serde_json::Value::String(s.clone()))
1484 .collect();
1485 serde_json::Value::Array(arr)
1486 }
1487
1488 SettingControl::Map(state) => state.to_value(),
1489
1490 SettingControl::ObjectArray(state) => state.to_value(),
1491
1492 SettingControl::Json(state) => {
1493 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1495 }
1496
1497 SettingControl::Complex { .. } => serde_json::Value::Null,
1498 }
1499}
1500
1501#[cfg(test)]
1502mod tests {
1503 use super::*;
1504
1505 fn sample_config() -> serde_json::Value {
1506 serde_json::json!({
1507 "theme": "monokai",
1508 "check_for_updates": false,
1509 "editor": {
1510 "tab_size": 2,
1511 "line_numbers": true
1512 }
1513 })
1514 }
1515
1516 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1518 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1520 std::sync::LazyLock::new(HashMap::new);
1521 BuildContext {
1522 config_value: config,
1523 layer_sources: &EMPTY_SOURCES,
1524 target_layer: ConfigLayer::User,
1525 }
1526 }
1527
1528 fn test_context_with_sources<'a>(
1530 config: &'a serde_json::Value,
1531 layer_sources: &'a HashMap<String, ConfigLayer>,
1532 target_layer: ConfigLayer,
1533 ) -> BuildContext<'a> {
1534 BuildContext {
1535 config_value: config,
1536 layer_sources,
1537 target_layer,
1538 }
1539 }
1540
1541 #[test]
1542 fn test_build_toggle_item() {
1543 let schema = SettingSchema {
1544 path: "/check_for_updates".to_string(),
1545 name: "Check For Updates".to_string(),
1546 description: Some("Check for updates".to_string()),
1547 setting_type: SettingType::Boolean,
1548 default: Some(serde_json::Value::Bool(true)),
1549 read_only: false,
1550 section: None,
1551 order: None,
1552 nullable: false,
1553 enum_from: None,
1554 dual_list_sibling: None,
1555 };
1556
1557 let config = sample_config();
1558 let ctx = test_context(&config);
1559 let item = build_item(&schema, &ctx);
1560
1561 assert_eq!(item.path, "/check_for_updates");
1562 assert!(!item.modified);
1565 assert_eq!(item.layer_source, ConfigLayer::System);
1566
1567 if let SettingControl::Toggle(state) = &item.control {
1568 assert!(!state.checked); } else {
1570 panic!("Expected toggle control");
1571 }
1572 }
1573
1574 #[test]
1575 fn test_build_toggle_item_modified_in_user_layer() {
1576 let schema = SettingSchema {
1577 path: "/check_for_updates".to_string(),
1578 name: "Check For Updates".to_string(),
1579 description: Some("Check for updates".to_string()),
1580 setting_type: SettingType::Boolean,
1581 default: Some(serde_json::Value::Bool(true)),
1582 read_only: false,
1583 section: None,
1584 order: None,
1585 nullable: false,
1586 enum_from: None,
1587 dual_list_sibling: None,
1588 };
1589
1590 let config = sample_config();
1591 let mut layer_sources = HashMap::new();
1592 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1593 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1594 let item = build_item(&schema, &ctx);
1595
1596 assert!(item.modified);
1599 assert_eq!(item.layer_source, ConfigLayer::User);
1600 }
1601
1602 #[test]
1603 fn test_build_number_item() {
1604 let schema = SettingSchema {
1605 path: "/editor/tab_size".to_string(),
1606 name: "Tab Size".to_string(),
1607 description: None,
1608 setting_type: SettingType::Integer {
1609 minimum: Some(1),
1610 maximum: Some(16),
1611 },
1612 default: Some(serde_json::Value::Number(4.into())),
1613 read_only: false,
1614 section: None,
1615 order: None,
1616 nullable: false,
1617 enum_from: None,
1618 dual_list_sibling: None,
1619 };
1620
1621 let config = sample_config();
1622 let ctx = test_context(&config);
1623 let item = build_item(&schema, &ctx);
1624
1625 assert!(!item.modified);
1627
1628 if let SettingControl::Number(state) = &item.control {
1629 assert_eq!(state.value, 2);
1630 assert_eq!(state.min, Some(1));
1631 assert_eq!(state.max, Some(16));
1632 } else {
1633 panic!("Expected number control");
1634 }
1635 }
1636
1637 #[test]
1638 fn test_build_text_item() {
1639 let schema = SettingSchema {
1640 path: "/theme".to_string(),
1641 name: "Theme".to_string(),
1642 description: None,
1643 setting_type: SettingType::String,
1644 default: Some(serde_json::Value::String("high-contrast".to_string())),
1645 read_only: false,
1646 section: None,
1647 order: None,
1648 nullable: false,
1649 enum_from: None,
1650 dual_list_sibling: None,
1651 };
1652
1653 let config = sample_config();
1654 let ctx = test_context(&config);
1655 let item = build_item(&schema, &ctx);
1656
1657 assert!(!item.modified);
1659
1660 if let SettingControl::Text(state) = &item.control {
1661 assert_eq!(state.value, "monokai");
1662 } else {
1663 panic!("Expected text control");
1664 }
1665 }
1666
1667 #[test]
1668 fn test_clean_description_keeps_full_desc_with_new_info() {
1669 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1671 assert!(result.is_some());
1672 let cleaned = result.unwrap();
1673 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1676 assert!(cleaned.contains("character"));
1677 }
1678
1679 #[test]
1680 fn test_clean_description_keeps_extra_info() {
1681 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1683 assert!(result.is_some());
1684 let cleaned = result.unwrap();
1685 assert!(cleaned.contains("gutter"));
1686 }
1687
1688 #[test]
1689 fn test_clean_description_returns_none_for_pure_redundancy() {
1690 let result = clean_description("Theme", Some("Theme"));
1692 assert!(result.is_none());
1693
1694 let result = clean_description("Theme", Some("The theme to use"));
1696 assert!(result.is_none());
1697 }
1698
1699 #[test]
1700 fn test_clean_description_returns_none_for_empty() {
1701 let result = clean_description("Theme", Some(""));
1702 assert!(result.is_none());
1703
1704 let result = clean_description("Theme", None);
1705 assert!(result.is_none());
1706 }
1707
1708 #[test]
1709 fn test_control_to_value() {
1710 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1711 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1712
1713 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1714 assert_eq!(control_to_value(&number), serde_json::json!(42));
1715
1716 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1717 assert_eq!(
1718 control_to_value(&text),
1719 serde_json::Value::String("hello".to_string())
1720 );
1721 }
1722}