@narumitw/pi-statusline 0.49.0 → 0.49.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/src/settings.ts DELETED
@@ -1,702 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
- import { INFORMATION_PROFILES } from "./information-profiles.js";
6
- import { segmentPaletteForPreset } from "./presets/index.js";
7
- import {
8
- type ConfigSegmentName,
9
- DENSITIES,
10
- LINE_BREAK_SEGMENT_NAME,
11
- PALETTE_NAMES,
12
- PALETTE_PRESET_NAMES,
13
- type PaletteName,
14
- SEGMENT_NAMES,
15
- SEPARATOR_NAMES,
16
- type SegmentName,
17
- type SegmentPalette,
18
- type StatuslineConfig,
19
- TRUNCATION_DIRECTIONS,
20
- } from "./types.js";
21
-
22
- export const SETTINGS_FILE_NAME = "pi-statusline.json";
23
- const LEGACY_SETTINGS_FILE_NAME = "pi-statusline-settings.json";
24
-
25
- const DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS: Record<string, string> = {
26
- accounts: "👤",
27
- caffeinate: "💊",
28
- "chrome-devtools": "🌐",
29
- firecrawl: "🔥",
30
- "github-pr": "🔎",
31
- goal: "🎯",
32
- "google-genai": "✨",
33
- lsp: "🧰",
34
- "plan-mode": "📝",
35
- retry: "🔁",
36
- subagents: "🧑‍🤝‍🧑",
37
- sync: "🔄",
38
- usage: "📊",
39
- };
40
-
41
- export const DEFAULT_EXTENSION_STATUS_ICONS: Record<string, string> = {
42
- ...DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS,
43
- "codex-usage": "📊",
44
- pisync: "🔄",
45
- "unknown-error-retry": "🔁",
46
- };
47
-
48
- const LEGACY_STATUS_ICON_KEYS = {
49
- pisync: "sync",
50
- "unknown-error-retry": "retry",
51
- } as const;
52
-
53
- const DEFAULT_SEGMENTS: SegmentName[] = [...INFORMATION_PROFILES.balanced];
54
-
55
- export const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {
56
- palettePreset: "tokyo-night",
57
- palette: segmentPaletteForPreset("tokyo-night"),
58
- density: "compact",
59
- separator: "none",
60
- segments: DEFAULT_SEGMENTS,
61
- segmentText: {
62
- brand: { prefix: "", suffix: "" },
63
- provider: { prefix: "🔌 ", suffix: "" },
64
- model: {
65
- prefix: "🤖 ",
66
- suffix: "",
67
- truncationLength: 36,
68
- truncationSymbol: "…",
69
- truncationDirection: "start",
70
- },
71
- thinking: { prefix: "🧠 ", suffix: "" },
72
- cwd: { prefix: "📁 ", suffix: "" },
73
- branch: { prefix: "🌿 ", suffix: "" },
74
- tools: { prefix: "", suffix: "" },
75
- context: { prefix: "🪟 ctx ", suffix: "" },
76
- tokens: { prefix: "🔢 ", suffix: "" },
77
- cache: { prefix: "📦 ", suffix: "" },
78
- cost: { prefix: "💸 $", suffix: "" },
79
- time: { prefix: "🕒 ", suffix: "" },
80
- turn: { prefix: "🔁 #", suffix: "" },
81
- },
82
- extensionStatusIcons: DEFAULT_EXTENSION_STATUS_ICONS,
83
- };
84
-
85
- const DEFAULT_STATUSLINE_DOCUMENT_CONFIG = {
86
- palettePreset: DEFAULT_STATUSLINE_CONFIG.palettePreset,
87
- density: DEFAULT_STATUSLINE_CONFIG.density,
88
- separator: DEFAULT_STATUSLINE_CONFIG.separator,
89
- segments: DEFAULT_SEGMENTS,
90
- segmentText: DEFAULT_STATUSLINE_CONFIG.segmentText,
91
- extensionStatusIcons: DEFAULT_DOCUMENT_EXTENSION_STATUS_ICONS,
92
- } satisfies Omit<StatuslineConfig, "palette">;
93
-
94
- export const DEFAULT_STATUSLINE_DOCUMENT = `${JSON.stringify(
95
- DEFAULT_STATUSLINE_DOCUMENT_CONFIG,
96
- null,
97
- "\t",
98
- )}\n`;
99
-
100
- export interface StatuslineConfigDiagnostic {
101
- severity: "warning" | "error";
102
- code: "unknown" | "invalid" | "parse" | "io";
103
- path: string;
104
- message: string;
105
- }
106
-
107
- export interface LoadedStatuslineSettings {
108
- config: StatuslineConfig;
109
- source: "built-in" | "user";
110
- settingsPath: string;
111
- rawDocument?: string;
112
- fileIdentity?: { dev: number; ino: number };
113
- diagnostics: StatuslineConfigDiagnostic[];
114
- }
115
-
116
- interface AtomicFileSystem {
117
- mkdirSync: typeof mkdirSync;
118
- writeFileSync: typeof writeFileSync;
119
- renameSync: typeof renameSync;
120
- rmSync: typeof rmSync;
121
- }
122
-
123
- let pendingSettingsNotice: string | undefined;
124
-
125
- export function settingsFilePath(agentDir = getAgentDir()): string {
126
- return join(agentDir, SETTINGS_FILE_NAME);
127
- }
128
-
129
- export function createDefaultConfig(): StatuslineConfig {
130
- return cloneConfig(DEFAULT_STATUSLINE_CONFIG);
131
- }
132
-
133
- export function normalizeStatuslineConfig(value: unknown): {
134
- config: StatuslineConfig;
135
- diagnostics: StatuslineConfigDiagnostic[];
136
- } {
137
- const config = createDefaultConfig();
138
- const diagnostics: StatuslineConfigDiagnostic[] = [];
139
- if (!isRecord(value)) {
140
- return {
141
- config,
142
- diagnostics: [invalidDiagnostic("", "Settings must contain a JSON object", "error")],
143
- };
144
- }
145
- const knownRoot = new Set([
146
- "palettePreset",
147
- "palette",
148
- "density",
149
- "separator",
150
- "segments",
151
- "segmentText",
152
- "extensionStatusIcons",
153
- ]);
154
- for (const key of Object.keys(value)) {
155
- if (!knownRoot.has(key)) diagnostics.push(unknownDiagnostic(key));
156
- }
157
-
158
- normalizePalette(value.palette, config, diagnostics);
159
- normalizeEnum(value, "palettePreset", PALETTE_PRESET_NAMES, config, diagnostics);
160
- if (!isRecord(value.palette) && isPaletteName(config.palettePreset)) {
161
- config.palette = segmentPaletteForPreset(config.palettePreset);
162
- }
163
- normalizeEnum(value, "density", DENSITIES, config, diagnostics);
164
- normalizeEnum(value, "separator", SEPARATOR_NAMES, config, diagnostics);
165
-
166
- if (value.segments !== undefined) {
167
- if (!Array.isArray(value.segments)) {
168
- diagnostics.push(invalidDiagnostic("segments", "Expected an array of segment names"));
169
- } else {
170
- const segments: ConfigSegmentName[] = [];
171
- const seen = new Set<SegmentName>();
172
- for (const [index, item] of value.segments.entries()) {
173
- const path = `segments[${index}]`;
174
- if (typeof item !== "string" || !isConfigSegmentName(item)) {
175
- diagnostics.push(invalidDiagnostic(path, "Unknown or non-string segment name"));
176
- continue;
177
- }
178
- if (item === LINE_BREAK_SEGMENT_NAME) {
179
- if (segments.at(-1) === LINE_BREAK_SEGMENT_NAME) {
180
- diagnostics.push(
181
- invalidDiagnostic(path, "Consecutive line_break segments are not allowed"),
182
- );
183
- continue;
184
- }
185
- segments.push(item);
186
- continue;
187
- }
188
- if (seen.has(item)) {
189
- diagnostics.push(invalidDiagnostic(path, `Duplicate segment ${JSON.stringify(item)}`));
190
- continue;
191
- }
192
- seen.add(item);
193
- segments.push(item);
194
- }
195
- config.segments = segments;
196
- }
197
- }
198
-
199
- if (value.segmentText !== undefined) {
200
- if (!isRecord(value.segmentText)) {
201
- diagnostics.push(invalidDiagnostic("segmentText", "Expected an object"));
202
- } else {
203
- for (const [name, presentation] of Object.entries(value.segmentText)) {
204
- const path = `segmentText.${name}`;
205
- if (!isSegmentName(name)) {
206
- diagnostics.push(unknownDiagnostic(path));
207
- continue;
208
- }
209
- if (!isRecord(presentation)) {
210
- diagnostics.push(invalidDiagnostic(path, "Expected an object"));
211
- continue;
212
- }
213
- const knownFields = new Set([
214
- "prefix",
215
- "suffix",
216
- ...(name === "model"
217
- ? ["truncationLength", "truncationSymbol", "truncationDirection"]
218
- : []),
219
- ]);
220
- for (const key of Object.keys(presentation)) {
221
- if (!knownFields.has(key)) diagnostics.push(unknownDiagnostic(`${path}.${key}`));
222
- }
223
- for (const field of ["prefix", "suffix"] as const) {
224
- const fieldValue = presentation[field];
225
- if (fieldValue === undefined) continue;
226
- if (!isSafeSegmentText(fieldValue, `${path}.${field}`, diagnostics)) continue;
227
- config.segmentText[name][field] = fieldValue;
228
- }
229
- if (name === "model") {
230
- normalizeModelTruncation(presentation, config, diagnostics);
231
- }
232
- }
233
- }
234
- }
235
-
236
- if (value.extensionStatusIcons !== undefined) {
237
- if (!isRecord(value.extensionStatusIcons)) {
238
- diagnostics.push(invalidDiagnostic("extensionStatusIcons", "Expected an object"));
239
- } else {
240
- for (const [key, icon] of Object.entries(value.extensionStatusIcons)) {
241
- if (typeof icon !== "string") {
242
- diagnostics.push(invalidDiagnostic(`extensionStatusIcons.${key}`, "Expected a string"));
243
- continue;
244
- }
245
- Object.defineProperty(config.extensionStatusIcons, key, {
246
- value: icon,
247
- enumerable: true,
248
- configurable: true,
249
- writable: true,
250
- });
251
- }
252
- for (const [legacyKey, canonicalKey] of Object.entries(LEGACY_STATUS_ICON_KEYS)) {
253
- const legacyIcon = Object.hasOwn(value.extensionStatusIcons, legacyKey)
254
- ? value.extensionStatusIcons[legacyKey]
255
- : undefined;
256
- const canonicalIcon = Object.hasOwn(value.extensionStatusIcons, canonicalKey)
257
- ? value.extensionStatusIcons[canonicalKey]
258
- : undefined;
259
- const targetKey = typeof canonicalIcon === "string" ? legacyKey : canonicalKey;
260
- const inheritedIcon =
261
- typeof canonicalIcon === "string"
262
- ? canonicalIcon
263
- : typeof legacyIcon === "string"
264
- ? legacyIcon
265
- : undefined;
266
- if (inheritedIcon === undefined) continue;
267
- Object.defineProperty(config.extensionStatusIcons, targetKey, {
268
- value: inheritedIcon,
269
- enumerable: true,
270
- configurable: true,
271
- writable: true,
272
- });
273
- }
274
- }
275
- }
276
-
277
- return { config, diagnostics };
278
- }
279
-
280
- export function loadStatuslineSettings(settingsPath: string): LoadedStatuslineSettings {
281
- let rawDocument: string;
282
- try {
283
- rawDocument = readFileSync(settingsPath, "utf8");
284
- } catch (error) {
285
- if ((error as NodeJS.ErrnoException).code === "ENOENT" && !pathExists(settingsPath)) {
286
- return builtInSettings(settingsPath);
287
- }
288
- return builtInSettings(settingsPath, [
289
- diagnostic("error", "io", "", `Unable to read settings: ${formatError(error)}`),
290
- ]);
291
- }
292
-
293
- let parsed: unknown;
294
- try {
295
- parsed = JSON.parse(rawDocument);
296
- } catch (error) {
297
- return {
298
- ...builtInSettings(settingsPath, [
299
- diagnostic("error", "parse", "", `Unable to parse JSON: ${formatError(error)}`),
300
- ]),
301
- rawDocument,
302
- };
303
- }
304
- const normalized = normalizeStatuslineConfig(parsed);
305
- return {
306
- config: normalized.config,
307
- source: normalized.diagnostics.some((item) => item.severity === "error") ? "built-in" : "user",
308
- settingsPath,
309
- rawDocument,
310
- diagnostics: normalized.diagnostics,
311
- };
312
- }
313
-
314
- export function loadStatuslineSettingsForAgent(agentDir = getAgentDir()): LoadedStatuslineSettings {
315
- pendingSettingsNotice = undefined;
316
- const canonicalPath = settingsFilePath(agentDir);
317
- const legacyPath = join(agentDir, LEGACY_SETTINGS_FILE_NAME);
318
- const canonical = loadStatuslineSettings(canonicalPath);
319
- if (!isMissingStatuslineSettings(canonical)) {
320
- if (!isMissingStatuslineSettings(loadStatuslineSettings(legacyPath))) {
321
- pendingSettingsNotice = `${LEGACY_SETTINGS_FILE_NAME} ignored because ${SETTINGS_FILE_NAME} takes precedence.`;
322
- }
323
- return canonical;
324
- }
325
- const legacy = loadStatuslineSettings(legacyPath);
326
- return isMissingStatuslineSettings(legacy)
327
- ? canonical
328
- : migrateLegacySettings(canonicalPath, legacy);
329
- }
330
-
331
- function migrateLegacySettings(
332
- canonicalPath: string,
333
- legacy: LoadedStatuslineSettings,
334
- ): LoadedStatuslineSettings {
335
- const racedCanonical = loadStatuslineSettings(canonicalPath);
336
- if (!isMissingStatuslineSettings(racedCanonical)) {
337
- pendingSettingsNotice = `${LEGACY_SETTINGS_FILE_NAME} ignored because ${SETTINGS_FILE_NAME} was created concurrently.`;
338
- return racedCanonical;
339
- }
340
- if (
341
- legacy.source !== "user" ||
342
- legacy.rawDocument === undefined ||
343
- blockingDiagnostics(legacy.diagnostics).length > 0
344
- ) {
345
- pendingSettingsNotice = `${LEGACY_SETTINGS_FILE_NAME} is invalid and was ignored.`;
346
- return legacy;
347
- }
348
- pendingSettingsNotice = `Using legacy ${LEGACY_SETTINGS_FILE_NAME}; rename it to ${SETTINGS_FILE_NAME}. Future saves write ${SETTINGS_FILE_NAME} without modifying the legacy file.`;
349
- return legacy;
350
- }
351
-
352
- function isMissingStatuslineSettings(settings: LoadedStatuslineSettings): boolean {
353
- return (
354
- settings.source === "built-in" &&
355
- settings.rawDocument === undefined &&
356
- settings.diagnostics.length === 0
357
- );
358
- }
359
-
360
- export function saveStatuslineSettingsDocument(
361
- settingsPath: string,
362
- rawDocument: string,
363
- overrides: Partial<AtomicFileSystem> = {},
364
- ): LoadedStatuslineSettings {
365
- let parsed: unknown;
366
- try {
367
- parsed = JSON.parse(rawDocument);
368
- } catch (error) {
369
- throw new Error(`Unable to parse JSON: ${formatError(error)}`);
370
- }
371
- const normalized = normalizeStatuslineConfig(parsed);
372
- const blocking = blockingDiagnostics(normalized.diagnostics);
373
- if (blocking.length > 0) {
374
- throw new Error(blocking.map((item) => `${item.path || "root"}: ${item.message}`).join("\n"));
375
- }
376
-
377
- const fs = { mkdirSync, writeFileSync, renameSync, rmSync, ...overrides };
378
- const replaceExisting = pathEntryExists(settingsPath);
379
- const temporaryPath = temporarySettingsPath(settingsPath);
380
- let fileIdentity: { dev: number; ino: number } | undefined;
381
- try {
382
- fs.mkdirSync(dirname(settingsPath), { recursive: true });
383
- fs.writeFileSync(temporaryPath, rawDocument, { encoding: "utf8", flag: "wx" });
384
- const info = lstatSync(temporaryPath);
385
- fileIdentity = { dev: info.dev, ino: info.ino };
386
- if (!replaceExisting && pathEntryExists(settingsPath)) {
387
- throw new Error(`${SETTINGS_FILE_NAME} was created concurrently; reopen settings and retry.`);
388
- }
389
- fs.renameSync(temporaryPath, settingsPath);
390
- } finally {
391
- removeTemporaryFile(fs.rmSync, temporaryPath);
392
- }
393
- return {
394
- config: normalized.config,
395
- source: "user",
396
- settingsPath,
397
- rawDocument,
398
- ...(fileIdentity ? { fileIdentity } : {}),
399
- diagnostics: normalized.diagnostics,
400
- };
401
- }
402
-
403
- export function removeStatuslineSettingsDocumentIfMatches(
404
- settingsPath: string,
405
- expectedRawDocument: string,
406
- expectedIdentity: { dev: number; ino: number },
407
- ): void {
408
- const quarantinePath = join(
409
- dirname(settingsPath),
410
- `.${SETTINGS_FILE_NAME}.${randomUUID()}.rollback`,
411
- );
412
- const before = lstatSync(settingsPath);
413
- if (before.dev !== expectedIdentity.dev || before.ino !== expectedIdentity.ino) {
414
- throw new Error("Statusline settings changed concurrently; the newer file was preserved");
415
- }
416
- renameSync(settingsPath, quarantinePath);
417
- const quarantined = lstatSync(quarantinePath);
418
- const quarantinedSavedFile =
419
- quarantined.isFile() &&
420
- !quarantined.isSymbolicLink() &&
421
- quarantined.dev === expectedIdentity.dev &&
422
- quarantined.ino === expectedIdentity.ino;
423
- if (quarantinedSavedFile && readFileSync(quarantinePath, "utf8") === expectedRawDocument) {
424
- rmSync(quarantinePath);
425
- return;
426
- }
427
- if (quarantinedSavedFile && !pathEntryExists(settingsPath)) {
428
- try {
429
- renameSync(quarantinePath, settingsPath);
430
- } catch {
431
- // Keep the quarantine for recovery when its atomic restoration fails.
432
- }
433
- }
434
- throw new Error("Statusline settings changed concurrently; the newer file was preserved");
435
- }
436
-
437
- export function consumeStatuslineSettingsNotice(): string | undefined {
438
- const notice = pendingSettingsNotice;
439
- pendingSettingsNotice = undefined;
440
- return notice;
441
- }
442
-
443
- export function readStatuslineSettings(settingsPath?: string): StatuslineConfig {
444
- return settingsPath
445
- ? loadStatuslineSettings(settingsPath).config
446
- : loadStatuslineSettingsForAgent().config;
447
- }
448
-
449
- export function normalizeStatuslineSettings(value: unknown): StatuslineConfig {
450
- return normalizeStatuslineConfig(value).config;
451
- }
452
-
453
- function normalizePalette(
454
- value: unknown,
455
- config: StatuslineConfig,
456
- diagnostics: StatuslineConfigDiagnostic[],
457
- ) {
458
- if (value === undefined) return;
459
- if (typeof value === "string") {
460
- if (!(PALETTE_NAMES as readonly string[]).includes(value)) {
461
- diagnostics.push(
462
- invalidDiagnostic(
463
- "palette",
464
- `Expected a palette object or one of: ${PALETTE_NAMES.join(", ")}`,
465
- ),
466
- );
467
- return;
468
- }
469
- config.palettePreset = value as (typeof PALETTE_NAMES)[number];
470
- return;
471
- }
472
- if (!isRecord(value)) {
473
- diagnostics.push(invalidDiagnostic("palette", "Expected a palette object"));
474
- return;
475
- }
476
-
477
- const palette: SegmentPalette = {};
478
- config.palette = palette;
479
- for (const [name, colors] of Object.entries(value)) {
480
- const path = `palette.${name}`;
481
- if (!isSegmentName(name)) {
482
- diagnostics.push(unknownDiagnostic(path));
483
- continue;
484
- }
485
- if (!isRecord(colors)) {
486
- diagnostics.push(invalidDiagnostic(path, "Expected an object"));
487
- continue;
488
- }
489
- const normalizedColors: NonNullable<SegmentPalette[SegmentName]> = {};
490
- palette[name] = normalizedColors;
491
- for (const [field, color] of Object.entries(colors)) {
492
- const colorPath = `${path}.${field}`;
493
- if (field !== "fg" && field !== "bg") {
494
- diagnostics.push(unknownDiagnostic(colorPath));
495
- continue;
496
- }
497
- if (typeof color !== "string" || !/^#[0-9a-f]{6}$/iu.test(color)) {
498
- diagnostics.push(invalidDiagnostic(colorPath, "Expected a full #RRGGBB hexadecimal color"));
499
- continue;
500
- }
501
- normalizedColors[field] = color.toLowerCase();
502
- }
503
- }
504
- config.palette = palette;
505
- config.palettePreset = "custom";
506
- }
507
-
508
- function normalizeModelTruncation(
509
- presentation: Record<string, unknown>,
510
- config: StatuslineConfig,
511
- diagnostics: StatuslineConfigDiagnostic[],
512
- ) {
513
- const path = "segmentText.model";
514
- const length = presentation.truncationLength;
515
- if (length !== undefined) {
516
- if (typeof length !== "number" || !Number.isInteger(length) || length < 0 || length > 1000) {
517
- diagnostics.push(
518
- invalidDiagnostic(`${path}.truncationLength`, "Expected an integer from 0 through 1000"),
519
- );
520
- } else config.segmentText.model.truncationLength = length;
521
- }
522
-
523
- const symbol = presentation.truncationSymbol;
524
- if (symbol !== undefined && isSafeSegmentText(symbol, `${path}.truncationSymbol`, diagnostics)) {
525
- config.segmentText.model.truncationSymbol = symbol;
526
- }
527
-
528
- const direction = presentation.truncationDirection;
529
- if (direction !== undefined) {
530
- if (
531
- typeof direction !== "string" ||
532
- !TRUNCATION_DIRECTIONS.includes(direction as (typeof TRUNCATION_DIRECTIONS)[number])
533
- ) {
534
- diagnostics.push(
535
- invalidDiagnostic(
536
- `${path}.truncationDirection`,
537
- `Expected one of: ${TRUNCATION_DIRECTIONS.join(", ")}`,
538
- ),
539
- );
540
- } else {
541
- config.segmentText.model.truncationDirection =
542
- direction as (typeof TRUNCATION_DIRECTIONS)[number];
543
- }
544
- }
545
- }
546
-
547
- function isSafeSegmentText(
548
- value: unknown,
549
- path: string,
550
- diagnostics: StatuslineConfigDiagnostic[],
551
- ): value is string {
552
- if (typeof value !== "string") {
553
- diagnostics.push(invalidDiagnostic(path, "Expected a string"));
554
- return false;
555
- }
556
- if (/[\r\n\u2028\u2029]/u.test(value)) {
557
- diagnostics.push(invalidDiagnostic(path, "Line breaks are not allowed; use line_break"));
558
- return false;
559
- }
560
- if (hasControlCharacter(value)) {
561
- diagnostics.push(invalidDiagnostic(path, "Control characters are not allowed"));
562
- return false;
563
- }
564
- return true;
565
- }
566
-
567
- function normalizeEnum<
568
- K extends "palettePreset" | "density" | "separator",
569
- T extends StatuslineConfig[K],
570
- >(
571
- value: Record<string, unknown>,
572
- field: K,
573
- accepted: readonly T[],
574
- config: StatuslineConfig,
575
- diagnostics: StatuslineConfigDiagnostic[],
576
- ) {
577
- const candidate = value[field];
578
- if (candidate === undefined) return;
579
- if (typeof candidate !== "string" || !accepted.includes(candidate as T)) {
580
- diagnostics.push(
581
- invalidDiagnostic(field, `Expected one of: ${accepted.map(String).join(", ")}`),
582
- );
583
- return;
584
- }
585
- config[field] = candidate as StatuslineConfig[K];
586
- }
587
-
588
- function cloneSegmentPalette(palette: SegmentPalette): SegmentPalette {
589
- return Object.fromEntries(
590
- Object.entries(palette).map(([name, colors]) => [name, { ...colors }]),
591
- ) as SegmentPalette;
592
- }
593
-
594
- function cloneConfig(config: StatuslineConfig): StatuslineConfig {
595
- return {
596
- ...config,
597
- palette: cloneSegmentPalette(config.palette),
598
- segments: [...config.segments],
599
- segmentText: Object.fromEntries(
600
- SEGMENT_NAMES.map((name) => [name, { ...config.segmentText[name] }]),
601
- ) as StatuslineConfig["segmentText"],
602
- extensionStatusIcons: { ...config.extensionStatusIcons },
603
- };
604
- }
605
-
606
- function builtInSettings(
607
- settingsPath: string,
608
- diagnostics: StatuslineConfigDiagnostic[] = [],
609
- ): LoadedStatuslineSettings {
610
- return {
611
- config: createDefaultConfig(),
612
- source: "built-in",
613
- settingsPath,
614
- diagnostics,
615
- };
616
- }
617
-
618
- function hasControlCharacter(value: string): boolean {
619
- for (const character of value) {
620
- const codePoint = character.codePointAt(0) ?? 0;
621
- if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) return true;
622
- }
623
- return false;
624
- }
625
-
626
- function isRecord(value: unknown): value is Record<string, unknown> {
627
- return typeof value === "object" && value !== null && !Array.isArray(value);
628
- }
629
-
630
- function isConfigSegmentName(value: string): value is ConfigSegmentName {
631
- return value === LINE_BREAK_SEGMENT_NAME || isSegmentName(value);
632
- }
633
-
634
- function isPaletteName(value: StatuslineConfig["palettePreset"]): value is PaletteName {
635
- return (PALETTE_NAMES as readonly StatuslineConfig["palettePreset"][]).includes(value);
636
- }
637
-
638
- function isSegmentName(value: string): value is SegmentName {
639
- return (SEGMENT_NAMES as readonly string[]).includes(value);
640
- }
641
-
642
- function blockingDiagnostics(
643
- diagnostics: readonly StatuslineConfigDiagnostic[],
644
- ): StatuslineConfigDiagnostic[] {
645
- return diagnostics.filter((item) => item.code !== "unknown");
646
- }
647
-
648
- function unknownDiagnostic(path: string): StatuslineConfigDiagnostic {
649
- return diagnostic("warning", "unknown", path, `Unknown setting ${JSON.stringify(path)}`);
650
- }
651
-
652
- function invalidDiagnostic(
653
- path: string,
654
- message: string,
655
- severity: StatuslineConfigDiagnostic["severity"] = "warning",
656
- ): StatuslineConfigDiagnostic {
657
- return diagnostic(severity, "invalid", path, message);
658
- }
659
-
660
- function diagnostic(
661
- severity: StatuslineConfigDiagnostic["severity"],
662
- code: StatuslineConfigDiagnostic["code"],
663
- path: string,
664
- message: string,
665
- ): StatuslineConfigDiagnostic {
666
- return { severity, code, path, message };
667
- }
668
-
669
- function temporarySettingsPath(settingsPath: string): string {
670
- return join(dirname(settingsPath), `.${SETTINGS_FILE_NAME}.${randomUUID()}.tmp`);
671
- }
672
-
673
- function removeTemporaryFile(remove: typeof rmSync, temporaryPath: string) {
674
- try {
675
- remove(temporaryPath, { force: true });
676
- } catch {
677
- // Best-effort cleanup must not replace the original operation result.
678
- }
679
- }
680
-
681
- function pathExists(path: string): boolean {
682
- try {
683
- lstatSync(path);
684
- return true;
685
- } catch {
686
- return false;
687
- }
688
- }
689
-
690
- function pathEntryExists(path: string): boolean {
691
- try {
692
- lstatSync(path);
693
- return true;
694
- } catch (error) {
695
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
696
- throw error;
697
- }
698
- }
699
-
700
- function formatError(error: unknown): string {
701
- return error instanceof Error ? error.message : String(error);
702
- }