@narumitw/pi-statusline 0.49.2 → 0.49.3

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