pi-one-ui 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.en.md +19 -6
  3. package/README.md +19 -6
  4. package/extensions/app/config/context.ts +42 -0
  5. package/extensions/app/config/editor.ts +80 -0
  6. package/extensions/app/config/footer.ts +291 -0
  7. package/extensions/app/config/panel.ts +1 -5
  8. package/extensions/app/config/renderer.ts +1 -6
  9. package/extensions/app/config/shell.ts +358 -1435
  10. package/extensions/app/config/store.ts +22 -1
  11. package/extensions/app/config/values.ts +33 -0
  12. package/extensions/app/config/working-line.ts +100 -0
  13. package/extensions/app/overlay/input-router.ts +56 -32
  14. package/extensions/app/overlay/overlay-manager.ts +183 -21
  15. package/extensions/app/runtime/event-coordinator.ts +90 -42
  16. package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
  17. package/extensions/app/runtime/render-scheduler.ts +35 -31
  18. package/extensions/app/runtime/tui-runtime.ts +315 -312
  19. package/extensions/app/settings/controller.ts +202 -0
  20. package/extensions/app/settings/panel.ts +81 -0
  21. package/extensions/app/settings/presets.ts +43 -0
  22. package/extensions/features/context-inspector/index.ts +83 -662
  23. package/extensions/layouts/context/index.ts +58 -161
  24. package/extensions/layouts/context/message/user-message.ts +1 -1
  25. package/extensions/layouts/context/renderer/index.ts +31 -13
  26. package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
  27. package/extensions/layouts/editor/controller.ts +9 -18
  28. package/extensions/layouts/editor/factory.ts +3 -3
  29. package/extensions/layouts/footer/controller.ts +30 -19
  30. package/extensions/layouts/footer/footer.ts +2 -2
  31. package/extensions/layouts/overlay/context-inspector.ts +497 -0
  32. package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
  33. package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
  34. package/extensions/layouts/overlay/settings-panel.ts +359 -0
  35. package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
  36. package/extensions/layouts/working-line/controller.ts +3 -29
  37. package/extensions/layouts/working-line/working-line.ts +17 -114
  38. package/extensions/shared/working-line-text.ts +108 -0
  39. package/package.json +1 -1
  40. package/extensions/app/host/pi-extension-port.ts +0 -25
  41. package/extensions/app/host/pi-ui-port.ts +0 -52
  42. package/extensions/app/host/tui-capabilities.ts +0 -27
  43. package/extensions/app/ownership/layout-registry.ts +0 -65
  44. package/extensions/app/ownership.ts +0 -32
  45. package/extensions/app/panel.ts +0 -770
  46. package/extensions/app/presets.ts +0 -35
  47. package/extensions/app/runtime/runtime-state.ts +0 -83
  48. /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
  49. /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
