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 SettingControl::Toggle(ToggleState::new(checked, &schema.name))
1265 }
1266
1267 SettingType::Integer { minimum, maximum } => {
1268 let value = current_value
1269 .and_then(|v| v.as_i64())
1270 .or_else(|| schema.default.as_ref().and_then(|d| d.as_i64()))
1271 .unwrap_or(0);
1272
1273 let mut state = NumberInputState::new(value, &schema.name);
1274 if let Some(min) = minimum {
1275 state = state.with_min(*min);
1276 }
1277 if let Some(max) = maximum {
1278 state = state.with_max(*max);
1279 }
1280 SettingControl::Number(state)
1281 }
1282
1283 SettingType::Number { minimum, maximum } => {
1284 let value = current_value
1285 .and_then(|v| v.as_f64())
1286 .or_else(|| schema.default.as_ref().and_then(|d| d.as_f64()))
1287 .unwrap_or(0.0);
1288
1289 let int_value = (value * 100.0).round() as i64;
1290 let mut state = NumberInputState::new(int_value, &schema.name).with_percentage();
1291 if let Some(min) = minimum {
1292 state = state.with_min((*min * 100.0) as i64);
1293 }
1294 if let Some(max) = maximum {
1295 state = state.with_max((*max * 100.0) as i64);
1296 }
1297 SettingControl::Number(state)
1298 }
1299
1300 SettingType::String => {
1301 let value = current_value
1302 .and_then(|v| v.as_str())
1303 .or_else(|| schema.default.as_ref().and_then(|d| d.as_str()))
1304 .unwrap_or("");
1305
1306 let state = TextInputState::new(&schema.name).with_value(value);
1307 SettingControl::Text(state)
1308 }
1309
1310 SettingType::Enum { options } => {
1311 let current = if current_value.map(|v| v.is_null()).unwrap_or(false) {
1313 "" } else {
1315 current_value
1316 .and_then(|v| v.as_str())
1317 .or_else(|| {
1318 let default = schema.default.as_ref()?;
1319 if default.is_null() {
1320 Some("")
1321 } else {
1322 default.as_str()
1323 }
1324 })
1325 .unwrap_or("")
1326 };
1327
1328 let display_names: Vec<String> = options.iter().map(|o| o.name.clone()).collect();
1329 let values: Vec<String> = options.iter().map(|o| o.value.clone()).collect();
1330 let selected = values.iter().position(|v| v == current).unwrap_or(0);
1331 let state = DropdownState::with_values(display_names, values, &schema.name)
1332 .with_selected(selected);
1333 SettingControl::Dropdown(state)
1334 }
1335
1336 SettingType::DualList { options, .. } => {
1337 SettingControl::DualList(build_dual_list_state(
1339 schema,
1340 options,
1341 current_value,
1342 vec![],
1343 available_status_bar_tokens,
1344 ))
1345 }
1346
1347 SettingType::StringArray => {
1348 let items: Vec<String> = current_value
1349 .and_then(|v| v.as_array())
1350 .map(|arr| {
1351 arr.iter()
1352 .filter_map(|v| v.as_str().map(String::from))
1353 .collect()
1354 })
1355 .or_else(|| {
1356 schema.default.as_ref().and_then(|d| {
1357 d.as_array().map(|arr| {
1358 arr.iter()
1359 .filter_map(|v| v.as_str().map(String::from))
1360 .collect()
1361 })
1362 })
1363 })
1364 .unwrap_or_default();
1365
1366 let state = TextListState::new(&schema.name).with_items(items);
1367 SettingControl::TextList(state)
1368 }
1369
1370 SettingType::IntegerArray => {
1371 let items: Vec<String> = current_value
1372 .and_then(|v| v.as_array())
1373 .map(|arr| {
1374 arr.iter()
1375 .filter_map(|v| {
1376 v.as_i64()
1377 .map(|n| n.to_string())
1378 .or_else(|| v.as_u64().map(|n| n.to_string()))
1379 .or_else(|| v.as_f64().map(|n| n.to_string()))
1380 })
1381 .collect()
1382 })
1383 .or_else(|| {
1384 schema.default.as_ref().and_then(|d| {
1385 d.as_array().map(|arr| {
1386 arr.iter()
1387 .filter_map(|v| {
1388 v.as_i64()
1389 .map(|n| n.to_string())
1390 .or_else(|| v.as_u64().map(|n| n.to_string()))
1391 .or_else(|| v.as_f64().map(|n| n.to_string()))
1392 })
1393 .collect()
1394 })
1395 })
1396 })
1397 .unwrap_or_default();
1398
1399 let state = TextListState::new(&schema.name)
1400 .with_items(items)
1401 .with_integer_mode();
1402 SettingControl::TextList(state)
1403 }
1404
1405 SettingType::Object { .. } => {
1406 json_control(&schema.name, current_value, schema.default.as_ref())
1407 }
1408
1409 SettingType::Map {
1410 value_schema,
1411 display_field,
1412 no_add,
1413 } => {
1414 let map_value = current_value
1415 .cloned()
1416 .or_else(|| schema.default.clone())
1417 .unwrap_or_else(|| serde_json::json!({}));
1418
1419 let mut state = MapState::new(&schema.name).with_entries(&map_value);
1420 state = state.with_value_schema((**value_schema).clone());
1421 if let Some(field) = display_field {
1422 state = state.with_display_field(field.clone());
1423 }
1424 if *no_add {
1425 state = state.with_no_add(true);
1426 }
1427 SettingControl::Map(state)
1428 }
1429
1430 SettingType::ObjectArray {
1431 item_schema,
1432 display_field,
1433 } => {
1434 let array_value = current_value
1435 .cloned()
1436 .or_else(|| schema.default.clone())
1437 .unwrap_or_else(|| serde_json::json!([]));
1438
1439 let mut state = KeybindingListState::new(&schema.name).with_bindings(&array_value);
1440 state = state.with_item_schema((**item_schema).clone());
1441 if let Some(field) = display_field {
1442 state = state.with_display_field(field.clone());
1443 }
1444 SettingControl::ObjectArray(state)
1445 }
1446
1447 SettingType::Complex => json_control(&schema.name, current_value, schema.default.as_ref()),
1448 };
1449
1450 let modified = match (¤t_value, &schema.default) {
1453 (Some(current), Some(default)) => *current != default,
1454 (Some(_), None) => true,
1455 _ => false,
1456 };
1457
1458 let is_auto_managed = matches!(&schema.setting_type, SettingType::Map { no_add: true, .. });
1460
1461 let is_null = schema.nullable
1462 && current_value
1463 .map(|v| v.is_null())
1464 .unwrap_or(schema.default.as_ref().map(|d| d.is_null()).unwrap_or(true));
1465
1466 SettingItem {
1467 path: schema.path.clone(),
1468 name: schema.name.clone(),
1469 description: schema.description.clone(),
1470 control,
1471 default: schema.default.clone(),
1472 modified,
1473 layer_source: ConfigLayer::System,
1475 read_only: schema.read_only,
1476 is_auto_managed,
1477 nullable: schema.nullable,
1478 is_null,
1479 section: schema.section.clone(),
1480 is_section_start: false, style: ItemBoxStyle::default(),
1482 dual_list_sibling: schema.dual_list_sibling.clone(),
1483 }
1484}
1485
1486pub fn control_to_value(control: &SettingControl) -> serde_json::Value {
1488 match control {
1489 SettingControl::Toggle(state) => serde_json::Value::Bool(state.checked),
1490
1491 SettingControl::Number(state) => {
1492 if state.is_percentage {
1493 let float_value = state.value as f64 / 100.0;
1495 serde_json::Number::from_f64(float_value)
1496 .map(serde_json::Value::Number)
1497 .unwrap_or(serde_json::Value::Number(state.value.into()))
1498 } else {
1499 serde_json::Value::Number(state.value.into())
1500 }
1501 }
1502
1503 SettingControl::Dropdown(state) => state
1504 .selected_value()
1505 .map(|s| {
1506 if s.is_empty() {
1507 serde_json::Value::Null
1509 } else {
1510 serde_json::Value::String(s.to_string())
1511 }
1512 })
1513 .unwrap_or(serde_json::Value::Null),
1514
1515 SettingControl::Text(state) => serde_json::Value::String(state.value.clone()),
1516
1517 SettingControl::TextList(state) => {
1518 let arr: Vec<serde_json::Value> = state
1519 .items
1520 .iter()
1521 .filter_map(|s| {
1522 if state.is_integer {
1523 s.parse::<i64>()
1524 .ok()
1525 .map(|n| serde_json::Value::Number(n.into()))
1526 } else {
1527 Some(serde_json::Value::String(s.clone()))
1528 }
1529 })
1530 .collect();
1531 serde_json::Value::Array(arr)
1532 }
1533
1534 SettingControl::DualList(state) => {
1535 let arr: Vec<serde_json::Value> = state
1536 .included
1537 .iter()
1538 .map(|s| serde_json::Value::String(s.clone()))
1539 .collect();
1540 serde_json::Value::Array(arr)
1541 }
1542
1543 SettingControl::Map(state) => state.to_value(),
1544
1545 SettingControl::ObjectArray(state) => state.to_value(),
1546
1547 SettingControl::Json(state) => {
1548 serde_json::from_str(&state.value()).unwrap_or(serde_json::Value::Null)
1550 }
1551
1552 SettingControl::Complex { .. } => serde_json::Value::Null,
1553 }
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558 use super::*;
1559
1560 fn sample_config() -> serde_json::Value {
1561 serde_json::json!({
1562 "theme": "monokai",
1563 "check_for_updates": false,
1564 "editor": {
1565 "tab_size": 2,
1566 "line_numbers": true
1567 }
1568 })
1569 }
1570
1571 fn test_context(config: &serde_json::Value) -> BuildContext<'_> {
1573 static EMPTY_SOURCES: std::sync::LazyLock<HashMap<String, ConfigLayer>> =
1575 std::sync::LazyLock::new(HashMap::new);
1576 static EMPTY_TOKENS: std::sync::LazyLock<HashMap<String, String>> =
1577 std::sync::LazyLock::new(HashMap::new);
1578 BuildContext {
1579 config_value: config,
1580 layer_sources: &EMPTY_SOURCES,
1581 target_layer: ConfigLayer::User,
1582 available_status_bar_tokens: &EMPTY_TOKENS,
1583 }
1584 }
1585
1586 fn test_context_with_sources<'a>(
1588 config: &'a serde_json::Value,
1589 layer_sources: &'a HashMap<String, ConfigLayer>,
1590 target_layer: ConfigLayer,
1591 ) -> BuildContext<'a> {
1592 static EMPTY_TOKENS: std::sync::LazyLock<HashMap<String, String>> =
1593 std::sync::LazyLock::new(HashMap::new);
1594 BuildContext {
1595 config_value: config,
1596 layer_sources,
1597 target_layer,
1598 available_status_bar_tokens: &EMPTY_TOKENS,
1599 }
1600 }
1601
1602 #[test]
1603 fn test_build_toggle_item() {
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 ctx = test_context(&config);
1621 let item = build_item(&schema, &ctx);
1622
1623 assert_eq!(item.path, "/check_for_updates");
1624 assert!(!item.modified);
1627 assert_eq!(item.layer_source, ConfigLayer::System);
1628
1629 if let SettingControl::Toggle(state) = &item.control {
1630 assert!(!state.checked); } else {
1632 panic!("Expected toggle control");
1633 }
1634 }
1635
1636 #[test]
1637 fn test_build_toggle_item_modified_in_user_layer() {
1638 let schema = SettingSchema {
1639 path: "/check_for_updates".to_string(),
1640 name: "Check For Updates".to_string(),
1641 description: Some("Check for updates".to_string()),
1642 setting_type: SettingType::Boolean,
1643 default: Some(serde_json::Value::Bool(true)),
1644 read_only: false,
1645 section: None,
1646 order: None,
1647 nullable: false,
1648 enum_from: None,
1649 dual_list_sibling: None,
1650 dynamically_extendable_status_bar_elements: false,
1651 };
1652
1653 let config = sample_config();
1654 let mut layer_sources = HashMap::new();
1655 layer_sources.insert("/check_for_updates".to_string(), ConfigLayer::User);
1656 let ctx = test_context_with_sources(&config, &layer_sources, ConfigLayer::User);
1657 let item = build_item(&schema, &ctx);
1658
1659 assert!(item.modified);
1662 assert_eq!(item.layer_source, ConfigLayer::User);
1663 }
1664
1665 #[test]
1666 fn test_build_number_item() {
1667 let schema = SettingSchema {
1668 path: "/editor/tab_size".to_string(),
1669 name: "Tab Size".to_string(),
1670 description: None,
1671 setting_type: SettingType::Integer {
1672 minimum: Some(1),
1673 maximum: Some(16),
1674 },
1675 default: Some(serde_json::Value::Number(4.into())),
1676 read_only: false,
1677 section: None,
1678 order: None,
1679 nullable: false,
1680 enum_from: None,
1681 dual_list_sibling: None,
1682 dynamically_extendable_status_bar_elements: false,
1683 };
1684
1685 let config = sample_config();
1686 let ctx = test_context(&config);
1687 let item = build_item(&schema, &ctx);
1688
1689 assert!(!item.modified);
1691
1692 if let SettingControl::Number(state) = &item.control {
1693 assert_eq!(state.value, 2);
1694 assert_eq!(state.min, Some(1));
1695 assert_eq!(state.max, Some(16));
1696 } else {
1697 panic!("Expected number control");
1698 }
1699 }
1700
1701 #[test]
1702 fn test_build_text_item() {
1703 let schema = SettingSchema {
1704 path: "/theme".to_string(),
1705 name: "Theme".to_string(),
1706 description: None,
1707 setting_type: SettingType::String,
1708 default: Some(serde_json::Value::String("high-contrast".to_string())),
1709 read_only: false,
1710 section: None,
1711 order: None,
1712 nullable: false,
1713 enum_from: None,
1714 dual_list_sibling: None,
1715 dynamically_extendable_status_bar_elements: false,
1716 };
1717
1718 let config = sample_config();
1719 let ctx = test_context(&config);
1720 let item = build_item(&schema, &ctx);
1721
1722 assert!(!item.modified);
1724
1725 if let SettingControl::Text(state) = &item.control {
1726 assert_eq!(state.value, "monokai");
1727 } else {
1728 panic!("Expected text control");
1729 }
1730 }
1731
1732 #[test]
1733 fn test_clean_description_keeps_full_desc_with_new_info() {
1734 let result = clean_description("Tab Size", Some("Number of spaces per tab character"));
1736 assert!(result.is_some());
1737 let cleaned = result.unwrap();
1738 assert!(cleaned.starts_with('N')); assert!(cleaned.contains("spaces"));
1741 assert!(cleaned.contains("character"));
1742 }
1743
1744 #[test]
1745 fn test_clean_description_keeps_extra_info() {
1746 let result = clean_description("Line Numbers", Some("Show line numbers in the gutter"));
1748 assert!(result.is_some());
1749 let cleaned = result.unwrap();
1750 assert!(cleaned.contains("gutter"));
1751 }
1752
1753 #[test]
1754 fn test_clean_description_returns_none_for_pure_redundancy() {
1755 let result = clean_description("Theme", Some("Theme"));
1757 assert!(result.is_none());
1758
1759 let result = clean_description("Theme", Some("The theme to use"));
1761 assert!(result.is_none());
1762 }
1763
1764 #[test]
1765 fn test_clean_description_returns_none_for_empty() {
1766 let result = clean_description("Theme", Some(""));
1767 assert!(result.is_none());
1768
1769 let result = clean_description("Theme", None);
1770 assert!(result.is_none());
1771 }
1772
1773 #[test]
1774 fn test_control_to_value() {
1775 let toggle = SettingControl::Toggle(ToggleState::new(true, "Test"));
1776 assert_eq!(control_to_value(&toggle), serde_json::Value::Bool(true));
1777
1778 let number = SettingControl::Number(NumberInputState::new(42, "Test"));
1779 assert_eq!(control_to_value(&number), serde_json::json!(42));
1780
1781 let text = SettingControl::Text(TextInputState::new("Test").with_value("hello"));
1782 assert_eq!(
1783 control_to_value(&text),
1784 serde_json::Value::String("hello".to_string())
1785 );
1786 }
1787}