@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/README.md +29 -209
- package/dist/chunks/{chunk-74QZGJ7L.js → chunk-J2ZSGEDQ.js} +18 -97
- package/dist/chunks/chunk-J2ZSGEDQ.js.map +7 -0
- package/dist/chunks/{commands-C2G2G7C6.js → commands-PPT3AJTL.js} +14 -58
- package/dist/chunks/commands-PPT3AJTL.js.map +7 -0
- package/dist/index.ts +21 -76
- package/dist/index.ts.map +2 -2
- package/docs/configuration.md +194 -0
- package/package.json +51 -51
- package/src/ansi.ts +18 -24
- package/src/command-contract.ts +6 -6
- package/src/commands.ts +759 -818
- package/src/directory.ts +27 -31
- package/src/extension-status.ts +186 -219
- package/src/git-status.ts +103 -111
- package/src/information-profiles.ts +12 -31
- package/src/powerline.ts +120 -127
- package/src/presets/candy.ts +1 -7
- package/src/presets/create-ramp.ts +21 -23
- package/src/presets/custom.ts +7 -7
- package/src/presets/forest.ts +1 -7
- package/src/presets/index.ts +30 -32
- package/src/presets/mono.ts +1 -7
- package/src/presets/neon.ts +1 -7
- package/src/presets/ocean.ts +1 -7
- package/src/presets/sunset.ts +1 -7
- package/src/presets/tokyo-night.ts +9 -9
- package/src/presets/types.ts +5 -5
- package/src/render.ts +277 -306
- package/src/settings.ts +516 -568
- package/src/statusline.ts +347 -372
- package/src/types.ts +34 -42
- package/src/usage.ts +40 -40
- package/dist/chunks/chunk-74QZGJ7L.js.map +0 -7
- package/dist/chunks/commands-C2G2G7C6.js.map +0 -7
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
122
|
+
return join(agentDir, SETTINGS_FILE_NAME);
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
export function createDefaultConfig(): StatuslineConfig {
|
|
130
|
-
|
|
126
|
+
return cloneConfig(DEFAULT_STATUSLINE_CONFIG);
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
export function normalizeStatuslineConfig(value: unknown): {
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
config: StatuslineConfig;
|
|
131
|
+
diagnostics: StatuslineConfigDiagnostic[];
|
|
136
132
|
} {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
340
|
+
settingsPath: string,
|
|
341
|
+
rawDocument: string,
|
|
342
|
+
overrides: Partial<AtomicFileSystem> = {},
|
|
364
343
|
): LoadedStatuslineSettings {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
383
|
+
settingsPath: string,
|
|
384
|
+
expectedRawDocument: string,
|
|
385
|
+
expectedIdentity: { dev: number; ino: number },
|
|
407
386
|
): void {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
414
|
+
const notice = pendingSettingsNotice;
|
|
415
|
+
pendingSettingsNotice = undefined;
|
|
416
|
+
return notice;
|
|
441
417
|
}
|
|
442
418
|
|
|
443
419
|
export function readStatuslineSettings(settingsPath?: string): StatuslineConfig {
|
|
444
|
-
|
|
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
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function normalizePalette(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
476
|
+
presentation: Record<string, unknown>,
|
|
477
|
+
config: StatuslineConfig,
|
|
478
|
+
diagnostics: StatuslineConfigDiagnostic[],
|
|
512
479
|
) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
608
|
-
|
|
557
|
+
settingsPath: string,
|
|
558
|
+
diagnostics: StatuslineConfigDiagnostic[] = [],
|
|
609
559
|
): LoadedStatuslineSettings {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
577
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
628
578
|
}
|
|
629
579
|
|
|
630
580
|
function isConfigSegmentName(value: string): value is ConfigSegmentName {
|
|
631
|
-
|
|
581
|
+
return value === LINE_BREAK_SEGMENT_NAME || isSegmentName(value);
|
|
632
582
|
}
|
|
633
583
|
|
|
634
584
|
function isPaletteName(value: StatuslineConfig["palettePreset"]): value is PaletteName {
|
|
635
|
-
|
|
585
|
+
return (PALETTE_NAMES as readonly StatuslineConfig["palettePreset"][]).includes(value);
|
|
636
586
|
}
|
|
637
587
|
|
|
638
588
|
function isSegmentName(value: string): value is SegmentName {
|
|
639
|
-
|
|
589
|
+
return (SEGMENT_NAMES as readonly string[]).includes(value);
|
|
640
590
|
}
|
|
641
591
|
|
|
642
|
-
function blockingDiagnostics(
|
|
643
|
-
|
|
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
|
-
|
|
597
|
+
return diagnostic("warning", "unknown", path, `Unknown setting ${JSON.stringify(path)}`);
|
|
650
598
|
}
|
|
651
599
|
|
|
652
600
|
function invalidDiagnostic(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
601
|
+
path: string,
|
|
602
|
+
message: string,
|
|
603
|
+
severity: StatuslineConfigDiagnostic["severity"] = "warning",
|
|
656
604
|
): StatuslineConfigDiagnostic {
|
|
657
|
-
|
|
605
|
+
return diagnostic(severity, "invalid", path, message);
|
|
658
606
|
}
|
|
659
607
|
|
|
660
608
|
function diagnostic(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
609
|
+
severity: StatuslineConfigDiagnostic["severity"],
|
|
610
|
+
code: StatuslineConfigDiagnostic["code"],
|
|
611
|
+
path: string,
|
|
612
|
+
message: string,
|
|
665
613
|
): StatuslineConfigDiagnostic {
|
|
666
|
-
|
|
614
|
+
return { severity, code, path, message };
|
|
667
615
|
}
|
|
668
616
|
|
|
669
617
|
function temporarySettingsPath(settingsPath: string): string {
|
|
670
|
-
|
|
618
|
+
return join(dirname(settingsPath), `.${SETTINGS_FILE_NAME}.${randomUUID()}.tmp`);
|
|
671
619
|
}
|
|
672
620
|
|
|
673
621
|
function removeTemporaryFile(remove: typeof rmSync, temporaryPath: string) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
649
|
+
return error instanceof Error ? error.message : String(error);
|
|
702
650
|
}
|