返回 CodeWhale
settings_schema.rs
根目录 / crates / config / src / settings_schema.rs
1 //! One declaration per setting: type, default, and an optional `ui` block.
2 //!
3 //! # What this replaces
4 //!
5 //! This table is the single source that the settings surface projects. It
6 //! replaces, and is the reason for deleting, these hand-maintained key tables
7 //! that used to live beside `ConfigView` in `crates/tui/src/tui/views/mod.rs`:
8 //!
9 //! - the `Vec<ConfigRow>` literal's `section:` field (row → heading),
10 //! and `ConfigCategory::for_section` / `ConfigRowFacts::category`
11 //! (row → rail tab),
12 //! - `config_boolean_key` and `config_integer_key` (key → editor kind),
13 //! - `config_choice_values` (key → enum values),
14 //! - `config_choice_label` / `config_choice_detail` (value → label/detail),
15 //! - `config_label_message` (key → label string),
16 //! - `config_hint_for_key` (key → description string).
17 //!
18 //! # The rule
19 //!
20 //! A setting is declared once, here. `ui: Some(..)` puts it on the settings
21 //! screen; `ui: None` keeps the declaration (type, default, and the fact that
22 //! the key is known) without giving it a row. Visibility is a property of the
23 //! declaration, not of the renderer.
24 //!
25 //! `label`, `description`, and the per-value `label`/`description` are
26 //! *message keys*, not prose: the localization pack owns the text in fifteen
27 //! languages, this table owns which string a setting shows. An empty key means
28 //! "no string" — the surface humanizes the setting key instead.
29 //!
30 //! `tab` and `group` are ids the settings screen resolves to its rail
31 //! categories and section headings. Declaration order is render order:
32 //! distinct tabs appear in the order they are first declared, groups in the
33 //! order they are first declared within a tab, and rows in declaration order
34 //! within a group.
35 //!
36 //! Two settings take their values from a runtime registry rather than this
37 //! table (`theme` from the shipped palettes, `locale` from the shipped packs).
38 //! They are declared `String`; the surface supplies the live value list.
39 //! `reasoning_effort` declares the canonical nine-spelling effort vocabulary
40 //! here so `/config` and `/effort` cannot disagree; the live settings screen
41 //! still narrows that list to the active route's rungs.
42
43 /// Fresh terminal installs, theme resets and the settings schema share this default.
44 pub const DEFAULT_TUI_THEME: &str = "underwater";
45
46 /// One selectable value of a [`SettingKind::Enum`] (or a boolean override).
47 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48 pub struct SettingOption {
49 pub value: &'static str,
50 /// Message key for the value's label; empty means "show the raw value".
51 pub label: &'static str,
52 /// Message key for the value's one-line detail; empty means none.
53 pub description: &'static str,
54 }
55
56 impl SettingOption {
57 const fn new(value: &'static str, label: &'static str, description: &'static str) -> Self {
58 Self {
59 value,
60 label,
61 description,
62 }
63 }
64 }
65
66 /// The value type of a setting, and for closed value sets, the values.
67 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
68 pub enum SettingKind {
69 /// `true` / `false`. An empty option slice means the surface's default
70 /// on/off labels; a non-empty one overrides them per value.
71 Bool(&'static [SettingOption]),
72 Int,
73 /// A fractional number such as a percent threshold. Values are served and
74 /// accepted in plain decimal form; bounds live in the write validator.
75 Float,
76 Enum(&'static [SettingOption]),
77 String,
78 }
79
80 /// What a declared settings row is. `Setting` is a writable preference;
81 /// `Action` opens another surface (the provider/model pickers, module
82 /// links); `Diagnostic` is a read-only receipt or managed-policy fact;
83 /// `Session` is editable for the running session but does not persist
84 /// through the config store.
85 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
86 pub enum SettingRowKind {
87 Setting,
88 Action,
89 Diagnostic,
90 Session,
91 }
92
93 /// Where a setting appears, and what it says. Absent ⇒ no row.
94 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
95 pub struct SettingUi {
96 pub tab: &'static str,
97 pub group: &'static str,
98 /// Message key for the row label; empty ⇒ humanize the setting key.
99 pub label: &'static str,
100 /// Message key for the row's description sentence.
101 pub description: &'static str,
102 /// Whether the row is a writable preference, a link to another surface,
103 /// a read-only receipt, or a session-scoped override. Surfaces that
104 /// cannot open the target or scope the write render non-`Setting` rows
105 /// read-only instead of guessing.
106 pub row: SettingRowKind,
107 }
108
109 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
110 pub struct SettingDef {
111 pub key: &'static str,
112 pub kind: SettingKind,
113 /// The value in force when nothing is configured, as it is written to
114 /// disk. Empty for session-owned rows, actions, and receipts.
115 pub default: &'static str,
116 pub ui: Option<SettingUi>,
117 }
118
119 impl SettingDef {
120 /// The closed value set, when this setting has one. `None` means the
121 /// surface supplies values (free text, an integer, or a runtime registry).
122 pub fn values(&self) -> Option<Vec<&'static str>> {
123 match self.kind {
124 SettingKind::Bool(_) => Some(vec!["false", "true"]),
125 SettingKind::Enum(options) => Some(options.iter().map(|o| o.value).collect()),
126 SettingKind::Int | SettingKind::String | SettingKind::Float => None,
127 }
128 }
129
130 /// Per-value label/description metadata for `value`, when declared.
131 pub fn option(&self, value: &str) -> Option<&'static SettingOption> {
132 let options = match self.kind {
133 SettingKind::Bool(options) | SettingKind::Enum(options) => options,
134 SettingKind::Int | SettingKind::String | SettingKind::Float => return None,
135 };
136 options.iter().find(|option| option.value == value)
137 }
138
139 pub fn is_bool(&self) -> bool {
140 matches!(self.kind, SettingKind::Bool(_))
141 }
142
143 pub fn is_int(&self) -> bool {
144 matches!(self.kind, SettingKind::Int)
145 }
146
147 pub fn is_float(&self) -> bool {
148 matches!(self.kind, SettingKind::Float)
149 }
150 }
151
152 const NOTIFICATION_SOUNDS: &[SettingOption] = &[
153 SettingOption::new("legacy", "ConfigChoiceNotificationLegacy", ""),
154 SettingOption::new("off", "ConfigValueOff", ""),
155 SettingOption::new("whale", "ConfigChoiceNotificationWhale", ""),
156 SettingOption::new("bell", "ConfigChoiceNotificationBell", ""),
157 SettingOption::new("beep", "ConfigChoiceNotificationBell", ""),
158 SettingOption::new("file", "ConfigChoiceNotificationFile", ""),
159 ];
160
161 const NOTIFICATION_COMPLETION_SOUNDS: &[SettingOption] = &[
162 SettingOption::new("off", "ConfigValueOff", ""),
163 SettingOption::new("whale", "ConfigChoiceNotificationWhale", ""),
164 SettingOption::new("bell", "ConfigChoiceNotificationBell", ""),
165 SettingOption::new("beep", "ConfigChoiceNotificationBell", ""),
166 SettingOption::new("file", "ConfigChoiceNotificationFile", ""),
167 ];
168
169 const NOTIFICATION_CONDITIONS: &[SettingOption] = &[
170 SettingOption::new("always", "ConfigChoiceNotificationAlways", ""),
171 SettingOption::new("unfocused", "ConfigChoiceNotificationUnfocused", ""),
172 SettingOption::new("never", "ConfigChoiceNotificationNever", ""),
173 ];
174
175 const NOTIFICATION_METHODS: &[SettingOption] = &[
176 SettingOption::new("auto", "", ""),
177 SettingOption::new("off", "", ""),
178 SettingOption::new("osc9", "", ""),
179 SettingOption::new("bel", "", ""),
180 SettingOption::new("kitty", "", ""),
181 SettingOption::new("ghostty", "", ""),
182 ];
183
184 const NOTIFICATION_SUBAGENTS: &[SettingOption] = &[
185 SettingOption::new("off", "", ""),
186 SettingOption::new("final-only", "", ""),
187 SettingOption::new("always", "", ""),
188 ];
189
190 const ON_OFF: &[SettingOption] = &[];
191
192 const TELEMETRY: &[SettingOption] = &[
193 SettingOption::new("false", "ConfigValueTelemetryOff", ""),
194 SettingOption::new("true", "ConfigValueTelemetryOn", ""),
195 ];
196
197 const LOW_MOTION: &[SettingOption] = &[
198 SettingOption::new("false", "ConfigValueOff", "ConfigChoiceDetailLowMotionOff"),
199 SettingOption::new("true", "ConfigValueOn", "ConfigChoiceDetailLowMotionOn"),
200 ];
201
202 const FANCY_ANIMATIONS: &[SettingOption] = &[
203 SettingOption::new("false", "ConfigValueOff", "ConfigChoiceDetailFancyOff"),
204 SettingOption::new("true", "ConfigValueOn", "ConfigChoiceDetailFancyOn"),
205 ];
206
207 const SHOW_THINKING: &[SettingOption] = &[
208 SettingOption::new(
209 "false",
210 "ConfigValueOff",
211 "ConfigChoiceDetailShowThinkingOff",
212 ),
213 SettingOption::new("true", "ConfigValueOn", "ConfigChoiceDetailShowThinkingOn"),
214 ];
215
216 const THINKING_HIGHLIGHT: &[SettingOption] = &[
217 SettingOption::new(
218 "false",
219 "ConfigValueOff",
220 "ConfigChoiceDetailThinkingHighlightOff",
221 ),
222 SettingOption::new(
223 "true",
224 "ConfigValueOn",
225 "ConfigChoiceDetailThinkingHighlightOn",
226 ),
227 ];
228
229 const PERMISSION_POSTURE: &[SettingOption] = &[
230 SettingOption::new("ask", "ConfigChoiceAsk", "ConfigChoiceDetailAsk"),
231 SettingOption::new(
232 "auto-review",
233 "ConfigChoiceAutoReview",
234 "ConfigChoiceDetailAutoReview",
235 ),
236 SettingOption::new(
237 "full-access",
238 "ConfigChoiceFullAccess",
239 "ConfigChoiceDetailFullAccess",
240 ),
241 ];
242
243 const APPROVAL_MODE: &[SettingOption] = &[
244 SettingOption::new("ask", "ConfigChoiceAsk", "ConfigChoiceDetailAsk"),
245 SettingOption::new(
246 "auto-review",
247 "ConfigChoiceAutoReview",
248 "ConfigChoiceDetailAutoReview",
249 ),
250 SettingOption::new(
251 "full-access",
252 "ConfigChoiceFullAccess",
253 "ConfigChoiceDetailFullAccess",
254 ),
255 ];
256
257 const APPROVAL_POLICY: &[SettingOption] = &[
258 SettingOption::new(
259 "use-tui-default",
260 "ConfigChoiceUseTuiDefault",
261 "ConfigChoiceDetailUseTuiDefault",
262 ),
263 SettingOption::new("ask", "ConfigChoiceAsk", "ConfigChoiceDetailAsk"),
264 SettingOption::new(
265 "auto-review",
266 "ConfigChoiceAutoReview",
267 "ConfigChoiceDetailAutoReview",
268 ),
269 SettingOption::new(
270 "full-access",
271 "ConfigChoiceFullAccess",
272 "ConfigChoiceDetailFullAccess",
273 ),
274 // `never` is a managed-policy value only: it is accepted from config.toml and
275 // shown read-only, never offered by the editor.
276 ];
277
278 const DEFAULT_MODE: &[SettingOption] = &[
279 SettingOption::new(
280 "agent",
281 "ConfigChoiceModeAct",
282 "ConfigChoiceDetailModeAgent",
283 ),
284 SettingOption::new("plan", "ConfigChoiceModePlan", "ConfigChoiceDetailModePlan"),
285 SettingOption::new(
286 "operate",
287 "ConfigChoiceModeOperate",
288 "ConfigChoiceDetailModeOperate",
289 ),
290 ];
291
292 const FOCUS_TEXTURE: &[SettingOption] = &[
293 SettingOption::new("off", "ConfigValueOff", ""),
294 SettingOption::new("scrim", "", ""),
295 SettingOption::new("grain", "", ""),
296 ];
297
298 const INLINE_DIFFS: &[SettingOption] = &[
299 SettingOption::new("full", "ConfigChoiceDiffFull", ""),
300 SettingOption::new("summary", "ConfigChoiceDiffSummary", ""),
301 SettingOption::new("off", "ConfigValueOff", ""),
302 ];
303
304 const STATUS_INDICATOR: &[SettingOption] = &[
305 // `whale` is retired: load migrates whale | 🐳 | 🐋 to the typographic
306 // mark, so the editor no longer offers it.
307 SettingOption::new("cw", "ConfigChoiceStatusCw", ""),
308 SettingOption::new("dots", "ConfigChoiceStatusDots", ""),
309 SettingOption::new("off", "ConfigValueOff", ""),
310 ];
311
312 const SYNCHRONIZED_OUTPUT: &[SettingOption] = &[
313 SettingOption::new("auto", "", ""),
314 SettingOption::new("on", "", ""),
315 SettingOption::new("off", "", ""),
316 ];
317
318 const COST_CURRENCY: &[SettingOption] = &[
319 SettingOption::new("usd", "", ""),
320 SettingOption::new("cny", "", ""),
321 ];
322
323 /// The canonical `ReasoningEffort::as_setting` spellings, in the order
324 /// `auto, off, minimal, low, medium, high, xhigh, ultra, max`.
325 ///
326 /// This is the same vocabulary `codewhale_tui::reasoning_preference::
327 /// ReasoningEffort::parse_strict` accepts (`/effort`), so the settings schema
328 /// and the command cannot drift. Labels stay empty: an undeclared label means
329 /// "show the raw value", and a capitalized literal would also have to be
330 /// localized in every shipped locale to satisfy the schema message-key check.
331 const REASONING_EFFORT: &[SettingOption] = &[
332 SettingOption::new("auto", "", ""),
333 SettingOption::new("off", "", ""),
334 SettingOption::new("minimal", "", ""),
335 SettingOption::new("low", "", ""),
336 SettingOption::new("medium", "", ""),
337 SettingOption::new("high", "", ""),
338 SettingOption::new("xhigh", "", ""),
339 SettingOption::new("ultra", "", ""),
340 SettingOption::new("max", "", ""),
341 ];
342
343 const DENSITY: &[SettingOption] = &[
344 SettingOption::new("compact", "", ""),
345 SettingOption::new("comfortable", "", ""),
346 SettingOption::new("spacious", "", ""),
347 ];
348
349 const TOOL_COLLAPSE: &[SettingOption] = &[
350 SettingOption::new("compact", "", ""),
351 SettingOption::new("expanded", "", ""),
352 SettingOption::new("calm", "", ""),
353 ];
354
355 const VIM_MODE: &[SettingOption] = &[
356 SettingOption::new("normal", "", ""),
357 SettingOption::new("vim", "", ""),
358 ];
359
360 const MENTION_MENU_BEHAVIOR: &[SettingOption] = &[
361 SettingOption::new("fuzzy", "", ""),
362 SettingOption::new("browser", "", ""),
363 ];
364
365 const WORK_SURFACE_PLACEMENT: &[SettingOption] = &[
366 SettingOption::new(
367 "top",
368 "ConfigChoicePlacementTop",
369 "ConfigChoiceDetailPlacementTop",
370 ),
371 SettingOption::new(
372 "bottom",
373 "ConfigChoicePlacementBottom",
374 "ConfigChoiceDetailPlacementBottom",
375 ),
376 SettingOption::new(
377 "left",
378 "ConfigChoicePlacementLeft",
379 "ConfigChoiceDetailPlacementLeft",
380 ),
381 SettingOption::new(
382 "right",
383 "ConfigChoicePlacementRight",
384 "ConfigChoiceDetailPlacementRight",
385 ),
386 SettingOption::new("off", "ConfigValueOff", "ConfigChoiceDetailPlacementOff"),
387 ];
388
389 const RAIL_PANEL: &[SettingOption] = &[
390 // The dock's own grammar is lowercase nouns, so the panels the classic
391 // sidebar never named ride on their raw value (`RailPanel::title`).
392 SettingOption::new(
393 "tasks",
394 "ConfigChoiceRailTasks",
395 "ConfigChoiceDetailRailTasks",
396 ),
397 SettingOption::new(
398 "agents",
399 "ConfigChoiceRailAgents",
400 "ConfigChoiceDetailRailAgents",
401 ),
402 SettingOption::new("background", "", ""),
403 SettingOption::new("files", "", ""),
404 SettingOption::new("notepad", "", ""),
405 SettingOption::new(
406 "context",
407 "ConfigChoiceRailContext",
408 "ConfigChoiceDetailRailContext",
409 ),
410 SettingOption::new("git", "", ""),
411 SettingOption::new("price", "", ""),
412 ];
413
414 /// Rail tab ids.
415 pub const TAB_APPEARANCE: &str = "appearance";
416 pub const TAB_MODELS: &str = "models";
417 pub const TAB_WORK: &str = "work";
418 pub const TAB_TOOLS: &str = "tools";
419 pub const TAB_TRUST: &str = "trust";
420 pub const TAB_MOTION: &str = "motion";
421 pub const TAB_ADVANCED: &str = "advanced";
422
423 const fn ui(
424 tab: &'static str,
425 group: &'static str,
426 label: &'static str,
427 description: &'static str,
428 ) -> Option<SettingUi> {
429 Some(SettingUi {
430 tab,
431 group,
432 label,
433 description,
434 row: SettingRowKind::Setting,
435 })
436 }
437
438 /// A row that opens another surface rather than editing a value in place.
439 const fn ui_action(
440 tab: &'static str,
441 group: &'static str,
442 label: &'static str,
443 description: &'static str,
444 ) -> Option<SettingUi> {
445 Some(SettingUi {
446 tab,
447 group,
448 label,
449 description,
450 row: SettingRowKind::Action,
451 })
452 }
453
454 /// A read-only receipt row — a managed-policy fact, live route value, or
455 /// descriptive pointer, never a writable control.
456 const fn ui_diagnostic(
457 tab: &'static str,
458 group: &'static str,
459 label: &'static str,
460 description: &'static str,
461 ) -> Option<SettingUi> {
462 Some(SettingUi {
463 tab,
464 group,
465 label,
466 description,
467 row: SettingRowKind::Diagnostic,
468 })
469 }
470
471 /// A row editable for the running session only; it does not persist through
472 /// the config store.
473 const fn ui_session(
474 tab: &'static str,
475 group: &'static str,
476 label: &'static str,
477 description: &'static str,
478 ) -> Option<SettingUi> {
479 Some(SettingUi {
480 tab,
481 group,
482 label,
483 description,
484 row: SettingRowKind::Session,
485 })
486 }
487
488 const fn def(
489 key: &'static str,
490 kind: SettingKind,
491 default: &'static str,
492 ui: Option<SettingUi>,
493 ) -> SettingDef {
494 SettingDef {
495 key,
496 kind,
497 default,
498 ui,
499 }
500 }
501
502 /// Every setting the shell knows about, in render order.
503 pub const SETTINGS_SCHEMA: &[SettingDef] = &[
504 // ── appearance ──────────────────────────────────────────────────────
505 def(
506 "theme",
507 SettingKind::String,
508 DEFAULT_TUI_THEME,
509 ui(
510 TAB_APPEARANCE,
511 "display",
512 "ConfigLabelTheme",
513 // Described by its shipped value list, not prose (`config_hint_for_key`).
514 "",
515 ),
516 ),
517 def(
518 "locale",
519 SettingKind::String,
520 "auto",
521 ui(
522 TAB_APPEARANCE,
523 "display",
524 "ConfigLabelLocale",
525 // Described by its shipped value list, not prose (`config_hint_for_key`).
526 "",
527 ),
528 ),
529 def(
530 "background_color",
531 SettingKind::String,
532 "",
533 ui(
534 TAB_APPEARANCE,
535 "display",
536 "ConfigLabelBackground",
537 "ConfigHintBackgroundColor",
538 ),
539 ),
540 // No sentence anywhere justifies this prototype toggle, so it keeps its
541 // declaration and loses its row.
542 def(
543 "focus_texture",
544 SettingKind::Enum(FOCUS_TEXTURE),
545 "off",
546 None,
547 ),
548 def(
549 "calm_mode",
550 SettingKind::Bool(ON_OFF),
551 "true",
552 ui(
553 TAB_APPEARANCE,
554 "display",
555 "ConfigLabelCalmMode",
556 "ConfigHintCalmMode",
557 ),
558 ),
559 def(
560 "show_thinking",
561 SettingKind::Bool(SHOW_THINKING),
562 "false",
563 ui(
564 TAB_APPEARANCE,
565 "display",
566 "ConfigLabelShowThinking",
567 "ConfigHintShowThinking",
568 ),
569 ),
570 def(
571 "thinking_default_expanded",
572 SettingKind::Bool(ON_OFF),
573 "false",
574 ui(
575 TAB_APPEARANCE,
576 "display",
577 "",
578 "ConfigHintThinkingDefaultExpanded",
579 ),
580 ),
581 def(
582 "thinking_preview_lines",
583 SettingKind::Int,
584 "2",
585 ui(
586 TAB_APPEARANCE,
587 "display",
588 "",
589 "ConfigHintThinkingPreviewLines",
590 ),
591 ),
592 def(
593 "thinking_highlight",
594 SettingKind::Bool(THINKING_HIGHLIGHT),
595 "true",
596 ui(
597 TAB_APPEARANCE,
598 "display",
599 "ConfigLabelThinkingHighlight",
600 "ConfigHintThinkingHighlight",
601 ),
602 ),
603 def(
604 "help_expand_groups",
605 SettingKind::Bool(ON_OFF),
606 "false",
607 ui(TAB_APPEARANCE, "display", "", "ConfigHintHelpExpandGroups"),
608 ),
609 def(
610 "contextual_tips",
611 SettingKind::Bool(ON_OFF),
612 "true",
613 ui(
614 TAB_APPEARANCE,
615 "display",
616 "ConfigLabelContextualTips",
617 "ConfigHintContextualTips",
618 ),
619 ),
620 def(
621 "pin_last_prompt",
622 SettingKind::Bool(ON_OFF),
623 "true",
624 ui(TAB_APPEARANCE, "display", "", "ConfigHintPinLastPrompt"),
625 ),
626 def(
627 "show_tool_details",
628 SettingKind::Bool(ON_OFF),
629 "false",
630 ui(
631 TAB_APPEARANCE,
632 "display",
633 "ConfigLabelShowToolDetails",
634 "ConfigHintBooleanValues",
635 ),
636 ),
637 def(
638 "inline_diffs",
639 SettingKind::Enum(INLINE_DIFFS),
640 "full",
641 ui(
642 TAB_APPEARANCE,
643 "display",
644 "ConfigLabelInlineDiffs",
645 "ConfigHintInlineDiffs",
646 ),
647 ),
648 // No sentence: the glyph set is discoverable from the row's own values.
649 def(
650 "status_indicator",
651 SettingKind::Enum(STATUS_INDICATOR),
652 "cw",
653 None,
654 ),
655 def(
656 "synchronized_output",
657 SettingKind::Enum(SYNCHRONIZED_OUTPUT),
658 "auto",
659 ui(
660 TAB_APPEARANCE,
661 "display",
662 "ConfigLabelSynchronizedOutput",
663 "ConfigHintSynchronizedOutput",
664 ),
665 ),
666 def(
667 "cost_currency",
668 SettingKind::Enum(COST_CURRENCY),
669 "usd",
670 ui(
671 TAB_APPEARANCE,
672 "display",
673 "ConfigLabelCostCurrency",
674 "ConfigHintCostCurrency",
675 ),
676 ),
677 def(
678 "transcript_spacing",
679 SettingKind::Enum(DENSITY),
680 "comfortable",
681 ui(
682 TAB_APPEARANCE,
683 "display",
684 "ConfigLabelTranscriptSpacing",
685 "ConfigHintDensity",
686 ),
687 ),
688 def(
689 "tool_collapse",
690 SettingKind::Enum(TOOL_COLLAPSE),
691 "compact",
692 ui(
693 TAB_APPEARANCE,
694 "display",
695 "ConfigLabelToolCollapse",
696 "ConfigHintToolCollapse",
697 ),
698 ),
699 // ── models & providers ──────────────────────────────────────────────
700 def(
701 "provider",
702 SettingKind::String,
703 "",
704 ui_action(
705 TAB_MODELS,
706 "provider",
707 "ConfigLabelProvider",
708 "ConfigHintProvider",
709 ),
710 ),
711 def(
712 "model",
713 SettingKind::String,
714 "",
715 ui_action(TAB_MODELS, "model", "ConfigLabelModel", "ConfigHintModel"),
716 ),
717 def(
718 "reasoning_effort",
719 SettingKind::Enum(REASONING_EFFORT),
720 "",
721 ui(
722 TAB_MODELS,
723 "model",
724 "ConfigLabelReasoningEffort",
725 "ConfigHintReasoningEffort",
726 ),
727 ),
728 // Sub-agent fan-out depth, next to the model rows that drive it. Fleet
729 // membership itself lives in the /fleet menu, so a one-row Fleet tab
730 // would only restate this table.
731 def(
732 "fleet.exec.max_spawn_depth",
733 SettingKind::Int,
734 "3",
735 ui_diagnostic(
736 TAB_MODELS,
737 "model",
738 "ConfigLabelFleetSpawnDepth",
739 "ConfigHintFleetMaxSpawnDepth",
740 ),
741 ),
742 // ── work ────────────────────────────────────────────────────────────
743 def(
744 "composer_density",
745 SettingKind::Enum(DENSITY),
746 "comfortable",
747 ui(
748 TAB_WORK,
749 "composer",
750 "ConfigLabelComposerDensity",
751 "ConfigHintDensity",
752 ),
753 ),
754 def(
755 "composer_border",
756 SettingKind::Bool(ON_OFF),
757 "true",
758 ui(
759 TAB_WORK,
760 "composer",
761 "ConfigLabelComposerBorder",
762 "ConfigHintBooleanValues",
763 ),
764 ),
765 def(
766 "composer_multiline_mode",
767 SettingKind::Bool(ON_OFF),
768 "false",
769 ui(
770 TAB_WORK,
771 "composer",
772 "ConfigLabelComposerMultilineMode",
773 "ConfigHintComposerMultilineMode",
774 ),
775 ),
776 // Settable via `/set`, surfaced through the composer keymap rather than a
777 // settings row.
778 def(
779 "composer_vim_mode",
780 SettingKind::Enum(VIM_MODE),
781 "normal",
782 None,
783 ),
784 // Terminal protocol toggle; available through `/set` but not given a row.
785 def("bracketed_paste", SettingKind::Bool(ON_OFF), "true", None),
786 def(
787 "paste_burst_detection",
788 SettingKind::Bool(ON_OFF),
789 "true",
790 ui(
791 TAB_WORK,
792 "composer",
793 "ConfigLabelPasteBurstDetection",
794 "ConfigHintBooleanValues",
795 ),
796 ),
797 // Mention-completion tuning knobs; exposed via `/set`, not the settings row.
798 def("mention_menu_limit", SettingKind::Int, "128", None),
799 def(
800 "mention_menu_behavior",
801 SettingKind::Enum(MENTION_MENU_BEHAVIOR),
802 "fuzzy",
803 None,
804 ),
805 def("mention_walk_depth", SettingKind::Int, "10", None),
806 // Workspace discovery option for symlinked layouts; advanced, `/set`-only.
807 def(
808 "workspace_follow_symlinks",
809 SettingKind::Bool(ON_OFF),
810 "false",
811 None,
812 ),
813 def(
814 "work_surface_placement",
815 SettingKind::Enum(WORK_SURFACE_PLACEMENT),
816 "bottom",
817 ui(
818 TAB_WORK,
819 "workbar",
820 "ConfigLabelWorkSurfacePlacement",
821 "ConfigHintWorkSurfacePlacement",
822 ),
823 ),
824 def(
825 "work_surface_top_height",
826 SettingKind::Int,
827 "8",
828 ui(
829 TAB_WORK,
830 "workbar",
831 "ConfigLabelTopHeight",
832 "ConfigHintWorkSurfaceTopHeight",
833 ),
834 ),
835 def(
836 "work_surface_side_width",
837 SettingKind::Int,
838 "30",
839 ui(
840 TAB_WORK,
841 "workbar",
842 "ConfigLabelSideWidth",
843 "ConfigHintWorkSurfaceSideWidth",
844 ),
845 ),
846 def(
847 "rail_panel",
848 SettingKind::Enum(RAIL_PANEL),
849 "tasks",
850 ui(TAB_WORK, "workbar", "", "ConfigHintRailPanel"),
851 ),
852 // Sidebar panel toggles; driven by view actions and startup flags, not rows.
853 def("context_panel", SettingKind::Bool(ON_OFF), "false", None),
854 def("sessions_rail", SettingKind::Bool(ON_OFF), "false", None),
855 def(
856 "session_auto_resume",
857 SettingKind::Bool(ON_OFF),
858 "false",
859 None,
860 ),
861 def(
862 "auto_compact",
863 SettingKind::Bool(ON_OFF),
864 "false",
865 ui(
866 TAB_WORK,
867 "history",
868 "ConfigLabelAutoCompact",
869 "ConfigHintBooleanValues",
870 ),
871 ),
872 def(
873 "auto_compact_threshold_percent",
874 SettingKind::Float,
875 "80",
876 ui(
877 TAB_WORK,
878 "history",
879 "ConfigLabelAutoCompactThreshold",
880 "ConfigHintAutoCompactThreshold",
881 ),
882 ),
883 // A computed receipt from auto_compact + threshold; no user-facing row.
884 def("effective_auto_compact", SettingKind::String, "", None),
885 def(
886 "max_history",
887 SettingKind::Int,
888 "100",
889 ui(
890 TAB_WORK,
891 "history",
892 "ConfigLabelMaxHistory",
893 "ConfigHintMaxHistory",
894 ),
895 ),
896 def(
897 "goal_command",
898 SettingKind::String,
899 "",
900 ui_diagnostic(
901 TAB_WORK,
902 "session",
903 "ConfigLabelGoalCommand",
904 "ConfigHintGoalCommand",
905 ),
906 ),
907 def(
908 "workflow",
909 SettingKind::String,
910 "",
911 ui_diagnostic(
912 TAB_WORK,
913 "workflow",
914 "ConfigLabelWorkflow",
915 "ConfigHintWorkflow",
916 ),
917 ),
918 def(
919 "notifications.quiet",
920 SettingKind::Bool(ON_OFF),
921 "false",
922 ui(
923 TAB_WORK,
924 "workflow",
925 "ConfigLabelNotificationQuiet",
926 "ConfigHintNotificationPolicy",
927 ),
928 ),
929 def(
930 "notifications.sound",
931 SettingKind::Enum(NOTIFICATION_SOUNDS),
932 "legacy",
933 ui(
934 TAB_WORK,
935 "workflow",
936 "ConfigLabelNotificationSound",
937 "ConfigHintNotificationSound",
938 ),
939 ),
940 def(
941 "notifications.condition",
942 SettingKind::Enum(NOTIFICATION_CONDITIONS),
943 "unfocused",
944 ui(
945 TAB_WORK,
946 "workflow",
947 "ConfigLabelNotificationCondition",
948 "ConfigHintNotificationPolicy",
949 ),
950 ),
951 def(
952 "notifications.method",
953 SettingKind::Enum(NOTIFICATION_METHODS),
954 "auto",
955 ui(
956 TAB_WORK,
957 "workflow",
958 "ConfigLabelNotificationMethod",
959 "ConfigHintNotificationPolicy",
960 ),
961 ),
962 def(
963 "notifications.threshold_secs",
964 SettingKind::Int,
965 "30",
966 ui(
967 TAB_WORK,
968 "workflow",
969 "ConfigLabelNotificationThreshold",
970 "ConfigHintNotificationPolicy",
971 ),
972 ),
973 def(
974 "notifications.include_summary",
975 SettingKind::Bool(ON_OFF),
976 "false",
977 ui(
978 TAB_WORK,
979 "workflow",
980 "ConfigLabelNotificationSummary",
981 "ConfigHintNotificationPolicy",
982 ),
983 ),
984 def(
985 "notifications.subagent_completion",
986 SettingKind::Enum(NOTIFICATION_SUBAGENTS),
987 "final-only",
988 ui(
989 TAB_WORK,
990 "workflow",
991 "ConfigLabelNotificationSubagents",
992 "ConfigHintNotificationPolicy",
993 ),
994 ),
995 def(
996 "notifications.events.turn-complete",
997 SettingKind::Bool(ON_OFF),
998 "true",
999 ui(
1000 TAB_WORK,
1001 "workflow",
1002 "ConfigLabelNotificationTurnComplete",
1003 "ConfigHintNotificationPolicy",
1004 ),
1005 ),
1006 def(
1007 "notifications.events.subagent-terminal",
1008 SettingKind::Bool(ON_OFF),
1009 "true",
1010 ui(
1011 TAB_WORK,
1012 "workflow",
1013 "ConfigLabelNotificationSubagentTerminal",
1014 "ConfigHintNotificationPolicy",
1015 ),
1016 ),
1017 def(
1018 "notifications.events.approval-needed",
1019 SettingKind::Bool(ON_OFF),
1020 "true",
1021 ui(
1022 TAB_WORK,
1023 "workflow",
1024 "ConfigLabelNotificationApprovalNeeded",
1025 "ConfigHintNotificationPolicy",
1026 ),
1027 ),
1028 def(
1029 "notifications.events.input-needed",
1030 SettingKind::Bool(ON_OFF),
1031 "true",
1032 ui(
1033 TAB_WORK,
1034 "workflow",
1035 "ConfigLabelNotificationInputNeeded",
1036 "ConfigHintNotificationPolicy",
1037 ),
1038 ),
1039 def(
1040 "notifications.events.elevation-needed",
1041 SettingKind::Bool(ON_OFF),
1042 "true",
1043 ui(
1044 TAB_WORK,
1045 "workflow",
1046 "ConfigLabelNotificationElevationNeeded",
1047 "ConfigHintNotificationPolicy",
1048 ),
1049 ),
1050 def(
1051 "notifications.events.model-notify",
1052 SettingKind::Bool(ON_OFF),
1053 "true",
1054 ui(
1055 TAB_WORK,
1056 "workflow",
1057 "ConfigLabelNotificationModelNotify",
1058 "ConfigHintNotificationPolicy",
1059 ),
1060 ),
1061 def(
1062 "notifications.completion_sound",
1063 SettingKind::Enum(NOTIFICATION_COMPLETION_SOUNDS),
1064 "off",
1065 ui(
1066 TAB_WORK,
1067 "workflow",
1068 "ConfigLabelNotificationCompletionSound",
1069 "ConfigHintNotificationLegacy",
1070 ),
1071 ),
1072 def(
1073 "notifications.sound_file",
1074 SettingKind::String,
1075 "",
1076 ui(
1077 TAB_WORK,
1078 "workflow",
1079 "ConfigLabelNotificationSoundFile",
1080 "ConfigHintNotificationSound",
1081 ),
1082 ),
1083 def(
1084 "notifications.event_sound.enabled",
1085 SettingKind::Bool(ON_OFF),
1086 "false",
1087 ui(
1088 TAB_WORK,
1089 "workflow",
1090 "ConfigLabelNotificationEventSoundEnabled",
1091 "ConfigHintNotificationLegacy",
1092 ),
1093 ),
1094 def(
1095 "notifications.event_sound.events",
1096 SettingKind::String,
1097 "[\"turn-complete\", \"approval-needed\"]",
1098 ui(
1099 TAB_WORK,
1100 "workflow",
1101 "ConfigLabelNotificationEventSoundEvents",
1102 "ConfigHintNotificationLegacy",
1103 ),
1104 ),
1105 def(
1106 "notifications.event_sound.min_interval_ms",
1107 SettingKind::Int,
1108 "2000",
1109 ui(
1110 TAB_WORK,
1111 "workflow",
1112 "ConfigLabelNotificationEventSoundInterval",
1113 "ConfigHintNotificationPolicy",
1114 ),
1115 ),
1116 def(
1117 "notifications.event_sound.quiet",
1118 SettingKind::Bool(ON_OFF),
1119 "false",
1120 ui(
1121 TAB_WORK,
1122 "workflow",
1123 "ConfigLabelNotificationEventSoundQuiet",
1124 "ConfigHintNotificationLegacy",
1125 ),
1126 ),
1127 // ── tools & MCP ─────────────────────────────────────────────────────
1128 def(
1129 "mcp_open",
1130 SettingKind::String,
1131 "",
1132 ui_action(TAB_TOOLS, "mcp", "ConfigLabelMcpOpen", "ConfigHintMcpOpen"),
1133 ),
1134 def(
1135 "mcp_reconnect",
1136 SettingKind::String,
1137 "",
1138 ui_action(
1139 TAB_TOOLS,
1140 "mcp",
1141 "ConfigLabelMcpReconnect",
1142 "ConfigHintMcpReconnect",
1143 ),
1144 ),
1145 def(
1146 "mcp_diagnose",
1147 SettingKind::String,
1148 "",
1149 ui_action(
1150 TAB_TOOLS,
1151 "mcp",
1152 "ConfigLabelMcpDiagnose",
1153 "ConfigHintMcpDiagnose",
1154 ),
1155 ),
1156 def(
1157 "plugins_open",
1158 SettingKind::String,
1159 "",
1160 ui_action(
1161 TAB_TOOLS,
1162 "mcp",
1163 "ConfigLabelPluginsOpen",
1164 "ConfigHintPluginsOpen",
1165 ),
1166 ),
1167 def(
1168 "mcp_config_path",
1169 SettingKind::String,
1170 "",
1171 ui(
1172 TAB_TOOLS,
1173 "mcp",
1174 "ConfigLabelMcpConfigPath",
1175 "ConfigHintMcpConfigPath",
1176 ),
1177 ),
1178 // ── trust ───────────────────────────────────────────────────────────
1179 def(
1180 "sandbox_details",
1181 SettingKind::String,
1182 "",
1183 ui_action(
1184 TAB_TRUST,
1185 "permissions",
1186 "SetupStepTrustSandboxTitle",
1187 "SetupStepTrustSandboxWhy",
1188 ),
1189 ),
1190 def(
1191 "approval_mode",
1192 SettingKind::Enum(APPROVAL_MODE),
1193 "",
1194 ui_session(
1195 TAB_TRUST,
1196 "permissions",
1197 "ConfigLabelApprovalMode",
1198 "ConfigHintApprovalMode",
1199 ),
1200 ),
1201 def(
1202 "permission_posture",
1203 SettingKind::Enum(PERMISSION_POSTURE),
1204 "ask",
1205 ui(
1206 TAB_TRUST,
1207 "permissions",
1208 "ConfigLabelPermissionPosture",
1209 "ConfigHintPermissionPosture",
1210 ),
1211 ),
1212 def(
1213 "approval_policy",
1214 SettingKind::Enum(APPROVAL_POLICY),
1215 "ask",
1216 ui(
1217 TAB_TRUST,
1218 "permissions",
1219 "ConfigLabelApprovalPolicy",
1220 "ConfigHintApprovalPolicy",
1221 ),
1222 ),
1223 def(
1224 "managed_approval_policy",
1225 SettingKind::String,
1226 "",
1227 ui_diagnostic(
1228 TAB_TRUST,
1229 "permissions",
1230 "ConfigLabelManagedApprovalPolicy",
1231 "ConfigHintManagedApprovalPolicy",
1232 ),
1233 ),
1234 def(
1235 "default_mode",
1236 SettingKind::Enum(DEFAULT_MODE),
1237 "agent",
1238 ui(
1239 TAB_TRUST,
1240 "permissions",
1241 "ConfigLabelDefaultMode",
1242 "ConfigHintDefaultMode",
1243 ),
1244 ),
1245 def(
1246 "allow_shell",
1247 SettingKind::Bool(ON_OFF),
1248 "true",
1249 ui(
1250 TAB_TRUST,
1251 "permissions",
1252 "ConfigLabelAllowShell",
1253 "ConfigHintAllowShell",
1254 ),
1255 ),
1256 def(
1257 "managed_allow_shell",
1258 SettingKind::String,
1259 "",
1260 ui_diagnostic(
1261 TAB_TRUST,
1262 "permissions",
1263 "ConfigLabelManagedAllowShell",
1264 "ConfigHintManagedAllowShell",
1265 ),
1266 ),
1267 def(
1268 "telemetry",
1269 SettingKind::Bool(TELEMETRY),
1270 "false",
1271 ui(
1272 TAB_TRUST,
1273 "network",
1274 "ConfigLabelTelemetry",
1275 "ConfigHintTelemetry",
1276 ),
1277 ),
1278 // ── motion ──────────────────────────────────────────────────────────
1279 def(
1280 "low_motion",
1281 SettingKind::Bool(LOW_MOTION),
1282 "false",
1283 ui(
1284 TAB_MOTION,
1285 "display",
1286 "ConfigLabelLowMotion",
1287 "ConfigHintLowMotion",
1288 ),
1289 ),
1290 def(
1291 "fancy_animations",
1292 SettingKind::Bool(FANCY_ANIMATIONS),
1293 "true",
1294 ui(
1295 TAB_MOTION,
1296 "display",
1297 "ConfigLabelFancyAnimations",
1298 "ConfigHintFancyAnimations",
1299 ),
1300 ),
1301 // ── advanced ────────────────────────────────────────────────────────
1302 def(
1303 "base_url",
1304 SettingKind::String,
1305 "",
1306 ui_diagnostic(
1307 TAB_ADVANCED,
1308 "provider",
1309 "ConfigLabelBaseUrlDeepseek",
1310 "ConfigHintBaseUrl",
1311 ),
1312 ),
1313 def(
1314 "provider_url",
1315 SettingKind::String,
1316 "",
1317 ui_diagnostic(
1318 TAB_ADVANCED,
1319 "provider",
1320 "ConfigLabelProviderUrl",
1321 "ConfigHintProviderUrl",
1322 ),
1323 ),
1324 def(
1325 "context_window",
1326 SettingKind::Int,
1327 "",
1328 ui_diagnostic(TAB_ADVANCED, "provider", "", "ConfigHintContextWindow"),
1329 ),
1330 def(
1331 "effective_context_window",
1332 SettingKind::String,
1333 "",
1334 ui_diagnostic(
1335 TAB_ADVANCED,
1336 "provider",
1337 "",
1338 "ConfigHintEffectiveContextWindow",
1339 ),
1340 ),
1341 // Trust receipts: which external credential file a provider may read, and
1342 // under what access. Read-only here; `/provider` owns changing them.
1343 def(
1344 "external_credentials.openai-codex",
1345 SettingKind::String,
1346 "",
1347 ui_diagnostic(
1348 TAB_ADVANCED,
1349 "provider",
1350 "",
1351 "ConfigHintExternalCredentials",
1352 ),
1353 ),
1354 def(
1355 "external_credentials.xai",
1356 SettingKind::String,
1357 "",
1358 ui_diagnostic(
1359 TAB_ADVANCED,
1360 "provider",
1361 "",
1362 "ConfigHintExternalCredentials",
1363 ),
1364 ),
1365 // Derived fast-sibling receipt, retired from the table: the /model picker
1366 // already names the fast sibling where a choice actually happens, and no
1367 // backend reads `fast_model` as a persisted key.
1368 def("fast_model", SettingKind::String, "", None),
1369 // A transport timeout; advanced networking, available through `/set` only.
1370 def("stream_chunk_timeout_secs", SettingKind::Int, "900", None),
1371 // Canonical transport keys. Runtime resolution stays in TUI Config.
1372 def("stream.open_timeout_secs", SettingKind::Int, "45", None),
1373 def("stream.chunk_timeout_secs", SettingKind::Int, "900", None),
1374 def("stream.max_resumes", SettingKind::Int, "3", None),
1375 def(
1376 "stream.max_transparent_retries",
1377 SettingKind::Int,
1378 "2",
1379 None,
1380 ),
1381 def("stream.max_stream_errors", SettingKind::Int, "5", None),
1382 def("stream.max_duration_secs", SettingKind::Int, "1800", None),
1383 def("stream.max_content_mb", SettingKind::Int, "10", None),
1384 def("stream.connect_timeout_secs", SettingKind::Int, "30", None),
1385 def("stream.tcp_keepalive_secs", SettingKind::Int, "30", None),
1386 def(
1387 "stream.http2_keep_alive_interval_secs",
1388 SettingKind::Int,
1389 "15",
1390 None,
1391 ),
1392 def(
1393 "stream.http2_keep_alive_timeout_secs",
1394 SettingKind::Int,
1395 "20",
1396 None,
1397 ),
1398 def(
1399 "stream.force_http1",
1400 SettingKind::Bool(ON_OFF),
1401 "false",
1402 None,
1403 ),
1404 // DeepSeek-only legacy fallback: the runtime still reads it, but it is
1405 // not a live choice, so it stays settable through `/set` without a row.
1406 def("default_model", SettingKind::String, "", None),
1407 // Beta vision flag: the feature backend stays live, but the row goes —
1408 // feature state is diagnosed where vision runs, not in Advanced.
1409 def("features.vision_model", SettingKind::String, "", None),
1410 def(
1411 "features.subagents",
1412 SettingKind::String,
1413 "",
1414 ui_diagnostic(
1415 TAB_ADVANCED,
1416 "experimental",
1417 "",
1418 "ConfigHintFeatureSubagents",
1419 ),
1420 ),
1421 def(
1422 "features.web_search",
1423 SettingKind::String,
1424 "",
1425 ui_diagnostic(
1426 TAB_ADVANCED,
1427 "experimental",
1428 "",
1429 "ConfigHintFeatureWebSearch",
1430 ),
1431 ),
1432 def(
1433 "features.apply_patch",
1434 SettingKind::String,
1435 "",
1436 ui_diagnostic(
1437 TAB_ADVANCED,
1438 "experimental",
1439 "",
1440 "ConfigHintFeatureApplyPatch",
1441 ),
1442 ),
1443 def(
1444 "features.mcp",
1445 SettingKind::String,
1446 "",
1447 ui_diagnostic(TAB_ADVANCED, "experimental", "", "ConfigHintFeatureMcp"),
1448 ),
1449 def(
1450 "features.exec_policy",
1451 SettingKind::String,
1452 "",
1453 ui_diagnostic(
1454 TAB_ADVANCED,
1455 "experimental",
1456 "",
1457 "ConfigHintFeatureExecPolicy",
1458 ),
1459 ),
1460 // ── persisted field names without a row ─────────────────────────────
1461 // Every field `Settings` serializes to settings.toml has a declaration;
1462 // these have `ui: None` because their value is owned elsewhere:
1463 // route pickers, `/set` aliases with their own canonical row, or
1464 // internal one-way flags. Declaration order is render order, and hidden
1465 // entries render nothing, so they live together at the end.
1466 //
1467 // Canonical names behind a `/set` alias row: `set()` accepts both
1468 // spellings, the row carries the alias.
1469 def("tool_collapse_mode", SettingKind::String, "compact", None),
1470 def("max_input_history", SettingKind::Int, "100", None),
1471 // Written by the route pickers, not by a settings row (the provider /
1472 // model rows persist to config.toml via `set_config_value`).
1473 def("default_provider", SettingKind::String, "", None),
1474 // Trust posture with a `/set` entry point but no row; surfaced where
1475 // sandboxing acts, not as a settings sentence.
1476 def("sandbox_mode", SettingKind::String, "", None),
1477 // Route memory written by the pickers: per-provider defaults, enabled
1478 // chooser sets, and pinned routes. Structured values no row could edit.
1479 def("provider_models", SettingKind::String, "", None),
1480 def("enabled_models", SettingKind::String, "", None),
1481 def("pinned_models", SettingKind::String, "", None),
1482 // One-way internal flags: shown an intro, shown a deprecation, counted
1483 // tip impressions. Read back to suppress repeats, never edited.
1484 def(
1485 "feature_intro_shown",
1486 SettingKind::Bool(ON_OFF),
1487 "false",
1488 None,
1489 ),
1490 def(
1491 "yolo_deprecation_shown",
1492 SettingKind::Bool(ON_OFF),
1493 "false",
1494 None,
1495 ),
1496 // Round 3 work-bar placement migration ran once (`top` → `bottom`).
1497 def(
1498 "work_surface_bottom_migrated",
1499 SettingKind::Bool(ON_OFF),
1500 "false",
1501 None,
1502 ),
1503 def("behavioral_tip_impressions", SettingKind::String, "", None),
1504 // Footer key-hint use counts: a hint retires to its bare state once its
1505 // binding has been used enough times. Written by the footer, never edited.
1506 def("footer_hint_uses", SettingKind::String, "", None),
1507 ];
1508
1509 /// The declaration for `key`, if the shell knows it.
1510 pub fn setting(key: &str) -> Option<&'static SettingDef> {
1511 SETTINGS_SCHEMA.iter().find(|def| def.key == key)
1512 }
1513
1514 /// Position of `key` in declaration order; `None` for unknown keys.
1515 pub fn setting_index(key: &str) -> Option<usize> {
1516 SETTINGS_SCHEMA.iter().position(|def| def.key == key)
1517 }
1518
1519 /// Distinct `ui.tab` ids, in the order they are first declared.
1520 pub fn schema_tabs() -> Vec<&'static str> {
1521 let mut tabs: Vec<&'static str> = Vec::new();
1522 for ui in SETTINGS_SCHEMA.iter().filter_map(|def| def.ui.as_ref()) {
1523 if !tabs.contains(&ui.tab) {
1524 tabs.push(ui.tab);
1525 }
1526 }
1527 tabs
1528 }
1529
1530 /// Distinct `ui.group` ids within `tab`, in the order they are first declared.
1531 pub fn schema_groups(tab: &str) -> Vec<&'static str> {
1532 let mut groups: Vec<&'static str> = Vec::new();
1533 for ui in SETTINGS_SCHEMA
1534 .iter()
1535 .filter_map(|def| def.ui.as_ref())
1536 .filter(|ui| ui.tab == tab)
1537 {
1538 if !groups.contains(&ui.group) {
1539 groups.push(ui.group);
1540 }
1541 }
1542 groups
1543 }
1544
1545 /// Settings declared with a row, in declaration order.
1546 pub fn schema_rows() -> impl Iterator<Item = &'static SettingDef> {
1547 SETTINGS_SCHEMA.iter().filter(|def| def.ui.is_some())
1548 }
1549
1550 #[cfg(test)]
1551 mod tests {
1552 use super::*;
1553 use std::collections::BTreeSet;
1554
1555 #[test]
1556 fn schema_keys_are_unique() {
1557 let mut seen = BTreeSet::new();
1558 for def in SETTINGS_SCHEMA {
1559 assert!(seen.insert(def.key), "duplicate setting key: {}", def.key);
1560 }
1561 assert_eq!(seen.len(), SETTINGS_SCHEMA.len());
1562 }
1563
1564 #[test]
1565 fn every_row_declares_a_tab_group_and_description() {
1566 for def in schema_rows() {
1567 let ui = def.ui.as_ref().expect("schema_rows filters on ui");
1568 assert!(!ui.tab.is_empty(), "{} has an empty tab", def.key);
1569 assert!(!ui.group.is_empty(), "{} has an empty group", def.key);
1570 // theme and locale are described by their shipped value lists,
1571 // which the TUI composes at render time; no prose key exists.
1572 if matches!(def.key, "theme" | "locale") {
1573 continue;
1574 }
1575 assert!(
1576 !ui.description.is_empty(),
1577 "{} has a row but no sentence justifying it",
1578 def.key
1579 );
1580 }
1581 }
1582
1583 #[test]
1584 fn declaration_order_groups_rows_by_tab_then_group() {
1585 // The surface renders runs of rows; a tab or group that reappears
1586 // after another one would paint two headings with the same name.
1587 let mut seen_tabs: Vec<&str> = Vec::new();
1588 let mut seen_pairs: Vec<(&str, &str)> = Vec::new();
1589 let mut current: Option<(&str, &str)> = None;
1590 for def in schema_rows() {
1591 let ui = def.ui.as_ref().expect("row");
1592 if current.map(|(tab, _)| tab) != Some(ui.tab) {
1593 assert!(!seen_tabs.contains(&ui.tab), "tab {} is split", ui.tab);
1594 seen_tabs.push(ui.tab);
1595 }
1596 if current != Some((ui.tab, ui.group)) {
1597 assert!(
1598 !seen_pairs.contains(&(ui.tab, ui.group)),
1599 "group {}/{} is split",
1600 ui.tab,
1601 ui.group
1602 );
1603 seen_pairs.push((ui.tab, ui.group));
1604 }
1605 current = Some((ui.tab, ui.group));
1606 }
1607 assert_eq!(schema_tabs(), seen_tabs);
1608 }
1609
1610 #[test]
1611 fn enum_values_are_unique_and_defaults_are_declared_values() {
1612 for def in SETTINGS_SCHEMA {
1613 if let Some(values) = def.values() {
1614 let unique: BTreeSet<_> = values.iter().collect();
1615 assert_eq!(unique.len(), values.len(), "{} repeats a value", def.key);
1616 if !def.default.is_empty() {
1617 assert!(
1618 values.contains(&def.default),
1619 "{} defaults to {} which is not one of its values",
1620 def.key,
1621 def.default
1622 );
1623 }
1624 }
1625 }
1626 }
1627 }
1628
1628 lines RUST