@narumitw/pi-statusline 0.23.0 → 0.28.0
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 +217 -119
- package/package.json +2 -2
- package/src/commands.ts +775 -9
- package/src/extension-status.ts +28 -2
- package/src/index.ts +1 -0
- package/src/information-profiles.ts +39 -0
- package/src/powerline.ts +168 -0
- package/src/presets/candy.ts +9 -0
- package/src/presets/create-ramp.ts +41 -0
- package/src/presets/custom.ts +11 -0
- package/src/presets/forest.ts +9 -0
- package/src/presets/index.ts +54 -0
- package/src/presets/mono.ts +9 -0
- package/src/presets/neon.ts +9 -0
- package/src/presets/ocean.ts +9 -0
- package/src/presets/sunset.ts +9 -0
- package/src/presets/tokyo-night.ts +13 -0
- package/src/presets/types.ts +12 -0
- package/src/render.ts +34 -23
- package/src/settings.ts +135 -22
- package/src/statusline.ts +42 -8
- package/src/types.ts +14 -3
- package/src/tokyo-night.ts +0 -180
package/src/commands.ts
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
type AutocompleteItem,
|
|
8
|
+
Container,
|
|
9
|
+
Key,
|
|
10
|
+
matchesKey,
|
|
11
|
+
type SelectItem,
|
|
12
|
+
SelectList,
|
|
13
|
+
Text,
|
|
14
|
+
truncateToWidth,
|
|
15
|
+
wrapTextWithAnsi,
|
|
16
|
+
} from "@earendil-works/pi-tui";
|
|
17
|
+
import {
|
|
18
|
+
INFORMATION_PROFILE_NAMES,
|
|
19
|
+
INFORMATION_PROFILES,
|
|
20
|
+
type InformationProfileName,
|
|
21
|
+
inferInformationProfile,
|
|
22
|
+
} from "./information-profiles.js";
|
|
23
|
+
import { segmentPaletteForPreset } from "./presets/index.js";
|
|
3
24
|
import {
|
|
4
25
|
DEFAULT_STATUSLINE_DOCUMENT,
|
|
5
26
|
type LoadedStatuslineSettings,
|
|
6
27
|
saveStatuslineSettingsDocument,
|
|
7
28
|
} from "./settings.js";
|
|
29
|
+
import {
|
|
30
|
+
type ConfigSegmentName,
|
|
31
|
+
LINE_BREAK_SEGMENT_NAME,
|
|
32
|
+
PALETTE_NAMES,
|
|
33
|
+
PALETTE_PRESET_NAMES,
|
|
34
|
+
type PaletteName,
|
|
35
|
+
type PalettePreset,
|
|
36
|
+
SEGMENT_NAMES,
|
|
37
|
+
type SegmentName,
|
|
38
|
+
} from "./types.js";
|
|
8
39
|
|
|
40
|
+
const EDIT_SETTINGS_LABEL = "Edit settings JSON";
|
|
41
|
+
const SEGMENT_VIEWPORT_SIZE = 6;
|
|
42
|
+
const NARROW_SEGMENT_VIEWPORT_SIZE = 3;
|
|
43
|
+
const SEGMENT_DESCRIPTIONS: Record<SegmentName, string> = {
|
|
44
|
+
brand: "Pi brand mark",
|
|
45
|
+
provider: "Current model provider",
|
|
46
|
+
model: "Current model name",
|
|
47
|
+
thinking: "Current thinking level",
|
|
48
|
+
cwd: "Current working directory",
|
|
49
|
+
branch: "Git branch, status, and linked pull request",
|
|
50
|
+
tools: "Current tool and streaming activity",
|
|
51
|
+
context: "Current context-window usage",
|
|
52
|
+
tokens: "Session token totals",
|
|
53
|
+
cost: "Session cost",
|
|
54
|
+
time: "Current local time",
|
|
55
|
+
turn: "Current session turn count",
|
|
56
|
+
};
|
|
9
57
|
const SUBCOMMANDS: AutocompleteItem[] = [
|
|
10
58
|
{ value: "settings", label: "settings", description: "Edit pi-statusline.json" },
|
|
11
59
|
{ value: "status", label: "status", description: "Show effective statusline settings" },
|
|
@@ -16,19 +64,34 @@ export interface StatuslineCommandOptions {
|
|
|
16
64
|
settingsPath: string;
|
|
17
65
|
getLoaded(): LoadedStatuslineSettings;
|
|
18
66
|
apply(loaded: LoadedStatuslineSettings, ctx: ExtensionCommandContext): void;
|
|
67
|
+
preview?(palettePreset: PalettePreset | undefined, ctx: ExtensionCommandContext): void;
|
|
19
68
|
save?: (settingsPath: string, rawDocument: string) => LoadedStatuslineSettings;
|
|
20
69
|
}
|
|
21
70
|
|
|
22
71
|
export function registerStatuslineCommand(pi: ExtensionAPI, options: StatuslineCommandOptions) {
|
|
23
72
|
pi.registerCommand("statusline", {
|
|
24
|
-
description: "
|
|
73
|
+
description: "Open or inspect the statusline settings",
|
|
25
74
|
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
26
75
|
const normalized = prefix.trim().toLowerCase();
|
|
27
76
|
const matches = SUBCOMMANDS.filter((item) => item.value.startsWith(normalized));
|
|
28
77
|
return matches.length > 0 ? matches : null;
|
|
29
78
|
},
|
|
30
79
|
handler: async (args, ctx) => {
|
|
31
|
-
const
|
|
80
|
+
const normalized = args.trim();
|
|
81
|
+
if (!normalized) {
|
|
82
|
+
await showMainMenu(ctx, options);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tokens = normalized.split(/\s+/u);
|
|
87
|
+
const subcommand = tokens[0]?.toLowerCase() ?? "";
|
|
88
|
+
if (tokens.length > 1) {
|
|
89
|
+
if (canNotify(ctx)) {
|
|
90
|
+
ctx.ui.notify(`/statusline ${subcommand} does not accept trailing arguments.`, "warning");
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
32
95
|
switch (subcommand) {
|
|
33
96
|
case "settings":
|
|
34
97
|
await editSettings(ctx, options);
|
|
@@ -48,6 +111,471 @@ export function registerStatuslineCommand(pi: ExtensionAPI, options: StatuslineC
|
|
|
48
111
|
});
|
|
49
112
|
}
|
|
50
113
|
|
|
114
|
+
async function showMainMenu(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
|
|
115
|
+
if (ctx.mode !== "tui") {
|
|
116
|
+
if (ctx.hasUI) ctx.ui.notify("/statusline requires an interactive Pi UI.", "error");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
while (true) {
|
|
120
|
+
const config = options.getLoaded().config;
|
|
121
|
+
const appearanceItem = `Appearance (${config.palettePreset})`;
|
|
122
|
+
const informationItem = `Information (${inferInformationProfile(config.segments)})`;
|
|
123
|
+
const selection = await ctx.ui.select("pi-statusline", [
|
|
124
|
+
appearanceItem,
|
|
125
|
+
informationItem,
|
|
126
|
+
"Advanced",
|
|
127
|
+
"Status",
|
|
128
|
+
"Help",
|
|
129
|
+
]);
|
|
130
|
+
if (selection === appearanceItem) {
|
|
131
|
+
await choosePalettePreset(ctx, options);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (selection === informationItem) {
|
|
135
|
+
await chooseInformationProfile(ctx, options);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
switch (selection) {
|
|
139
|
+
case "Advanced":
|
|
140
|
+
if (await showAdvancedMenu(ctx, options)) continue;
|
|
141
|
+
return;
|
|
142
|
+
case "Status":
|
|
143
|
+
showStatus(ctx, options);
|
|
144
|
+
return;
|
|
145
|
+
case "Help":
|
|
146
|
+
showHelp(ctx, options.settingsPath);
|
|
147
|
+
return;
|
|
148
|
+
default:
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function showAdvancedMenu(
|
|
155
|
+
ctx: ExtensionCommandContext,
|
|
156
|
+
options: StatuslineCommandOptions,
|
|
157
|
+
): Promise<boolean> {
|
|
158
|
+
const config = options.getLoaded().config;
|
|
159
|
+
const visibleSegmentCount = config.segments.filter(
|
|
160
|
+
(segment): segment is SegmentName => segment !== LINE_BREAK_SEGMENT_NAME,
|
|
161
|
+
).length;
|
|
162
|
+
const layoutItem = `Custom layout (${visibleSegmentCount}/${SEGMENT_NAMES.length} shown)`;
|
|
163
|
+
const selection = await ctx.ui.select("pi-statusline — Advanced", [
|
|
164
|
+
layoutItem,
|
|
165
|
+
EDIT_SETTINGS_LABEL,
|
|
166
|
+
"Back",
|
|
167
|
+
]);
|
|
168
|
+
if (selection === layoutItem) {
|
|
169
|
+
await chooseSegments(ctx, options);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (selection === EDIT_SETTINGS_LABEL) {
|
|
173
|
+
await editSettings(ctx, options);
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return selection === "Back";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function choosePalettePreset(
|
|
180
|
+
ctx: ExtensionCommandContext,
|
|
181
|
+
options: StatuslineCommandOptions,
|
|
182
|
+
) {
|
|
183
|
+
if (ctx.mode !== "tui") {
|
|
184
|
+
if (ctx.hasUI) ctx.ui.notify(`Edit palettePreset manually: ${options.settingsPath}`, "info");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const current = options.getLoaded();
|
|
188
|
+
let selection: PalettePreset | undefined;
|
|
189
|
+
try {
|
|
190
|
+
selection = await showPalettePresetPicker(ctx, current.config.palettePreset, options);
|
|
191
|
+
} finally {
|
|
192
|
+
options.preview?.(undefined, ctx);
|
|
193
|
+
}
|
|
194
|
+
if (selection === undefined) return;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const rawDocument = palettePresetDocument(current, selection);
|
|
198
|
+
const loaded = (options.save ?? saveStatuslineSettingsDocument)(
|
|
199
|
+
options.settingsPath,
|
|
200
|
+
rawDocument,
|
|
201
|
+
);
|
|
202
|
+
options.apply(loaded, ctx);
|
|
203
|
+
const message =
|
|
204
|
+
loaded.config.palettePreset === "custom"
|
|
205
|
+
? "Custom palette applied. Edit colors via /statusline → Advanced → Edit settings JSON."
|
|
206
|
+
: `Palette preset applied: ${loaded.config.palettePreset}.`;
|
|
207
|
+
ctx.ui.notify(message, "info");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
ctx.ui.notify(`Palette preset was not saved: ${formatError(error)}`, "error");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function showPalettePresetPicker(
|
|
214
|
+
ctx: ExtensionCommandContext,
|
|
215
|
+
current: PalettePreset,
|
|
216
|
+
options: StatuslineCommandOptions,
|
|
217
|
+
): Promise<PalettePreset | undefined> {
|
|
218
|
+
const items: SelectItem[] = PALETTE_PRESET_NAMES.map((palettePreset) => ({
|
|
219
|
+
value: palettePreset,
|
|
220
|
+
label: palettePreset,
|
|
221
|
+
description:
|
|
222
|
+
[
|
|
223
|
+
palettePreset === current ? "current" : undefined,
|
|
224
|
+
palettePreset === "custom" ? "per-segment colors from settings JSON" : undefined,
|
|
225
|
+
]
|
|
226
|
+
.filter((part): part is string => part !== undefined)
|
|
227
|
+
.join(" • ") || undefined,
|
|
228
|
+
}));
|
|
229
|
+
const selectedIndex = PALETTE_PRESET_NAMES.indexOf(current);
|
|
230
|
+
const result = await ctx.ui.custom<PalettePreset | null>((tui, theme, _keybindings, done) => {
|
|
231
|
+
const container = new Container();
|
|
232
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
233
|
+
const title = new Text("", 1, 0);
|
|
234
|
+
container.addChild(title);
|
|
235
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
236
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
237
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
238
|
+
description: (text) => theme.fg("muted", text),
|
|
239
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
240
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
241
|
+
});
|
|
242
|
+
list.setSelectedIndex(selectedIndex);
|
|
243
|
+
list.onSelectionChange = (item) => {
|
|
244
|
+
options.preview?.(item.value as PalettePreset, ctx);
|
|
245
|
+
};
|
|
246
|
+
list.onSelect = (item) => done(item.value as PalettePreset);
|
|
247
|
+
list.onCancel = () => done(null);
|
|
248
|
+
container.addChild(list);
|
|
249
|
+
const hint = new Text("", 1, 0);
|
|
250
|
+
container.addChild(hint);
|
|
251
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
252
|
+
const updateThemedText = () => {
|
|
253
|
+
title.setText(theme.fg("accent", theme.bold(`Palette preset (current: ${current})`)));
|
|
254
|
+
hint.setText(theme.fg("dim", "↑↓ preview • enter apply • esc cancel"));
|
|
255
|
+
};
|
|
256
|
+
updateThemedText();
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
render: (width: number) => container.render(width),
|
|
260
|
+
invalidate() {
|
|
261
|
+
container.invalidate();
|
|
262
|
+
updateThemedText();
|
|
263
|
+
},
|
|
264
|
+
handleInput(data: string) {
|
|
265
|
+
list.handleInput(data);
|
|
266
|
+
tui.requestRender();
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
return result ?? undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function chooseInformationProfile(
|
|
274
|
+
ctx: ExtensionCommandContext,
|
|
275
|
+
options: StatuslineCommandOptions,
|
|
276
|
+
) {
|
|
277
|
+
if (ctx.mode !== "tui") {
|
|
278
|
+
if (ctx.hasUI) ctx.ui.notify(`Edit segments manually: ${options.settingsPath}`, "info");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const current = options.getLoaded();
|
|
282
|
+
const currentProfile = inferInformationProfile(current.config.segments);
|
|
283
|
+
const selection = await showInformationProfilePicker(ctx, currentProfile);
|
|
284
|
+
if (selection === undefined) return;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const change = informationProfileDocument(current, selection);
|
|
288
|
+
const loaded = applySegmentsDocumentChange(change, ctx, options);
|
|
289
|
+
ctx.ui.notify(
|
|
290
|
+
`Information level applied: ${inferInformationProfile(loaded.config.segments)}.`,
|
|
291
|
+
"info",
|
|
292
|
+
);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
ctx.ui.notify(`Information level was not saved: ${formatError(error)}`, "error");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function showInformationProfilePicker(
|
|
299
|
+
ctx: ExtensionCommandContext,
|
|
300
|
+
current: ReturnType<typeof inferInformationProfile>,
|
|
301
|
+
): Promise<InformationProfileName | undefined> {
|
|
302
|
+
const items: SelectItem[] = INFORMATION_PROFILE_NAMES.map((profile) => ({
|
|
303
|
+
value: profile,
|
|
304
|
+
label: `${profile[0]?.toUpperCase() ?? ""}${profile.slice(1)}`,
|
|
305
|
+
description: `${INFORMATION_PROFILES[profile].length} segments${profile === current ? " • current" : ""}`,
|
|
306
|
+
}));
|
|
307
|
+
const initialProfile = current === "custom" ? "balanced" : current;
|
|
308
|
+
const selectedIndex = INFORMATION_PROFILE_NAMES.indexOf(initialProfile);
|
|
309
|
+
const result = await ctx.ui.custom<InformationProfileName | null>(
|
|
310
|
+
(tui, theme, _keybindings, done) => {
|
|
311
|
+
const container = new Container();
|
|
312
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
313
|
+
const title = new Text("", 1, 0);
|
|
314
|
+
container.addChild(title);
|
|
315
|
+
const list = new SelectList(items, items.length, {
|
|
316
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
317
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
318
|
+
description: (text) => theme.fg("muted", text),
|
|
319
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
320
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
321
|
+
});
|
|
322
|
+
list.setSelectedIndex(selectedIndex);
|
|
323
|
+
let selectedProfile = initialProfile;
|
|
324
|
+
const details = new Text("", 1, 0);
|
|
325
|
+
const hint = new Text("", 1, 0);
|
|
326
|
+
const updateThemedText = () => {
|
|
327
|
+
title.setText(theme.fg("accent", theme.bold(`Information level (current: ${current})`)));
|
|
328
|
+
details.setText(
|
|
329
|
+
theme.fg("muted", `Segments: ${INFORMATION_PROFILES[selectedProfile].join(" · ")}`),
|
|
330
|
+
);
|
|
331
|
+
hint.setText(theme.fg("dim", "↑↓ preview contents • enter apply • esc cancel"));
|
|
332
|
+
};
|
|
333
|
+
list.onSelectionChange = (item) => {
|
|
334
|
+
selectedProfile = item.value as InformationProfileName;
|
|
335
|
+
updateThemedText();
|
|
336
|
+
};
|
|
337
|
+
list.onSelect = (item) => done(item.value as InformationProfileName);
|
|
338
|
+
list.onCancel = () => done(null);
|
|
339
|
+
container.addChild(list);
|
|
340
|
+
container.addChild(details);
|
|
341
|
+
container.addChild(hint);
|
|
342
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
343
|
+
updateThemedText();
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
render: (width: number) => container.render(width),
|
|
347
|
+
invalidate() {
|
|
348
|
+
container.invalidate();
|
|
349
|
+
updateThemedText();
|
|
350
|
+
},
|
|
351
|
+
handleInput(data: string) {
|
|
352
|
+
list.handleInput(data);
|
|
353
|
+
tui.requestRender();
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
return result ?? undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function chooseSegments(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
|
|
362
|
+
if (ctx.mode !== "tui") {
|
|
363
|
+
if (ctx.hasUI) ctx.ui.notify(`Edit segments manually: ${options.settingsPath}`, "info");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
let current = options.getLoaded();
|
|
367
|
+
let names = segmentMenuOrder(current);
|
|
368
|
+
let selectedIndex = 0;
|
|
369
|
+
let moveMode = false;
|
|
370
|
+
let feedback: string | undefined;
|
|
371
|
+
|
|
372
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
373
|
+
const commit = (
|
|
374
|
+
name: SegmentName,
|
|
375
|
+
change: { nextDocument: string; previousDocument: string },
|
|
376
|
+
): boolean => {
|
|
377
|
+
try {
|
|
378
|
+
current = applySegmentsDocumentChange(change, ctx, options);
|
|
379
|
+
names = segmentMenuOrder(current);
|
|
380
|
+
selectedIndex = Math.max(0, names.indexOf(name));
|
|
381
|
+
feedback = undefined;
|
|
382
|
+
return true;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
ctx.ui.notify(`Statusline segments were not saved: ${formatError(error)}`, "error");
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const toggleSelected = () => {
|
|
390
|
+
const name = names[selectedIndex];
|
|
391
|
+
if (!name) return;
|
|
392
|
+
moveMode = false;
|
|
393
|
+
commit(name, segmentsDocument(current, name, !current.config.segments.includes(name)));
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const toggleLineBreakAfterSelected = () => {
|
|
397
|
+
const name = names[selectedIndex];
|
|
398
|
+
if (!name) return;
|
|
399
|
+
moveMode = false;
|
|
400
|
+
const segmentIndex = current.config.segments.indexOf(name);
|
|
401
|
+
if (segmentIndex < 0) {
|
|
402
|
+
feedback = `Show ${name} before adding a line break.`;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const hasLineBreak = current.config.segments[segmentIndex + 1] === LINE_BREAK_SEGMENT_NAME;
|
|
406
|
+
const hasFollowingSegment = current.config.segments
|
|
407
|
+
.slice(segmentIndex + 1)
|
|
408
|
+
.some((segment) => segment !== LINE_BREAK_SEGMENT_NAME);
|
|
409
|
+
if (!hasLineBreak && !hasFollowingSegment) {
|
|
410
|
+
feedback = `Add another visible segment after ${name} before adding a line break.`;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
commit(name, lineBreakAfterDocument(current, name));
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const reorderSelected = (direction: -1 | 1) => {
|
|
417
|
+
const name = names[selectedIndex];
|
|
418
|
+
if (!name) return;
|
|
419
|
+
const visibleNames = visibleSegmentNames(current);
|
|
420
|
+
const currentIndex = visibleNames.indexOf(name);
|
|
421
|
+
if (currentIndex < 0) {
|
|
422
|
+
feedback = `Show ${name} before moving it.`;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const targetIndex = currentIndex + direction;
|
|
426
|
+
if (targetIndex < 0 || targetIndex >= visibleNames.length) {
|
|
427
|
+
feedback = `${name} is already the ${direction < 0 ? "first" : "last"} visible segment.`;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const change = reorderedSegmentsDocument(current, name, direction);
|
|
431
|
+
if (change) commit(name, change);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const enterMoveMode = () => {
|
|
435
|
+
const name = names[selectedIndex];
|
|
436
|
+
if (!name) return;
|
|
437
|
+
if (!current.config.segments.includes(name)) {
|
|
438
|
+
feedback = `Show ${name} before moving it.`;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
moveMode = true;
|
|
442
|
+
feedback = undefined;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const moveSelection = (offset: number) => {
|
|
446
|
+
selectedIndex = Math.max(0, Math.min(names.length - 1, selectedIndex + offset));
|
|
447
|
+
feedback = undefined;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const renderList = (width: number): string[] => {
|
|
451
|
+
const safeWidth = Math.max(1, width);
|
|
452
|
+
const visibleNames = visibleSegmentNames(current);
|
|
453
|
+
const visible = new Set(visibleNames);
|
|
454
|
+
const placements = segmentPlacements(current.config.segments);
|
|
455
|
+
const viewportSize = safeWidth < 30 ? NARROW_SEGMENT_VIEWPORT_SIZE : SEGMENT_VIEWPORT_SIZE;
|
|
456
|
+
const startIndex = Math.max(
|
|
457
|
+
0,
|
|
458
|
+
Math.min(
|
|
459
|
+
selectedIndex - Math.floor(viewportSize / 2),
|
|
460
|
+
Math.max(0, names.length - viewportSize),
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
const endIndex = Math.min(names.length, startIndex + viewportSize);
|
|
464
|
+
const lines: string[] = [];
|
|
465
|
+
let previousSection: "visible" | "hidden" | undefined;
|
|
466
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
467
|
+
const name = names[index];
|
|
468
|
+
if (!name) continue;
|
|
469
|
+
const section = visible.has(name) ? "visible" : "hidden";
|
|
470
|
+
if (section !== previousSection) {
|
|
471
|
+
const heading =
|
|
472
|
+
section === "visible" ? "Visible — render order" : "Hidden — not rendered";
|
|
473
|
+
lines.push(theme.fg("muted", theme.bold(truncateToWidth(` ${heading}`, safeWidth))));
|
|
474
|
+
previousSection = section;
|
|
475
|
+
}
|
|
476
|
+
const isSelected = index === selectedIndex;
|
|
477
|
+
const prefix = isSelected ? "→ " : " ";
|
|
478
|
+
const placement = placements.get(name);
|
|
479
|
+
const lineBreakLabel = hasLineBreakAfter(current.config.segments, name)
|
|
480
|
+
? " · break after"
|
|
481
|
+
: "";
|
|
482
|
+
const row = placement
|
|
483
|
+
? `${prefix}${`${placement.order}.`.padStart(3)} row ${placement.row} · ${name.padEnd(8)} · visible${lineBreakLabel}`
|
|
484
|
+
: `${prefix} · ${name.padEnd(8)} · hidden`;
|
|
485
|
+
const truncated = truncateToWidth(row, safeWidth);
|
|
486
|
+
if (isSelected) lines.push(theme.fg("accent", truncated));
|
|
487
|
+
else lines.push(section === "visible" ? truncated : theme.fg("muted", truncated));
|
|
488
|
+
}
|
|
489
|
+
if (startIndex > 0 || endIndex < names.length) {
|
|
490
|
+
lines.push(
|
|
491
|
+
theme.fg(
|
|
492
|
+
"dim",
|
|
493
|
+
truncateToWidth(` (${startIndex + 1}-${endIndex}/${names.length})`, safeWidth),
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const selected = names[selectedIndex];
|
|
499
|
+
if (selected) {
|
|
500
|
+
lines.push("");
|
|
501
|
+
const detail = feedback ?? SEGMENT_DESCRIPTIONS[selected];
|
|
502
|
+
const color = feedback ? "warning" : "muted";
|
|
503
|
+
for (const line of wrapTextWithAnsi(detail, Math.max(1, safeWidth - 2))) {
|
|
504
|
+
lines.push(theme.fg(color, truncateToWidth(` ${line}`, safeWidth)));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
lines.push("");
|
|
509
|
+
const hints = segmentControlHints(safeWidth, moveMode, selected);
|
|
510
|
+
for (const hint of hints) {
|
|
511
|
+
lines.push(theme.fg("dim", truncateToWidth(` ${hint}`, safeWidth)));
|
|
512
|
+
}
|
|
513
|
+
return lines;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const container = new Container();
|
|
517
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
518
|
+
const title = new Text("", 1, 0);
|
|
519
|
+
container.addChild(title);
|
|
520
|
+
container.addChild({ render: renderList, invalidate() {} });
|
|
521
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
522
|
+
const updateThemedText = () => {
|
|
523
|
+
title.setText(theme.fg("accent", theme.bold("Statusline segments")));
|
|
524
|
+
};
|
|
525
|
+
updateThemedText();
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
render: (width: number) => container.render(width),
|
|
529
|
+
invalidate() {
|
|
530
|
+
container.invalidate();
|
|
531
|
+
updateThemedText();
|
|
532
|
+
},
|
|
533
|
+
handleInput(data: string) {
|
|
534
|
+
const moveModeKey = matchesKey(data, "m") || matchesKey(data, Key.shift("m"));
|
|
535
|
+
const lineBreakKey = matchesKey(data, "b") || matchesKey(data, Key.shift("b"));
|
|
536
|
+
if (matchesKey(data, Key.alt("up"))) {
|
|
537
|
+
reorderSelected(-1);
|
|
538
|
+
} else if (matchesKey(data, Key.alt("down"))) {
|
|
539
|
+
reorderSelected(1);
|
|
540
|
+
} else if (moveMode) {
|
|
541
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
542
|
+
reorderSelected(-1);
|
|
543
|
+
} else if (keybindings.matches(data, "tui.select.down")) {
|
|
544
|
+
reorderSelected(1);
|
|
545
|
+
} else if (
|
|
546
|
+
moveModeKey ||
|
|
547
|
+
keybindings.matches(data, "tui.select.confirm") ||
|
|
548
|
+
data === " " ||
|
|
549
|
+
keybindings.matches(data, "tui.select.cancel")
|
|
550
|
+
) {
|
|
551
|
+
moveMode = false;
|
|
552
|
+
feedback = undefined;
|
|
553
|
+
}
|
|
554
|
+
} else if (keybindings.matches(data, "tui.select.up")) {
|
|
555
|
+
selectedIndex = selectedIndex === 0 ? names.length - 1 : selectedIndex - 1;
|
|
556
|
+
feedback = undefined;
|
|
557
|
+
} else if (keybindings.matches(data, "tui.select.down")) {
|
|
558
|
+
selectedIndex = selectedIndex === names.length - 1 ? 0 : selectedIndex + 1;
|
|
559
|
+
feedback = undefined;
|
|
560
|
+
} else if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
561
|
+
moveSelection(-SEGMENT_VIEWPORT_SIZE);
|
|
562
|
+
} else if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
563
|
+
moveSelection(SEGMENT_VIEWPORT_SIZE);
|
|
564
|
+
} else if (moveModeKey) {
|
|
565
|
+
enterMoveMode();
|
|
566
|
+
} else if (lineBreakKey) {
|
|
567
|
+
toggleLineBreakAfterSelected();
|
|
568
|
+
} else if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
|
|
569
|
+
toggleSelected();
|
|
570
|
+
} else if (keybindings.matches(data, "tui.select.cancel")) {
|
|
571
|
+
done(undefined);
|
|
572
|
+
}
|
|
573
|
+
tui.requestRender();
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
51
579
|
async function editSettings(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
|
|
52
580
|
if (ctx.mode !== "tui") {
|
|
53
581
|
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${options.settingsPath}`, "info");
|
|
@@ -72,6 +600,227 @@ async function editSettings(ctx: ExtensionCommandContext, options: StatuslineCom
|
|
|
72
600
|
}
|
|
73
601
|
}
|
|
74
602
|
|
|
603
|
+
function palettePresetDocument(
|
|
604
|
+
current: LoadedStatuslineSettings,
|
|
605
|
+
palettePreset: PalettePreset,
|
|
606
|
+
): string {
|
|
607
|
+
const { parsed } = editableSettings(current, "choosing a palette preset");
|
|
608
|
+
if (palettePreset === "custom" && !isRecord(parsed.palette)) {
|
|
609
|
+
const seedPreset = isPaletteName(current.config.palettePreset)
|
|
610
|
+
? current.config.palettePreset
|
|
611
|
+
: "tokyo-night";
|
|
612
|
+
parsed.palette = segmentPaletteForPreset(seedPreset);
|
|
613
|
+
} else if (palettePreset !== "custom" && typeof parsed.palette === "string") {
|
|
614
|
+
delete parsed.palette;
|
|
615
|
+
}
|
|
616
|
+
parsed.palettePreset = palettePreset;
|
|
617
|
+
return `${JSON.stringify(parsed, null, "\t")}\n`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function informationProfileDocument(
|
|
621
|
+
current: LoadedStatuslineSettings,
|
|
622
|
+
profile: InformationProfileName,
|
|
623
|
+
): { nextDocument: string; previousDocument: string } {
|
|
624
|
+
const { parsed, rawDocument: previousDocument } = editableSettings(
|
|
625
|
+
current,
|
|
626
|
+
"changing information level",
|
|
627
|
+
);
|
|
628
|
+
parsed.segments = [...INFORMATION_PROFILES[profile]];
|
|
629
|
+
return {
|
|
630
|
+
nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
|
|
631
|
+
previousDocument,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function visibleSegmentNames(current: LoadedStatuslineSettings): SegmentName[] {
|
|
636
|
+
return current.config.segments.filter(
|
|
637
|
+
(segment): segment is SegmentName => segment !== LINE_BREAK_SEGMENT_NAME,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function segmentMenuOrder(current: LoadedStatuslineSettings): SegmentName[] {
|
|
642
|
+
const visible = visibleSegmentNames(current);
|
|
643
|
+
const visibleSet = new Set(visible);
|
|
644
|
+
return [...visible, ...SEGMENT_NAMES.filter((name) => !visibleSet.has(name))];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function hasLineBreakAfter(segments: readonly ConfigSegmentName[], name: SegmentName): boolean {
|
|
648
|
+
const index = segments.indexOf(name);
|
|
649
|
+
return index >= 0 && segments[index + 1] === LINE_BREAK_SEGMENT_NAME;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function segmentPlacements(
|
|
653
|
+
segments: readonly ConfigSegmentName[],
|
|
654
|
+
): Map<SegmentName, { order: number; row: number }> {
|
|
655
|
+
const placements = new Map<SegmentName, { order: number; row: number }>();
|
|
656
|
+
let order = 0;
|
|
657
|
+
let row = 1;
|
|
658
|
+
for (const segment of segments) {
|
|
659
|
+
if (segment === LINE_BREAK_SEGMENT_NAME) {
|
|
660
|
+
row += 1;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
placements.set(segment, { order: ++order, row });
|
|
664
|
+
}
|
|
665
|
+
return placements;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function segmentControlHints(
|
|
669
|
+
width: number,
|
|
670
|
+
moveMode: boolean,
|
|
671
|
+
selected: SegmentName | undefined,
|
|
672
|
+
): string[] {
|
|
673
|
+
if (moveMode) {
|
|
674
|
+
return width < 30
|
|
675
|
+
? [
|
|
676
|
+
`Move mode: ${selected ?? "segment"}`,
|
|
677
|
+
"↑↓ move segment",
|
|
678
|
+
"Enter/Space finish",
|
|
679
|
+
"Esc leave move mode",
|
|
680
|
+
]
|
|
681
|
+
: [
|
|
682
|
+
`Move mode — ${selected ?? "segment"}`,
|
|
683
|
+
"↑↓ move · Enter/Space finish · Esc leave move mode",
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
return width < 30
|
|
687
|
+
? [
|
|
688
|
+
"↑↓ navigate",
|
|
689
|
+
"Enter/Space toggle",
|
|
690
|
+
"M move · B line break",
|
|
691
|
+
"Alt+↑/↓ quick move",
|
|
692
|
+
"Esc close",
|
|
693
|
+
]
|
|
694
|
+
: [
|
|
695
|
+
"↑↓ navigate · Enter/Space show/hide · M move mode",
|
|
696
|
+
"B add/remove line break after · Alt+↑/↓ quick move · Esc close",
|
|
697
|
+
];
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function segmentsDocument(
|
|
701
|
+
current: LoadedStatuslineSettings,
|
|
702
|
+
name: SegmentName,
|
|
703
|
+
shouldShow: boolean,
|
|
704
|
+
): { nextDocument: string; previousDocument: string } {
|
|
705
|
+
const { parsed, rawDocument: previousDocument } = editableSettings(current, "changing segments");
|
|
706
|
+
const segments = shouldShow
|
|
707
|
+
? [...current.config.segments, ...(current.config.segments.includes(name) ? [] : [name])]
|
|
708
|
+
: current.config.segments.filter((segment) => segment !== name);
|
|
709
|
+
parsed.segments = normalizeLineBreaks(segments);
|
|
710
|
+
return {
|
|
711
|
+
nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
|
|
712
|
+
previousDocument,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function lineBreakAfterDocument(
|
|
717
|
+
current: LoadedStatuslineSettings,
|
|
718
|
+
name: SegmentName,
|
|
719
|
+
): { nextDocument: string; previousDocument: string } {
|
|
720
|
+
const { parsed, rawDocument: previousDocument } = editableSettings(
|
|
721
|
+
current,
|
|
722
|
+
"changing line breaks",
|
|
723
|
+
);
|
|
724
|
+
const segments = [...current.config.segments];
|
|
725
|
+
const segmentIndex = segments.indexOf(name);
|
|
726
|
+
if (segments[segmentIndex + 1] === LINE_BREAK_SEGMENT_NAME) {
|
|
727
|
+
segments.splice(segmentIndex + 1, 1);
|
|
728
|
+
} else {
|
|
729
|
+
segments.splice(segmentIndex + 1, 0, LINE_BREAK_SEGMENT_NAME);
|
|
730
|
+
}
|
|
731
|
+
parsed.segments = segments;
|
|
732
|
+
return {
|
|
733
|
+
nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
|
|
734
|
+
previousDocument,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function reorderedSegmentsDocument(
|
|
739
|
+
current: LoadedStatuslineSettings,
|
|
740
|
+
name: SegmentName,
|
|
741
|
+
direction: -1 | 1,
|
|
742
|
+
): { nextDocument: string; previousDocument: string } | undefined {
|
|
743
|
+
const dataIndexes = current.config.segments.flatMap((segment, index) =>
|
|
744
|
+
segment === LINE_BREAK_SEGMENT_NAME ? [] : [index],
|
|
745
|
+
);
|
|
746
|
+
const currentDataIndex = dataIndexes.findIndex(
|
|
747
|
+
(index) => current.config.segments[index] === name,
|
|
748
|
+
);
|
|
749
|
+
const targetDataIndex = currentDataIndex + direction;
|
|
750
|
+
if (currentDataIndex < 0 || targetDataIndex < 0 || targetDataIndex >= dataIndexes.length) {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
const { parsed, rawDocument: previousDocument } = editableSettings(
|
|
754
|
+
current,
|
|
755
|
+
"reordering segments",
|
|
756
|
+
);
|
|
757
|
+
const segments = [...current.config.segments];
|
|
758
|
+
const currentIndex = dataIndexes[currentDataIndex];
|
|
759
|
+
const targetIndex = dataIndexes[targetDataIndex];
|
|
760
|
+
if (currentIndex === undefined || targetIndex === undefined) return undefined;
|
|
761
|
+
[segments[currentIndex], segments[targetIndex]] = [segments[targetIndex], segments[currentIndex]];
|
|
762
|
+
parsed.segments = segments;
|
|
763
|
+
return {
|
|
764
|
+
nextDocument: `${JSON.stringify(parsed, null, "\t")}\n`,
|
|
765
|
+
previousDocument,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function applySegmentsDocumentChange(
|
|
770
|
+
change: { nextDocument: string; previousDocument: string },
|
|
771
|
+
ctx: ExtensionCommandContext,
|
|
772
|
+
options: StatuslineCommandOptions,
|
|
773
|
+
): LoadedStatuslineSettings {
|
|
774
|
+
const save = options.save ?? saveStatuslineSettingsDocument;
|
|
775
|
+
const next = save(options.settingsPath, change.nextDocument);
|
|
776
|
+
try {
|
|
777
|
+
options.apply(next, ctx);
|
|
778
|
+
} catch (applyError) {
|
|
779
|
+
try {
|
|
780
|
+
const restored = save(options.settingsPath, change.previousDocument);
|
|
781
|
+
options.apply(restored, ctx);
|
|
782
|
+
} catch (rollbackError) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`runtime update failed: ${formatError(applyError)}; rollback failed: ${formatError(rollbackError)}`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
throw applyError;
|
|
788
|
+
}
|
|
789
|
+
return next;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function normalizeLineBreaks(segments: readonly ConfigSegmentName[]): ConfigSegmentName[] {
|
|
793
|
+
const normalized: ConfigSegmentName[] = [];
|
|
794
|
+
for (const segment of segments) {
|
|
795
|
+
if (
|
|
796
|
+
segment === LINE_BREAK_SEGMENT_NAME &&
|
|
797
|
+
(normalized.length === 0 || normalized.at(-1) === LINE_BREAK_SEGMENT_NAME)
|
|
798
|
+
) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
normalized.push(segment);
|
|
802
|
+
}
|
|
803
|
+
if (normalized.at(-1) === LINE_BREAK_SEGMENT_NAME) normalized.pop();
|
|
804
|
+
return normalized;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function editableSettings(
|
|
808
|
+
current: LoadedStatuslineSettings,
|
|
809
|
+
action: string,
|
|
810
|
+
): { parsed: Record<string, unknown>; rawDocument: string } {
|
|
811
|
+
if (
|
|
812
|
+
current.source !== "user" ||
|
|
813
|
+
current.rawDocument === undefined ||
|
|
814
|
+
current.diagnostics.some((item) => item.code !== "unknown")
|
|
815
|
+
) {
|
|
816
|
+
throw new Error(`Fix pi-statusline.json before ${action}`);
|
|
817
|
+
}
|
|
818
|
+
const rawDocument = current.rawDocument;
|
|
819
|
+
const parsed = JSON.parse(rawDocument) as unknown;
|
|
820
|
+
if (!isRecord(parsed)) throw new Error("Settings must contain a JSON object");
|
|
821
|
+
return { parsed, rawDocument };
|
|
822
|
+
}
|
|
823
|
+
|
|
75
824
|
function showStatus(ctx: ExtensionCommandContext, options: StatuslineCommandOptions) {
|
|
76
825
|
if (!canNotify(ctx)) return;
|
|
77
826
|
const loaded = options.getLoaded();
|
|
@@ -83,9 +832,10 @@ function showStatus(ctx: ExtensionCommandContext, options: StatuslineCommandOpti
|
|
|
83
832
|
[
|
|
84
833
|
`pi-statusline source: ${loaded.source}`,
|
|
85
834
|
`path: ${options.settingsPath}`,
|
|
86
|
-
`palette: ${loaded.config.
|
|
835
|
+
`palette preset: ${loaded.config.palettePreset}`,
|
|
87
836
|
`density: ${loaded.config.density}`,
|
|
88
837
|
`separator: ${loaded.config.separator}`,
|
|
838
|
+
`information: ${inferInformationProfile(loaded.config.segments)}`,
|
|
89
839
|
`segments: ${loaded.config.segments.join(", ") || "none"}`,
|
|
90
840
|
diagnostics ? `warnings: ${diagnostics}` : "warnings: none",
|
|
91
841
|
].join("\n"),
|
|
@@ -97,18 +847,34 @@ function showHelp(ctx: ExtensionCommandContext, settingsPath: string) {
|
|
|
97
847
|
if (!canNotify(ctx)) return;
|
|
98
848
|
ctx.ui.notify(
|
|
99
849
|
[
|
|
850
|
+
"/statusline — open Appearance, Information, Advanced, Status, and Help",
|
|
100
851
|
"/statusline settings — edit and apply JSON",
|
|
101
|
-
"/statusline status — show source, path, and warnings",
|
|
852
|
+
"/statusline status — show source, path, information level, and warnings",
|
|
102
853
|
"/statusline help — show this help",
|
|
854
|
+
"Menu actions: Appearance, Information, Advanced, Status, Help.",
|
|
855
|
+
"Information levels: minimal, balanced, detailed; any other segment array is custom.",
|
|
856
|
+
"Advanced actions: Custom layout, Edit settings JSON, Back.",
|
|
103
857
|
`Settings: ${settingsPath}`,
|
|
104
|
-
"Fields: palette, density, separator, segments, segmentText, extensionStatusIcons",
|
|
105
|
-
"
|
|
106
|
-
"
|
|
858
|
+
"Fields: palettePreset, palette, density, separator, segments, segmentText, extensionStatusIcons",
|
|
859
|
+
"Named presets ignore but preserve palette; custom uses its per-segment fg/bg colors.",
|
|
860
|
+
"Responsive rows retain context, model, location, and active work before decorative data.",
|
|
861
|
+
"Custom layout can show, hide, reorder, or split data segments across rows.",
|
|
862
|
+
"Press M for move mode, Alt+Up/Alt+Down for quick move, and B for a line break.",
|
|
863
|
+
"Line breaks (line_break) may repeat when separated by data segments, but cannot be consecutive.",
|
|
864
|
+
"segmentText supports prefix and suffix strings around Pi-owned dynamic values.",
|
|
107
865
|
].join("\n"),
|
|
108
866
|
"info",
|
|
109
867
|
);
|
|
110
868
|
}
|
|
111
869
|
|
|
870
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
871
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function isPaletteName(value: PalettePreset): value is PaletteName {
|
|
875
|
+
return (PALETTE_NAMES as readonly PalettePreset[]).includes(value);
|
|
876
|
+
}
|
|
877
|
+
|
|
112
878
|
function canNotify(ctx: ExtensionCommandContext): boolean {
|
|
113
879
|
return ctx.mode === "tui" || ctx.hasUI;
|
|
114
880
|
}
|