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 available_status_bar_tokens: &HashMap<String, String>,
243) -> DualListState {
244 let mut all_options: Vec<(String, String)> = options
246 .iter()
247 .map(|o| (o.value.clone(), o.name.clone()))
248 .collect();
249
250 if schema.dynamically_extendable_status_bar_elements {
252 for (key, display) in available_status_bar_tokens {
253 let token = format!("{{{}}}", key);
254 if !all_options.iter().any(|(v, _)| v == &token) {
255 all_options.push((token, display.clone()));
256 }
257 }
258 }
259
260 let included = value_as_string_array(current_value, schema.default.as_ref());
261 DualListState::new(&schema.name, all_options)
262 .with_included(included)
263 .with_excluded(excluded)
264}
265
266#[derive(Debug, Clone)]
268pub struct SettingItem {
269 pub path: String,
271 pub name: String,
273 pub description: Option<String>,
275 pub control: SettingControl,
277 pub default: Option<serde_json::Value>,
279 pub modified: bool,
283 pub layer_source: ConfigLayer,
286 pub read_only: bool,
288 pub is_auto_managed: bool,
290 pub nullable: bool,
292 pub is_null: bool,
294 pub section: Option<String>,
296 pub is_section_start: bool,
298 pub style: ItemBoxStyle,
303 pub dual_list_sibling: Option<String>,
305}
306
307#[derive(Debug, Clone)]
309pub enum SettingControl {
310 Toggle(ToggleState),
311 Number(NumberInputState),
312 Dropdown(DropdownState),
313 Text(TextInputState),
314 TextList(TextListState),
315 DualList(DualListState),
317 Map(MapState),
319 ObjectArray(KeybindingListState),
321 Json(JsonEditState),
323 Complex {
325 type_name: String,
326 },
327}
328
329impl SettingControl {
330 pub fn control_height(&self) -> u16 {
332 match self {
333 Self::TextList(state) => {
335 (state.items.len() + 2) as u16
337 }
338 Self::DualList(state) => 2 + state.body_rows() as u16,
340 Self::Map(state) => {
342 let header_row = if state.display_field.is_some() { 1 } else { 0 };
343 let add_new_row = if state.no_add { 0 } else { 1 };
344 let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
347 .expanded
348 .iter()
349 .filter_map(|&idx| state.entries.get(idx))
350 .map(|(_, v)| {
351 if let Some(obj) = v.as_object() {
352 obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
353 } else {
354 0
355 }
356 })
357 .sum();
358 (base + expanded_height) as u16
359 }
360 Self::Dropdown(state) => {
362 if state.open {
363 1 + state.options.len().min(8) as u16
365 } else {
366 1
367 }
368 }
369 Self::ObjectArray(state) => {
371 (state.bindings.len() + 2) as u16
373 }
374 Self::Json(state) => {
376 1 + state.display_height() as u16
378 }
379 _ => 1,
381 }
382 }
383
384 pub fn is_composite(&self) -> bool {
388 matches!(
389 self,
390 Self::TextList(_) | Self::DualList(_) | Self::Map(_) | Self::ObjectArray(_)
391 )
392 }
393
394 pub fn focused_sub_row(&self) -> u16 {
398 match self {
399 Self::TextList(state) => {
400 match state.focused_item {
402 Some(idx) => 1 + idx as u16, None => 1 + state.items.len() as u16, }
405 }
406 Self::DualList(state) => {
407 use crate::view::controls::DualListColumn;
409 let row = match state.active_column {
410 DualListColumn::Available => state.available_cursor,
411 DualListColumn::Included => state.included_cursor,
412 };
413 2 + row as u16
414 }
415 Self::ObjectArray(state) => {
416 match state.focused_index {
418 Some(idx) => 1 + idx as u16,
419 None => 1 + state.bindings.len() as u16,
420 }
421 }
422 Self::Map(state) => {
423 let header_offset = if state.display_field.is_some() { 1 } else { 0 };
425 match state.focused_entry {
426 Some(idx) => 1 + header_offset + idx as u16,
427 None => 1 + header_offset + state.entries.len() as u16,
428 }
429 }
430 _ => 0,
431 }
432 }
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450pub struct ItemBoxStyle {
451 pub section_header_rows: u16,
454 pub card_border_rows: u16,
456 pub card_border_cols: u16,
458 pub focus_indicator_cols: u16,
461 pub description_right_padding_cols: u16,
464}
465
466impl ItemBoxStyle {
467 pub const fn cards() -> Self {
470 Self {
471 section_header_rows: 2,
472 card_border_rows: 1,
473 card_border_cols: 1,
474 focus_indicator_cols: 3,
475 description_right_padding_cols: 2,
476 }
477 }
478
479 pub const fn flat() -> Self {
482 Self {
483 section_header_rows: 2,
484 card_border_rows: 0,
485 card_border_cols: 0,
486 focus_indicator_cols: 3,
487 description_right_padding_cols: 2,
488 }
489 }
490
491 pub fn inner_text_width(&self, card_outer_width: u16) -> u16 {
495 card_outer_width
496 .saturating_sub(2 * self.card_border_cols)
497 .saturating_sub(self.focus_indicator_cols)
498 .saturating_sub(self.description_right_padding_cols)
499 }
500}
501
502impl Default for ItemBoxStyle {
503 fn default() -> Self {
504 Self::cards()
505 }
506}
507
508#[derive(Debug, Clone, Copy, Default)]
516pub struct ItemBox {
517 pub section_header_rows: u16,
519 pub top_border_rows: u16,
521 pub control_rows: u16,
523 pub description_rows: u16,
525 pub bottom_border_rows: u16,
527}
528
529impl ItemBox {
530 pub fn total_rows(&self) -> u16 {
531 self.section_header_rows
532 + self.top_border_rows
533 + self.control_rows
534 + self.description_rows
535 + self.bottom_border_rows
536 }
537
538 pub fn card_top_y(&self) -> u16 {
540 self.section_header_rows
541 }
542
543 pub fn control_y(&self) -> u16 {
545 self.card_top_y() + self.top_border_rows
546 }
547
548 pub fn description_y(&self) -> u16 {
550 self.control_y() + self.control_rows
551 }
552
553 pub fn bottom_border_y(&self) -> u16 {
555 self.description_y() + self.description_rows
556 }
557
558 pub fn card_height(&self) -> u16 {
560 self.top_border_rows + self.control_rows + self.description_rows + self.bottom_border_rows
561 }
562
563 pub fn content_rows(&self) -> u16 {
565 self.control_rows + self.description_rows
566 }
567}
568
569impl SettingItem {
570 pub fn layout_box(&self, width: u16, style: &ItemBoxStyle) -> ItemBox {
574 ItemBox {
575 section_header_rows: if self.is_section_start {
576 style.section_header_rows
577 } else {
578 0
579 },
580 top_border_rows: style.card_border_rows,
581 control_rows: self.control.control_height(),
582 description_rows: self.description_rows_for(style.inner_text_width(width)),
583 bottom_border_rows: style.card_border_rows,
584 }
585 }
586
587 pub fn description_rows_for(&self, inner_width: u16) -> u16 {
594 let Some(desc) = self.description.as_deref() else {
595 return 0;
596 };
597 if desc.is_empty() {
598 return 0;
599 }
600 if inner_width == 0 {
601 return 1;
602 }
603 desc.len().div_ceil(inner_width as usize) as u16
604 }
605}
606
607pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
610 let desc = description?;
611 if desc.is_empty() {
612 return None;
613 }
614
615 let name_words: HashSet<String> = name
617 .to_lowercase()
618 .split(|c: char| !c.is_alphanumeric())
619 .filter(|w| !w.is_empty() && w.len() > 2)
620 .map(String::from)
621 .collect();
622
623 let filler_words: HashSet<&str> = [
625 "the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
626 "whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
627 "disable", "set", "use", "allow", "default", "true", "false",
628 ]
629 .into_iter()
630 .collect();
631
632 let desc_words: Vec<&str> = desc
634 .split(|c: char| !c.is_alphanumeric())
635 .filter(|w| !w.is_empty())
636 .collect();
637
638 let has_new_info = desc_words.iter().any(|word| {
640 let lower = word.to_lowercase();
641 lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
642 });
643
644 if !has_new_info {
645 return None;
646 }
647
648 Some(desc.to_string())
649}
650
651impl ScrollItem for SettingItem {
652 fn height(&self, width: u16) -> u16 {
653 self.layout_box(width, &self.style).total_rows()
654 }
655
656 fn focus_regions(&self, width: u16) -> Vec<FocusRegion> {
657 let plan = self.layout_box(width, &self.style);
666 let label_y = plan.control_y();
667
668 match &self.control {
669 SettingControl::TextList(state) => {
671 let mut regions = Vec::new();
672 regions.push(FocusRegion {
674 id: 0,
675 y_offset: label_y,
676 height: 1,
677 });
678 for i in 0..state.items.len() {
680 regions.push(FocusRegion {
681 id: 1 + i,
682 y_offset: label_y + 1 + i as u16,
683 height: 1,
684 });
685 }
686 regions.push(FocusRegion {
688 id: 1 + state.items.len(),
689 y_offset: label_y + 1 + state.items.len() as u16,
690 height: 1,
691 });
692 regions
693 }
694 SettingControl::DualList(state) => {
696 let mut regions = Vec::new();
697 regions.push(FocusRegion {
699 id: 0,
700 y_offset: label_y,
701 height: 1,
702 });
703 let body = state.body_rows();
706 for i in 0..body {
707 regions.push(FocusRegion {
708 id: 1 + i,
709 y_offset: label_y + 2 + i as u16, height: 1,
711 });
712 }
713 regions
714 }
715 SettingControl::Map(state) => {
717 let mut regions = Vec::new();
718 let mut y = label_y;
719
720 regions.push(FocusRegion {
722 id: 0,
723 y_offset: y,
724 height: 1,
725 });
726 y += 1;
727
728 if state.display_field.is_some() {
730 y += 1;
731 }
732
733 for (i, (_, v)) in state.entries.iter().enumerate() {
735 let mut entry_height = 1u16;
736 if state.expanded.contains(&i) {
738 if let Some(obj) = v.as_object() {
739 entry_height += obj.len().min(5) as u16;
740 if obj.len() > 5 {
741 entry_height += 1;
742 }
743 }
744 }
745 regions.push(FocusRegion {
746 id: 1 + i,
747 y_offset: y,
748 height: entry_height,
749 });
750 y += entry_height;
751 }
752
753 regions.push(FocusRegion {
755 id: 1 + state.entries.len(),
756 y_offset: y,
757 height: 1,
758 });
759 regions
760 }
761 SettingControl::ObjectArray(state) => {
763 let mut regions = Vec::new();
764 regions.push(FocusRegion {
766 id: 0,
767 y_offset: label_y,
768 height: 1,
769 });
770 for i in 0..state.bindings.len() {
772 regions.push(FocusRegion {
773 id: 1 + i,
774 y_offset: label_y + 1 + i as u16,
775 height: 1,
776 });
777 }
778 regions.push(FocusRegion {
780 id: 1 + state.bindings.len(),
781 y_offset: label_y + 1 + state.bindings.len() as u16,
782 height: 1,
783 });
784 regions
785 }
786 _ => {
788 vec![FocusRegion {
789 id: 0,
790 y_offset: label_y,
791 height: plan.content_rows(),
792 }]
793 }
794 }
795 }
796}
797
798#[derive(Debug, Clone)]
800pub struct SettingsPage {
801 pub name: String,
803 pub path: String,
805 pub description: Option<String>,
807 pub nullable: bool,
809 pub items: Vec<SettingItem>,
811 pub subpages: Vec<SettingsPage>,
813 pub sections: Vec<SectionInfo>,
816}
817
818#[derive(Debug, Clone)]
821pub struct SectionInfo {
822 pub name: String,
823 pub first_item_index: usize,
824}
825
826pub struct BuildContext<'a> {
828 pub config_value: &'a serde_json::Value,
830 pub layer_sources: &'a HashMap<String, ConfigLayer>,
832 pub target_layer: ConfigLayer,
834 pub available_status_bar_tokens: &'a HashMap<String, String>,
837}
838
839pub fn build_pages(
841 categories: &[SettingCategory],
842 config_value: &serde_json::Value,
843 layer_sources: &HashMap<String, ConfigLayer>,
844 target_layer: ConfigLayer,
845 available_status_bar_tokens: &HashMap<String, String>,
846) -> Vec<SettingsPage> {
847 let ctx = BuildContext {
848 config_value,
849 layer_sources,
850 target_layer,
851 available_status_bar_tokens,
852 };
853 categories.iter().map(|cat| build_page(cat, &ctx)).collect()
854}
855
856fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
858 let mut items: Vec<SettingItem> = category
859 .settings
860 .iter()
861 .flat_map(|s| expand_or_build(s, ctx))
862 .collect();
863
864 items.sort_by(|a, b| match (&a.section, &b.section) {
866 (Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
867 (Some(_), None) => std::cmp::Ordering::Less,
868 (None, Some(_)) => std::cmp::Ordering::Greater,
869 (None, None) => a.name.cmp(&b.name),
870 });
871
872 let mut sections: Vec<SectionInfo> = Vec::new();
875 let mut prev_section: Option<&String> = None;
876 for (idx, item) in items.iter_mut().enumerate() {
877 let is_new_section = match (&item.section, prev_section) {
878 (Some(sec), Some(prev)) => sec != prev,
879 (Some(_), None) => true,
880 (None, Some(_)) => false, (None, None) => false,
882 };
883 item.is_section_start = is_new_section;
884 if is_new_section {
885 if let Some(name) = item.section.clone() {
886 sections.push(SectionInfo {
887 name,
888 first_item_index: idx,
889 });
890 }
891 }
892 prev_section = item.section.as_ref();
893 }
894
895 let subpages = category
896 .subcategories
897 .iter()
898 .map(|sub| build_page(sub, ctx))
899 .collect();
900
901 SettingsPage {
902 name: category.name.clone(),
903 path: category.path.clone(),
904 description: category.description.clone(),
905 nullable: category.nullable,
906 items,
907 subpages,
908 sections,
909 }
910}
911
912fn expand_or_build(schema: &SettingSchema, ctx: &BuildContext) -> Vec<SettingItem> {
918 if let SettingType::Object { properties } = &schema.setting_type {
919 let all_native = !properties.is_empty()
920 && properties.iter().all(|child| {
921 !matches!(
922 child.setting_type,
923 SettingType::Object { .. } | SettingType::Complex
924 )
925 });
926 if all_native {
927 return properties
931 .iter()
932 .map(|child| {
933 let mut child = child.clone();
934 if !child.path.starts_with(&schema.path) {
935 child.path = format!("{}{}", schema.path, child.path);
936 }
937 if let Some(ref mut sib) = child.dual_list_sibling {
938 if !sib.starts_with(&schema.path) {
939 *sib = format!("{}{}", schema.path, sib);
940 }
941 }
942 build_item(&child, ctx)
943 })
944 .collect();
945 }
946 }
947 vec![build_item(schema, ctx)]
948}
949
950pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
952 let current_value = ctx.config_value.pointer(&schema.path);
954
955 let is_null = schema.nullable
957 && current_value
958 .map(|v| v.is_null())
959 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
960
961 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
963
964 let control = match &schema.setting_type {
966 SettingType::Boolean => {
967 let checked = current_value
968 .and_then(|v| v.as_bool())
969 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
970 .unwrap_or(false);
971 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
972 }
973
974 SettingType::Integer { minimum, maximum } => {
975 let value = current_value
976 .and_then(|v| v.as_i64())
977 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
978 .unwrap_or(0);
979
980 let mut state = NumberInputState::new(value, &schema.name);
981 if let Some(min) = minimum {
982 state = state.with_min(*min);
983 }
984 if let Some(max) = maximum {
985 state = state.with_max(*max);
986 }
987 SettingControl::Number(state)
988 }
989
990 SettingType::Number { minimum, maximum } => {
991 let value = current_value
993 .and_then(|v| v.as_f64())
994 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
995 .unwrap_or(0.0);
996
997 let int_value = (value * 100.0).round() as i64;
999 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1000 if let Some(min) = minimum {
1001 state = state.with_min((*min * 100.0) as i64);
1002 }
1003 if let Some(max) = maximum {
1004 state = state.with_max((*max * 100.0) as i64);
1005 }
1006 SettingControl::Number(state)
1007 }
1008
1009 SettingType::String => {
1010 let value = current_value
1011 .and_then(|v| v.as_str())
1012 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1013 .unwrap_or("");
1014
1015 if let Some(ref source_path) = schema.enum_from {
1017 let mut options: Vec<String> = ctx
1018 .config_value
1019 .pointer(source_path)
1020 .and_then(|v| v.as_object())
1021 .map(|obj| obj.keys().cloned().collect())
1022 .unwrap_or_default();
1023 options.sort();
1024
1025 let mut display_names = Vec::new();
1027 let mut values = Vec::new();
1028 if schema.nullable {
1029 display_names.push("(none)".to_string());
1030 values.push(String::new());
1031 }
1032 for key in &options {
1033 display_names.push(key.clone());
1034 values.push(key.clone());
1035 }
1036
1037 let current = if is_null { "" } else { value };
1038 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1039 let state = DropdownState::with_values(display_names, values, &schema.name)
1040 .with_selected(selected);
1041 SettingControl::Dropdown(state)
1042 } else {
1043 let state = TextInputState::new(&schema.name).with_value(value);
1044 SettingControl::Text(state)
1045 }
1046 }
1047
1048 SettingType::Enum { options } => {
1049 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1051 "" } else {
1053 current_value
1054 .and_then(|v| v.as_str())
1055 .or_else(|| {
1056 let default = schema.default.as_ref()?;
1057 if default.is_null() {
1058 Some("")
1059 } else {
1060 default.as_str()
1061 }
1062 })
1063 .unwrap_or("")
1064 };
1065
1066 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1067 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1068 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1069 let state = DropdownState::with_values(display_names, values, &schema.name)
1070 .with_selected(selected);
1071 SettingControl::Dropdown(state)
1072 }
1073
1074 SettingType::DualList {
1075 options,
1076 sibling_path,
1077 } => {
1078 let excluded = sibling_path
1079 .as_ref()
1080 .and_then(|path| ctx.config_value.pointer(path))
1081 .map(|v| value_as_string_array(Some(v), None))
1082 .unwrap_or_default();
1083 SettingControl::DualList(build_dual_list_state(
1084 schema,
1085 options,
1086 current_value,
1087 excluded,
1088 ctx.available_status_bar_tokens,
1089 ))
1090 }
1091
1092 SettingType::StringArray => {
1093 let items = value_as_string_array(current_value, schema.default.as_ref());
1094 let state = TextListState::new(&schema.name).with_items(items);
1095 SettingControl::TextList(state)
1096 }
1097
1098 SettingType::IntegerArray => {
1099 let items: Vec<String> = current_value
1100 .and_then(|v| v.as_array())
1101 .map(|arr| {
1102 arr.iter()
1103 .filter_map(|v| {
1104 v.as_i64()
1105 .map(|n| n.to_string())
1106 .or_else(|| v.as_u64().map(|n| n.to_string()))
1107 .or_else(|| v.as_f64().map(|n| n.to_string()))
1108 })
1109 .collect()
1110 })
1111 .or_else(|| {
1112 schema.default.as_ref().and_then(|d| {
1113 d.as_array().map(|arr| {
1114 arr.iter()
1115 .filter_map(|v| {
1116 v.as_i64()
1117 .map(|n| n.to_string())
1118 .or_else(|| v.as_u64().map(|n| n.to_string()))
1119 .or_else(|| v.as_f64().map(|n| n.to_string()))
1120 })
1121 .collect()
1122 })
1123 })
1124 })
1125 .unwrap_or_default();
1126
1127 let state = TextListState::new(&schema.name)
1128 .with_items(items)
1129 .with_integer_mode();
1130 SettingControl::TextList(state)
1131 }
1132
1133 SettingType::Object { .. } => {
1134 json_control(&schema.name, current_value, schema.default.as_ref())
1135 }
1136
1137 SettingType::Map {
1138 value_schema,
1139 display_field,
1140 no_add,
1141 } => {
1142 let map_value = current_value
1144 .cloned()
1145 .or_else(|| schema.default.clone())
1146 .unwrap_or_else(|| serde_json::json!({}));
1147
1148 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1149 state = state.with_value_schema((**value_schema).clone());
1150 if let Some(field) = display_field {
1151 state = state.with_display_field(field.clone());
1152 }
1153 if *no_add {
1154 state = state.with_no_add(true);
1155 }
1156 SettingControl::Map(state)
1157 }
1158
1159 SettingType::ObjectArray {
1160 item_schema,
1161 display_field,
1162 } => {
1163 let array_value = current_value
1165 .cloned()
1166 .or_else(|| schema.default.clone())
1167 .unwrap_or_else(|| serde_json::json!([]));
1168
1169 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1170 state = state.with_item_schema((**item_schema).clone());
1171 if let Some(field) = display_field {
1172 state = state.with_display_field(field.clone());
1173 }
1174 SettingControl::ObjectArray(state)
1175 }
1176
1177 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1178 };
1179
1180 let layer_source = ctx
1182 .layer_sources
1183 .get(&schema.path)
1184 .copied()
1185 .unwrap_or(ConfigLayer::System);
1186
1187 let modified = if is_auto_managed {
1190 false } else {
1192 layer_source == ctx.target_layer
1193 };
1194
1195 let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
1197
1198 SettingItem {
1199 path: schema.path.clone(),
1200 name: schema.name.clone(),
1201 description: cleaned_description,
1202 control,
1203 default: schema.default.clone(),
1204 modified,
1205 layer_source,
1206 read_only: schema.read_only,
1207 is_auto_managed,
1208 nullable: schema.nullable,
1209 is_null,
1210 section: schema.section.clone(),
1211 is_section_start: false, style: ItemBoxStyle::default(),
1213 dual_list_sibling: schema.dual_list_sibling.clone(),
1214 }
1215}
1216
1217pub fn build_item_from_value(
1219 schema: &SettingSchema,
1220 current_value: Option<&serde_json::Value>,
1221 available_status_bar_tokens: &HashMap<String, String>,
1222) -> SettingItem {
1223 let control = match &schema.setting_type {
1225 SettingType::Boolean => {
1226 let checked = current_value
1227 .and_then(|v| v.as_bool())
1228 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
1229 .unwrap_or(false);
1230 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
1231 }
1232
1233 SettingType::Integer { minimum, maximum } => {
1234 let value = current_value
1235 .and_then(|v| v.as_i64())
1236 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
1237 .unwrap_or(0);
1238
1239 let mut state = NumberInputState::new(value, &schema.name);
1240 if let Some(min) = minimum {
1241 state = state.with_min(*min);
1242 }
1243 if let Some(max) = maximum {
1244 state = state.with_max(*max);
1245 }
1246 SettingControl::Number(state)
1247 }
1248
1249 SettingType::Number { minimum, maximum } => {
1250 let value = current_value
1251 .and_then(|v| v.as_f64())
1252 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
1253 .unwrap_or(0.0);
1254
1255 let int_value = (value * 100.0).round() as i64;
1256 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1257 if let Some(min) = minimum {
1258 state = state.with_min((*min * 100.0) as i64);
1259 }
1260 if let Some(max) = maximum {
1261 state = state.with_max((*max * 100.0) as i64);
1262 }
1263 SettingControl::Number(state)
1264 }
1265
1266 SettingType::String => {
1267 let value = current_value
1268 .and_then(|v| v.as_str())
1269 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1270 .unwrap_or("");
1271
1272 let state = TextInputState::new(&schema.name).with_value(value);
1273 SettingControl::Text(state)
1274 }
1275
1276 SettingType::Enum { options } => {
1277 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1279 "" } else {
1281 current_value
1282 .and_then(|v| v.as_str())
1283 .or_else(|| {
1284 let default = schema.default.as_ref()?;
1285 if default.is_null() {
1286 Some("")
1287 } else {
1288 default.as_str()
1289 }
1290 })
1291 .unwrap_or("")
1292 };
1293
1294 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1295 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1296 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1297 let state = DropdownState::with_values(display_names, values, &schema.name)
1298 .with_selected(selected);
1299 SettingControl::Dropdown(state)
1300 }
1301
1302 SettingType::DualList { options, .. } => {
1303 SettingControl::DualList(build_dual_list_state(
1305 schema,
1306 options,
1307 current_value,
1308 vec![],
1309 available_status_bar_tokens,
1310 ))
1311 }
1312
1313 SettingType::StringArray => {
1314 let items: Vec<String> = current_value
1315 .and_then(|v| v.as_array())
1316 .map(|arr| {
1317 arr.iter()
1318 .filter_map(|v| v.as_str().map(String::from))
1319 .collect()
1320 })
1321 .or_else(|| {
1322 schema.default.as_ref().and_then(|d| {
1323 d.as_array().map(|arr| {
1324 arr.iter()
1325 .filter_map(|v| v.as_str().map(String::from))
1326 .collect()
1327 })
1328 })
1329 })
1330 .unwrap_or_default();
1331
1332 let state = TextListState::new(&schema.name).with_items(items);
1333 SettingControl::TextList(state)
1334 }
1335
1336 SettingType::IntegerArray => {
1337 let items: Vec<String> = current_value
1338 .and_then(|v| v.as_array())
1339 .map(|arr| {
1340 arr.iter()
1341 .filter_map(|v| {
1342 v.as_i64()
1343 .map(|n| n.to_string())
1344 .or_else(|| v.as_u64().map(|n| n.to_string()))
1345 .or_else(|| v.as_f64().map(|n| n.to_string()))
1346 })
1347 .collect()
1348 })
1349 .or_else(|| {
1350 schema.default.as_ref().and_then(|d| {
1351 d.as_array().map(|arr| {
1352 arr.iter()
1353 .filter_map(|v| {
1354 v.as_i64()
1355 .map(|n| n.to_string())
1356 .or_else(|| v.as_u64().map(|n| n.to_string()))
1357 .or_else(|| v.as_f64().map(|n| n.to_string()))
1358 })
1359 .collect()
1360 })
1361 })
1362 })
1363 .unwrap_or_default();
1364
1365 let state = TextListState::new(&schema.name)
1366 .with_items(items)
1367 .with_integer_mode();
1368 SettingControl::TextList(state)
1369 }
1370
1371 SettingType::Object { .. } => {
1372 json_control(&schema.name, current_value, schema.default.as_ref())
1373 }
1374
1375 SettingType::Map {
1376 value_schema,
1377 display_field,
1378 no_add,
1379 } => {
1380 let map_value = current_value
1381 .cloned()
1382 .or_else(|| schema.default.clone())
1383 .unwrap_or_else(|| serde_json::json!({}));
1384
1385 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1386 state = state.with_value_schema((**value_schema).clone());
1387 if let Some(field) = display_field {
1388 state = state.with_display_field(field.clone());
1389 }
1390 if *no_add {
1391 state = state.with_no_add(true);
1392 }
1393 SettingControl::Map(state)
1394 }
1395
1396 SettingType::ObjectArray {
1397 item_schema,
1398 display_field,
1399 } => {
1400 let array_value = current_value
1401 .cloned()
1402 .or_else(|| schema.default.clone())
1403 .unwrap_or_else(|| serde_json::json!([]));
1404
1405 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1406 state = state.with_item_schema((**item_schema).clone());
1407 if let Some(field) = display_field {
1408 state = state.with_display_field(field.clone());
1409 }
1410 SettingControl::ObjectArray(state)
1411 }
1412
1413 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1414 };
1415
1416 let modified = match (¤t_value, &schema.default) {
1419 (Some(current), Some(default)) => *current != default,
1420 (Some(_), None) => true,
1421 _ => false,
1422 };
1423
1424 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
1426
1427 let is_null = schema.nullable
1428 && current_value
1429 .map(|v| v.is_null())
1430 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
1431
1432 SettingItem {
1433 path: schema.path.clone(),
1434 name: schema.name.clone(),
1435 description: schema.description.clone(),
1436 control,
1437 default: schema.default.clone(),
1438 modified,
1439 layer_source: ConfigLayer::System,
1441 read_only: schema.read_only,
1442 is_auto_managed,
1443 nullable: schema.nullable,
1444 is_null,
1445 section: schema.section.clone(),
1446 is_section_start: false, style: ItemBoxStyle::default(),
1448 dual_list_sibling: schema.dual_list_sibling.clone(),
1449 }
1450}
1451
1452pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
1454 match control {
1455 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
1456
1457 SettingControl::Number(state) => {
1458 if state.is_percentage {
1459 let float_value = state.value as f64 / 100.0;
1461 serde_json::Number::from_f64(float_value)
1462 .map(serde_json::Value::Number)
1463 .unwrap_or(serde_json::Value::Number(state.value.into()))
1464 } else {
1465 serde_json::Value::Number(state.value.into())
1466 }
1467 }
1468
1469 SettingControl::Dropdown(state) => state
1470 .selected_value()
1471 .map(|s| {
1472 if s.is_empty() {
1473 serde_json::Value::Null
1475 } else {
1476 serde_json::Value::String(s.to_string())
1477 }
1478 })
1479 .unwrap_or(serde_json::Value::Null),
1480
1481 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1482
1483 SettingControl::TextList(state) => {
1484 let arr: Vec<serde_json::Value> = state
1485 .items
1486 .iter()
1487 .filter_map(|s| {
1488 if state.is_integer {
1489 s.parse::<i64>()
1490 .ok()
1491 .map(|n| serde_json::Value::Number(n.into()))
1492 } else {
1493 Some(serde_json::Value::String(s.clone()))
1494 }
1495 })
1496 .collect();
1497 serde_json::Value::Array(arr)
1498 }
1499
1500 SettingControl::DualList(state) => {
1501 let arr: Vec<serde_json::Value> = state
1502 .included
1503 .iter()
1504 .map(|s| serde_json::Value::String(s.clone()))
1505 .collect();
1506 serde_json::Value::Array(arr)
1507 }
1508
1509 SettingControl::Map(state) => state.to_value(),
1510
1511 SettingControl::ObjectArray(state) => state.to_value(),
1512
1513 SettingControl::Json(state) => {
1514 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1516 }
1517
1518 SettingControl::Complex { .. } => serde_json::Value::Null,
1519 }
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use super::*;
1525
1526 fn sample_config() -> serde_json::Value {
1527 serde_json::json!({
1528 "theme": "monokai",
1529 "check_for_updates": false,
1530 "editor": {
1531 "tab_size": 2,
1532 "line_numbers": true
1533 }
1534 })
1535 }
1536
1537 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1539 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1541 std::sync::LazyLock::new(HashMap::new);
1542 static EMPTY_TOKENS: std::sync::LazyLock<HashMap<String, String>> =
1543 std::sync::LazyLock::new(HashMap::new);
1544 BuildContext {
1545 config_value: config,
1546 layer_sources: &EMPTY_SOURCES,
1547 target_layer: ConfigLayer::User,
1548 available_status_bar_tokens: &EMPTY_TOKENS,
1549 }
1550 }
1551
1552 fn test_context_with_sources<'a>(
1554 config: &'a serde_json::Value,
1555 layer_sources: &'a HashMap<String, ConfigLayer>,
1556 target_layer: ConfigLayer,
1557 ) -> BuildContext<'a> {
1558 static EMPTY_TOKENS: std::sync::LazyLock<HashMap<String, String>> =
1559 std::sync::LazyLock::new(HashMap::new);
1560 BuildContext {
1561 config_value: config,
1562 layer_sources,
1563 target_layer,
1564 available_status_bar_tokens: &EMPTY_TOKENS,
1565 }
1566 }
1567
1568 #[test]
1569 fn test_build_toggle_item() {
1570 let schema = SettingSchema {
1571 path: "/check_for_updates".to_string(),
1572 name: "Check For Updates".to_string(),
1573 description: Some("Check for updates".to_string()),
1574 setting_type: SettingType::Boolean,
1575 default: Some(serde_json::Value::Bool(true)),
1576 read_only: false,
1577 section: None,
1578 order: None,
1579 nullable: false,
1580 enum_from: None,
1581 dual_list_sibling: None,
1582 dynamically_extendable_status_bar_elements: false,
1583 };
1584
1585 let config = sample_config();
1586 let ctx = test_context(&config);
1587 let item = build_item(&schema, &ctx);
1588
1589 assert_eq!(item.path, "/check_for_updates");
1590 assert!(!item.modified);
1593 assert_eq!(item.layer_source, ConfigLayer::System);
1594
1595 if let SettingControl::Toggle(state) = &item.control {
1596 assert!(!state.checked); } else {
1598 panic!("Expected toggle control");
1599 }
1600 }
1601
1602 #[test]
1603 fn test_build_toggle_item_modified_in_user_layer() {
1604 let schema = SettingSchema {
1605 path: "/check_for_updates".to_string(),
1606 name: "Check For Updates".to_string(),
1607 description: Some("Check for updates".to_string()),
1608 setting_type: SettingType::Boolean,
1609 default: Some(serde_json::Value::Bool(true)),
1610 read_only: false,
1611 section: None,
1612 order: None,
1613 nullable: false,
1614 enum_from: None,
1615 dual_list_sibling: None,
1616 dynamically_extendable_status_bar_elements: false,
1617 };
1618
1619 let config = sample_config();
1620 let mut layer_sources = HashMap::new();
1621 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1622 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1623 let item = build_item(&schema, &ctx);
1624
1625 assert!(item.modified);
1628 assert_eq!(item.layer_source, ConfigLayer::User);
1629 }
1630
1631 #[test]
1632 fn test_build_number_item() {
1633 let schema = SettingSchema {
1634 path: "/editor/tab_size".to_string(),
1635 name: "Tab Size".to_string(),
1636 description: None,
1637 setting_type: SettingType::Integer {
1638 minimum: Some(1),
1639 maximum: Some(16),
1640 },
1641 default: Some(serde_json::Value::Number(4.into())),
1642 read_only: false,
1643 section: None,
1644 order: None,
1645 nullable: false,
1646 enum_from: None,
1647 dual_list_sibling: None,
1648 dynamically_extendable_status_bar_elements: false,
1649 };
1650
1651 let config = sample_config();
1652 let ctx = test_context(&config);
1653 let item = build_item(&schema, &ctx);
1654
1655 assert!(!item.modified);
1657
1658 if let SettingControl::Number(state) = &item.control {
1659 assert_eq!(state.value, 2);
1660 assert_eq!(state.min, Some(1));
1661 assert_eq!(state.max, Some(16));
1662 } else {
1663 panic!("Expected number control");
1664 }
1665 }
1666
1667 #[test]
1668 fn test_build_text_item() {
1669 let schema = SettingSchema {
1670 path: "/theme".to_string(),
1671 name: "Theme".to_string(),
1672 description: None,
1673 setting_type: SettingType::String,
1674 default: Some(serde_json::Value::String("high-contrast".to_string())),
1675 read_only: false,
1676 section: None,
1677 order: None,
1678 nullable: false,
1679 enum_from: None,
1680 dual_list_sibling: None,
1681 dynamically_extendable_status_bar_elements: false,
1682 };
1683
1684 let config = sample_config();
1685 let ctx = test_context(&config);
1686 let item = build_item(&schema, &ctx);
1687
1688 assert!(!item.modified);
1690
1691 if let SettingControl::Text(state) = &item.control {
1692 assert_eq!(state.value, "monokai");
1693 } else {
1694 panic!("Expected text control");
1695 }
1696 }
1697
1698 #[test]
1699 fn test_clean_description_keeps_full_desc_with_new_info() {
1700 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1702 assert!(result.is_some());
1703 let cleaned = result.unwrap();
1704 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1707 assert!(cleaned.contains("character"));
1708 }
1709
1710 #[test]
1711 fn test_clean_description_keeps_extra_info() {
1712 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1714 assert!(result.is_some());
1715 let cleaned = result.unwrap();
1716 assert!(cleaned.contains("gutter"));
1717 }
1718
1719 #[test]
1720 fn test_clean_description_returns_none_for_pure_redundancy() {
1721 let result = clean_description("Theme", Some("Theme"));
1723 assert!(result.is_none());
1724
1725 let result = clean_description("Theme", Some("The theme to use"));
1727 assert!(result.is_none());
1728 }
1729
1730 #[test]
1731 fn test_clean_description_returns_none_for_empty() {
1732 let result = clean_description("Theme", Some(""));
1733 assert!(result.is_none());
1734
1735 let result = clean_description("Theme", None);
1736 assert!(result.is_none());
1737 }
1738
1739 #[test]
1740 fn test_control_to_value() {
1741 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1742 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1743
1744 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1745 assert_eq!(control_to_value(&number), serde_json::json!(42));
1746
1747 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1748 assert_eq!(
1749 control_to_value(&text),
1750 serde_json::Value::String("hello".to_string())
1751 );
1752 }
1753}