@rosetears/aili-pi 0.1.0 → 0.1.3
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.
- package/README.md +15 -6
- package/THIRD_PARTY_NOTICES.md +14 -3
- package/extensions/header/index.ts +92 -0
- package/extensions/matrix/index.ts +375 -0
- package/extensions/zentui/config.ts +1014 -0
- package/extensions/zentui/extension-status.ts +96 -0
- package/extensions/zentui/fixed-editor/cluster.ts +98 -0
- package/extensions/zentui/fixed-editor/compositor.ts +719 -0
- package/extensions/zentui/fixed-editor/index.ts +223 -0
- package/extensions/zentui/fixed-editor/input.ts +85 -0
- package/extensions/zentui/fixed-editor/pi-compat.ts +296 -0
- package/extensions/zentui/fixed-editor/selection.ts +217 -0
- package/extensions/zentui/fixed-editor/terminal-modes.ts +75 -0
- package/extensions/zentui/fixed-editor/types.ts +37 -0
- package/extensions/zentui/footer-format.ts +279 -0
- package/extensions/zentui/footer.ts +595 -0
- package/extensions/zentui/format.ts +434 -0
- package/extensions/zentui/git.ts +384 -0
- package/extensions/zentui/gradient.ts +70 -0
- package/extensions/zentui/icons.ts +252 -0
- package/extensions/zentui/index.ts +580 -0
- package/extensions/zentui/live-context.ts +75 -0
- package/extensions/zentui/package-version.ts +650 -0
- package/extensions/zentui/project-refresh.ts +104 -0
- package/extensions/zentui/project-state.ts +59 -0
- package/extensions/zentui/prototype-patch-registry.ts +111 -0
- package/extensions/zentui/runtime.ts +841 -0
- package/extensions/zentui/selector-border.ts +77 -0
- package/extensions/zentui/session-lifecycle.ts +60 -0
- package/extensions/zentui/settings-command.ts +897 -0
- package/extensions/zentui/state.ts +55 -0
- package/extensions/zentui/style.ts +332 -0
- package/extensions/zentui/thinking-message.ts +159 -0
- package/extensions/zentui/tool-execution.ts +189 -0
- package/extensions/zentui/ui.ts +618 -0
- package/extensions/zentui/user-message.ts +252 -0
- package/licenses/pi-sakura-cyberdeck-MIT.txt +21 -0
- package/licenses/pi-zentui-MIT.txt +21 -0
- package/manifests/capabilities.json +1 -1
- package/manifests/live-verification.json +14 -8
- package/manifests/provenance.json +18 -5
- package/manifests/sbom.json +19 -3
- package/notices/pi-sakura-cyberdeck-NOTICE.txt +6 -0
- package/package.json +12 -3
- package/scripts/local-package-e2e.ts +1 -1
- package/scripts/sync-global-skills.d.mts +13 -0
- package/scripts/sync-global-skills.mjs +134 -0
- package/src/runtime/credential-guard.ts +58 -0
- package/src/runtime/doctor.ts +1 -1
- package/src/runtime/index.ts +2 -2
- package/src/runtime/path-boundaries.ts +1 -1
- package/src/runtime/registry.ts +6 -2
- package/src/runtime/rem-head.txt +38 -0
- package/src/runtime/rose-context.ts +1 -1
- package/src/runtime/subagents.ts +74 -403
- package/templates/APPEND_SYSTEM.md +10 -8
- package/themes/rem-cyberdeck.json +32 -0
- package/upstream/opencode-global-agents.lock.json +34 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fchmodSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
realpathSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
statSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
17
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import {
|
|
19
|
+
ICON_GLYPH_KEYS,
|
|
20
|
+
type IconGlyphs,
|
|
21
|
+
type IconMode,
|
|
22
|
+
NERD_DEFAULT_ICONS,
|
|
23
|
+
normalizeIconMode,
|
|
24
|
+
type ResolvedIcons,
|
|
25
|
+
resolveConfiguredIcons,
|
|
26
|
+
} from "./icons.js";
|
|
27
|
+
import { isSupportedColorSpec } from "./style.js";
|
|
28
|
+
|
|
29
|
+
export type ColorSpec = string;
|
|
30
|
+
export type ColorSource = "theme" | "terminal";
|
|
31
|
+
export type { IconMode } from "./icons.js";
|
|
32
|
+
|
|
33
|
+
export type ContextStyle = "text" | "gauge" | "text+gauge";
|
|
34
|
+
export type SeparatorStyle = "pipe" | "dot" | "chevron" | "none";
|
|
35
|
+
|
|
36
|
+
export type ContextThresholds = {
|
|
37
|
+
warning: number;
|
|
38
|
+
error: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PathDisplayMode = "basename" | "full";
|
|
42
|
+
|
|
43
|
+
export type PathDisplayConfig = {
|
|
44
|
+
mode: PathDisplayMode;
|
|
45
|
+
/** Trailing directories to show in full mode. 0 = unlimited; clamped to 0..5. */
|
|
46
|
+
depth: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type GitBranchMaxLength = "full" | number;
|
|
50
|
+
|
|
51
|
+
export type GitBranchConfig = {
|
|
52
|
+
maxLength: GitBranchMaxLength;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ColorSourcesConfig = {
|
|
56
|
+
starship: ColorSource;
|
|
57
|
+
editor: ColorSource;
|
|
58
|
+
userMessages: ColorSource;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type UiFeaturesConfig = {
|
|
62
|
+
editor: boolean;
|
|
63
|
+
statusLine: boolean;
|
|
64
|
+
copyFriendly: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type FooterSegmentsConfig = {
|
|
68
|
+
cwd: boolean;
|
|
69
|
+
gitBranch: boolean;
|
|
70
|
+
gitStatus: boolean;
|
|
71
|
+
gitCounts: boolean;
|
|
72
|
+
gitCommit: boolean;
|
|
73
|
+
gitMetrics: boolean;
|
|
74
|
+
runtime: boolean;
|
|
75
|
+
context: boolean;
|
|
76
|
+
tokens: boolean;
|
|
77
|
+
cost: boolean;
|
|
78
|
+
sessionDuration: boolean;
|
|
79
|
+
username: boolean;
|
|
80
|
+
time: boolean;
|
|
81
|
+
os: boolean;
|
|
82
|
+
packageVersion: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type FixedEditorConfig = {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
mouseScroll: boolean;
|
|
88
|
+
copyNotice: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
|
|
92
|
+
export type ExtensionStatusColorMode = "zentui" | "original";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Starship `git_commit`-style options.
|
|
96
|
+
* See https://starship.rs/config/#git-commit
|
|
97
|
+
*/
|
|
98
|
+
export type GitCommitConfig = {
|
|
99
|
+
hashLength: number;
|
|
100
|
+
onlyDetached: boolean;
|
|
101
|
+
showTag: boolean;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Starship `git_metrics`-style options.
|
|
106
|
+
* See https://starship.rs/config/#git-metrics
|
|
107
|
+
*/
|
|
108
|
+
export type GitMetricsConfig = {
|
|
109
|
+
onlyNonzero: boolean;
|
|
110
|
+
ignoreSubmodules: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const DEFAULT_EXTENSION_STATUS_COLOR_MODE: ExtensionStatusColorMode = "zentui";
|
|
114
|
+
|
|
115
|
+
export type ExtensionStatusesConfig = {
|
|
116
|
+
defaultPlacement: ExtensionStatusPlacement;
|
|
117
|
+
placements: Record<string, ExtensionStatusPlacement>;
|
|
118
|
+
colorModes: Record<string, ExtensionStatusColorMode>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
|
|
122
|
+
const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
|
|
123
|
+
|
|
124
|
+
export type PolishedTuiConfig = {
|
|
125
|
+
projectRefreshIntervalMs: number;
|
|
126
|
+
footerFormat: string;
|
|
127
|
+
separator: SeparatorStyle;
|
|
128
|
+
contextStyle: ContextStyle;
|
|
129
|
+
contextThresholds: ContextThresholds;
|
|
130
|
+
pathDisplay: PathDisplayConfig;
|
|
131
|
+
gitBranch: GitBranchConfig;
|
|
132
|
+
icons: ResolvedIcons;
|
|
133
|
+
colors: {
|
|
134
|
+
cwd: ColorSpec;
|
|
135
|
+
gitBranch: ColorSpec;
|
|
136
|
+
gitStatus: ColorSpec;
|
|
137
|
+
contextNormal: ColorSpec;
|
|
138
|
+
contextWarning: ColorSpec;
|
|
139
|
+
contextError: ColorSpec;
|
|
140
|
+
tokens: ColorSpec;
|
|
141
|
+
cost: ColorSpec;
|
|
142
|
+
separator: ColorSpec;
|
|
143
|
+
runtimePrefix: ColorSpec;
|
|
144
|
+
extensionStatus: ColorSpec;
|
|
145
|
+
sessionDuration: ColorSpec;
|
|
146
|
+
packageVersion: ColorSpec;
|
|
147
|
+
gitCommit: ColorSpec;
|
|
148
|
+
gitMetricsAdded: ColorSpec;
|
|
149
|
+
gitMetricsDeleted: ColorSpec;
|
|
150
|
+
username: ColorSpec;
|
|
151
|
+
time: ColorSpec;
|
|
152
|
+
os: ColorSpec;
|
|
153
|
+
editorAccent?: ColorSpec;
|
|
154
|
+
editorPrompt?: ColorSpec;
|
|
155
|
+
editorBorder?: ColorSpec;
|
|
156
|
+
editorModel?: ColorSpec;
|
|
157
|
+
editorProvider?: ColorSpec;
|
|
158
|
+
editorThinking?: ColorSpec;
|
|
159
|
+
editorThinkingMinimal?: ColorSpec;
|
|
160
|
+
editorThinkingLow?: ColorSpec;
|
|
161
|
+
editorThinkingMedium?: ColorSpec;
|
|
162
|
+
editorThinkingHigh?: ColorSpec;
|
|
163
|
+
editorThinkingXhigh?: ColorSpec;
|
|
164
|
+
};
|
|
165
|
+
colorSources: ColorSourcesConfig;
|
|
166
|
+
features: UiFeaturesConfig;
|
|
167
|
+
footerSegments: FooterSegmentsConfig;
|
|
168
|
+
gitCommit: GitCommitConfig;
|
|
169
|
+
gitMetrics: GitMetricsConfig;
|
|
170
|
+
extensionStatuses: ExtensionStatusesConfig;
|
|
171
|
+
fixedEditor: FixedEditorConfig;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Canonical footer format variable names. In a `footerFormat` string these
|
|
176
|
+
* are written as `$name` or `${name}`.
|
|
177
|
+
*/
|
|
178
|
+
export const FOOTER_FORMAT_VARIABLES = [
|
|
179
|
+
"cwd",
|
|
180
|
+
"git_branch",
|
|
181
|
+
"git_status",
|
|
182
|
+
"git_state",
|
|
183
|
+
"runtime",
|
|
184
|
+
"session_duration",
|
|
185
|
+
"username",
|
|
186
|
+
"os",
|
|
187
|
+
"time",
|
|
188
|
+
"context",
|
|
189
|
+
"tokens",
|
|
190
|
+
"cost",
|
|
191
|
+
"package",
|
|
192
|
+
"package_version",
|
|
193
|
+
"git_commit",
|
|
194
|
+
"git_tag",
|
|
195
|
+
"git_metrics",
|
|
196
|
+
"git_added",
|
|
197
|
+
"git_deleted",
|
|
198
|
+
"sep",
|
|
199
|
+
] as const;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Alias → canonical variable name mapping for `footerFormat`.
|
|
203
|
+
* `$fill` is special (not a variable) and handled by the parser.
|
|
204
|
+
*/
|
|
205
|
+
export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
|
|
206
|
+
directory: "cwd",
|
|
207
|
+
branch: "git_branch",
|
|
208
|
+
status: "git_status",
|
|
209
|
+
state: "git_state",
|
|
210
|
+
commit: "git_commit",
|
|
211
|
+
tag: "git_tag",
|
|
212
|
+
duration: "session_duration",
|
|
213
|
+
separator: "sep",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const configPath = join(getAgentDir(), "rem-cyberdeck-zentui.json");
|
|
217
|
+
|
|
218
|
+
export const defaultConfig: PolishedTuiConfig = {
|
|
219
|
+
projectRefreshIntervalMs: 60_000,
|
|
220
|
+
footerFormat:
|
|
221
|
+
"$cwd( $git_branch)( $git_status)( $session_duration)",
|
|
222
|
+
separator: "chevron",
|
|
223
|
+
contextStyle: "text+gauge",
|
|
224
|
+
contextThresholds: { warning: 70, error: 90 },
|
|
225
|
+
pathDisplay: { mode: "basename", depth: 0 },
|
|
226
|
+
gitBranch: { maxLength: 30 },
|
|
227
|
+
icons: {
|
|
228
|
+
mode: "nerd",
|
|
229
|
+
...NERD_DEFAULT_ICONS,
|
|
230
|
+
rail: "▐",
|
|
231
|
+
editorPrompt: "",
|
|
232
|
+
},
|
|
233
|
+
colors: {
|
|
234
|
+
cwd: "bold #88B8FF",
|
|
235
|
+
gitBranch: "bold #BCA7FF",
|
|
236
|
+
gitStatus: "bold #D6F4FF",
|
|
237
|
+
contextNormal: "#8CE6C2",
|
|
238
|
+
contextWarning: "bold #F3CE83",
|
|
239
|
+
contextError: "bold #FF93B1",
|
|
240
|
+
tokens: "#9DA9C8",
|
|
241
|
+
cost: "bold #8CE6C2",
|
|
242
|
+
separator: "#64708F",
|
|
243
|
+
runtimePrefix: "#7DE4FF",
|
|
244
|
+
extensionStatus: "#BCA7FF",
|
|
245
|
+
sessionDuration: "#F3CE83",
|
|
246
|
+
packageVersion: "#D6F4FF",
|
|
247
|
+
gitCommit: "#8CE6C2",
|
|
248
|
+
gitMetricsAdded: "#8CE6C2",
|
|
249
|
+
gitMetricsDeleted: "#FF93B1",
|
|
250
|
+
username: "#F3CE83",
|
|
251
|
+
time: "#F3CE83",
|
|
252
|
+
os: "#F7EEF8",
|
|
253
|
+
editorAccent: "bold #88B8FF",
|
|
254
|
+
editorPrompt: "bold #88B8FF",
|
|
255
|
+
editorBorder: "sakura-macaron-gradient",
|
|
256
|
+
editorModel: "bold #88B8FF",
|
|
257
|
+
editorProvider: "#B8BEDD",
|
|
258
|
+
editorThinking: "#BCA7FF",
|
|
259
|
+
editorThinkingMinimal: "#64708F",
|
|
260
|
+
editorThinkingLow: "#7DE4FF",
|
|
261
|
+
editorThinkingMedium: "#BCA7FF",
|
|
262
|
+
editorThinkingHigh: "bold #88B8FF",
|
|
263
|
+
editorThinkingXhigh: "bold #BCA7FF",
|
|
264
|
+
},
|
|
265
|
+
colorSources: {
|
|
266
|
+
starship: "terminal",
|
|
267
|
+
editor: "terminal",
|
|
268
|
+
userMessages: "theme",
|
|
269
|
+
},
|
|
270
|
+
features: {
|
|
271
|
+
editor: true,
|
|
272
|
+
statusLine: true,
|
|
273
|
+
copyFriendly: false,
|
|
274
|
+
},
|
|
275
|
+
footerSegments: {
|
|
276
|
+
cwd: true,
|
|
277
|
+
gitBranch: true,
|
|
278
|
+
gitStatus: true,
|
|
279
|
+
gitCounts: false,
|
|
280
|
+
runtime: false,
|
|
281
|
+
context: false,
|
|
282
|
+
tokens: false,
|
|
283
|
+
cost: false,
|
|
284
|
+
sessionDuration: true,
|
|
285
|
+
username: false,
|
|
286
|
+
time: false,
|
|
287
|
+
os: false,
|
|
288
|
+
packageVersion: false,
|
|
289
|
+
gitCommit: false,
|
|
290
|
+
gitMetrics: false,
|
|
291
|
+
},
|
|
292
|
+
gitCommit: {
|
|
293
|
+
hashLength: 7,
|
|
294
|
+
onlyDetached: true,
|
|
295
|
+
showTag: true,
|
|
296
|
+
},
|
|
297
|
+
gitMetrics: {
|
|
298
|
+
onlyNonzero: true,
|
|
299
|
+
ignoreSubmodules: false,
|
|
300
|
+
},
|
|
301
|
+
extensionStatuses: {
|
|
302
|
+
defaultPlacement: "right",
|
|
303
|
+
placements: {
|
|
304
|
+
"pi-quota-status": "right",
|
|
305
|
+
"dual-subscription-quota": "left",
|
|
306
|
+
"codex-goal": "middle",
|
|
307
|
+
"xai-usage": "right",
|
|
308
|
+
},
|
|
309
|
+
colorModes: {},
|
|
310
|
+
},
|
|
311
|
+
fixedEditor: {
|
|
312
|
+
enabled: true,
|
|
313
|
+
mouseScroll: true,
|
|
314
|
+
copyNotice: true,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
type ConfigRecord = Record<string, unknown>;
|
|
319
|
+
|
|
320
|
+
function isRecord(value: unknown): value is ConfigRecord {
|
|
321
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function parseProjectRefreshIntervalMs(value: unknown): number {
|
|
325
|
+
if (value === 0) return 0;
|
|
326
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
327
|
+
return DEFAULT_PROJECT_REFRESH_INTERVAL_MS;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const interval = Math.round(value);
|
|
331
|
+
if (interval <= 0) return 0;
|
|
332
|
+
return Math.max(MIN_PROJECT_REFRESH_INTERVAL_MS, interval);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function clampPercent(value: number): number {
|
|
336
|
+
return Math.max(0, Math.min(100, value));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function parseContextStyle(value: unknown): ContextStyle {
|
|
340
|
+
if (value === "text" || value === "gauge" || value === "text+gauge") return value;
|
|
341
|
+
return defaultConfig.contextStyle;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function isSeparatorStyle(value: unknown): value is SeparatorStyle {
|
|
345
|
+
return value === "pipe" || value === "dot" || value === "chevron" || value === "none";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseSeparatorStyle(value: unknown): SeparatorStyle {
|
|
349
|
+
return isSeparatorStyle(value) ? value : defaultConfig.separator;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function parseContextThresholds(value: unknown): ContextThresholds {
|
|
353
|
+
const defaults = defaultConfig.contextThresholds;
|
|
354
|
+
if (!isRecord(value)) return { ...defaults };
|
|
355
|
+
|
|
356
|
+
const warningRaw = value.warning;
|
|
357
|
+
const errorRaw = value.error;
|
|
358
|
+
let warning =
|
|
359
|
+
typeof warningRaw === "number" && Number.isFinite(warningRaw)
|
|
360
|
+
? clampPercent(Math.round(warningRaw))
|
|
361
|
+
: defaults.warning;
|
|
362
|
+
let error =
|
|
363
|
+
typeof errorRaw === "number" && Number.isFinite(errorRaw)
|
|
364
|
+
? clampPercent(Math.round(errorRaw))
|
|
365
|
+
: defaults.error;
|
|
366
|
+
if (error < warning) {
|
|
367
|
+
const swapped = warning;
|
|
368
|
+
warning = error;
|
|
369
|
+
error = swapped;
|
|
370
|
+
}
|
|
371
|
+
return { warning, error };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function parsePathDisplay(value: unknown): PathDisplayConfig {
|
|
375
|
+
const defaults = defaultConfig.pathDisplay;
|
|
376
|
+
if (!isRecord(value)) return { ...defaults };
|
|
377
|
+
const mode = value.mode === "full" || value.mode === "basename" ? value.mode : defaults.mode;
|
|
378
|
+
const rawDepth = value.depth;
|
|
379
|
+
const depth =
|
|
380
|
+
typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 0
|
|
381
|
+
? Math.min(5, Math.floor(rawDepth))
|
|
382
|
+
: defaults.depth;
|
|
383
|
+
return { mode, depth };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeGitBranchMaxLength(value: unknown): GitBranchMaxLength {
|
|
387
|
+
if (value === "full") return value;
|
|
388
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
389
|
+
return defaultConfig.gitBranch.maxLength;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function parseGitBranchConfig(value: unknown): GitBranchConfig {
|
|
393
|
+
const defaults = defaultConfig.gitBranch;
|
|
394
|
+
if (!isRecord(value)) return { ...defaults };
|
|
395
|
+
return {
|
|
396
|
+
maxLength: normalizeGitBranchMaxLength(value.maxLength),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function stringValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
401
|
+
const value = record[key];
|
|
402
|
+
return typeof value === "string" ? value : undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function colorValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
406
|
+
const value = stringValue(record, key);
|
|
407
|
+
if (key === "editorBorder" && value === "sakura-macaron-gradient") return value;
|
|
408
|
+
return value !== undefined && isSupportedColorSpec(value) ? value : undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function colorSourceValue(
|
|
412
|
+
record: Record<string, unknown>,
|
|
413
|
+
key: keyof ColorSourcesConfig,
|
|
414
|
+
): ColorSource {
|
|
415
|
+
const value = record[key];
|
|
416
|
+
return value === "terminal" || value === "theme" ? value : defaultConfig.colorSources[key];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function booleanValue(record: Record<string, unknown>, key: keyof UiFeaturesConfig): boolean {
|
|
420
|
+
const value = record[key];
|
|
421
|
+
return typeof value === "boolean" ? value : defaultConfig.features[key];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function footerSegmentValue(
|
|
425
|
+
record: Record<string, unknown>,
|
|
426
|
+
key: keyof FooterSegmentsConfig,
|
|
427
|
+
): boolean {
|
|
428
|
+
const value = record[key];
|
|
429
|
+
return typeof value === "boolean" ? value : defaultConfig.footerSegments[key];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function definedColors(
|
|
433
|
+
colors: Partial<Record<keyof PolishedTuiConfig["colors"], string | undefined>>,
|
|
434
|
+
): Partial<PolishedTuiConfig["colors"]> {
|
|
435
|
+
return Object.fromEntries(
|
|
436
|
+
Object.entries(colors).filter(
|
|
437
|
+
(entry): entry is [keyof PolishedTuiConfig["colors"], string] => typeof entry[1] === "string",
|
|
438
|
+
),
|
|
439
|
+
) as Partial<PolishedTuiConfig["colors"]>;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeIconOverrides(record: Record<string, unknown>): Partial<IconGlyphs> {
|
|
443
|
+
return Object.fromEntries(
|
|
444
|
+
ICON_GLYPH_KEYS.flatMap((key) => {
|
|
445
|
+
const value = stringValue(record, key);
|
|
446
|
+
return value === undefined ? [] : [[key, value]];
|
|
447
|
+
}),
|
|
448
|
+
) as Partial<IconGlyphs>;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiConfig["colors"]> {
|
|
452
|
+
return definedColors({
|
|
453
|
+
cwd: colorValue(record, "cwd") ?? colorValue(record, "cwdText"),
|
|
454
|
+
gitBranch: colorValue(record, "gitBranch") ?? colorValue(record, "git"),
|
|
455
|
+
gitStatus: colorValue(record, "gitStatus"),
|
|
456
|
+
contextNormal: colorValue(record, "contextNormal"),
|
|
457
|
+
contextWarning: colorValue(record, "contextWarning"),
|
|
458
|
+
contextError: colorValue(record, "contextError"),
|
|
459
|
+
tokens: colorValue(record, "tokens"),
|
|
460
|
+
cost: colorValue(record, "cost"),
|
|
461
|
+
separator: colorValue(record, "separator"),
|
|
462
|
+
runtimePrefix: colorValue(record, "runtimePrefix"),
|
|
463
|
+
extensionStatus: colorValue(record, "extensionStatus"),
|
|
464
|
+
sessionDuration: colorValue(record, "sessionDuration"),
|
|
465
|
+
packageVersion: colorValue(record, "packageVersion"),
|
|
466
|
+
gitCommit: colorValue(record, "gitCommit"),
|
|
467
|
+
gitMetricsAdded: colorValue(record, "gitMetricsAdded"),
|
|
468
|
+
gitMetricsDeleted: colorValue(record, "gitMetricsDeleted"),
|
|
469
|
+
username: colorValue(record, "username"),
|
|
470
|
+
time: colorValue(record, "time"),
|
|
471
|
+
os: colorValue(record, "os"),
|
|
472
|
+
editorAccent: colorValue(record, "editorAccent"),
|
|
473
|
+
editorPrompt: colorValue(record, "editorPrompt"),
|
|
474
|
+
editorBorder: colorValue(record, "editorBorder"),
|
|
475
|
+
editorModel: colorValue(record, "editorModel"),
|
|
476
|
+
editorProvider: colorValue(record, "editorProvider"),
|
|
477
|
+
editorThinking: colorValue(record, "editorThinking"),
|
|
478
|
+
editorThinkingMinimal: colorValue(record, "editorThinkingMinimal"),
|
|
479
|
+
editorThinkingLow: colorValue(record, "editorThinkingLow"),
|
|
480
|
+
editorThinkingMedium: colorValue(record, "editorThinkingMedium"),
|
|
481
|
+
editorThinkingHigh: colorValue(record, "editorThinkingHigh"),
|
|
482
|
+
editorThinkingXhigh: colorValue(record, "editorThinkingXhigh"),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function normalizeColorSources(record: Record<string, unknown>): ColorSourcesConfig {
|
|
487
|
+
return {
|
|
488
|
+
starship: colorSourceValue(record, "starship"),
|
|
489
|
+
editor: colorSourceValue(record, "editor"),
|
|
490
|
+
userMessages: colorSourceValue(record, "userMessages"),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function normalizeUiFeatures(record: Record<string, unknown>): UiFeaturesConfig {
|
|
495
|
+
return {
|
|
496
|
+
editor: booleanValue(record, "editor"),
|
|
497
|
+
statusLine: booleanValue(record, "statusLine"),
|
|
498
|
+
copyFriendly: booleanValue(record, "copyFriendly"),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function normalizeFooterSegments(record: Record<string, unknown>): FooterSegmentsConfig {
|
|
503
|
+
return {
|
|
504
|
+
cwd: footerSegmentValue(record, "cwd"),
|
|
505
|
+
gitBranch: footerSegmentValue(record, "gitBranch"),
|
|
506
|
+
gitStatus: footerSegmentValue(record, "gitStatus"),
|
|
507
|
+
gitCounts: footerSegmentValue(record, "gitCounts"),
|
|
508
|
+
runtime: footerSegmentValue(record, "runtime"),
|
|
509
|
+
context: footerSegmentValue(record, "context"),
|
|
510
|
+
tokens: footerSegmentValue(record, "tokens"),
|
|
511
|
+
cost: footerSegmentValue(record, "cost"),
|
|
512
|
+
sessionDuration: footerSegmentValue(record, "sessionDuration"),
|
|
513
|
+
username: footerSegmentValue(record, "username"),
|
|
514
|
+
time: footerSegmentValue(record, "time"),
|
|
515
|
+
os: footerSegmentValue(record, "os"),
|
|
516
|
+
packageVersion: footerSegmentValue(record, "packageVersion"),
|
|
517
|
+
gitCommit: footerSegmentValue(record, "gitCommit"),
|
|
518
|
+
gitMetrics: footerSegmentValue(record, "gitMetrics"),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Clamp hashLength to Git's valid abbreviation range [4, 40]. */
|
|
523
|
+
function normalizeGitHashLength(value: unknown): number {
|
|
524
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
525
|
+
if (!Number.isFinite(parsed)) return defaultConfig.gitCommit.hashLength;
|
|
526
|
+
const rounded = Math.round(parsed);
|
|
527
|
+
return Math.min(40, Math.max(4, rounded));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function normalizeGitCommitConfig(record: Record<string, unknown>): GitCommitConfig {
|
|
531
|
+
return {
|
|
532
|
+
hashLength: normalizeGitHashLength(record.hashLength),
|
|
533
|
+
onlyDetached:
|
|
534
|
+
typeof record.onlyDetached === "boolean"
|
|
535
|
+
? record.onlyDetached
|
|
536
|
+
: defaultConfig.gitCommit.onlyDetached,
|
|
537
|
+
showTag: typeof record.showTag === "boolean" ? record.showTag : defaultConfig.gitCommit.showTag,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function normalizeGitMetricsConfig(record: Record<string, unknown>): GitMetricsConfig {
|
|
542
|
+
return {
|
|
543
|
+
onlyNonzero:
|
|
544
|
+
typeof record.onlyNonzero === "boolean"
|
|
545
|
+
? record.onlyNonzero
|
|
546
|
+
: defaultConfig.gitMetrics.onlyNonzero,
|
|
547
|
+
ignoreSubmodules:
|
|
548
|
+
typeof record.ignoreSubmodules === "boolean"
|
|
549
|
+
? record.ignoreSubmodules
|
|
550
|
+
: defaultConfig.gitMetrics.ignoreSubmodules,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function isExtensionStatusPlacement(value: unknown): value is ExtensionStatusPlacement {
|
|
555
|
+
return value === "off" || value === "left" || value === "middle" || value === "right";
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function isExtensionStatusColorMode(value: unknown): value is ExtensionStatusColorMode {
|
|
559
|
+
return value === "zentui" || value === "original";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function normalizeExtensionStatuses(record: Record<string, unknown>): ExtensionStatusesConfig {
|
|
563
|
+
const defaultPlacement = isExtensionStatusPlacement(record.defaultPlacement)
|
|
564
|
+
? record.defaultPlacement
|
|
565
|
+
: defaultConfig.extensionStatuses.defaultPlacement;
|
|
566
|
+
const placements = isRecord(record.placements)
|
|
567
|
+
? Object.fromEntries(
|
|
568
|
+
Object.entries(record.placements).filter(
|
|
569
|
+
(entry): entry is [string, ExtensionStatusPlacement] =>
|
|
570
|
+
isExtensionStatusPlacement(entry[1]),
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
: {};
|
|
574
|
+
const colorModes = isRecord(record.colorModes)
|
|
575
|
+
? Object.fromEntries(
|
|
576
|
+
Object.entries(record.colorModes).filter(
|
|
577
|
+
(entry): entry is [string, ExtensionStatusColorMode] =>
|
|
578
|
+
isExtensionStatusColorMode(entry[1]),
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
: {};
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
defaultPlacement,
|
|
585
|
+
placements,
|
|
586
|
+
colorModes,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function normalizeFixedEditorConfig(record: Record<string, unknown>): FixedEditorConfig {
|
|
591
|
+
return {
|
|
592
|
+
enabled:
|
|
593
|
+
typeof record.enabled === "boolean" ? record.enabled : defaultConfig.fixedEditor.enabled,
|
|
594
|
+
mouseScroll:
|
|
595
|
+
typeof record.mouseScroll === "boolean"
|
|
596
|
+
? record.mouseScroll
|
|
597
|
+
: defaultConfig.fixedEditor.mouseScroll,
|
|
598
|
+
copyNotice:
|
|
599
|
+
typeof record.copyNotice === "boolean"
|
|
600
|
+
? record.copyNotice
|
|
601
|
+
: defaultConfig.fixedEditor.copyNotice,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
|
|
606
|
+
return value === "starship" || value === "editor" || value === "userMessages";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function isUiFeatureKey(value: string): value is keyof UiFeaturesConfig {
|
|
610
|
+
return value === "editor" || value === "statusLine" || value === "copyFriendly";
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function isFooterSegmentKey(value: string): value is keyof FooterSegmentsConfig {
|
|
614
|
+
return (
|
|
615
|
+
value === "cwd" ||
|
|
616
|
+
value === "gitBranch" ||
|
|
617
|
+
value === "gitStatus" ||
|
|
618
|
+
value === "gitCounts" ||
|
|
619
|
+
value === "runtime" ||
|
|
620
|
+
value === "context" ||
|
|
621
|
+
value === "tokens" ||
|
|
622
|
+
value === "cost" ||
|
|
623
|
+
value === "sessionDuration" ||
|
|
624
|
+
value === "username" ||
|
|
625
|
+
value === "time" ||
|
|
626
|
+
value === "os" ||
|
|
627
|
+
value === "packageVersion" ||
|
|
628
|
+
value === "gitCommit" ||
|
|
629
|
+
value === "gitMetrics"
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function validColorSourceEntries(record: Record<string, unknown>): Partial<ColorSourcesConfig> {
|
|
634
|
+
return Object.fromEntries(
|
|
635
|
+
Object.entries(record).filter((entry): entry is [keyof ColorSourcesConfig, ColorSource] => {
|
|
636
|
+
const [key, value] = entry;
|
|
637
|
+
return isColorSourceKey(key) && (value === "theme" || value === "terminal");
|
|
638
|
+
}),
|
|
639
|
+
) as Partial<ColorSourcesConfig>;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function validUiFeatureEntries(record: Record<string, unknown>): Partial<UiFeaturesConfig> {
|
|
643
|
+
return Object.fromEntries(
|
|
644
|
+
Object.entries(record).filter((entry): entry is [keyof UiFeaturesConfig, boolean] => {
|
|
645
|
+
const [key, value] = entry;
|
|
646
|
+
return isUiFeatureKey(key) && typeof value === "boolean";
|
|
647
|
+
}),
|
|
648
|
+
) as Partial<UiFeaturesConfig>;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function validFooterSegmentEntries(record: Record<string, unknown>): Partial<FooterSegmentsConfig> {
|
|
652
|
+
return Object.fromEntries(
|
|
653
|
+
Object.entries(record).filter((entry): entry is [keyof FooterSegmentsConfig, boolean] => {
|
|
654
|
+
const [key, value] = entry;
|
|
655
|
+
return isFooterSegmentKey(key) && typeof value === "boolean";
|
|
656
|
+
}),
|
|
657
|
+
) as Partial<FooterSegmentsConfig>;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
type ConfigFileState =
|
|
661
|
+
| { kind: "missing"; record: ConfigRecord; writePath: string }
|
|
662
|
+
| { kind: "valid"; record: ConfigRecord; writePath: string; mode: number }
|
|
663
|
+
| { kind: "corrupt"; error: unknown };
|
|
664
|
+
|
|
665
|
+
function errorCode(error: unknown): string | undefined {
|
|
666
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
667
|
+
? String(error.code)
|
|
668
|
+
: undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function readConfigFileState(path: string): ConfigFileState {
|
|
672
|
+
let writePath = path;
|
|
673
|
+
try {
|
|
674
|
+
const pathStat = lstatSync(path);
|
|
675
|
+
if (pathStat.isSymbolicLink()) writePath = realpathSync(path);
|
|
676
|
+
const targetStat = statSync(writePath);
|
|
677
|
+
const parsed = JSON.parse(readFileSync(writePath, "utf8"));
|
|
678
|
+
return isRecord(parsed)
|
|
679
|
+
? { kind: "valid", record: parsed, writePath, mode: targetStat.mode & 0o7777 }
|
|
680
|
+
: { kind: "corrupt", error: new Error("top-level value must be a JSON object") };
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (errorCode(error) === "ENOENT") {
|
|
683
|
+
try {
|
|
684
|
+
lstatSync(path);
|
|
685
|
+
} catch (pathError) {
|
|
686
|
+
if (errorCode(pathError) === "ENOENT")
|
|
687
|
+
return { kind: "missing", record: {}, writePath: path };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return { kind: "corrupt", error };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function writeConfigAtomically(path: string, record: ConfigRecord, mode?: number): void {
|
|
695
|
+
const tempPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
696
|
+
let file: number | undefined;
|
|
697
|
+
try {
|
|
698
|
+
file = openSync(tempPath, "wx", mode ?? 0o666);
|
|
699
|
+
if (mode !== undefined) fchmodSync(file, mode);
|
|
700
|
+
writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
701
|
+
fsyncSync(file);
|
|
702
|
+
closeSync(file);
|
|
703
|
+
file = undefined;
|
|
704
|
+
renameSync(tempPath, path);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (file !== undefined) {
|
|
707
|
+
try {
|
|
708
|
+
closeSync(file);
|
|
709
|
+
} catch {}
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
unlinkSync(tempPath);
|
|
713
|
+
} catch (cleanupError) {
|
|
714
|
+
if (errorCode(cleanupError) !== "ENOENT") {
|
|
715
|
+
// Preserve the persistence failure; the best-effort cleanup error is secondary.
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function mutateConfig(path: string, mutate: (record: ConfigRecord) => void): PolishedTuiConfig {
|
|
723
|
+
const state = readConfigFileState(path);
|
|
724
|
+
if (state.kind === "corrupt") {
|
|
725
|
+
const detail = state.error instanceof Error ? ` (${state.error.message})` : "";
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Refusing to save Zentui config because ${path} is corrupt or unreadable; fix or remove it first.${detail}`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
mutate(state.record);
|
|
731
|
+
writeConfigAtomically(
|
|
732
|
+
state.writePath,
|
|
733
|
+
state.record,
|
|
734
|
+
state.kind === "valid" ? state.mode : undefined,
|
|
735
|
+
);
|
|
736
|
+
return mergeConfig(state.record);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function ensureConfigExists(): void {
|
|
740
|
+
// Intentionally left as a no-op. Zentui config is user-owned and
|
|
741
|
+
// compatibility-sensitive: runtime defaults come from `mergeConfig({})`, and
|
|
742
|
+
// the extension should not persist opinionated defaults unless the user
|
|
743
|
+
// explicitly changes a setting.
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
747
|
+
const config = isRecord(parsed) ? parsed : {};
|
|
748
|
+
const iconsRecord = isRecord(config.icons) ? (config.icons as Record<string, unknown>) : {};
|
|
749
|
+
const iconMode = normalizeIconMode(iconsRecord.mode);
|
|
750
|
+
const iconOverrides = normalizeIconOverrides(iconsRecord);
|
|
751
|
+
const colors = isRecord(config.colors)
|
|
752
|
+
? normalizeColors(config.colors as Record<string, unknown>)
|
|
753
|
+
: {};
|
|
754
|
+
const colorSources = isRecord(config.colorSources)
|
|
755
|
+
? normalizeColorSources(config.colorSources as Record<string, unknown>)
|
|
756
|
+
: defaultConfig.colorSources;
|
|
757
|
+
const features = isRecord(config.features)
|
|
758
|
+
? normalizeUiFeatures(config.features as Record<string, unknown>)
|
|
759
|
+
: defaultConfig.features;
|
|
760
|
+
const footerSegments = isRecord(config.footerSegments)
|
|
761
|
+
? normalizeFooterSegments(config.footerSegments as Record<string, unknown>)
|
|
762
|
+
: defaultConfig.footerSegments;
|
|
763
|
+
const extensionStatuses = isRecord(config.extensionStatuses)
|
|
764
|
+
? normalizeExtensionStatuses(config.extensionStatuses as Record<string, unknown>)
|
|
765
|
+
: defaultConfig.extensionStatuses;
|
|
766
|
+
const gitCommit = isRecord(config.gitCommit)
|
|
767
|
+
? normalizeGitCommitConfig(config.gitCommit as Record<string, unknown>)
|
|
768
|
+
: defaultConfig.gitCommit;
|
|
769
|
+
const gitMetrics = isRecord(config.gitMetrics)
|
|
770
|
+
? normalizeGitMetricsConfig(config.gitMetrics as Record<string, unknown>)
|
|
771
|
+
: defaultConfig.gitMetrics;
|
|
772
|
+
const gitBranch = parseGitBranchConfig(config.gitBranch);
|
|
773
|
+
const fixedEditor = isRecord(config.fixedEditor)
|
|
774
|
+
? normalizeFixedEditorConfig(config.fixedEditor as Record<string, unknown>)
|
|
775
|
+
: defaultConfig.fixedEditor;
|
|
776
|
+
return {
|
|
777
|
+
projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
|
|
778
|
+
footerFormat: stringValue(config, "footerFormat") ?? "",
|
|
779
|
+
separator: parseSeparatorStyle(config.separator),
|
|
780
|
+
contextStyle: parseContextStyle(config.contextStyle),
|
|
781
|
+
contextThresholds: parseContextThresholds(config.contextThresholds),
|
|
782
|
+
pathDisplay: parsePathDisplay(config.pathDisplay),
|
|
783
|
+
gitBranch,
|
|
784
|
+
icons: resolveConfiguredIcons(iconMode, iconOverrides),
|
|
785
|
+
colors: {
|
|
786
|
+
...defaultConfig.colors,
|
|
787
|
+
...colors,
|
|
788
|
+
},
|
|
789
|
+
colorSources: { ...colorSources },
|
|
790
|
+
features: { ...features },
|
|
791
|
+
footerSegments: { ...footerSegments },
|
|
792
|
+
gitCommit,
|
|
793
|
+
gitMetrics,
|
|
794
|
+
extensionStatuses: {
|
|
795
|
+
defaultPlacement: extensionStatuses.defaultPlacement,
|
|
796
|
+
placements: { ...extensionStatuses.placements },
|
|
797
|
+
colorModes: { ...extensionStatuses.colorModes },
|
|
798
|
+
},
|
|
799
|
+
fixedEditor,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function getExtensionStatusPlacement(
|
|
804
|
+
config: PolishedTuiConfig,
|
|
805
|
+
key: string,
|
|
806
|
+
): ExtensionStatusPlacement {
|
|
807
|
+
return config.extensionStatuses.placements[key] ?? config.extensionStatuses.defaultPlacement;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function getExtensionStatusColorMode(
|
|
811
|
+
config: PolishedTuiConfig,
|
|
812
|
+
key: string,
|
|
813
|
+
): ExtensionStatusColorMode {
|
|
814
|
+
return config.extensionStatuses.colorModes[key] ?? DEFAULT_EXTENSION_STATUS_COLOR_MODE;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export function loadConfig(): PolishedTuiConfig {
|
|
818
|
+
try {
|
|
819
|
+
if (!existsSync(configPath)) return mergeConfig({});
|
|
820
|
+
return mergeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
821
|
+
} catch {
|
|
822
|
+
return mergeConfig({});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function saveColorSourcesPatch(
|
|
827
|
+
patch: Partial<ColorSourcesConfig>,
|
|
828
|
+
path = configPath,
|
|
829
|
+
): PolishedTuiConfig {
|
|
830
|
+
return mutateConfig(path, (record) => {
|
|
831
|
+
const existing = isRecord(record.colorSources)
|
|
832
|
+
? { ...(record.colorSources as Record<string, unknown>) }
|
|
833
|
+
: {};
|
|
834
|
+
record.colorSources = {
|
|
835
|
+
...existing,
|
|
836
|
+
...validColorSourceEntries(patch),
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export function saveUiFeaturesPatch(
|
|
842
|
+
patch: Partial<UiFeaturesConfig>,
|
|
843
|
+
path = configPath,
|
|
844
|
+
): PolishedTuiConfig {
|
|
845
|
+
return mutateConfig(path, (record) => {
|
|
846
|
+
const existing = isRecord(record.features)
|
|
847
|
+
? { ...(record.features as Record<string, unknown>) }
|
|
848
|
+
: {};
|
|
849
|
+
record.features = {
|
|
850
|
+
...existing,
|
|
851
|
+
...validUiFeatureEntries(patch),
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function saveFooterSegmentsPatch(
|
|
857
|
+
patch: Partial<FooterSegmentsConfig>,
|
|
858
|
+
path = configPath,
|
|
859
|
+
): PolishedTuiConfig {
|
|
860
|
+
return mutateConfig(path, (record) => {
|
|
861
|
+
const existing = isRecord(record.footerSegments)
|
|
862
|
+
? { ...(record.footerSegments as Record<string, unknown>) }
|
|
863
|
+
: {};
|
|
864
|
+
record.footerSegments = {
|
|
865
|
+
...existing,
|
|
866
|
+
...validFooterSegmentEntries(patch),
|
|
867
|
+
};
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function saveFooterFormatPatch(value: string, path = configPath): PolishedTuiConfig {
|
|
872
|
+
return mutateConfig(path, (record) => {
|
|
873
|
+
record.footerFormat = typeof value === "string" ? value : "";
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export function saveIconsModePatch(mode: IconMode, path = configPath): PolishedTuiConfig {
|
|
878
|
+
return mutateConfig(path, (record) => {
|
|
879
|
+
const existing = isRecord(record.icons) ? { ...(record.icons as Record<string, unknown>) } : {};
|
|
880
|
+
record.icons = {
|
|
881
|
+
...existing,
|
|
882
|
+
mode: normalizeIconMode(mode),
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
export function saveContextStylePatch(style: ContextStyle, path = configPath): PolishedTuiConfig {
|
|
888
|
+
return mutateConfig(path, (record) => {
|
|
889
|
+
record.contextStyle = parseContextStyle(style);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export function saveSeparatorPatch(
|
|
894
|
+
separator: SeparatorStyle,
|
|
895
|
+
path = configPath,
|
|
896
|
+
): PolishedTuiConfig {
|
|
897
|
+
return mutateConfig(path, (record) => {
|
|
898
|
+
record.separator = parseSeparatorStyle(separator);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function saveContextThresholdsPatch(
|
|
903
|
+
thresholds: Partial<ContextThresholds>,
|
|
904
|
+
path = configPath,
|
|
905
|
+
): PolishedTuiConfig {
|
|
906
|
+
return mutateConfig(path, (record) => {
|
|
907
|
+
const existing = isRecord(record.contextThresholds)
|
|
908
|
+
? { ...(record.contextThresholds as Record<string, unknown>) }
|
|
909
|
+
: {};
|
|
910
|
+
record.contextThresholds = {
|
|
911
|
+
...existing,
|
|
912
|
+
...thresholds,
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export function savePathDisplayPatch(
|
|
918
|
+
patch: Partial<PathDisplayConfig>,
|
|
919
|
+
path = configPath,
|
|
920
|
+
): PolishedTuiConfig {
|
|
921
|
+
return mutateConfig(path, (record) => {
|
|
922
|
+
const existing = isRecord(record.pathDisplay)
|
|
923
|
+
? { ...(record.pathDisplay as Record<string, unknown>) }
|
|
924
|
+
: {};
|
|
925
|
+
if (patch.mode !== undefined) existing.mode = patch.mode;
|
|
926
|
+
if (patch.depth !== undefined) existing.depth = patch.depth;
|
|
927
|
+
record.pathDisplay = existing;
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function saveGitBranchPatch(
|
|
932
|
+
patch: Partial<GitBranchConfig>,
|
|
933
|
+
path = configPath,
|
|
934
|
+
): PolishedTuiConfig {
|
|
935
|
+
return mutateConfig(path, (record) => {
|
|
936
|
+
const existing = isRecord(record.gitBranch)
|
|
937
|
+
? { ...(record.gitBranch as Record<string, unknown>) }
|
|
938
|
+
: {};
|
|
939
|
+
if (patch.maxLength !== undefined)
|
|
940
|
+
existing.maxLength = normalizeGitBranchMaxLength(patch.maxLength);
|
|
941
|
+
record.gitBranch = existing;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export function saveExtensionStatusPlacement(
|
|
946
|
+
key: string,
|
|
947
|
+
placement: ExtensionStatusPlacement,
|
|
948
|
+
path = configPath,
|
|
949
|
+
): PolishedTuiConfig {
|
|
950
|
+
return mutateConfig(path, (record) => {
|
|
951
|
+
const existingExtensionStatuses = isRecord(record.extensionStatuses)
|
|
952
|
+
? { ...(record.extensionStatuses as Record<string, unknown>) }
|
|
953
|
+
: {};
|
|
954
|
+
const existingPlacements = isRecord(existingExtensionStatuses.placements)
|
|
955
|
+
? { ...(existingExtensionStatuses.placements as Record<string, unknown>) }
|
|
956
|
+
: {};
|
|
957
|
+
|
|
958
|
+
Object.defineProperty(existingPlacements, key, {
|
|
959
|
+
value: placement,
|
|
960
|
+
enumerable: true,
|
|
961
|
+
configurable: true,
|
|
962
|
+
writable: true,
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
record.extensionStatuses = {
|
|
966
|
+
...existingExtensionStatuses,
|
|
967
|
+
placements: existingPlacements,
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export function saveExtensionStatusColorMode(
|
|
973
|
+
key: string,
|
|
974
|
+
colorMode: ExtensionStatusColorMode,
|
|
975
|
+
path = configPath,
|
|
976
|
+
): PolishedTuiConfig {
|
|
977
|
+
return mutateConfig(path, (record) => {
|
|
978
|
+
const existingExtensionStatuses = isRecord(record.extensionStatuses)
|
|
979
|
+
? { ...(record.extensionStatuses as Record<string, unknown>) }
|
|
980
|
+
: {};
|
|
981
|
+
const existingColorModes = isRecord(existingExtensionStatuses.colorModes)
|
|
982
|
+
? { ...(existingExtensionStatuses.colorModes as Record<string, unknown>) }
|
|
983
|
+
: {};
|
|
984
|
+
|
|
985
|
+
Object.defineProperty(existingColorModes, key, {
|
|
986
|
+
value: colorMode,
|
|
987
|
+
enumerable: true,
|
|
988
|
+
configurable: true,
|
|
989
|
+
writable: true,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
record.extensionStatuses = {
|
|
993
|
+
...existingExtensionStatuses,
|
|
994
|
+
colorModes: existingColorModes,
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function saveFixedEditorPatch(
|
|
1000
|
+
patch: Partial<FixedEditorConfig>,
|
|
1001
|
+
path = configPath,
|
|
1002
|
+
): PolishedTuiConfig {
|
|
1003
|
+
return mutateConfig(path, (record) => {
|
|
1004
|
+
const existing = isRecord(record.fixedEditor)
|
|
1005
|
+
? { ...(record.fixedEditor as Record<string, unknown>) }
|
|
1006
|
+
: {};
|
|
1007
|
+
record.fixedEditor = {
|
|
1008
|
+
...existing,
|
|
1009
|
+
...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
|
|
1010
|
+
...(patch.mouseScroll !== undefined ? { mouseScroll: patch.mouseScroll } : {}),
|
|
1011
|
+
...(patch.copyNotice !== undefined ? { copyNotice: patch.copyNotice } : {}),
|
|
1012
|
+
};
|
|
1013
|
+
});
|
|
1014
|
+
}
|