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