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 is_unset(&self) -> bool {
54 self.editor.value().trim() == "null"
55 }
56
57 pub fn clear_placeholder_for_edit(&mut self) {
62 if self.is_unset() {
63 self.editor.set_value("");
64 }
65 }
66
67 pub fn restore_unset_if_empty(&mut self) {
71 if self.editor.value().trim().is_empty() {
72 self.editor.set_value("null");
73 }
74 }
75
76 pub fn revert(&mut self) {
78 self.editor.set_value(&self.original_text);
79 self.scroll_offset = 0;
80 }
81
82 pub fn commit(&mut self) {
84 self.original_text = self.editor.value();
85 }
86
87 pub fn value(&self) -> String {
89 self.editor.value()
90 }
91
92 pub fn is_valid(&self) -> bool {
99 let text = self.value();
100 text.trim().is_empty() || serde_json::from_str::<serde_json::Value>(&text).is_ok()
101 }
102
103 pub fn display_height(&self) -> usize {
105 self.editor.line_count()
106 }
107
108 pub fn display_height_capped(&self) -> usize {
110 self.editor.line_count().min(self.max_visible_lines)
111 }
112
113 pub fn lines(&self) -> &[String] {
115 &self.editor.lines
116 }
117
118 pub fn cursor_pos(&self) -> (usize, usize) {
120 (self.editor.cursor_row, self.editor.cursor_col)
121 }
122
123 pub fn insert(&mut self, c: char) {
125 self.editor.insert_char(c);
126 }
127
128 pub fn insert_str(&mut self, s: &str) {
129 self.editor.insert_str(s);
130 }
131
132 pub fn backspace(&mut self) {
133 self.editor.backspace();
134 }
135
136 pub fn delete(&mut self) {
137 self.editor.delete();
138 }
139
140 pub fn move_left(&mut self) {
141 self.editor.move_left();
142 }
143
144 pub fn move_right(&mut self) {
145 self.editor.move_right();
146 }
147
148 pub fn move_up(&mut self) {
149 self.editor.move_up();
150 }
151
152 pub fn move_down(&mut self) {
153 self.editor.move_down();
154 }
155
156 pub fn move_home(&mut self) {
157 self.editor.move_home();
158 }
159
160 pub fn move_end(&mut self) {
161 self.editor.move_end();
162 }
163
164 pub fn move_word_left(&mut self) {
165 self.editor.move_word_left();
166 }
167
168 pub fn move_word_right(&mut self) {
169 self.editor.move_word_right();
170 }
171
172 pub fn has_selection(&self) -> bool {
174 self.editor.has_selection()
175 }
176
177 pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
178 self.editor.selection_range()
179 }
180
181 pub fn selected_text(&self) -> Option<String> {
182 self.editor.selected_text()
183 }
184
185 pub fn delete_selection(&mut self) -> Option<String> {
186 self.editor.delete_selection()
187 }
188
189 pub fn clear_selection(&mut self) {
190 self.editor.clear_selection();
191 }
192
193 pub fn move_left_selecting(&mut self) {
194 self.editor.move_left_selecting();
195 }
196
197 pub fn move_right_selecting(&mut self) {
198 self.editor.move_right_selecting();
199 }
200
201 pub fn move_up_selecting(&mut self) {
202 self.editor.move_up_selecting();
203 }
204
205 pub fn move_down_selecting(&mut self) {
206 self.editor.move_down_selecting();
207 }
208
209 pub fn move_home_selecting(&mut self) {
210 self.editor.move_home_selecting();
211 }
212
213 pub fn move_end_selecting(&mut self) {
214 self.editor.move_end_selecting();
215 }
216
217 pub fn move_word_left_selecting(&mut self) {
218 self.editor.move_word_left_selecting();
219 }
220
221 pub fn move_word_right_selecting(&mut self) {
222 self.editor.move_word_right_selecting();
223 }
224
225 pub fn select_all(&mut self) {
226 self.editor.select_all();
227 }
228
229 pub fn delete_word_forward(&mut self) {
230 self.editor.delete_word_forward();
231 }
232
233 pub fn delete_word_backward(&mut self) {
234 self.editor.delete_word_backward();
235 }
236
237 pub fn delete_to_end(&mut self) {
238 self.editor.delete_to_end();
239 }
240}
241
242fn json_control(
244 name: &str,
245 current_value: Option<&serde_json::Value>,
246 default: Option<&serde_json::Value>,
247) -> SettingControl {
248 let value = current_value.or(default);
249 SettingControl::Json(JsonEditState::new(name, value))
250}
251
252fn value_as_string_array(
254 current: Option<&serde_json::Value>,
255 default: Option<&serde_json::Value>,
256) -> Vec<String> {
257 let from = |v: &serde_json::Value| -> Option<Vec<String>> {
258 v.as_array().map(|arr| {
259 arr.iter()
260 .filter_map(|v| v.as_str().map(String::from))
261 .collect()
262 })
263 };
264 current
265 .and_then(from)
266 .or_else(|| default.and_then(from))
267 .unwrap_or_default()
268}
269
270fn build_dual_list_state(
272 schema: &SettingSchema,
273 options: &[crate::view::settings::schema::EnumOption],
274 current_value: Option<&serde_json::Value>,
275 excluded: Vec<String>,
276 available_status_bar_tokens: &HashMap<String, String>,
277) -> DualListState {
278 let mut all_options: Vec<(String, String)> = options
280 .iter()
281 .map(|o| (o.value.clone(), o.name.clone()))
282 .collect();
283
284 if schema.dynamically_extendable_status_bar_elements {
286 for (key, display) in available_status_bar_tokens {
287 let token = format!("{{{}}}", key);
288 if !all_options.iter().any(|(v, _)| v == &token) {
289 all_options.push((token, display.clone()));
290 }
291 }
292 }
293
294 let included = value_as_string_array(current_value, schema.default.as_ref());
295 DualListState::new(&schema.name, all_options)
296 .with_included(included)
297 .with_excluded(excluded)
298}
299
300#[derive(Debug, Clone)]
302pub struct SettingItem {
303 pub path: String,
305 pub name: String,
307 pub description: Option<String>,
309 pub control: SettingControl,
311 pub default: Option<serde_json::Value>,
313 pub modified: bool,
317 pub layer_source: ConfigLayer,
320 pub read_only: bool,
322 pub is_auto_managed: bool,
324 pub nullable: bool,
326 pub is_null: bool,
328 pub section: Option<String>,
330 pub is_section_start: bool,
332 pub style: ItemBoxStyle,
337 pub dual_list_sibling: Option<String>,
339}
340
341#[derive(Debug, Clone)]
343pub enum SettingControl {
344 Toggle(ToggleState),
345 Number(NumberInputState),
346 Dropdown(DropdownState),
347 Text(TextInputState),
348 TextList(TextListState),
349 DualList(DualListState),
351 Map(MapState),
353 ObjectArray(KeybindingListState),
355 Json(JsonEditState),
357 Complex {
359 type_name: String,
360 },
361}
362
363impl SettingControl {
364 pub fn control_height(&self) -> u16 {
366 match self {
367 Self::TextList(state) => {
369 (state.items.len() + 2) as u16
371 }
372 Self::DualList(state) => 2 + state.body_rows() as u16,
374 Self::Map(state) => {
376 let header_row = if state.display_field.is_some() { 1 } else { 0 };
377 let add_new_row = if state.no_add { 0 } else { 1 };
378 let base = 1 + header_row + state.entries.len() + add_new_row; let expanded_height: usize = state
381 .expanded
382 .iter()
383 .filter_map(|&idx| state.entries.get(idx))
384 .map(|(_, v)| {
385 if let Some(obj) = v.as_object() {
386 obj.len().min(5) + if obj.len() > 5 { 1 } else { 0 }
387 } else {
388 0
389 }
390 })
391 .sum();
392 (base + expanded_height) as u16
393 }
394 Self::Dropdown(state) => {
396 if state.open {
397 1 + state.options.len().min(8) as u16
399 } else {
400 1
401 }
402 }
403 Self::ObjectArray(state) => {
405 (state.bindings.len() + 2) as u16
407 }
408 Self::Json(state) => {
410 1 + state.display_height() as u16
412 }
413 _ => 1,
415 }
416 }
417
418 pub fn is_composite(&self) -> bool {
422 matches!(
423 self,
424 Self::TextList(_) | Self::DualList(_) | Self::Map(_) | Self::ObjectArray(_)
425 )
426 }
427
428 pub fn focused_sub_row(&self) -> u16 {
432 match self {
433 Self::TextList(state) => {
434 match state.focused_item {
436 Some(idx) => 1 + idx as u16, None => 1 + state.items.len() as u16, }
439 }
440 Self::DualList(state) => {
441 use crate::view::controls::DualListColumn;
443 let row = match state.active_column {
444 DualListColumn::Available => state.available_cursor,
445 DualListColumn::Included => state.included_cursor,
446 };
447 2 + row as u16
448 }
449 Self::ObjectArray(state) => {
450 match state.focused_index {
452 Some(idx) => 1 + idx as u16,
453 None => 1 + state.bindings.len() as u16,
454 }
455 }
456 Self::Map(state) => {
457 let header_offset = if state.display_field.is_some() { 1 } else { 0 };
459 match state.focused_entry {
460 Some(idx) => 1 + header_offset + idx as u16,
461 None => 1 + header_offset + state.entries.len() as u16,
462 }
463 }
464 _ => 0,
465 }
466 }
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq)]
484pub struct ItemBoxStyle {
485 pub section_header_rows: u16,
488 pub card_border_rows: u16,
490 pub card_border_cols: u16,
492 pub focus_indicator_cols: u16,
495 pub description_right_padding_cols: u16,
498}
499
500impl ItemBoxStyle {
501 pub const fn cards() -> Self {
504 Self {
505 section_header_rows: 2,
506 card_border_rows: 1,
507 card_border_cols: 1,
508 focus_indicator_cols: 3,
509 description_right_padding_cols: 2,
510 }
511 }
512
513 pub const fn flat() -> Self {
516 Self {
517 section_header_rows: 2,
518 card_border_rows: 0,
519 card_border_cols: 0,
520 focus_indicator_cols: 3,
521 description_right_padding_cols: 2,
522 }
523 }
524
525 pub fn inner_text_width(&self, card_outer_width: u16) -> u16 {
529 card_outer_width
530 .saturating_sub(2 * self.card_border_cols)
531 .saturating_sub(self.focus_indicator_cols)
532 .saturating_sub(self.description_right_padding_cols)
533 }
534}
535
536impl Default for ItemBoxStyle {
537 fn default() -> Self {
538 Self::cards()
539 }
540}
541
542#[derive(Debug, Clone, Copy, Default)]
550pub struct ItemBox {
551 pub section_header_rows: u16,
553 pub top_border_rows: u16,
555 pub control_rows: u16,
557 pub description_rows: u16,
559 pub bottom_border_rows: u16,
561}
562
563impl ItemBox {
564 pub fn total_rows(&self) -> u16 {
565 self.section_header_rows
566 + self.top_border_rows
567 + self.control_rows
568 + self.description_rows
569 + self.bottom_border_rows
570 }
571
572 pub fn card_top_y(&self) -> u16 {
574 self.section_header_rows
575 }
576
577 pub fn control_y(&self) -> u16 {
579 self.card_top_y() + self.top_border_rows
580 }
581
582 pub fn description_y(&self) -> u16 {
584 self.control_y() + self.control_rows
585 }
586
587 pub fn bottom_border_y(&self) -> u16 {
589 self.description_y() + self.description_rows
590 }
591
592 pub fn card_height(&self) -> u16 {
594 self.top_border_rows + self.control_rows + self.description_rows + self.bottom_border_rows
595 }
596
597 pub fn content_rows(&self) -> u16 {
599 self.control_rows + self.description_rows
600 }
601}
602
603impl SettingItem {
604 pub fn layout_box(&self, width: u16, style: &ItemBoxStyle) -> ItemBox {
608 ItemBox {
609 section_header_rows: if self.is_section_start {
610 style.section_header_rows
611 } else {
612 0
613 },
614 top_border_rows: style.card_border_rows,
615 control_rows: self.control.control_height(),
616 description_rows: self.description_rows_for(style.inner_text_width(width)),
617 bottom_border_rows: style.card_border_rows,
618 }
619 }
620
621 pub fn description_rows_for(&self, inner_width: u16) -> u16 {
628 let Some(desc) = self.description.as_deref() else {
629 return 0;
630 };
631 if desc.is_empty() {
632 return 0;
633 }
634 if inner_width == 0 {
635 return 1;
636 }
637 desc.len().div_ceil(inner_width as usize) as u16
638 }
639}
640
641pub fn clean_description(name: &str, description: Option<&str>) -> Option<String> {
644 let desc = description?;
645 if desc.is_empty() {
646 return None;
647 }
648
649 let name_words: HashSet<String> = name
651 .to_lowercase()
652 .split(|c: char| !c.is_alphanumeric())
653 .filter(|w| !w.is_empty() && w.len() > 2)
654 .map(String::from)
655 .collect();
656
657 let filler_words: HashSet<&str> = [
659 "the", "a", "an", "to", "for", "of", "in", "on", "is", "are", "be", "and", "or", "when",
660 "whether", "if", "this", "that", "with", "from", "by", "as", "at", "show", "enable",
661 "disable", "set", "use", "allow", "default", "true", "false",
662 ]
663 .into_iter()
664 .collect();
665
666 let desc_words: Vec<&str> = desc
668 .split(|c: char| !c.is_alphanumeric())
669 .filter(|w| !w.is_empty())
670 .collect();
671
672 let has_new_info = desc_words.iter().any(|word| {
674 let lower = word.to_lowercase();
675 lower.len() > 2 && !name_words.contains(&lower) && !filler_words.contains(lower.as_str())
676 });
677
678 if !has_new_info {
679 return None;
680 }
681
682 Some(desc.to_string())
683}
684
685impl ScrollItem for SettingItem {
686 fn height(&self, width: u16) -> u16 {
687 self.layout_box(width, &self.style).total_rows()
688 }
689
690 fn focus_regions(&self, width: u16) -> Vec<FocusRegion> {
691 let plan = self.layout_box(width, &self.style);
700 let label_y = plan.control_y();
701
702 match &self.control {
703 SettingControl::TextList(state) => {
705 let mut regions = Vec::new();
706 regions.push(FocusRegion {
708 id: 0,
709 y_offset: label_y,
710 height: 1,
711 });
712 for i in 0..state.items.len() {
714 regions.push(FocusRegion {
715 id: 1 + i,
716 y_offset: label_y + 1 + i as u16,
717 height: 1,
718 });
719 }
720 regions.push(FocusRegion {
722 id: 1 + state.items.len(),
723 y_offset: label_y + 1 + state.items.len() as u16,
724 height: 1,
725 });
726 regions
727 }
728 SettingControl::DualList(state) => {
730 let mut regions = Vec::new();
731 regions.push(FocusRegion {
733 id: 0,
734 y_offset: label_y,
735 height: 1,
736 });
737 let body = state.body_rows();
740 for i in 0..body {
741 regions.push(FocusRegion {
742 id: 1 + i,
743 y_offset: label_y + 2 + i as u16, height: 1,
745 });
746 }
747 regions
748 }
749 SettingControl::Map(state) => {
751 let mut regions = Vec::new();
752 let mut y = label_y;
753
754 regions.push(FocusRegion {
756 id: 0,
757 y_offset: y,
758 height: 1,
759 });
760 y += 1;
761
762 if state.display_field.is_some() {
764 y += 1;
765 }
766
767 for (i, (_, v)) in state.entries.iter().enumerate() {
769 let mut entry_height = 1u16;
770 if state.expanded.contains(&i) {
772 if let Some(obj) = v.as_object() {
773 entry_height += obj.len().min(5) as u16;
774 if obj.len() > 5 {
775 entry_height += 1;
776 }
777 }
778 }
779 regions.push(FocusRegion {
780 id: 1 + i,
781 y_offset: y,
782 height: entry_height,
783 });
784 y += entry_height;
785 }
786
787 regions.push(FocusRegion {
789 id: 1 + state.entries.len(),
790 y_offset: y,
791 height: 1,
792 });
793 regions
794 }
795 SettingControl::ObjectArray(state) => {
797 let mut regions = Vec::new();
798 regions.push(FocusRegion {
800 id: 0,
801 y_offset: label_y,
802 height: 1,
803 });
804 for i in 0..state.bindings.len() {
806 regions.push(FocusRegion {
807 id: 1 + i,
808 y_offset: label_y + 1 + i as u16,
809 height: 1,
810 });
811 }
812 regions.push(FocusRegion {
814 id: 1 + state.bindings.len(),
815 y_offset: label_y + 1 + state.bindings.len() as u16,
816 height: 1,
817 });
818 regions
819 }
820 _ => {
822 vec![FocusRegion {
823 id: 0,
824 y_offset: label_y,
825 height: plan.content_rows(),
826 }]
827 }
828 }
829 }
830}
831
832#[derive(Debug, Clone)]
834pub struct SettingsPage {
835 pub name: String,
837 pub path: String,
839 pub description: Option<String>,
841 pub nullable: bool,
843 pub items: Vec<SettingItem>,
845 pub subpages: Vec<SettingsPage>,
847 pub sections: Vec<SectionInfo>,
850}
851
852#[derive(Debug, Clone)]
855pub struct SectionInfo {
856 pub name: String,
857 pub first_item_index: usize,
858}
859
860pub struct BuildContext<'a> {
862 pub config_value: &'a serde_json::Value,
864 pub layer_sources: &'a HashMap<String, ConfigLayer>,
866 pub target_layer: ConfigLayer,
868 pub available_status_bar_tokens: &'a HashMap<String, String>,
871}
872
873pub fn build_pages(
875 categories: &[SettingCategory],
876 config_value: &serde_json::Value,
877 layer_sources: &HashMap<String, ConfigLayer>,
878 target_layer: ConfigLayer,
879 available_status_bar_tokens: &HashMap<String, String>,
880) -> Vec<SettingsPage> {
881 let ctx = BuildContext {
882 config_value,
883 layer_sources,
884 target_layer,
885 available_status_bar_tokens,
886 };
887 categories.iter().map(|cat| build_page(cat, &ctx)).collect()
888}
889
890fn build_page(category: &SettingCategory, ctx: &BuildContext) -> SettingsPage {
892 let mut items: Vec<SettingItem> = category
893 .settings
894 .iter()
895 .flat_map(|s| expand_or_build(s, ctx))
896 .collect();
897
898 items.sort_by(|a, b| match (&a.section, &b.section) {
900 (Some(sec_a), Some(sec_b)) => sec_a.cmp(sec_b).then_with(|| a.name.cmp(&b.name)),
901 (Some(_), None) => std::cmp::Ordering::Less,
902 (None, Some(_)) => std::cmp::Ordering::Greater,
903 (None, None) => a.name.cmp(&b.name),
904 });
905
906 let mut sections: Vec<SectionInfo> = Vec::new();
909 let mut prev_section: Option<&String> = None;
910 for (idx, item) in items.iter_mut().enumerate() {
911 let is_new_section = match (&item.section, prev_section) {
912 (Some(sec), Some(prev)) => sec != prev,
913 (Some(_), None) => true,
914 (None, Some(_)) => false, (None, None) => false,
916 };
917 item.is_section_start = is_new_section;
918 if is_new_section {
919 if let Some(name) = item.section.clone() {
920 sections.push(SectionInfo {
921 name,
922 first_item_index: idx,
923 });
924 }
925 }
926 prev_section = item.section.as_ref();
927 }
928
929 let subpages = category
930 .subcategories
931 .iter()
932 .map(|sub| build_page(sub, ctx))
933 .collect();
934
935 SettingsPage {
936 name: category.name.clone(),
937 path: category.path.clone(),
938 description: category.description.clone(),
939 nullable: category.nullable,
940 items,
941 subpages,
942 sections,
943 }
944}
945
946fn expand_or_build(schema: &SettingSchema, ctx: &BuildContext) -> Vec<SettingItem> {
952 if let SettingType::Object { properties } = &schema.setting_type {
953 let all_native = !properties.is_empty()
954 && properties.iter().all(|child| {
955 !matches!(
956 child.setting_type,
957 SettingType::Object { .. } | SettingType::Complex
958 )
959 });
960 if all_native {
961 return properties
965 .iter()
966 .map(|child| {
967 let mut child = child.clone();
968 if !child.path.starts_with(&schema.path) {
969 child.path = format!("{}{}", schema.path, child.path);
970 }
971 if let Some(ref mut sib) = child.dual_list_sibling {
972 if !sib.starts_with(&schema.path) {
973 *sib = format!("{}{}", schema.path, sib);
974 }
975 }
976 build_item(&child, ctx)
977 })
978 .collect();
979 }
980 }
981 vec![build_item(schema, ctx)]
982}
983
984pub fn build_item(schema: &SettingSchema, ctx: &BuildContext) -> SettingItem {
986 let current_value = ctx.config_value.pointer(&schema.path);
988
989 let is_null = schema.nullable
991 && current_value
992 .map(|v| v.is_null())
993 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
994
995 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
997
998 let control = match &schema.setting_type {
1000 SettingType::Boolean => {
1001 let checked = current_value
1002 .and_then(|v| v.as_bool())
1003 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
1004 .unwrap_or(false);
1005 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
1006 }
1007
1008 SettingType::Integer { minimum, maximum } => {
1009 let value = current_value
1010 .and_then(|v| v.as_i64())
1011 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
1012 .unwrap_or(0);
1013
1014 let mut state = NumberInputState::new(value, &schema.name);
1015 if let Some(min) = minimum {
1016 state = state.with_min(*min);
1017 }
1018 if let Some(max) = maximum {
1019 state = state.with_max(*max);
1020 }
1021 SettingControl::Number(state)
1022 }
1023
1024 SettingType::Number { minimum, maximum } => {
1025 let value = current_value
1027 .and_then(|v| v.as_f64())
1028 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
1029 .unwrap_or(0.0);
1030
1031 let int_value = (value * 100.0).round() as i64;
1033 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1034 if let Some(min) = minimum {
1035 state = state.with_min((*min * 100.0) as i64);
1036 }
1037 if let Some(max) = maximum {
1038 state = state.with_max((*max * 100.0) as i64);
1039 }
1040 SettingControl::Number(state)
1041 }
1042
1043 SettingType::String => {
1044 let value = current_value
1045 .and_then(|v| v.as_str())
1046 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1047 .unwrap_or("");
1048
1049 if let Some(ref source_path) = schema.enum_from {
1051 let mut options: Vec<String> = ctx
1052 .config_value
1053 .pointer(source_path)
1054 .and_then(|v| v.as_object())
1055 .map(|obj| obj.keys().cloned().collect())
1056 .unwrap_or_default();
1057 options.sort();
1058
1059 let mut display_names = Vec::new();
1061 let mut values = Vec::new();
1062 if schema.nullable {
1063 display_names.push("(none)".to_string());
1064 values.push(String::new());
1065 }
1066 for key in &options {
1067 display_names.push(key.clone());
1068 values.push(key.clone());
1069 }
1070
1071 let current = if is_null { "" } else { value };
1072 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1073 let state = DropdownState::with_values(display_names, values, &schema.name)
1074 .with_selected(selected);
1075 SettingControl::Dropdown(state)
1076 } else {
1077 let state = TextInputState::new(&schema.name).with_value(value);
1078 SettingControl::Text(state)
1079 }
1080 }
1081
1082 SettingType::Enum { options } => {
1083 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1085 "" } else {
1087 current_value
1088 .and_then(|v| v.as_str())
1089 .or_else(|| {
1090 let default = schema.default.as_ref()?;
1091 if default.is_null() {
1092 Some("")
1093 } else {
1094 default.as_str()
1095 }
1096 })
1097 .unwrap_or("")
1098 };
1099
1100 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1101 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1102 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1103 let state = DropdownState::with_values(display_names, values, &schema.name)
1104 .with_selected(selected);
1105 SettingControl::Dropdown(state)
1106 }
1107
1108 SettingType::DualList {
1109 options,
1110 sibling_path,
1111 } => {
1112 let excluded = sibling_path
1113 .as_ref()
1114 .and_then(|path| ctx.config_value.pointer(path))
1115 .map(|v| value_as_string_array(Some(v), None))
1116 .unwrap_or_default();
1117 SettingControl::DualList(build_dual_list_state(
1118 schema,
1119 options,
1120 current_value,
1121 excluded,
1122 ctx.available_status_bar_tokens,
1123 ))
1124 }
1125
1126 SettingType::StringArray => {
1127 let items = value_as_string_array(current_value, schema.default.as_ref());
1128 let state = TextListState::new(&schema.name).with_items(items);
1129 SettingControl::TextList(state)
1130 }
1131
1132 SettingType::IntegerArray => {
1133 let items: Vec<String> = current_value
1134 .and_then(|v| v.as_array())
1135 .map(|arr| {
1136 arr.iter()
1137 .filter_map(|v| {
1138 v.as_i64()
1139 .map(|n| n.to_string())
1140 .or_else(|| v.as_u64().map(|n| n.to_string()))
1141 .or_else(|| v.as_f64().map(|n| n.to_string()))
1142 })
1143 .collect()
1144 })
1145 .or_else(|| {
1146 schema.default.as_ref().and_then(|d| {
1147 d.as_array().map(|arr| {
1148 arr.iter()
1149 .filter_map(|v| {
1150 v.as_i64()
1151 .map(|n| n.to_string())
1152 .or_else(|| v.as_u64().map(|n| n.to_string()))
1153 .or_else(|| v.as_f64().map(|n| n.to_string()))
1154 })
1155 .collect()
1156 })
1157 })
1158 })
1159 .unwrap_or_default();
1160
1161 let state = TextListState::new(&schema.name)
1162 .with_items(items)
1163 .with_integer_mode();
1164 SettingControl::TextList(state)
1165 }
1166
1167 SettingType::Object { .. } => {
1168 json_control(&schema.name, current_value, schema.default.as_ref())
1169 }
1170
1171 SettingType::Map {
1172 value_schema,
1173 display_field,
1174 no_add,
1175 } => {
1176 let map_value = current_value
1178 .cloned()
1179 .or_else(|| schema.default.clone())
1180 .unwrap_or_else(|| serde_json::json!({}));
1181
1182 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1183 state = state.with_value_schema((**value_schema).clone());
1184 if let Some(field) = display_field {
1185 state = state.with_display_field(field.clone());
1186 }
1187 if *no_add {
1188 state = state.with_no_add(true);
1189 }
1190 SettingControl::Map(state)
1191 }
1192
1193 SettingType::ObjectArray {
1194 item_schema,
1195 display_field,
1196 } => {
1197 let array_value = current_value
1199 .cloned()
1200 .or_else(|| schema.default.clone())
1201 .unwrap_or_else(|| serde_json::json!([]));
1202
1203 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1204 state = state.with_item_schema((**item_schema).clone());
1205 if let Some(field) = display_field {
1206 state = state.with_display_field(field.clone());
1207 }
1208 SettingControl::ObjectArray(state)
1209 }
1210
1211 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1212 };
1213
1214 let layer_source = ctx
1216 .layer_sources
1217 .get(&schema.path)
1218 .copied()
1219 .unwrap_or(ConfigLayer::System);
1220
1221 let modified = if is_auto_managed {
1224 false } else {
1226 layer_source == ctx.target_layer
1227 };
1228
1229 let cleaned_description = clean_description(&schema.name, schema.description.as_deref());
1231
1232 SettingItem {
1233 path: schema.path.clone(),
1234 name: schema.name.clone(),
1235 description: cleaned_description,
1236 control,
1237 default: schema.default.clone(),
1238 modified,
1239 layer_source,
1240 read_only: schema.read_only,
1241 is_auto_managed,
1242 nullable: schema.nullable,
1243 is_null,
1244 section: schema.section.clone(),
1245 is_section_start: false, style: ItemBoxStyle::default(),
1247 dual_list_sibling: schema.dual_list_sibling.clone(),
1248 }
1249}
1250
1251pub fn build_item_from_value(
1253 schema: &SettingSchema,
1254 current_value: Option<&serde_json::Value>,
1255 available_status_bar_tokens: &HashMap<String, String>,
1256) -> SettingItem {
1257 let control = match &schema.setting_type {
1259 SettingType::Boolean => {
1260 let checked = current_value
1261 .and_then(|v| v.as_bool())
1262 .or_else(|| schema.default.as_ref().and_then(|d| d.as_bool()))
1263 .unwrap_or(false);
1264 let inherited = schema.nullable
1268 && current_value
1269 .map(|v| v.is_null())
1270 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
1271 SettingControl::Toggle(
1272 ToggleState::new(checked, &schema.name).with_inherited(inherited),
1273 )
1274 }
1275
1276 SettingType::Integer { minimum, maximum } => {
1277 let value = current_value
1278 .and_then(|v| v.as_i64())
1279 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
1280 .unwrap_or(0);
1281
1282 let mut state = NumberInputState::new(value, &schema.name);
1283 if let Some(min) = minimum {
1284 state = state.with_min(*min);
1285 }
1286 if let Some(max) = maximum {
1287 state = state.with_max(*max);
1288 }
1289 SettingControl::Number(state)
1290 }
1291
1292 SettingType::Number { minimum, maximum } => {
1293 let value = current_value
1294 .and_then(|v| v.as_f64())
1295 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
1296 .unwrap_or(0.0);
1297
1298 let int_value = (value * 100.0).round() as i64;
1299 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1300 if let Some(min) = minimum {
1301 state = state.with_min((*min * 100.0) as i64);
1302 }
1303 if let Some(max) = maximum {
1304 state = state.with_max((*max * 100.0) as i64);
1305 }
1306 SettingControl::Number(state)
1307 }
1308
1309 SettingType::String => {
1310 let value = current_value
1311 .and_then(|v| v.as_str())
1312 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1313 .unwrap_or("");
1314
1315 let state = TextInputState::new(&schema.name).with_value(value);
1316 SettingControl::Text(state)
1317 }
1318
1319 SettingType::Enum { options } => {
1320 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1322 "" } else {
1324 current_value
1325 .and_then(|v| v.as_str())
1326 .or_else(|| {
1327 let default = schema.default.as_ref()?;
1328 if default.is_null() {
1329 Some("")
1330 } else {
1331 default.as_str()
1332 }
1333 })
1334 .unwrap_or("")
1335 };
1336
1337 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1338 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1339 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1340 let state = DropdownState::with_values(display_names, values, &schema.name)
1341 .with_selected(selected);
1342 SettingControl::Dropdown(state)
1343 }
1344
1345 SettingType::DualList { options, .. } => {
1346 SettingControl::DualList(build_dual_list_state(
1348 schema,
1349 options,
1350 current_value,
1351 vec![],
1352 available_status_bar_tokens,
1353 ))
1354 }
1355
1356 SettingType::StringArray => {
1357 let items: Vec<String> = current_value
1358 .and_then(|v| v.as_array())
1359 .map(|arr| {
1360 arr.iter()
1361 .filter_map(|v| v.as_str().map(String::from))
1362 .collect()
1363 })
1364 .or_else(|| {
1365 schema.default.as_ref().and_then(|d| {
1366 d.as_array().map(|arr| {
1367 arr.iter()
1368 .filter_map(|v| v.as_str().map(String::from))
1369 .collect()
1370 })
1371 })
1372 })
1373 .unwrap_or_default();
1374
1375 let state = TextListState::new(&schema.name).with_items(items);
1376 SettingControl::TextList(state)
1377 }
1378
1379 SettingType::IntegerArray => {
1380 let items: Vec<String> = current_value
1381 .and_then(|v| v.as_array())
1382 .map(|arr| {
1383 arr.iter()
1384 .filter_map(|v| {
1385 v.as_i64()
1386 .map(|n| n.to_string())
1387 .or_else(|| v.as_u64().map(|n| n.to_string()))
1388 .or_else(|| v.as_f64().map(|n| n.to_string()))
1389 })
1390 .collect()
1391 })
1392 .or_else(|| {
1393 schema.default.as_ref().and_then(|d| {
1394 d.as_array().map(|arr| {
1395 arr.iter()
1396 .filter_map(|v| {
1397 v.as_i64()
1398 .map(|n| n.to_string())
1399 .or_else(|| v.as_u64().map(|n| n.to_string()))
1400 .or_else(|| v.as_f64().map(|n| n.to_string()))
1401 })
1402 .collect()
1403 })
1404 })
1405 })
1406 .unwrap_or_default();
1407
1408 let state = TextListState::new(&schema.name)
1409 .with_items(items)
1410 .with_integer_mode();
1411 SettingControl::TextList(state)
1412 }
1413
1414 SettingType::Object { .. } => {
1415 json_control(&schema.name, current_value, schema.default.as_ref())
1416 }
1417
1418 SettingType::Map {
1419 value_schema,
1420 display_field,
1421 no_add,
1422 } => {
1423 let map_value = current_value
1424 .cloned()
1425 .or_else(|| schema.default.clone())
1426 .unwrap_or_else(|| serde_json::json!({}));
1427
1428 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1429 state = state.with_value_schema((**value_schema).clone());
1430 if let Some(field) = display_field {
1431 state = state.with_display_field(field.clone());
1432 }
1433 if *no_add {
1434 state = state.with_no_add(true);
1435 }
1436 SettingControl::Map(state)
1437 }
1438
1439 SettingType::ObjectArray {
1440 item_schema,
1441 display_field,
1442 } => {
1443 let array_value = current_value
1444 .cloned()
1445 .or_else(|| schema.default.clone())
1446 .unwrap_or_else(|| serde_json::json!([]));
1447
1448 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1449 state = state.with_item_schema((**item_schema).clone());
1450 if let Some(field) = display_field {
1451 state = state.with_display_field(field.clone());
1452 }
1453 SettingControl::ObjectArray(state)
1454 }
1455
1456 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1457 };
1458
1459 let modified = match (¤t_value, &schema.default) {
1462 (Some(current), Some(default)) => *current != default,
1463 (Some(_), None) => true,
1464 _ => false,
1465 };
1466
1467 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
1469
1470 let is_null = schema.nullable
1471 && current_value
1472 .map(|v| v.is_null())
1473 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
1474
1475 SettingItem {
1476 path: schema.path.clone(),
1477 name: schema.name.clone(),
1478 description: schema.description.clone(),
1479 control,
1480 default: schema.default.clone(),
1481 modified,
1482 layer_source: ConfigLayer::System,
1484 read_only: schema.read_only,
1485 is_auto_managed,
1486 nullable: schema.nullable,
1487 is_null,
1488 section: schema.section.clone(),
1489 is_section_start: false, style: ItemBoxStyle::default(),
1491 dual_list_sibling: schema.dual_list_sibling.clone(),
1492 }
1493}
1494
1495pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
1497 match control {
1498 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
1499
1500 SettingControl::Number(state) => {
1501 if state.is_percentage {
1502 let float_value = state.value as f64 / 100.0;
1504 serde_json::Number::from_f64(float_value)
1505 .map(serde_json::Value::Number)
1506 .unwrap_or(serde_json::Value::Number(state.value.into()))
1507 } else {
1508 serde_json::Value::Number(state.value.into())
1509 }
1510 }
1511
1512 SettingControl::Dropdown(state) => state
1513 .selected_value()
1514 .map(|s| {
1515 if s.is_empty() {
1516 serde_json::Value::Null
1518 } else {
1519 serde_json::Value::String(s.to_string())
1520 }
1521 })
1522 .unwrap_or(serde_json::Value::Null),
1523
1524 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1525
1526 SettingControl::TextList(state) => {
1527 let arr: Vec<serde_json::Value> = state
1528 .items
1529 .iter()
1530 .filter_map(|s| {
1531 if state.is_integer {
1532 s.parse::<i64>()
1533 .ok()
1534 .map(|n| serde_json::Value::Number(n.into()))
1535 } else {
1536 Some(serde_json::Value::String(s.clone()))
1537 }
1538 })
1539 .collect();
1540 serde_json::Value::Array(arr)
1541 }
1542
1543 SettingControl::DualList(state) => {
1544 let arr: Vec<serde_json::Value> = state
1545 .included
1546 .iter()
1547 .map(|s| serde_json::Value::String(s.clone()))
1548 .collect();
1549 serde_json::Value::Array(arr)
1550 }
1551
1552 SettingControl::Map(state) => state.to_value(),
1553
1554 SettingControl::ObjectArray(state) => state.to_value(),
1555
1556 SettingControl::Json(state) => {
1557 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1559 }
1560
1561 SettingControl::Complex { .. } => serde_json::Value::Null,
1562 }
1563}
1564
1565#[cfg(test)]
1566mod tests {
1567 use super::*;
1568
1569 fn sample_config() -> serde_json::Value {
1570 serde_json::json!({
1571 "theme": "monokai",
1572 "check_for_updates": false,
1573 "editor": {
1574 "tab_size": 2,
1575 "line_numbers": true
1576 }
1577 })
1578 }
1579
1580 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1582 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1584 std::sync::LazyLock::new(HashMap::new);
1585 static EMPTY_TOKENS: std::sync::LazyLock<HashMap<String, String>> =
1586 std::sync::LazyLock::new(HashMap::new);
1587 BuildContext {
1588 config_value: config,
1589 layer_sources: &EMPTY_SOURCES,
1590 target_layer: ConfigLayer::User,
1591 available_status_bar_tokens: &EMPTY_TOKENS,
1592 }
1593 }
1594
1595 fn test_context_with_sources<'a>(
1597 config: &'a serde_json::Value,
1598 layer_sources: &'a HashMap<String, ConfigLayer>,
1599 target_layer: ConfigLayer,
1600 ) -> BuildContext<'a> {
1601 static EMPTY_TOKENS: std::sync::LazyLock<HashMap<String, String>> =
1602 std::sync::LazyLock::new(HashMap::new);
1603 BuildContext {
1604 config_value: config,
1605 layer_sources,
1606 target_layer,
1607 available_status_bar_tokens: &EMPTY_TOKENS,
1608 }
1609 }
1610
1611 #[test]
1612 fn test_build_toggle_item() {
1613 let schema = SettingSchema {
1614 path: "/check_for_updates".to_string(),
1615 name: "Check For Updates".to_string(),
1616 description: Some("Check for updates".to_string()),
1617 setting_type: SettingType::Boolean,
1618 default: Some(serde_json::Value::Bool(true)),
1619 read_only: false,
1620 section: None,
1621 order: None,
1622 nullable: false,
1623 enum_from: None,
1624 dual_list_sibling: None,
1625 dynamically_extendable_status_bar_elements: false,
1626 };
1627
1628 let config = sample_config();
1629 let ctx = test_context(&config);
1630 let item = build_item(&schema, &ctx);
1631
1632 assert_eq!(item.path, "/check_for_updates");
1633 assert!(!item.modified);
1636 assert_eq!(item.layer_source, ConfigLayer::System);
1637
1638 if let SettingControl::Toggle(state) = &item.control {
1639 assert!(!state.checked); } else {
1641 panic!("Expected toggle control");
1642 }
1643 }
1644
1645 #[test]
1646 fn test_build_toggle_item_modified_in_user_layer() {
1647 let schema = SettingSchema {
1648 path: "/check_for_updates".to_string(),
1649 name: "Check For Updates".to_string(),
1650 description: Some("Check for updates".to_string()),
1651 setting_type: SettingType::Boolean,
1652 default: Some(serde_json::Value::Bool(true)),
1653 read_only: false,
1654 section: None,
1655 order: None,
1656 nullable: false,
1657 enum_from: None,
1658 dual_list_sibling: None,
1659 dynamically_extendable_status_bar_elements: false,
1660 };
1661
1662 let config = sample_config();
1663 let mut layer_sources = HashMap::new();
1664 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1665 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1666 let item = build_item(&schema, &ctx);
1667
1668 assert!(item.modified);
1671 assert_eq!(item.layer_source, ConfigLayer::User);
1672 }
1673
1674 #[test]
1675 fn test_build_number_item() {
1676 let schema = SettingSchema {
1677 path: "/editor/tab_size".to_string(),
1678 name: "Tab Size".to_string(),
1679 description: None,
1680 setting_type: SettingType::Integer {
1681 minimum: Some(1),
1682 maximum: Some(16),
1683 },
1684 default: Some(serde_json::Value::Number(4.into())),
1685 read_only: false,
1686 section: None,
1687 order: None,
1688 nullable: false,
1689 enum_from: None,
1690 dual_list_sibling: None,
1691 dynamically_extendable_status_bar_elements: false,
1692 };
1693
1694 let config = sample_config();
1695 let ctx = test_context(&config);
1696 let item = build_item(&schema, &ctx);
1697
1698 assert!(!item.modified);
1700
1701 if let SettingControl::Number(state) = &item.control {
1702 assert_eq!(state.value, 2);
1703 assert_eq!(state.min, Some(1));
1704 assert_eq!(state.max, Some(16));
1705 } else {
1706 panic!("Expected number control");
1707 }
1708 }
1709
1710 #[test]
1711 fn test_build_text_item() {
1712 let schema = SettingSchema {
1713 path: "/theme".to_string(),
1714 name: "Theme".to_string(),
1715 description: None,
1716 setting_type: SettingType::String,
1717 default: Some(serde_json::Value::String("high-contrast".to_string())),
1718 read_only: false,
1719 section: None,
1720 order: None,
1721 nullable: false,
1722 enum_from: None,
1723 dual_list_sibling: None,
1724 dynamically_extendable_status_bar_elements: false,
1725 };
1726
1727 let config = sample_config();
1728 let ctx = test_context(&config);
1729 let item = build_item(&schema, &ctx);
1730
1731 assert!(!item.modified);
1733
1734 if let SettingControl::Text(state) = &item.control {
1735 assert_eq!(state.value, "monokai");
1736 } else {
1737 panic!("Expected text control");
1738 }
1739 }
1740
1741 #[test]
1742 fn test_clean_description_keeps_full_desc_with_new_info() {
1743 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1745 assert!(result.is_some());
1746 let cleaned = result.unwrap();
1747 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1750 assert!(cleaned.contains("character"));
1751 }
1752
1753 #[test]
1754 fn test_clean_description_keeps_extra_info() {
1755 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1757 assert!(result.is_some());
1758 let cleaned = result.unwrap();
1759 assert!(cleaned.contains("gutter"));
1760 }
1761
1762 #[test]
1763 fn test_clean_description_returns_none_for_pure_redundancy() {
1764 let result = clean_description("Theme", Some("Theme"));
1766 assert!(result.is_none());
1767
1768 let result = clean_description("Theme", Some("The theme to use"));
1770 assert!(result.is_none());
1771 }
1772
1773 #[test]
1774 fn test_clean_description_returns_none_for_empty() {
1775 let result = clean_description("Theme", Some(""));
1776 assert!(result.is_none());
1777
1778 let result = clean_description("Theme", None);
1779 assert!(result.is_none());
1780 }
1781
1782 #[test]
1783 fn test_control_to_value() {
1784 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1785 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1786
1787 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1788 assert_eq!(control_to_value(&number), serde_json::json!(42));
1789
1790 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1791 assert_eq!(
1792 control_to_value(&text),
1793 serde_json::Value::String("hello".to_string())
1794 );
1795 }
1796}