@@ -1,5 +1,3 @@
1
- import { normalizeWorkingLineMessages } from "../../layouts/working-line/working-line.ts";
2
- import { PI_WORKING_LINE_MESSAGES } from "../../layouts/working-line/working-line-messages.ts";
3
1
  import {
4
2
  ICON_GLYPH_KEYS,
5
3
  type IconGlyphs,
@@ -10,208 +8,81 @@ import {
10
8
  resolveConfiguredIcons,
11
9
  } from "../../shared/icons.ts";
12
10
  import { isSupportedColorSpec } from "../../shared/style.ts";
11
+ import {
12
+ normalizeUserMessages,
13
+ type UserMessagesComponentConfig,
14
+ } from "./context.ts";
15
+ import {
16
+ defaultEditor,
17
+ type EditorBorderColorMode,
18
+ type EditorComponentConfig,
19
+ type EditorStyle,
20
+ type MinimalistConfig,
21
+ type MinimalistEditorStyleConfig,
22
+ type ModelLabelSource,
23
+ normalizeEditor,
24
+ parseEditorBorderColorMode,
25
+ parseEditorModelLabel,
26
+ parseEditorStyle,
27
+ } from "./editor.ts";
28
+ import {
29
+ type CompactFooterMaxLines,
30
+ type ContextStyle,
31
+ type ContextThresholds,
32
+ defaultFooter,
33
+ defaultFooterSegments,
34
+ type ExtensionStatusColorMode,
35
+ type ExtensionStatusPlacement,
36
+ type FooterComponentConfig,
37
+ type FooterSegmentsConfig,
38
+ type GitBranchConfig,
39
+ type GitCommitConfig,
40
+ type GitMetricsConfig,
41
+ isExtensionStatusColorMode,
42
+ isExtensionStatusPlacement,
43
+ normalizeFooter,
44
+ type PathDisplayConfig,
45
+ parseCompactFooterMaxLines,
46
+ parseContextStyle,
47
+ parseSeparatorStyle,
48
+ type SeparatorStyle,
49
+ type StarshipFooterStyleConfig,
50
+ } from "./footer.ts";
13
51
  import {
14
52
  type ConfigRecord,
15
53
  configStore,
16
54
  configPath as defaultConfigPath,
17
55
  mutateConfigFile,
18
56
  } from "./store.ts";
57
+ import { booleanValue, overlayKnown, recordValue } from "./values.ts";
58
+ import {
59
+ defaultWorkingLine,
60
+ normalizeWorkingLine,
61
+ type WorkingLineComponentConfig,
62
+ type WorkingLineComponentPatch,
63
+ } from "./working-line.ts";
19
64
 
20
- export type ColorSpec = string;
21
65
  export type { IconMode } from "../../shared/icons.ts";
22
-
23
- export type ContextStyle = "text" | "gauge" | "text+gauge";
24
- export type SeparatorStyle = "pipe" | "dot" | "chevron" | "none";
25
- export type ModelLabelSource = "id" | "name";
26
- export type EditorStyle = "on" | "off";
27
- export type UserMessageStyle =
28
- | "framed"
29
- | "framed-copy-friendly"
30
- | "compact"
31
- | "labeled";
66
+ export * from "./context.ts";
67
+ export * from "./editor.ts";
68
+ export * from "./footer.ts";
69
+ export * from "./working-line.ts";
70
+ export type ColorSpec = string;
32
71
  export type SelectorBorderStyle = "zentui";
33
- export type FooterStyle = "native" | "starship" | "hidden";
34
- export type WorkingLineSpinner =
35
- | "braille"
36
- | "star-bloom"
37
- | "pinwheel"
38
- | "claude-inspired"
39
- | "pulse";
40
- export type WorkingLineTextAnimation = "classic" | "kitt" | "disabled";
72
+ export type SelectorBordersComponentConfig = {
73
+ enabled: boolean;
74
+ style: SelectorBorderStyle;
75
+ };
41
76
  export type ComponentStyleOwner =
42
77
  | "editor"
43
78
  | "userMessages"
44
79
  | "selectorBorders"
45
80
  | "footer";
46
- export type MinimalistPathDisplayMode = "compact" | "project" | "full";
47
- export type EditorBorderColorMode = "static" | "adaptive";
48
- export type CompactFooterMaxLines = 1 | 2 | 3 | "unlimited";
49
-
50
- export const DEFAULT_COMPACT_FOOTER_FORMAT =
51
- "$cwd$wrap(in $session_name)$wrap(on $git_branch) $git_status$wrap$context$wrap_sep$tokens";
52
-
53
- export type ContextThresholds = {
54
- warning: number;
55
- error: number;
56
- };
57
-
58
- export type PathDisplayMode = "basename" | "full";
59
-
60
- export type PathDisplayConfig = {
61
- mode: PathDisplayMode;
62
- /** Trailing directories to show in full mode. 0 = unlimited; clamped to 0..5. */
63
- depth: number;
64
- };
65
-
66
- export type GitBranchMaxLength = "full" | number;
67
-
68
- export type GitBranchConfig = {
69
- maxLength: GitBranchMaxLength;
70
- };
71
-
72
81
  export type UiFeaturesConfig = {
73
82
  editor: boolean;
74
83
  statusLine: boolean;
75
84
  viewportIndicators: boolean;
76
85
  };
77
-
78
- export type FooterSegmentsConfig = {
79
- cwd: boolean;
80
- sessionName: boolean;
81
- gitBranch: boolean;
82
- gitStatus: boolean;
83
- gitCounts: boolean;
84
- gitCommit: boolean;
85
- gitMetrics: boolean;
86
- runtime: boolean;
87
- modelInfo: boolean;
88
- context: boolean;
89
- tokens: boolean;
90
- cost: boolean;
91
- sessionDuration: boolean;
92
- username: boolean;
93
- time: boolean;
94
- os: boolean;
95
- packageVersion: boolean;
96
- };
97
-
98
- export type MinimalistEditorStyleConfig = {
99
- pathDisplay: MinimalistPathDisplayMode;
100
- /** Controls the Editor directory label independently of its format and the Footer. */
101
- showCwd: boolean;
102
- showSessionName: boolean;
103
- showTimer: boolean;
104
- showCost: boolean;
105
- showGit: boolean;
106
- };
107
-
108
- /** Temporary name retained for existing settings consumers. */
109
- export type MinimalistConfig = MinimalistEditorStyleConfig;
110
-
111
- export type EditorStylesConfig = {
112
- minimalist: MinimalistEditorStyleConfig;
113
- };
114
-
115
- export type EditorComponentConfig = {
116
- style: EditorStyle;
117
- borderColorMode: EditorBorderColorMode;
118
- modelLabel: ModelLabelSource;
119
- viewportIndicators: boolean;
120
- styles: {
121
- minimalist: MinimalistEditorStyleConfig;
122
- };
123
- };
124
-
125
- export type FramedUserMessageStyleConfig = Record<string, never>;
126
- export type FramedCopyFriendlyUserMessageStyleConfig = Record<string, never>;
127
- export type CompactUserMessageStyleConfig = Record<string, never>;
128
- export type LabeledUserMessageStyleConfig = Record<string, never>;
129
-
130
- export type UserMessagesComponentConfig = {
131
- enabled: boolean;
132
- style: UserMessageStyle;
133
- styles: {
134
- framed: FramedUserMessageStyleConfig;
135
- "framed-copy-friendly": FramedCopyFriendlyUserMessageStyleConfig;
136
- compact: CompactUserMessageStyleConfig;
137
- labeled: LabeledUserMessageStyleConfig;
138
- };
139
- };
140
-
141
- export type SelectorBordersComponentConfig = {
142
- enabled: boolean;
143
- style: SelectorBorderStyle;
144
- };
145
-
146
- export type StarshipFooterStyleConfig = {
147
- format: string;
148
- responsive: boolean;
149
- compactFormat: string;
150
- compactMaxLines: CompactFooterMaxLines;
151
- separator: SeparatorStyle;
152
- contextStyle: ContextStyle;
153
- contextThresholds: ContextThresholds;
154
- pathDisplay: PathDisplayConfig;
155
- segments: FooterSegmentsConfig;
156
- gitBranch: GitBranchConfig;
157
- gitCommit: GitCommitConfig;
158
- gitMetrics: GitMetricsConfig;
159
- extensionStatuses: ExtensionStatusesConfig;
160
- };
161
-
162
- export type FooterComponentConfig = {
163
- style: FooterStyle;
164
- modelLabel: ModelLabelSource;
165
- styles: {
166
- starship: StarshipFooterStyleConfig;
167
- };
168
- };
169
-
170
- export type WorkingLineMessagesConfig = {
171
- custom: boolean;
172
- values: string[];
173
- };
174
-
175
- export type WorkingLineSegmentsConfig = {
176
- tool: boolean;
177
- elapsed: boolean;
178
- thought: boolean;
179
- tokens: boolean;
180
- };
181
-
182
- export const DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS = 100;
183
- export const DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS = 60;
184
- export const MIN_WORKING_LINE_INTERVAL_MS = 30;
185
- export const MAX_WORKING_LINE_INTERVAL_MS = 1000;
186
-
187
- export function isValidWorkingLineIntervalMs(value: unknown): value is number {
188
- return (
189
- typeof value === "number" &&
190
- Number.isSafeInteger(value) &&
191
- value >= MIN_WORKING_LINE_INTERVAL_MS &&
192
- value <= MAX_WORKING_LINE_INTERVAL_MS
193
- );
194
- }
195
-
196
- export type WorkingLineComponentConfig = {
197
- enabled: boolean;
198
- turnSummary: boolean;
199
- spinner: WorkingLineSpinner;
200
- spinnerIntervalMs: number;
201
- animateSpinnerColor: boolean;
202
- textIntervalMs: number;
203
- textAnimation: WorkingLineTextAnimation;
204
- messages: WorkingLineMessagesConfig;
205
- segments: WorkingLineSegmentsConfig;
206
- };
207
-
208
- export type WorkingLineComponentPatch = Partial<
209
- Omit<WorkingLineComponentConfig, "messages" | "segments">
210
- > & {
211
- messages?: Partial<WorkingLineMessagesConfig>;
212
- segments?: Partial<WorkingLineSegmentsConfig>;
213
- };
214
-
215
86
  export type ComponentsConfig = {
216
87
  editor: EditorComponentConfig;
217
88
  userMessages: UserMessagesComponentConfig;
@@ -219,49 +90,6 @@ export type ComponentsConfig = {
219
90
  selectorBorders: SelectorBordersComponentConfig;
220
91
  footer: FooterComponentConfig;
221
92
  };
222
-
223
- export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
224
- export type ExtensionStatusColorMode = "zentui" | "original";
225
-
226
- /**
227
- * Starship `git_commit`-style options.
228
- * See https://starship.rs/config/#git-commit
229
- */
230
- export type GitCommitConfig = {
231
- hashLength: number;
232
- onlyDetached: boolean;
233
- showTag: boolean;
234
- };
235
-
236
- /**
237
- * Starship `git_metrics`-style options.
238
- * See https://starship.rs/config/#git-metrics
239
- */
240
- export type GitMetricsConfig = {
241
- onlyNonzero: boolean;
242
- ignoreSubmodules: boolean;
243
- };
244
-
245
- const DEFAULT_EXTENSION_STATUS_PLACEMENT: ExtensionStatusPlacement = "right";
246
- const DEFAULT_EXTENSION_STATUS_COLOR_MODE: ExtensionStatusColorMode = "zentui";
247
-
248
- export type ExtensionStatusesConfig = {
249
- defaultPlacement: ExtensionStatusPlacement;
250
- placements: Record<string, ExtensionStatusPlacement>;
251
- colorModes: Record<string, ExtensionStatusColorMode>;
252
- };
253
-
254
- const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
255
- const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
256
- export const DEFAULT_EDITOR_METADATA_FORMAT = "$model $provider( $thinking)";
257
-
258
- export type ZentuiConfig = {
259
- projectRefreshIntervalMs: number;
260
- icons: ResolvedIcons;
261
- colors: PolishedTuiColors;
262
- components: ComponentsConfig;
263
- };
264
-
265
93
  export type PolishedTuiColors = {
266
94
  cwd?: ColorSpec;
267
95
  sessionName: ColorSpec;
@@ -299,172 +127,17 @@ export type PolishedTuiColors = {
299
127
  workingLineMid?: ColorSpec;
300
128
  workingLineHigh?: ColorSpec;
301
129
  };
302
-
303
- /** Canonical configuration plus a derived flat runtime view for existing callers. */
304
- export type PolishedTuiConfig = ZentuiConfig & {
305
- footerFormat: string;
306
- responsiveFooter: boolean;
307
- compactFooterFormat: string;
308
- compactFooterMaxLines: CompactFooterMaxLines;
309
- editorMetadataFormat: string;
310
- separator: SeparatorStyle;
311
- contextStyle: ContextStyle;
312
- editorModelLabel: ModelLabelSource;
313
- editorStyle: EditorStyle;
314
- editorStyles: EditorStylesConfig;
315
- editorBorderColorMode: EditorBorderColorMode;
316
- contextThresholds: ContextThresholds;
317
- pathDisplay: PathDisplayConfig;
318
- gitBranch: GitBranchConfig;
319
- features: UiFeaturesConfig;
320
- footerSegments: FooterSegmentsConfig;
321
- gitCommit: GitCommitConfig;
322
- gitMetrics: GitMetricsConfig;
323
- extensionStatuses: ExtensionStatusesConfig;
324
- };
325
-
326
- /**
327
- * Canonical footer format variable names. In a `footerFormat` string these
328
- * are written as `$name` or `${name}`.
329
- */
330
- export const FOOTER_FORMAT_VARIABLES = [
331
- "cwd",
332
- "session_name",
333
- "git_branch",
334
- "git_status",
335
- "git_state",
336
- "runtime",
337
- "model",
338
- "provider",
339
- "session_duration",
340
- "username",
341
- "os",
342
- "time",
343
- "context",
344
- "tokens",
345
- "cache_read",
346
- "cache_write",
347
- "cost",
348
- "subscription",
349
- "auto_compaction",
350
- "package",
351
- "package_version",
352
- "git_commit",
353
- "git_tag",
354
- "git_metrics",
355
- "git_added",
356
- "git_deleted",
357
- "sep",
358
- ] as const;
359
-
360
- /**
361
- * Alias → canonical variable name mapping for `footerFormat`.
362
- * `$fill` is special (not a variable) and handled by the parser.
363
- */
364
- export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
365
- directory: "cwd",
366
- branch: "git_branch",
367
- status: "git_status",
368
- state: "git_state",
369
- commit: "git_commit",
370
- tag: "git_tag",
371
- duration: "session_duration",
372
- separator: "sep",
130
+ export type ZentuiConfig = {
131
+ projectRefreshIntervalMs: number;
132
+ icons: ResolvedIcons;
133
+ colors: PolishedTuiColors;
134
+ components: ComponentsConfig;
373
135
  };
374
-
375
- /** Shared configuration file used by the pi-one-ui package. */
136
+ export type PolishedTuiConfig = ZentuiConfig;
376
137
  export const configPath = defaultConfigPath;
377
138
 
378
- const defaultFooterSegments: FooterSegmentsConfig = {
379
- cwd: true,
380
- sessionName: true,
381
- gitBranch: true,
382
- gitStatus: true,
383
- gitCounts: false,
384
- gitCommit: false,
385
- gitMetrics: false,
386
- runtime: true,
387
- modelInfo: false,
388
- context: true,
389
- tokens: true,
390
- cost: true,
391
- sessionDuration: false,
392
- username: false,
393
- time: false,
394
- os: false,
395
- packageVersion: false,
396
- };
397
-
398
- const defaultMinimalistStyle: MinimalistEditorStyleConfig = {
399
- pathDisplay: "compact",
400
- showCwd: true,
401
- showSessionName: true,
402
- showTimer: true,
403
- showCost: true,
404
- showGit: true,
405
- };
406
-
407
- const defaultStarshipStyle: StarshipFooterStyleConfig = {
408
- format: "",
409
- responsive: true,
410
- compactFormat: DEFAULT_COMPACT_FOOTER_FORMAT,
411
- compactMaxLines: 2,
412
- separator: "pipe",
413
- contextStyle: "text",
414
- contextThresholds: { warning: 70, error: 90 },
415
- pathDisplay: { mode: "basename", depth: 0 },
416
- segments: defaultFooterSegments,
417
- gitBranch: { maxLength: "full" },
418
- gitCommit: { hashLength: 7, onlyDetached: true, showTag: true },
419
- gitMetrics: { onlyNonzero: true, ignoreSubmodules: false },
420
- extensionStatuses: {
421
- defaultPlacement: "right",
422
- placements: {},
423
- colorModes: {},
424
- },
425
- };
426
-
427
- const defaultComponents: ComponentsConfig = {
428
- editor: {
429
- style: "on",
430
- borderColorMode: "static",
431
- modelLabel: "id",
432
- viewportIndicators: true,
433
- styles: {
434
- minimalist: defaultMinimalistStyle,
435
- },
436
- },
437
- userMessages: {
438
- enabled: true,
439
- style: "framed",
440
- styles: {
441
- framed: {},
442
- "framed-copy-friendly": {},
443
- compact: {},
444
- labeled: {},
445
- },
446
- },
447
- workingLine: {
448
- enabled: false,
449
- turnSummary: true,
450
- spinner: "star-bloom",
451
- spinnerIntervalMs: DEFAULT_WORKING_LINE_SPINNER_INTERVAL_MS,
452
- animateSpinnerColor: false,
453
- textIntervalMs: DEFAULT_WORKING_LINE_TEXT_INTERVAL_MS,
454
- textAnimation: "classic",
455
- messages: { custom: true, values: [...PI_WORKING_LINE_MESSAGES] },
456
- segments: { tool: true, elapsed: true, thought: true, tokens: true },
457
- },
458
- selectorBorders: { enabled: true, style: "zentui" },
459
- footer: {
460
- style: "starship",
461
- modelLabel: "id",
462
- styles: { starship: defaultStarshipStyle },
463
- },
464
- };
465
-
466
- export const defaultConfig: PolishedTuiConfig = {
467
- projectRefreshIntervalMs: DEFAULT_PROJECT_REFRESH_INTERVAL_MS,
139
+ export const defaultConfig: ZentuiConfig = {
140
+ projectRefreshIntervalMs: 30_000,
468
141
  icons: { mode: "auto", ...NERD_DEFAULT_ICONS },
469
142
  colors: {
470
143
  sessionName: "bold green",
@@ -487,706 +160,52 @@ export const defaultConfig: PolishedTuiConfig = {
487
160
  time: "bold yellow",
488
161
  os: "bold white",
489
162
  },
490
- components: defaultComponents,
491
- footerFormat: defaultStarshipStyle.format,
492
- responsiveFooter: defaultStarshipStyle.responsive,
493
- compactFooterFormat: defaultStarshipStyle.compactFormat,
494
- compactFooterMaxLines: defaultStarshipStyle.compactMaxLines,
495
- editorMetadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
496
- separator: defaultStarshipStyle.separator,
497
- contextStyle: defaultStarshipStyle.contextStyle,
498
- editorModelLabel: defaultComponents.editor.modelLabel,
499
- editorStyle: defaultComponents.editor.style,
500
- editorStyles: { minimalist: defaultMinimalistStyle },
501
- editorBorderColorMode: defaultComponents.editor.borderColorMode,
502
- contextThresholds: defaultStarshipStyle.contextThresholds,
503
- pathDisplay: defaultStarshipStyle.pathDisplay,
504
- gitBranch: defaultStarshipStyle.gitBranch,
505
- features: {
506
- editor: true,
507
- statusLine: true,
508
- viewportIndicators: true,
163
+ components: {
164
+ editor: defaultEditor,
165
+ userMessages: normalizeUserMessages(undefined),
166
+ workingLine: defaultWorkingLine,
167
+ selectorBorders: { enabled: true, style: "zentui" },
168
+ footer: defaultFooter,
509
169
  },
510
- footerSegments: defaultFooterSegments,
511
- gitCommit: defaultStarshipStyle.gitCommit,
512
- gitMetrics: defaultStarshipStyle.gitMetrics,
513
- extensionStatuses: defaultStarshipStyle.extensionStatuses,
514
170
  };
515
171
 
516
- function isRecord(value: unknown): value is ConfigRecord {
517
- return value !== null && typeof value === "object" && !Array.isArray(value);
518
- }
519
-
520
- function parseProjectRefreshIntervalMs(value: unknown): number {
521
- if (value === 0) return 0;
522
- if (typeof value !== "number" || !Number.isFinite(value)) {
523
- return DEFAULT_PROJECT_REFRESH_INTERVAL_MS;
524
- }
525
-
526
- const interval = Math.round(value);
527
- if (interval <= 0) return 0;
528
- return Math.max(MIN_PROJECT_REFRESH_INTERVAL_MS, interval);
529
- }
530
-
531
- function clampPercent(value: number): number {
532
- return Math.max(0, Math.min(100, value));
533
- }
534
-
535
- function parseContextStyle(value: unknown): ContextStyle {
536
- if (value === "text" || value === "gauge" || value === "text+gauge")
537
- return value;
538
- return defaultConfig.contextStyle;
539
- }
540
-
541
- function parseEditorModelLabel(
542
- value: unknown,
543
- fallback: ModelLabelSource = defaultComponents.editor.modelLabel,
544
- ): ModelLabelSource {
545
- if (value === "id" || value === "name") return value;
546
- return fallback;
547
- }
548
-
549
- function parseEditorStyle(value: unknown): EditorStyle {
550
- if (value === "on" || value === "off") return value;
551
- // 迁移:旧 opencode / minimalist 样式统一收敛为 on(minimalist)。
552
- if (value === "opencode" || value === "minimalist") return "on";
553
- return defaultComponents.editor.style;
554
- }
555
-
556
- function parseEditorBorderColorMode(value: unknown): EditorBorderColorMode {
557
- if (value === "static" || value === "adaptive") return value;
558
- return defaultConfig.editorBorderColorMode;
559
- }
560
-
561
- export function isSeparatorStyle(value: unknown): value is SeparatorStyle {
562
- return (
563
- value === "pipe" ||
564
- value === "dot" ||
565
- value === "chevron" ||
566
- value === "none"
567
- );
568
- }
569
-
570
- function parseSeparatorStyle(value: unknown): SeparatorStyle {
571
- return isSeparatorStyle(value) ? value : defaultConfig.separator;
572
- }
573
-
574
- function parseContextThresholds(
575
- value: unknown,
576
- defaults: ContextThresholds = defaultStarshipStyle.contextThresholds,
577
- ): ContextThresholds {
578
- if (!isRecord(value)) return { ...defaults };
579
-
580
- const warningRaw = value.warning;
581
- const errorRaw = value.error;
582
- let warning =
583
- typeof warningRaw === "number" && Number.isFinite(warningRaw)
584
- ? clampPercent(Math.round(warningRaw))
585
- : defaults.warning;
586
- let error =
587
- typeof errorRaw === "number" && Number.isFinite(errorRaw)
588
- ? clampPercent(Math.round(errorRaw))
589
- : defaults.error;
590
- if (error < warning) {
591
- const swapped = warning;
592
- warning = error;
593
- error = swapped;
594
- }
595
- return { warning, error };
596
- }
597
-
598
- function parsePathDisplay(value: unknown): PathDisplayConfig {
599
- const defaults = defaultConfig.pathDisplay;
600
- if (!isRecord(value)) return { ...defaults };
601
- const mode =
602
- value.mode === "full" || value.mode === "basename"
603
- ? value.mode
604
- : defaults.mode;
605
- const rawDepth = value.depth;
606
- const depth =
607
- typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 0
608
- ? Math.min(5, Math.floor(rawDepth))
609
- : defaults.depth;
610
- return { mode, depth };
611
- }
612
-
613
- function normalizeGitBranchMaxLength(value: unknown): GitBranchMaxLength {
614
- if (value === "full") return value;
615
- if (typeof value === "number" && Number.isInteger(value) && value > 0)
616
- return value;
617
- return defaultConfig.gitBranch.maxLength;
618
- }
619
-
620
- function parseGitBranchConfig(value: unknown): GitBranchConfig {
621
- const defaults = defaultConfig.gitBranch;
622
- if (!isRecord(value)) return { ...defaults };
623
- return {
624
- maxLength: normalizeGitBranchMaxLength(value.maxLength),
625
- };
626
- }
627
-
628
- function stringValue(
629
- record: Record<string, unknown>,
630
- key: string,
631
- ): string | undefined {
632
- const value = record[key];
633
- return typeof value === "string" ? value : undefined;
634
- }
635
-
636
- function parseCompactFooterMaxLines(value: unknown): CompactFooterMaxLines {
637
- return value === 1 || value === 2 || value === 3 || value === "unlimited"
638
- ? value
639
- : 2;
640
- }
641
-
642
- function colorValue(
643
- record: Record<string, unknown>,
644
- key: string,
645
- ): string | undefined {
646
- const value = stringValue(record, key);
647
- return value !== undefined && isSupportedColorSpec(value) ? value : undefined;
648
- }
649
-
650
- function definedColors(
651
- colors: Partial<
652
- Record<keyof PolishedTuiConfig["colors"], string | undefined>
653
- >,
654
- ): Partial<PolishedTuiConfig["colors"]> {
655
- return Object.fromEntries(
656
- Object.entries(colors).filter(
657
- (entry): entry is [keyof PolishedTuiConfig["colors"], string] =>
658
- typeof entry[1] === "string",
659
- ),
660
- ) as Partial<PolishedTuiConfig["colors"]>;
661
- }
662
-
663
- function normalizeIconOverrides(
664
- record: Record<string, unknown>,
665
- ): Partial<IconGlyphs> {
666
- return Object.fromEntries(
667
- ICON_GLYPH_KEYS.flatMap((key) => {
668
- const value = stringValue(record, key);
669
- return value === undefined ? [] : [[key, value]];
670
- }),
671
- ) as Partial<IconGlyphs>;
672
- }
673
-
674
- function normalizeColors(
675
- record: Record<string, unknown>,
676
- ): Partial<PolishedTuiConfig["colors"]> {
677
- return definedColors({
678
- cwd: colorValue(record, "cwd"),
679
- sessionName: colorValue(record, "sessionName"),
680
- gitBranch: colorValue(record, "gitBranch"),
681
- gitStatus: colorValue(record, "gitStatus"),
682
- contextNormal: colorValue(record, "contextNormal"),
683
- contextWarning: colorValue(record, "contextWarning"),
684
- contextError: colorValue(record, "contextError"),
685
- tokens: colorValue(record, "tokens"),
686
- cost: colorValue(record, "cost"),
687
- separator: colorValue(record, "separator"),
688
- runtimePrefix: colorValue(record, "runtimePrefix"),
689
- extensionStatus: colorValue(record, "extensionStatus"),
690
- sessionDuration: colorValue(record, "sessionDuration"),
691
- packageVersion: colorValue(record, "packageVersion"),
692
- gitCommit: colorValue(record, "gitCommit"),
693
- gitMetricsAdded: colorValue(record, "gitMetricsAdded"),
694
- gitMetricsDeleted: colorValue(record, "gitMetricsDeleted"),
695
- username: colorValue(record, "username"),
696
- time: colorValue(record, "time"),
697
- os: colorValue(record, "os"),
698
- editorAccent: colorValue(record, "editorAccent"),
699
- editorBorder: colorValue(record, "editorBorder"),
700
- editorGitBranch: colorValue(record, "editorGitBranch"),
701
- editorModel: colorValue(record, "editorModel"),
702
- editorProvider: colorValue(record, "editorProvider"),
703
- editorThinking: colorValue(record, "editorThinking"),
704
- editorThinkingMinimal: colorValue(record, "editorThinkingMinimal"),
705
- editorThinkingLow: colorValue(record, "editorThinkingLow"),
706
- editorThinkingMedium: colorValue(record, "editorThinkingMedium"),
707
- editorThinkingHigh: colorValue(record, "editorThinkingHigh"),
708
- editorThinkingXhigh: colorValue(record, "editorThinkingXhigh"),
709
- editorThinkingMax: colorValue(record, "editorThinkingMax"),
710
- workingLineLow: colorValue(record, "workingLineLow"),
711
- workingLineMid: colorValue(record, "workingLineMid"),
712
- workingLineHigh: colorValue(record, "workingLineHigh"),
713
- });
714
- }
715
-
716
- /** Clamp hashLength to Git's valid abbreviation range [4, 40]. */
717
- function normalizeGitHashLength(value: unknown): number {
718
- const parsed = typeof value === "number" ? value : Number(value);
719
- if (!Number.isFinite(parsed)) return defaultConfig.gitCommit.hashLength;
720
- const rounded = Math.round(parsed);
721
- return Math.min(40, Math.max(4, rounded));
722
- }
723
-
724
- function normalizeGitCommitConfig(
725
- record: Record<string, unknown>,
726
- ): GitCommitConfig {
727
- return {
728
- hashLength: normalizeGitHashLength(record.hashLength),
729
- onlyDetached:
730
- typeof record.onlyDetached === "boolean"
731
- ? record.onlyDetached
732
- : defaultConfig.gitCommit.onlyDetached,
733
- showTag:
734
- typeof record.showTag === "boolean"
735
- ? record.showTag
736
- : defaultConfig.gitCommit.showTag,
737
- };
738
- }
739
-
740
- function normalizeGitMetricsConfig(
741
- record: Record<string, unknown>,
742
- ): GitMetricsConfig {
743
- return {
744
- onlyNonzero:
745
- typeof record.onlyNonzero === "boolean"
746
- ? record.onlyNonzero
747
- : defaultConfig.gitMetrics.onlyNonzero,
748
- ignoreSubmodules:
749
- typeof record.ignoreSubmodules === "boolean"
750
- ? record.ignoreSubmodules
751
- : defaultConfig.gitMetrics.ignoreSubmodules,
752
- };
753
- }
754
-
755
- export function isExtensionStatusPlacement(
756
- value: unknown,
757
- ): value is ExtensionStatusPlacement {
758
- return (
759
- value === "off" ||
760
- value === "left" ||
761
- value === "middle" ||
762
- value === "right"
763
- );
764
- }
765
-
766
- export function isExtensionStatusColorMode(
767
- value: unknown,
768
- ): value is ExtensionStatusColorMode {
769
- return value === "zentui" || value === "original";
770
- }
771
-
772
- function normalizeExtensionStatuses(
773
- record: Record<string, unknown>,
774
- ): ExtensionStatusesConfig {
775
- const defaultPlacement = isExtensionStatusPlacement(record.defaultPlacement)
776
- ? record.defaultPlacement
777
- : defaultConfig.extensionStatuses.defaultPlacement;
778
- const placements = isRecord(record.placements)
779
- ? Object.fromEntries(
780
- Object.entries(record.placements).filter(
781
- (entry): entry is [string, ExtensionStatusPlacement] =>
782
- isExtensionStatusPlacement(entry[1]),
783
- ),
784
- )
785
- : {};
786
- const colorModes = isRecord(record.colorModes)
787
- ? Object.fromEntries(
788
- Object.entries(record.colorModes).filter(
789
- (entry): entry is [string, ExtensionStatusColorMode] =>
790
- isExtensionStatusColorMode(entry[1]),
791
- ),
792
- )
793
- : {};
794
-
795
- return {
796
- defaultPlacement,
797
- placements,
798
- colorModes,
799
- };
800
- }
801
-
802
- function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
803
- return (
804
- value === "editor" ||
805
- value === "statusLine" ||
806
- value === "viewportIndicators"
807
- );
808
- }
809
-
810
- function isFooterSegmentKey(
811
- value: string,
812
- ): value is keyof FooterSegmentsConfig {
813
- return (
814
- value === "cwd" ||
815
- value === "sessionName" ||
816
- value === "gitBranch" ||
817
- value === "gitStatus" ||
818
- value === "gitCounts" ||
819
- value === "runtime" ||
820
- value === "modelInfo" ||
821
- value === "context" ||
822
- value === "tokens" ||
823
- value === "cost" ||
824
- value === "sessionDuration" ||
825
- value === "username" ||
826
- value === "time" ||
827
- value === "os" ||
828
- value === "packageVersion" ||
829
- value === "gitCommit" ||
830
- value === "gitMetrics"
831
- );
832
- }
833
-
834
- function validUiFeatureEntries(
835
- record: Record<string, unknown>,
836
- ): Partial<UiFeaturesConfig> {
837
- return Object.fromEntries(
838
- Object.entries(record).filter(
839
- (entry): entry is [keyof UiFeaturesConfig, boolean] => {
840
- const [key, value] = entry;
841
- return isUiFeatureKey(key) && typeof value === "boolean";
842
- },
843
- ),
844
- ) as Partial<UiFeaturesConfig>;
845
- }
846
-
847
- function validFooterSegmentEntries(
848
- record: Record<string, unknown>,
849
- ): Partial<FooterSegmentsConfig> {
850
- return Object.fromEntries(
851
- Object.entries(record).filter(
852
- (entry): entry is [keyof FooterSegmentsConfig, boolean] => {
853
- const [key, value] = entry;
854
- return isFooterSegmentKey(key) && typeof value === "boolean";
855
- },
856
- ),
857
- ) as Partial<FooterSegmentsConfig>;
858
- }
859
-
860
- /** Applies one canonical mutation through the shared config writer. */
861
- function mutateConfig(
862
- path: string,
863
- mutate: (record: ConfigRecord) => void,
864
- ): PolishedTuiConfig {
865
- const update = (record: ConfigRecord): void => {
866
- record.version = 1;
867
- mutate(record);
868
- };
869
- const record =
870
- path === configPath
871
- ? configStore.update(update)
872
- : mutateConfigFile(path, update);
873
- return mergeConfig(record);
874
- }
875
-
876
- export function ensureConfigExists(_path = configPath): void {
877
- // Intentionally left as a no-op. Runtime defaults come from `mergeConfig({})`;
878
- // persist the canonical file only after the user changes a setting.
879
- }
880
-
881
- function hasOwn(record: ConfigRecord, key: string): boolean {
882
- return Object.hasOwn(record, key);
883
- }
884
-
885
- function recordValue(value: unknown): ConfigRecord {
886
- return isRecord(value) ? value : {};
887
- }
888
-
889
- function parseBoolean(value: unknown, fallback: boolean): boolean {
890
- return typeof value === "boolean" ? value : fallback;
891
- }
892
-
893
- function parseSelectorBorderStyle(value: unknown): SelectorBorderStyle {
894
- return value === "zentui" ? value : "zentui";
895
- }
896
-
897
- function parseFooterStyle(value: unknown): FooterStyle {
898
- return value === "native" || value === "starship" || value === "hidden"
899
- ? value
900
- : defaultComponents.footer.style;
901
- }
902
-
903
- function parseUserMessageStyle(value: unknown): UserMessageStyle {
904
- return value === "framed" ||
905
- value === "framed-copy-friendly" ||
906
- value === "compact" ||
907
- value === "labeled"
908
- ? value
909
- : defaultComponents.userMessages.style;
910
- }
911
-
912
- function parseNonEmptyString(value: unknown, fallback: string): string {
913
- return typeof value === "string" && value.length > 0 ? value : fallback;
914
- }
915
-
916
- function resolveContextThresholds(
917
- value: unknown,
918
- defaults: ContextThresholds,
919
- ): ContextThresholds {
920
- return parseContextThresholds(value, defaults);
921
- }
922
-
923
- function resolvePathDisplay(value: unknown): PathDisplayConfig {
924
- return parsePathDisplay(value);
925
- }
926
-
927
- function resolveGitBranch(value: unknown): GitBranchConfig {
928
- return parseGitBranchConfig(value);
929
- }
930
-
931
- function resolveGitCommit(value: unknown): GitCommitConfig {
932
- return normalizeGitCommitConfig(recordValue(value));
933
- }
934
-
935
- function resolveGitMetrics(value: unknown): GitMetricsConfig {
936
- return normalizeGitMetricsConfig(recordValue(value));
937
- }
938
-
939
- function resolveExtensionStatuses(value: unknown): ExtensionStatusesConfig {
940
- return normalizeExtensionStatuses(recordValue(value));
941
- }
942
-
943
- const FOOTER_SEGMENT_KEYS = Object.keys(defaultFooterSegments) as Array<
944
- keyof FooterSegmentsConfig
945
- >;
946
-
947
- function resolveFooterSegments(value: unknown): FooterSegmentsConfig {
948
- const record = recordValue(value);
949
- return Object.fromEntries(
950
- FOOTER_SEGMENT_KEYS.map((key) => [
951
- key,
952
- parseBoolean(record[key], defaultFooterSegments[key]),
953
- ]),
954
- ) as FooterSegmentsConfig;
955
- }
956
-
957
- function resolveWorkingLineMessages(
958
- messages: ConfigRecord,
959
- ): WorkingLineMessagesConfig {
960
- return {
961
- custom: parseBoolean(messages.custom, true),
962
- values: hasOwn(messages, "values")
963
- ? normalizeWorkingLineMessages(messages.values)
964
- : [...PI_WORKING_LINE_MESSAGES],
965
- };
966
- }
967
-
968
- function resolveComponents(config: ConfigRecord): ComponentsConfig {
969
- const components = recordValue(config.components);
970
- const editor = recordValue(components.editor);
971
- const editorStyles = recordValue(editor.styles);
972
- const minimalist = recordValue(editorStyles.minimalist);
973
- const userMessages = recordValue(components.userMessages);
974
- const workingLine = recordValue(components.workingLine);
975
- const workingLineMessages = recordValue(workingLine.messages);
976
- const workingLineSegments = recordValue(workingLine.segments);
977
- const selectorBorders = recordValue(components.selectorBorders);
978
- const footer = recordValue(components.footer);
979
- const footerStyles = recordValue(footer.styles);
980
- const starship = recordValue(footerStyles.starship);
981
-
982
- return {
983
- editor: {
984
- // 迁移:旧 enabled:false 收敛为 style:"off"(enabled 字段已移除)。
985
- style:
986
- hasOwn(editor, "enabled") &&
987
- parseBoolean(editor.enabled, true) === false
988
- ? "off"
989
- : parseEditorStyle(editor.style),
990
- borderColorMode: parseEditorBorderColorMode(editor.borderColorMode),
991
- modelLabel: parseEditorModelLabel(
992
- editor.modelLabel,
993
- defaultComponents.editor.modelLabel,
994
- ),
995
- viewportIndicators: parseBoolean(
996
- editor.viewportIndicators,
997
- defaultComponents.editor.viewportIndicators,
998
- ),
999
- styles: {
1000
- minimalist: {
1001
- pathDisplay:
1002
- minimalist.pathDisplay === "compact" ||
1003
- minimalist.pathDisplay === "project" ||
1004
- minimalist.pathDisplay === "full"
1005
- ? minimalist.pathDisplay
1006
- : defaultMinimalistStyle.pathDisplay,
1007
- showCwd: parseBoolean(
1008
- minimalist.showCwd,
1009
- defaultMinimalistStyle.showCwd,
1010
- ),
1011
- showSessionName: parseBoolean(
1012
- minimalist.showSessionName,
1013
- defaultMinimalistStyle.showSessionName,
1014
- ),
1015
- showTimer: parseBoolean(
1016
- minimalist.showTimer,
1017
- defaultMinimalistStyle.showTimer,
1018
- ),
1019
- showCost: parseBoolean(
1020
- minimalist.showCost,
1021
- defaultMinimalistStyle.showCost,
1022
- ),
1023
- showGit: parseBoolean(
1024
- minimalist.showGit,
1025
- defaultMinimalistStyle.showGit,
1026
- ),
1027
- },
1028
- },
1029
- },
1030
- userMessages: {
1031
- enabled: parseBoolean(
1032
- userMessages.enabled,
1033
- defaultComponents.userMessages.enabled,
1034
- ),
1035
- style: parseUserMessageStyle(userMessages.style),
1036
- styles: {
1037
- framed: {},
1038
- "framed-copy-friendly": {},
1039
- compact: {},
1040
- labeled: {},
1041
- },
1042
- },
1043
- workingLine: {
1044
- enabled: parseBoolean(
1045
- workingLine.enabled,
1046
- defaultComponents.workingLine.enabled,
1047
- ),
1048
- turnSummary: parseBoolean(
1049
- workingLine.turnSummary,
1050
- defaultComponents.workingLine.turnSummary,
1051
- ),
1052
- spinner:
1053
- workingLine.spinner === "braille" ||
1054
- workingLine.spinner === "star-bloom" ||
1055
- workingLine.spinner === "pinwheel" ||
1056
- workingLine.spinner === "claude-inspired" ||
1057
- workingLine.spinner === "pulse"
1058
- ? workingLine.spinner
1059
- : defaultComponents.workingLine.spinner,
1060
- spinnerIntervalMs: isValidWorkingLineIntervalMs(
1061
- workingLine.spinnerIntervalMs,
1062
- )
1063
- ? workingLine.spinnerIntervalMs
1064
- : defaultComponents.workingLine.spinnerIntervalMs,
1065
- animateSpinnerColor: parseBoolean(
1066
- workingLine.animateSpinnerColor,
1067
- defaultComponents.workingLine.animateSpinnerColor,
1068
- ),
1069
- textIntervalMs: isValidWorkingLineIntervalMs(workingLine.textIntervalMs)
1070
- ? workingLine.textIntervalMs
1071
- : defaultComponents.workingLine.textIntervalMs,
1072
- textAnimation:
1073
- workingLine.textAnimation === "classic" ||
1074
- workingLine.textAnimation === "kitt" ||
1075
- workingLine.textAnimation === "disabled"
1076
- ? workingLine.textAnimation
1077
- : defaultComponents.workingLine.textAnimation,
1078
- messages: resolveWorkingLineMessages(workingLineMessages),
1079
- segments: {
1080
- tool: parseBoolean(
1081
- workingLineSegments.tool,
1082
- defaultComponents.workingLine.segments.tool,
1083
- ),
1084
- elapsed: parseBoolean(
1085
- workingLineSegments.elapsed,
1086
- defaultComponents.workingLine.segments.elapsed,
1087
- ),
1088
- thought: parseBoolean(
1089
- workingLineSegments.thought,
1090
- defaultComponents.workingLine.segments.thought,
1091
- ),
1092
- tokens: parseBoolean(
1093
- workingLineSegments.tokens,
1094
- defaultComponents.workingLine.segments.tokens,
1095
- ),
1096
- },
1097
- },
1098
- selectorBorders: {
1099
- enabled: parseBoolean(
1100
- selectorBorders.enabled,
1101
- defaultComponents.selectorBorders.enabled,
1102
- ),
1103
- style: parseSelectorBorderStyle(selectorBorders.style),
1104
- },
1105
- footer: {
1106
- style: parseFooterStyle(footer.style),
1107
- modelLabel: parseEditorModelLabel(
1108
- footer.modelLabel,
1109
- defaultComponents.footer.modelLabel,
1110
- ),
1111
- styles: {
1112
- starship: {
1113
- format:
1114
- typeof starship.format === "string"
1115
- ? starship.format
1116
- : defaultStarshipStyle.format,
1117
- responsive: parseBoolean(
1118
- starship.responsive,
1119
- defaultStarshipStyle.responsive,
1120
- ),
1121
- compactFormat: parseNonEmptyString(
1122
- starship.compactFormat,
1123
- defaultStarshipStyle.compactFormat,
1124
- ),
1125
- compactMaxLines: parseCompactFooterMaxLines(starship.compactMaxLines),
1126
- separator: parseSeparatorStyle(starship.separator),
1127
- contextStyle: parseContextStyle(starship.contextStyle),
1128
- contextThresholds: resolveContextThresholds(
1129
- starship.contextThresholds,
1130
- defaultStarshipStyle.contextThresholds,
1131
- ),
1132
- pathDisplay: resolvePathDisplay(starship.pathDisplay),
1133
- segments: resolveFooterSegments(starship.segments),
1134
- gitBranch: resolveGitBranch(starship.gitBranch),
1135
- gitCommit: resolveGitCommit(starship.gitCommit),
1136
- gitMetrics: resolveGitMetrics(starship.gitMetrics),
1137
- extensionStatuses: resolveExtensionStatuses(
1138
- starship.extensionStatuses,
1139
- ),
1140
- },
1141
- },
1142
- },
1143
- };
1144
- }
1145
-
1146
- function derivedRuntimeView(config: ZentuiConfig): PolishedTuiConfig {
1147
- const starship = config.components.footer.styles.starship;
1148
- const minimalist = config.components.editor.styles.minimalist;
1149
- return {
1150
- ...config,
1151
- footerFormat: starship.format,
1152
- responsiveFooter: starship.responsive,
1153
- compactFooterFormat: starship.compactFormat,
1154
- compactFooterMaxLines: starship.compactMaxLines,
1155
- separator: starship.separator,
1156
- contextStyle: starship.contextStyle,
1157
- contextThresholds: starship.contextThresholds,
1158
- pathDisplay: starship.pathDisplay,
1159
- gitBranch: starship.gitBranch,
1160
- footerSegments: starship.segments,
1161
- gitCommit: starship.gitCommit,
1162
- gitMetrics: starship.gitMetrics,
1163
- extensionStatuses: starship.extensionStatuses,
1164
- editorStyle: config.components.editor.style,
1165
- editorStyles: { minimalist },
1166
- editorMetadataFormat: DEFAULT_EDITOR_METADATA_FORMAT,
1167
- editorBorderColorMode: config.components.editor.borderColorMode,
1168
- editorModelLabel: config.components.editor.modelLabel,
1169
- features: {
1170
- editor: config.components.editor.style === "on",
1171
- statusLine: config.components.footer.style === "starship",
1172
- viewportIndicators: config.components.editor.viewportIndicators,
1173
- },
1174
- };
1175
- }
1176
-
1177
- const unsupportedComponentStyles = new WeakMap<
1178
- ZentuiConfig,
1179
- ReadonlySet<ComponentStyleOwner>
1180
- >();
1181
-
1182
- // Retired selections migrate to the sole "on" (minimalist) style instead of
1183
- // disabling the custom Editor. opencode itself is retired along with its variants.
1184
- const retiredEditorStyleIds: ReadonlySet<string> = new Set([
1185
- "opencode",
1186
- "opencode-copy-friendly",
1187
- "accent-rail",
1188
- ]);
1189
-
172
+ const colorKeys: readonly (keyof PolishedTuiColors)[] = [
173
+ "cwd",
174
+ "sessionName",
175
+ "gitBranch",
176
+ "gitStatus",
177
+ "contextNormal",
178
+ "contextWarning",
179
+ "contextError",
180
+ "tokens",
181
+ "cost",
182
+ "separator",
183
+ "runtimePrefix",
184
+ "extensionStatus",
185
+ "sessionDuration",
186
+ "packageVersion",
187
+ "gitCommit",
188
+ "gitMetricsAdded",
189
+ "gitMetricsDeleted",
190
+ "username",
191
+ "time",
192
+ "os",
193
+ "editorAccent",
194
+ "editorBorder",
195
+ "editorGitBranch",
196
+ "editorModel",
197
+ "editorProvider",
198
+ "editorThinking",
199
+ "editorThinkingMinimal",
200
+ "editorThinkingLow",
201
+ "editorThinkingMedium",
202
+ "editorThinkingHigh",
203
+ "editorThinkingXhigh",
204
+ "editorThinkingMax",
205
+ "workingLineLow",
206
+ "workingLineMid",
207
+ "workingLineHigh",
208
+ ];
1190
209
  const knownComponentStyleIds: Record<
1191
210
  ComponentStyleOwner,
1192
211
  ReadonlySet<string>
@@ -1201,14 +220,24 @@ const knownComponentStyleIds: Record<
1201
220
  selectorBorders: new Set(["zentui"]),
1202
221
  footer: new Set(["native", "starship", "hidden"]),
1203
222
  };
223
+ const retiredEditorStyleIds = new Set([
224
+ "opencode",
225
+ "opencode-copy-friendly",
226
+ "accent-rail",
227
+ ]);
228
+ const unsupportedComponentStyles = new WeakMap<
229
+ ZentuiConfig,
230
+ ReadonlySet<ComponentStyleOwner>
231
+ >();
232
+ const styleOwners = Object.keys(
233
+ knownComponentStyleIds,
234
+ ) as ComponentStyleOwner[];
1204
235
 
1205
236
  function unsupportedSelectedStyleId(
1206
237
  record: ConfigRecord,
1207
238
  owner: ComponentStyleOwner,
1208
239
  ): string | undefined {
1209
- const component = recordValue(recordValue(record.components)[owner]);
1210
- if (!hasOwn(component, "style")) return undefined;
1211
- const style = component.style;
240
+ const style = recordValue(recordValue(record.components)[owner]).style;
1212
241
  return typeof style === "string" &&
1213
242
  style.trim() &&
1214
243
  !knownComponentStyleIds[owner].has(style) &&
@@ -1224,38 +253,69 @@ export function hasUnsupportedComponentStyle(
1224
253
  return unsupportedComponentStyles.get(config)?.has(owner) ?? false;
1225
254
  }
1226
255
 
1227
- export function mergeConfig(parsed: unknown): PolishedTuiConfig {
1228
- const config = isRecord(parsed) ? parsed : {};
1229
- const iconsRecord = recordValue(config.icons);
1230
- const colorsRecord = recordValue(config.colors);
1231
- const colors = normalizeColors(colorsRecord);
1232
- const canonical: ZentuiConfig = {
1233
- projectRefreshIntervalMs: parseProjectRefreshIntervalMs(
1234
- config.projectRefreshIntervalMs,
1235
- ),
256
+ function resolveComponents(value: unknown): ComponentsConfig {
257
+ const source = recordValue(value);
258
+ const selector = recordValue(source.selectorBorders);
259
+ return {
260
+ editor: normalizeEditor(source.editor),
261
+ userMessages: normalizeUserMessages(source.userMessages),
262
+ workingLine: normalizeWorkingLine(source.workingLine),
263
+ selectorBorders: {
264
+ enabled: booleanValue(selector.enabled, true),
265
+ style: "zentui",
266
+ },
267
+ footer: normalizeFooter(source.footer),
268
+ };
269
+ }
270
+
271
+ /** 各配置领域在此组合,运行时仅使用 canonical 结构。 */
272
+ export function mergeConfig(parsed: unknown): ZentuiConfig {
273
+ const record = recordValue(parsed);
274
+ const icons = recordValue(record.icons);
275
+ const colors = recordValue(record.colors);
276
+ const interval = record.projectRefreshIntervalMs;
277
+ const config: ZentuiConfig = {
278
+ projectRefreshIntervalMs:
279
+ typeof interval === "number" && Number.isFinite(interval)
280
+ ? Math.round(interval) <= 0
281
+ ? 0
282
+ : Math.max(5_000, Math.round(interval))
283
+ : 30_000,
1236
284
  icons: resolveConfiguredIcons(
1237
- normalizeIconMode(iconsRecord.mode),
1238
- normalizeIconOverrides(iconsRecord),
285
+ normalizeIconMode(icons.mode),
286
+ Object.fromEntries(
287
+ ICON_GLYPH_KEYS.filter((key) => typeof icons[key] === "string").map(
288
+ (key) => [key, icons[key]],
289
+ ),
290
+ ) as Partial<IconGlyphs>,
1239
291
  ),
1240
292
  colors: {
1241
293
  ...defaultConfig.colors,
1242
- ...colors,
294
+ ...Object.fromEntries(
295
+ colorKeys
296
+ .filter(
297
+ (key) =>
298
+ typeof colors[key] === "string" &&
299
+ isSupportedColorSpec(colors[key]),
300
+ )
301
+ .map((key) => [key, colors[key]]),
302
+ ),
1243
303
  },
1244
- components: resolveComponents(config),
304
+ components: resolveComponents(record.components),
1245
305
  };
1246
- const view = derivedRuntimeView(canonical);
1247
- const unsupported = new Set<ComponentStyleOwner>();
1248
- for (const owner of [
1249
- "editor",
1250
- "userMessages",
1251
- "selectorBorders",
1252
- "footer",
1253
- ] as const) {
1254
- if (unsupportedSelectedStyleId(config, owner) !== undefined)
1255
- unsupported.add(owner);
306
+ const unsupported = new Set(
307
+ styleOwners.filter(
308
+ (owner) => unsupportedSelectedStyleId(record, owner) !== undefined,
309
+ ),
310
+ );
311
+ if (unsupported.size > 0) {
312
+ unsupportedComponentStyles.set(config, unsupported);
1256
313
  }
1257
- if (unsupported.size > 0) unsupportedComponentStyles.set(view, unsupported);
1258
- return view;
314
+ return config;
315
+ }
316
+
317
+ export function loadConfig(): ZentuiConfig {
318
+ return mergeConfig(configStore.read());
1259
319
  }
1260
320
 
1261
321
  export function getExtensionStatusPlacement(
@@ -1263,113 +323,78 @@ export function getExtensionStatusPlacement(
1263
323
  key: string,
1264
324
  ): ExtensionStatusPlacement {
1265
325
  const statuses = config.components.footer.styles.starship.extensionStatuses;
1266
- if (Object.hasOwn(statuses.placements, key)) {
1267
- const placement = statuses.placements[key];
1268
- if (isExtensionStatusPlacement(placement)) return placement;
326
+ if (
327
+ Object.hasOwn(statuses.placements, key) &&
328
+ isExtensionStatusPlacement(statuses.placements[key])
329
+ ) {
330
+ return statuses.placements[key];
1269
331
  }
1270
332
  return isExtensionStatusPlacement(statuses.defaultPlacement)
1271
333
  ? statuses.defaultPlacement
1272
- : DEFAULT_EXTENSION_STATUS_PLACEMENT;
334
+ : "right";
1273
335
  }
1274
336
 
1275
337
  export function getExtensionStatusColorMode(
1276
338
  config: ZentuiConfig,
1277
339
  key: string,
1278
340
  ): ExtensionStatusColorMode {
1279
- const colorModes =
341
+ const modes =
1280
342
  config.components.footer.styles.starship.extensionStatuses.colorModes;
1281
- if (Object.hasOwn(colorModes, key)) {
1282
- const colorMode = colorModes[key];
1283
- if (isExtensionStatusColorMode(colorMode)) return colorMode;
1284
- }
1285
- return DEFAULT_EXTENSION_STATUS_COLOR_MODE;
343
+ return Object.hasOwn(modes, key) && isExtensionStatusColorMode(modes[key])
344
+ ? modes[key]
345
+ : "zentui";
1286
346
  }
1287
347
 
1288
- /**
1289
- * Loads and normalizes the shared raw configuration for shell consumers.
1290
- */
1291
- export function loadConfig(): PolishedTuiConfig {
1292
- try {
1293
- return mergeConfig(configStore.read());
1294
- } catch {
1295
- return mergeConfig({});
1296
- }
1297
- }
1298
-
1299
- function overlayKnown(raw: unknown, known: unknown): unknown {
1300
- if (!isRecord(known)) return known;
1301
- const output: ConfigRecord = isRecord(raw) ? { ...raw } : {};
1302
- for (const [key, value] of Object.entries(known)) {
1303
- Object.defineProperty(output, key, {
1304
- value: overlayKnown(output[key], value),
1305
- enumerable: true,
1306
- configurable: true,
1307
- writable: true,
1308
- });
348
+ /** 纯配置修改可在一次 ConfigStore.update 中组合成 Preset。 */
349
+ export function mutateComponentsRecord(
350
+ record: ConfigRecord,
351
+ update: (components: ComponentsConfig) => void,
352
+ replacedStyles: readonly ComponentStyleOwner[] = [],
353
+ ): void {
354
+ const preserved = styleOwners.map(
355
+ (owner) => [owner, unsupportedSelectedStyleId(record, owner)] as const,
356
+ );
357
+ const components = resolveComponents(record.components);
358
+ update(components);
359
+ record.version = 1;
360
+ const merged = recordValue(
361
+ overlayKnown(record.components, resolveComponents(components)),
362
+ );
363
+ for (const [owner, style] of preserved) {
364
+ if (style !== undefined && !replacedStyles.includes(owner)) {
365
+ recordValue(merged[owner]).style = style;
366
+ }
1309
367
  }
1310
- return output;
368
+ record.components = merged;
1311
369
  }
1312
370
 
1313
- type PreservedStyleIds = Partial<Record<ComponentStyleOwner, string>>;
1314
-
1315
- function unknownSelectedStyleIds(record: ConfigRecord): PreservedStyleIds {
1316
- return {
1317
- editor: unsupportedSelectedStyleId(record, "editor"),
1318
- userMessages: unsupportedSelectedStyleId(record, "userMessages"),
1319
- selectorBorders: unsupportedSelectedStyleId(record, "selectorBorders"),
1320
- footer: unsupportedSelectedStyleId(record, "footer"),
371
+ function mutateConfig(
372
+ path: string,
373
+ update: (record: ConfigRecord) => void,
374
+ ): ZentuiConfig {
375
+ const mutate = (record: ConfigRecord) => {
376
+ record.version = 1;
377
+ update(record);
1321
378
  };
1322
- }
1323
-
1324
- function restoreUnknownSelectedStyleIds(
1325
- record: ConfigRecord,
1326
- preserved: PreservedStyleIds,
1327
- replacedStyle?: ComponentStyleOwner,
1328
- ): void {
1329
- const components = recordValue(record.components);
1330
- for (const [owner, style] of Object.entries(preserved) as [
1331
- ComponentStyleOwner,
1332
- string,
1333
- ][]) {
1334
- if (style === undefined || owner === replacedStyle) continue;
1335
- const component = recordValue(components[owner]);
1336
- component.style = style;
1337
- components[owner] = component;
1338
- }
1339
- record.components = components;
379
+ return mergeConfig(
380
+ path === configPath
381
+ ? configStore.update(mutate)
382
+ : mutateConfigFile(path, mutate),
383
+ );
1340
384
  }
1341
385
 
1342
386
  function saveComponentsMutation(
1343
387
  update: (components: ComponentsConfig) => void,
1344
388
  path: string,
1345
389
  replacedStyle?: ComponentStyleOwner,
1346
- ): PolishedTuiConfig {
1347
- return mutateConfig(path, (record) => {
1348
- const preservedStyles = unknownSelectedStyleIds(record);
1349
- const components = mergeConfig(record).components;
1350
- update(components);
1351
- const normalized = resolveComponents({ components });
1352
- record.components = overlayKnown(record.components, normalized);
1353
- restoreUnknownSelectedStyleIds(record, preservedStyles, replacedStyle);
1354
- });
1355
- }
1356
-
1357
- function applyEditorComponentPatch(
1358
- component: EditorComponentConfig,
1359
- patch: Partial<
1360
- Pick<
1361
- EditorComponentConfig,
1362
- "style" | "borderColorMode" | "modelLabel" | "viewportIndicators"
1363
- >
1364
- >,
1365
- ): void {
1366
- if (patch.style !== undefined) component.style = patch.style;
1367
- if (patch.borderColorMode !== undefined)
1368
- component.borderColorMode = patch.borderColorMode;
1369
- if (patch.modelLabel !== undefined) component.modelLabel = patch.modelLabel;
1370
- if (patch.viewportIndicators !== undefined) {
1371
- component.viewportIndicators = patch.viewportIndicators;
1372
- }
390
+ ): ZentuiConfig {
391
+ return mutateConfig(path, (record) =>
392
+ mutateComponentsRecord(
393
+ record,
394
+ update,
395
+ replacedStyle ? [replacedStyle] : [],
396
+ ),
397
+ );
1373
398
  }
1374
399
 
1375
400
  export function saveEditorComponentPatch(
@@ -1380,236 +405,173 @@ export function saveEditorComponentPatch(
1380
405
  >
1381
406
  >,
1382
407
  path = configPath,
1383
- ): PolishedTuiConfig {
408
+ ): ZentuiConfig {
1384
409
  return saveComponentsMutation(
1385
- (components) => applyEditorComponentPatch(components.editor, patch),
410
+ (components) => {
411
+ components.editor = overlayKnown(
412
+ components.editor,
413
+ patch,
414
+ ) as EditorComponentConfig;
415
+ },
1386
416
  path,
1387
417
  patch.style !== undefined ? "editor" : undefined,
1388
418
  );
1389
419
  }
1390
-
1391
- function applyMinimalistStylePatch(
1392
- style: MinimalistEditorStyleConfig,
1393
- patch: Partial<MinimalistEditorStyleConfig>,
1394
- ): void {
1395
- if (patch.pathDisplay !== undefined) style.pathDisplay = patch.pathDisplay;
1396
- if (patch.showCwd !== undefined) {
1397
- style.showCwd = patch.showCwd;
1398
- }
1399
- if (patch.showSessionName !== undefined)
1400
- style.showSessionName = patch.showSessionName;
1401
- if (patch.showTimer !== undefined) style.showTimer = patch.showTimer;
1402
- if (patch.showCost !== undefined) style.showCost = patch.showCost;
1403
- if (patch.showGit !== undefined) style.showGit = patch.showGit;
1404
- }
1405
-
1406
420
  export function saveMinimalistEditorStylePatch(
1407
421
  patch: Partial<MinimalistEditorStyleConfig>,
1408
422
  path = configPath,
1409
- ): PolishedTuiConfig {
1410
- return saveComponentsMutation(
1411
- (components) =>
1412
- applyMinimalistStylePatch(components.editor.styles.minimalist, patch),
1413
- path,
1414
- );
423
+ ): ZentuiConfig {
424
+ return saveComponentsMutation((components) => {
425
+ components.editor.styles.minimalist = overlayKnown(
426
+ components.editor.styles.minimalist,
427
+ patch,
428
+ ) as MinimalistEditorStyleConfig;
429
+ }, path);
1415
430
  }
1416
-
1417
431
  export function saveUserMessagesComponentPatch(
1418
432
  patch: Partial<Pick<UserMessagesComponentConfig, "enabled" | "style">>,
1419
433
  path = configPath,
1420
- ): PolishedTuiConfig {
434
+ ): ZentuiConfig {
1421
435
  return saveComponentsMutation(
1422
436
  (components) => {
1423
- const component = components.userMessages;
1424
- if (patch.enabled !== undefined) component.enabled = patch.enabled;
1425
- if (patch.style !== undefined) component.style = patch.style;
437
+ components.userMessages = overlayKnown(
438
+ components.userMessages,
439
+ patch,
440
+ ) as UserMessagesComponentConfig;
1426
441
  },
1427
442
  path,
1428
443
  patch.style !== undefined ? "userMessages" : undefined,
1429
444
  );
1430
445
  }
1431
-
1432
446
  export function saveWorkingLineComponentPatch(
1433
447
  patch: WorkingLineComponentPatch,
1434
448
  path = configPath,
1435
- ): PolishedTuiConfig {
449
+ ): ZentuiConfig {
1436
450
  return saveComponentsMutation((components) => {
1437
- const component = components.workingLine;
1438
- if (patch.enabled !== undefined) component.enabled = patch.enabled;
1439
- if (patch.turnSummary !== undefined)
1440
- component.turnSummary = patch.turnSummary;
1441
- if (patch.spinner !== undefined) component.spinner = patch.spinner;
1442
- if (patch.spinnerIntervalMs !== undefined)
1443
- component.spinnerIntervalMs = patch.spinnerIntervalMs;
1444
- if (patch.animateSpinnerColor !== undefined)
1445
- component.animateSpinnerColor = patch.animateSpinnerColor;
1446
- if (patch.textIntervalMs !== undefined)
1447
- component.textIntervalMs = patch.textIntervalMs;
1448
- if (patch.textAnimation !== undefined)
1449
- component.textAnimation = patch.textAnimation;
1450
- if (patch.messages?.custom !== undefined)
1451
- component.messages.custom = patch.messages.custom;
1452
- if (patch.messages?.values !== undefined) {
1453
- component.messages.values = normalizeWorkingLineMessages([
1454
- ...patch.messages.values,
1455
- ]);
1456
- }
1457
- if (patch.segments?.tool !== undefined)
1458
- component.segments.tool = patch.segments.tool;
1459
- if (patch.segments?.elapsed !== undefined)
1460
- component.segments.elapsed = patch.segments.elapsed;
1461
- if (patch.segments?.thought !== undefined)
1462
- component.segments.thought = patch.segments.thought;
1463
- if (patch.segments?.tokens !== undefined)
1464
- component.segments.tokens = patch.segments.tokens;
451
+ components.workingLine = overlayKnown(
452
+ components.workingLine,
453
+ patch,
454
+ ) as WorkingLineComponentConfig;
1465
455
  }, path);
1466
456
  }
1467
-
1468
457
  export function saveSelectorBordersComponentPatch(
1469
458
  patch: Partial<SelectorBordersComponentConfig>,
1470
459
  path = configPath,
1471
- ): PolishedTuiConfig {
460
+ ): ZentuiConfig {
1472
461
  return saveComponentsMutation(
1473
462
  (components) => {
1474
- const component = components.selectorBorders;
1475
- if (patch.enabled !== undefined) component.enabled = patch.enabled;
1476
- if (patch.style !== undefined) component.style = patch.style;
463
+ components.selectorBorders = overlayKnown(
464
+ components.selectorBorders,
465
+ patch,
466
+ ) as SelectorBordersComponentConfig;
1477
467
  },
1478
468
  path,
1479
469
  patch.style !== undefined ? "selectorBorders" : undefined,
1480
470
  );
1481
471
  }
1482
-
1483
472
  export function saveFooterComponentPatch(
1484
473
  patch: Partial<Pick<FooterComponentConfig, "style" | "modelLabel">>,
1485
474
  path = configPath,
1486
- ): PolishedTuiConfig {
475
+ ): ZentuiConfig {
1487
476
  return saveComponentsMutation(
1488
477
  (components) => {
1489
- const component = components.footer;
1490
- if (patch.style !== undefined) component.style = patch.style;
1491
- if (patch.modelLabel !== undefined)
1492
- component.modelLabel = patch.modelLabel;
478
+ components.footer = overlayKnown(
479
+ components.footer,
480
+ patch,
481
+ ) as FooterComponentConfig;
1493
482
  },
1494
483
  path,
1495
484
  patch.style !== undefined ? "footer" : undefined,
1496
485
  );
1497
486
  }
1498
-
1499
- function applyStarshipStylePatch(
1500
- style: StarshipFooterStyleConfig,
1501
- patch: Partial<StarshipFooterStyleConfig>,
1502
- ): void {
1503
- if (patch.format !== undefined) style.format = patch.format;
1504
- if (patch.responsive !== undefined) style.responsive = patch.responsive;
1505
- if (patch.compactFormat !== undefined)
1506
- style.compactFormat = patch.compactFormat;
1507
- if (patch.compactMaxLines !== undefined)
1508
- style.compactMaxLines = patch.compactMaxLines;
1509
- if (patch.separator !== undefined) style.separator = patch.separator;
1510
- if (patch.contextStyle !== undefined) style.contextStyle = patch.contextStyle;
1511
- if (patch.contextThresholds !== undefined) {
1512
- style.contextThresholds = {
1513
- ...style.contextThresholds,
1514
- ...patch.contextThresholds,
1515
- };
1516
- }
1517
- if (patch.pathDisplay !== undefined) {
1518
- style.pathDisplay = { ...style.pathDisplay, ...patch.pathDisplay };
1519
- }
1520
- if (patch.segments !== undefined)
1521
- style.segments = { ...style.segments, ...patch.segments };
1522
- if (patch.gitBranch !== undefined)
1523
- style.gitBranch = { ...style.gitBranch, ...patch.gitBranch };
1524
- if (patch.gitCommit !== undefined)
1525
- style.gitCommit = { ...style.gitCommit, ...patch.gitCommit };
1526
- if (patch.gitMetrics !== undefined)
1527
- style.gitMetrics = { ...style.gitMetrics, ...patch.gitMetrics };
1528
- if (patch.extensionStatuses !== undefined) {
1529
- style.extensionStatuses = {
1530
- ...style.extensionStatuses,
1531
- ...patch.extensionStatuses,
1532
- placements: {
1533
- ...style.extensionStatuses.placements,
1534
- ...patch.extensionStatuses.placements,
1535
- },
1536
- colorModes: {
1537
- ...style.extensionStatuses.colorModes,
1538
- ...patch.extensionStatuses.colorModes,
1539
- },
1540
- };
1541
- }
1542
- }
487
+ export type StarshipFooterStylePatch = Partial<
488
+ Omit<
489
+ StarshipFooterStyleConfig,
490
+ | "contextThresholds"
491
+ | "pathDisplay"
492
+ | "segments"
493
+ | "gitBranch"
494
+ | "gitCommit"
495
+ | "gitMetrics"
496
+ >
497
+ > & {
498
+ contextThresholds?: Partial<ContextThresholds>;
499
+ pathDisplay?: Partial<PathDisplayConfig>;
500
+ segments?: Partial<FooterSegmentsConfig>;
501
+ gitBranch?: Partial<GitBranchConfig>;
502
+ gitCommit?: Partial<GitCommitConfig>;
503
+ gitMetrics?: Partial<GitMetricsConfig>;
504
+ };
1543
505
 
1544
506
  export function saveStarshipFooterStylePatch(
1545
- patch: Partial<StarshipFooterStyleConfig>,
507
+ patch: StarshipFooterStylePatch,
1546
508
  path = configPath,
1547
- ): PolishedTuiConfig {
1548
- return saveComponentsMutation(
1549
- (components) =>
1550
- applyStarshipStylePatch(components.footer.styles.starship, patch),
1551
- path,
1552
- );
509
+ ): ZentuiConfig {
510
+ return saveComponentsMutation((components) => {
511
+ components.footer.styles.starship = overlayKnown(
512
+ components.footer.styles.starship,
513
+ patch,
514
+ ) as StarshipFooterStyleConfig;
515
+ }, path);
1553
516
  }
1554
-
1555
517
  export function saveUiFeaturesPatch(
1556
518
  patch: Partial<UiFeaturesConfig>,
1557
519
  path = configPath,
1558
- ): PolishedTuiConfig {
1559
- const valid = validUiFeatureEntries(patch);
520
+ ): ZentuiConfig {
1560
521
  return saveComponentsMutation(
1561
522
  (components) => {
1562
- if (valid.editor !== undefined) {
1563
- components.editor.style = valid.editor ? "on" : "off";
1564
- components.userMessages.enabled = valid.editor;
1565
- components.selectorBorders.enabled = valid.editor;
523
+ if (typeof patch.editor === "boolean") {
524
+ components.editor.style = patch.editor ? "on" : "off";
525
+ components.userMessages.enabled = patch.editor;
526
+ components.selectorBorders.enabled = patch.editor;
1566
527
  }
1567
- if (valid.statusLine !== undefined) {
1568
- components.footer.style = valid.statusLine ? "starship" : "native";
528
+ if (typeof patch.statusLine === "boolean") {
529
+ components.footer.style = patch.statusLine ? "starship" : "native";
1569
530
  }
1570
- if (valid.viewportIndicators !== undefined) {
1571
- components.editor.viewportIndicators = valid.viewportIndicators;
531
+ if (typeof patch.viewportIndicators === "boolean") {
532
+ components.editor.viewportIndicators = patch.viewportIndicators;
1572
533
  }
1573
534
  },
1574
535
  path,
1575
- valid.statusLine !== undefined ? "footer" : undefined,
536
+ typeof patch.statusLine === "boolean" ? "footer" : undefined,
1576
537
  );
1577
538
  }
1578
-
1579
539
  export function saveFooterSegmentsPatch(
1580
540
  patch: Partial<FooterSegmentsConfig>,
1581
541
  path = configPath,
1582
- ): PolishedTuiConfig {
1583
- return saveStarshipFooterStylePatch(
1584
- { segments: validFooterSegmentEntries(patch) as FooterSegmentsConfig },
1585
- path,
1586
- );
542
+ ): ZentuiConfig {
543
+ const segments = Object.fromEntries(
544
+ Object.entries(patch).filter(
545
+ ([key, value]) =>
546
+ Object.hasOwn(defaultFooterSegments, key) && typeof value === "boolean",
547
+ ),
548
+ ) as FooterSegmentsConfig;
549
+ return saveStarshipFooterStylePatch({ segments }, path);
1587
550
  }
1588
-
1589
551
  export function saveFooterFormatPatch(
1590
552
  value: string,
1591
553
  path = configPath,
1592
- ): PolishedTuiConfig {
554
+ ): ZentuiConfig {
1593
555
  return saveStarshipFooterStylePatch(
1594
556
  { format: typeof value === "string" ? value : "" },
1595
557
  path,
1596
558
  );
1597
559
  }
1598
-
1599
560
  export function saveResponsiveFooterPatch(
1600
- patch: Partial<
1601
- Pick<
1602
- PolishedTuiConfig,
1603
- "responsiveFooter" | "compactFooterFormat" | "compactFooterMaxLines"
1604
- >
1605
- >,
561
+ patch: {
562
+ responsiveFooter?: boolean;
563
+ compactFooterFormat?: string;
564
+ compactFooterMaxLines?: CompactFooterMaxLines;
565
+ },
1606
566
  path = configPath,
1607
- ): PolishedTuiConfig {
567
+ ): ZentuiConfig {
1608
568
  const canonical: Partial<StarshipFooterStyleConfig> = {};
1609
- if (typeof patch.responsiveFooter === "boolean")
569
+ if (typeof patch.responsiveFooter === "boolean") {
1610
570
  canonical.responsive = patch.responsiveFooter;
1611
- if (typeof patch.compactFooterFormat === "string")
571
+ }
572
+ if (typeof patch.compactFooterFormat === "string") {
1612
573
  canonical.compactFormat = patch.compactFooterFormat;
574
+ }
1613
575
  if (patch.compactFooterMaxLines !== undefined) {
1614
576
  canonical.compactMaxLines = parseCompactFooterMaxLines(
1615
577
  patch.compactFooterMaxLines,
@@ -1617,164 +579,126 @@ export function saveResponsiveFooterPatch(
1617
579
  }
1618
580
  return saveStarshipFooterStylePatch(canonical, path);
1619
581
  }
1620
-
1621
582
  export function saveIconsModePatch(
1622
583
  mode: IconMode,
1623
584
  path = configPath,
1624
- ): PolishedTuiConfig {
585
+ ): ZentuiConfig {
1625
586
  return mutateConfig(path, (record) => {
1626
- const existing = isRecord(record.icons) ? { ...record.icons } : {};
1627
- record.icons = { ...existing, mode: normalizeIconMode(mode) };
587
+ record.icons = {
588
+ ...recordValue(record.icons),
589
+ mode: normalizeIconMode(mode),
590
+ };
1628
591
  });
1629
592
  }
1630
-
1631
593
  export function saveContextStylePatch(
1632
594
  style: ContextStyle,
1633
595
  path = configPath,
1634
- ): PolishedTuiConfig {
596
+ ): ZentuiConfig {
1635
597
  return saveStarshipFooterStylePatch(
1636
598
  { contextStyle: parseContextStyle(style) },
1637
599
  path,
1638
600
  );
1639
601
  }
1640
-
1641
602
  export function saveSeparatorPatch(
1642
603
  separator: SeparatorStyle,
1643
604
  path = configPath,
1644
- ): PolishedTuiConfig {
605
+ ): ZentuiConfig {
1645
606
  return saveStarshipFooterStylePatch(
1646
607
  { separator: parseSeparatorStyle(separator) },
1647
608
  path,
1648
609
  );
1649
610
  }
1650
-
1651
611
  export function saveContextThresholdsPatch(
1652
- thresholds: Partial<ContextThresholds>,
612
+ patch: Partial<ContextThresholds>,
1653
613
  path = configPath,
1654
- ): PolishedTuiConfig {
1655
- if (
1656
- typeof thresholds.warning === "bigint" ||
1657
- typeof thresholds.error === "bigint"
1658
- ) {
614
+ ): ZentuiConfig {
615
+ if (typeof patch.warning === "bigint" || typeof patch.error === "bigint") {
1659
616
  throw new TypeError("Context thresholds must be JSON-serializable numbers");
1660
617
  }
1661
- return saveComponentsMutation((components) => {
1662
- components.footer.styles.starship.contextThresholds = {
1663
- ...components.footer.styles.starship.contextThresholds,
1664
- ...thresholds,
1665
- };
1666
- }, path);
618
+ return saveStarshipFooterStylePatch({ contextThresholds: patch }, path);
1667
619
  }
1668
-
1669
620
  export function savePathDisplayPatch(
1670
621
  patch: Partial<PathDisplayConfig>,
1671
622
  path = configPath,
1672
- ): PolishedTuiConfig {
1673
- return saveComponentsMutation((components) => {
1674
- components.footer.styles.starship.pathDisplay = {
1675
- ...components.footer.styles.starship.pathDisplay,
1676
- ...patch,
1677
- };
1678
- }, path);
623
+ ): ZentuiConfig {
624
+ return saveStarshipFooterStylePatch({ pathDisplay: patch }, path);
1679
625
  }
1680
-
1681
626
  export function saveGitBranchPatch(
1682
627
  patch: Partial<GitBranchConfig>,
1683
628
  path = configPath,
1684
- ): PolishedTuiConfig {
1685
- return saveComponentsMutation((components) => {
1686
- components.footer.styles.starship.gitBranch = {
1687
- ...components.footer.styles.starship.gitBranch,
1688
- ...patch,
1689
- };
1690
- }, path);
629
+ ): ZentuiConfig {
630
+ return saveStarshipFooterStylePatch({ gitBranch: patch }, path);
1691
631
  }
1692
-
1693
632
  export function saveEditorModelLabel(
1694
633
  value: ModelLabelSource,
1695
634
  path = configPath,
1696
- ): PolishedTuiConfig {
635
+ ): ZentuiConfig {
1697
636
  return saveComponentsMutation((components) => {
1698
- const normalized = parseEditorModelLabel(value);
1699
- components.editor.modelLabel = normalized;
1700
- components.footer.modelLabel = normalized;
637
+ components.editor.modelLabel = parseEditorModelLabel(value);
638
+ components.footer.modelLabel = parseEditorModelLabel(value);
1701
639
  }, path);
1702
640
  }
1703
-
1704
641
  export function saveEditorStyle(
1705
642
  value: EditorStyle,
1706
643
  path = configPath,
1707
- ): PolishedTuiConfig {
644
+ ): ZentuiConfig {
1708
645
  return saveEditorComponentPatch({ style: parseEditorStyle(value) }, path);
1709
646
  }
1710
-
1711
647
  export function saveMinimalistPatch(
1712
648
  patch: Partial<MinimalistConfig>,
1713
649
  path = configPath,
1714
- ): PolishedTuiConfig {
650
+ ): ZentuiConfig {
1715
651
  return saveMinimalistEditorStylePatch(patch, path);
1716
652
  }
1717
-
1718
653
  export function saveEditorBorderColorMode(
1719
654
  value: EditorBorderColorMode,
1720
655
  path = configPath,
1721
- ): PolishedTuiConfig {
656
+ ): ZentuiConfig {
1722
657
  return saveEditorComponentPatch(
1723
658
  { borderColorMode: parseEditorBorderColorMode(value) },
1724
659
  path,
1725
660
  );
1726
661
  }
1727
-
1728
662
  export function saveGitCommitPatch(
1729
663
  patch: Partial<Pick<GitCommitConfig, "onlyDetached" | "showTag">>,
1730
664
  path = configPath,
1731
- ): PolishedTuiConfig {
1732
- const valid: Partial<GitCommitConfig> = {};
1733
- if (typeof patch.onlyDetached === "boolean")
1734
- valid.onlyDetached = patch.onlyDetached;
1735
- if (typeof patch.showTag === "boolean") valid.showTag = patch.showTag;
1736
- return saveComponentsMutation((components) => {
1737
- components.footer.styles.starship.gitCommit = {
1738
- ...components.footer.styles.starship.gitCommit,
1739
- ...valid,
1740
- };
1741
- }, path);
665
+ ): ZentuiConfig {
666
+ const valid = Object.fromEntries(
667
+ Object.entries(patch).filter(
668
+ ([key, value]) =>
669
+ (key === "onlyDetached" || key === "showTag") &&
670
+ typeof value === "boolean",
671
+ ),
672
+ );
673
+ return saveStarshipFooterStylePatch({ gitCommit: valid }, path);
1742
674
  }
1743
-
1744
675
  export function saveGitMetricsPatch(
1745
676
  patch: Partial<GitMetricsConfig>,
1746
677
  path = configPath,
1747
- ): PolishedTuiConfig {
1748
- const valid: Partial<GitMetricsConfig> = {};
1749
- if (typeof patch.onlyNonzero === "boolean")
1750
- valid.onlyNonzero = patch.onlyNonzero;
1751
- if (typeof patch.ignoreSubmodules === "boolean")
1752
- valid.ignoreSubmodules = patch.ignoreSubmodules;
1753
- return saveComponentsMutation((components) => {
1754
- components.footer.styles.starship.gitMetrics = {
1755
- ...components.footer.styles.starship.gitMetrics,
1756
- ...valid,
1757
- };
1758
- }, path);
678
+ ): ZentuiConfig {
679
+ const valid = Object.fromEntries(
680
+ Object.entries(patch).filter(
681
+ ([key, value]) =>
682
+ (key === "onlyNonzero" || key === "ignoreSubmodules") &&
683
+ typeof value === "boolean",
684
+ ),
685
+ );
686
+ return saveStarshipFooterStylePatch({ gitMetrics: valid }, path);
1759
687
  }
1760
-
1761
688
  export function saveExtensionStatusDefaultPlacement(
1762
689
  placement: ExtensionStatusPlacement,
1763
690
  path = configPath,
1764
- ): PolishedTuiConfig {
691
+ ): ZentuiConfig {
1765
692
  return saveComponentsMutation((components) => {
1766
693
  components.footer.styles.starship.extensionStatuses.defaultPlacement =
1767
- isExtensionStatusPlacement(placement)
1768
- ? placement
1769
- : defaultConfig.extensionStatuses.defaultPlacement;
694
+ isExtensionStatusPlacement(placement) ? placement : "right";
1770
695
  }, path);
1771
696
  }
1772
-
1773
697
  export function saveExtensionStatusPlacement(
1774
698
  key: string,
1775
699
  placement: ExtensionStatusPlacement,
1776
700
  path = configPath,
1777
- ): PolishedTuiConfig {
701
+ ): ZentuiConfig {
1778
702
  return saveComponentsMutation((components) => {
1779
703
  Object.defineProperty(
1780
704
  components.footer.styles.starship.extensionStatuses.placements,
@@ -1788,12 +712,11 @@ export function saveExtensionStatusPlacement(
1788
712
  );
1789
713
  }, path);
1790
714
  }
1791
-
1792
715
  export function saveExtensionStatusColorMode(
1793
716
  key: string,
1794
717
  colorMode: ExtensionStatusColorMode,
1795
718
  path = configPath,
1796
- ): PolishedTuiConfig {
719
+ ): ZentuiConfig {
1797
720
  return saveComponentsMutation((components) => {
1798
721
  Object.defineProperty(
1799
722
  components.footer.styles.starship.extensionStatuses.colorModes,