@xzzpig/pi-starline 0.3.0 → 0.4.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 +2 -1
- package/extensions/starline/config.ts +20 -3
- package/extensions/starline/extension-status.ts +3 -0
- package/extensions/starline/index.ts +2 -1
- package/extensions/starline/mouse/index.ts +2 -2
- package/extensions/starline/settings-command.ts +1 -0
- package/extensions/starline/ui.ts +59 -6
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ pieces of one product, not a theme:
|
|
|
30
30
|
- Built-in footer segments can be shown or hidden individually from `/starline`
|
|
31
31
|
- Fully custom Starship-style layout via a `footerFormat` template string — see [Footer Format Template](https://github.com/Andy8647/pi-starline/blob/main/docs/configuration.md#footer-format-template)
|
|
32
32
|
- Third-party Pi extension statuses from `ctx.ui.setStatus()` can be shown on the left,
|
|
33
|
-
middle, or right side,
|
|
33
|
+
middle, or right side of the footer, on the editor's metadata row, or hidden per
|
|
34
|
+
status key from `/starline`
|
|
34
35
|
|
|
35
36
|
### Editor (Opencode-inspired)
|
|
36
37
|
|
|
@@ -150,7 +150,15 @@ export function migrateFixedEditorKeys(raw: Record<string, unknown>): {
|
|
|
150
150
|
return { config: { ...rest, mouse }, migrated: true };
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Where a third-party status is drawn.
|
|
155
|
+
*
|
|
156
|
+
* `editor` is the right-hand side of the editor's metadata row, alongside the
|
|
157
|
+
* vim-mode label and the copy/paste hints — not the footer. It needs Starline's
|
|
158
|
+
* own footer installed to have data at all, because the status map only reaches
|
|
159
|
+
* the extension through the footer factory.
|
|
160
|
+
*/
|
|
161
|
+
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right" | "editor";
|
|
154
162
|
export type ExtensionStatusColorMode = "themed" | "original";
|
|
155
163
|
|
|
156
164
|
/**
|
|
@@ -756,7 +764,13 @@ function normalizeGitMetricsConfig(record: Record<string, unknown>): GitMetricsC
|
|
|
756
764
|
}
|
|
757
765
|
|
|
758
766
|
export function isExtensionStatusPlacement(value: unknown): value is ExtensionStatusPlacement {
|
|
759
|
-
return
|
|
767
|
+
return (
|
|
768
|
+
value === "off" ||
|
|
769
|
+
value === "left" ||
|
|
770
|
+
value === "middle" ||
|
|
771
|
+
value === "right" ||
|
|
772
|
+
value === "editor"
|
|
773
|
+
);
|
|
760
774
|
}
|
|
761
775
|
|
|
762
776
|
export function isExtensionStatusColorMode(value: unknown): value is ExtensionStatusColorMode {
|
|
@@ -940,7 +954,10 @@ function writeConfigAtomically(path: string, record: ConfigRecord, mode?: number
|
|
|
940
954
|
if (file !== undefined) {
|
|
941
955
|
try {
|
|
942
956
|
closeSync(file);
|
|
943
|
-
} catch {
|
|
957
|
+
} catch {
|
|
958
|
+
// Best-effort close during error cleanup; the outer catch
|
|
959
|
+
// already reports the primary failure.
|
|
960
|
+
}
|
|
944
961
|
}
|
|
945
962
|
try {
|
|
946
963
|
unlinkSync(tempPath);
|
|
@@ -17,6 +17,7 @@ export type ExtensionStatusSegmentsByPlacement = {
|
|
|
17
17
|
left: ExtensionStatusSegment[];
|
|
18
18
|
middle: ExtensionStatusSegment[];
|
|
19
19
|
right: ExtensionStatusSegment[];
|
|
20
|
+
editor: ExtensionStatusSegment[];
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
const safeSgrPattern = /\x1b\[[0-9;:]*m/g;
|
|
@@ -65,6 +66,7 @@ export function collectExtensionStatusSegments(
|
|
|
65
66
|
left: [],
|
|
66
67
|
middle: [],
|
|
67
68
|
right: [],
|
|
69
|
+
editor: [],
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
for (const [key, value] of statuses.entries()) {
|
|
@@ -84,5 +86,6 @@ export function collectExtensionStatusSegments(
|
|
|
84
86
|
segments.left.sort(compareKeys);
|
|
85
87
|
segments.middle.sort(compareKeys);
|
|
86
88
|
segments.right.sort(compareKeys);
|
|
89
|
+
segments.editor.sort(compareKeys);
|
|
87
90
|
return segments;
|
|
88
91
|
}
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
Theme,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
8
|
-
import type { MouseEventsApi } from "@xzzpig/pi-mouse-events/api";
|
|
9
8
|
import {
|
|
10
9
|
type ColorSourcesConfig,
|
|
11
10
|
type ContextStyle,
|
|
@@ -324,6 +323,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
324
323
|
sessionName: ctx.sessionManager.getSessionName() ?? "",
|
|
325
324
|
}),
|
|
326
325
|
getThinkingLevel,
|
|
326
|
+
() => getActiveExtensionStatuses(),
|
|
327
327
|
);
|
|
328
328
|
applyPasteCollapse(editor);
|
|
329
329
|
// Nothing on Pi's renderer points at the live editor, and the
|
|
@@ -357,6 +357,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
357
357
|
sessionName: ctx.sessionManager.getSessionName() ?? "",
|
|
358
358
|
}),
|
|
359
359
|
getThinkingLevel,
|
|
360
|
+
() => getActiveExtensionStatuses(),
|
|
360
361
|
);
|
|
361
362
|
// Two references, because these are two different objects here: the
|
|
362
363
|
// wrapper is what gets mounted and hit-tested, while the draft's
|
|
@@ -686,8 +686,8 @@ function expandClickTarget(
|
|
|
686
686
|
* a non-empty `pressedUrl` means Pi is about to open a link — the box stays
|
|
687
687
|
* as it is and the release goes through unconsumed.
|
|
688
688
|
*/
|
|
689
|
-
function pressedUrlAt(receiver: MouseCapableReceiver):
|
|
690
|
-
return (receiver as { pressedUrl?: unknown }).pressedUrl;
|
|
689
|
+
function pressedUrlAt(receiver: MouseCapableReceiver): string | undefined {
|
|
690
|
+
return (receiver as { pressedUrl?: unknown }).pressedUrl as string | undefined;
|
|
691
691
|
}
|
|
692
692
|
|
|
693
693
|
/**
|
|
@@ -42,6 +42,7 @@ const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
|
|
|
42
42
|
"left",
|
|
43
43
|
"middle",
|
|
44
44
|
"right",
|
|
45
|
+
"editor",
|
|
45
46
|
];
|
|
46
47
|
const extensionStatusColorModeValues: ExtensionStatusColorMode[] = ["themed", "original"];
|
|
47
48
|
const contextStyleValues: ContextStyle[] = ["text", "gauge", "text+gauge"];
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { PolishedTuiConfig } from "./config";
|
|
12
12
|
import { applyEditorCursorStyleToLines } from "./editor-cursor";
|
|
13
13
|
import { renderEditorMetadataFormat } from "./editor-metadata-format";
|
|
14
|
+
import { collectExtensionStatusSegments } from "./extension-status";
|
|
14
15
|
import { activeSelectionHintText, externalEditorHintText } from "./mouse";
|
|
15
16
|
import { composeHints } from "./mouse/hint";
|
|
16
17
|
import { pasteExpandHintText } from "./paste-collapse";
|
|
@@ -85,6 +86,8 @@ type PolishedFrameOptions = {
|
|
|
85
86
|
modelMeta: EditorMeta;
|
|
86
87
|
thinkingLevel: string | undefined;
|
|
87
88
|
rightStatus?: string;
|
|
89
|
+
/** Third-party statuses placed on the editor row (`placement: "editor"`). */
|
|
90
|
+
editorStatus?: string;
|
|
88
91
|
splitBaseFrame?: (lines: string[]) => PolishedFrameSplit | undefined;
|
|
89
92
|
};
|
|
90
93
|
|
|
@@ -132,7 +135,8 @@ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset:
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
/**
|
|
135
|
-
* The right side of the metadata row: vim mode,
|
|
138
|
+
* The right side of the metadata row: vim mode, third-party statuses placed
|
|
139
|
+
* there, and the paste/selection hints.
|
|
136
140
|
*
|
|
137
141
|
* The hint used to live on the editor's bottom border, drawn there by the fixed
|
|
138
142
|
* editor's compositor. Pi 0.84 supersedes the fixed editor, so the hint needs a
|
|
@@ -140,8 +144,15 @@ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset:
|
|
|
140
144
|
* `isHorizontalBorder` requires an unbroken rule to find the frame again. The
|
|
141
145
|
* metadata row is always rendered, even when `editorMetadataFormat` is blank,
|
|
142
146
|
* so the hint shows up whatever the user has done to that template.
|
|
147
|
+
*
|
|
148
|
+
* Hints come last: `composeMetadataLine` truncates this string from the right
|
|
149
|
+
* when it cannot fit, and the hints are the transient, actionable part.
|
|
143
150
|
*/
|
|
144
|
-
function composeRightStatus(
|
|
151
|
+
function composeRightStatus(
|
|
152
|
+
vimStatus: string | undefined,
|
|
153
|
+
editorStatus: string | undefined,
|
|
154
|
+
uiTheme: Theme,
|
|
155
|
+
): string | undefined {
|
|
145
156
|
// A live selection hint already spells the external editor out ("N
|
|
146
157
|
// characters selected, ctrl+x to copy ⋅ ctrl+g to edit in $EDITOR"), so it
|
|
147
158
|
// takes precedence; the always-on hint only speaks when nothing else does.
|
|
@@ -149,9 +160,31 @@ function composeRightStatus(vimStatus: string | undefined, uiTheme: Theme): stri
|
|
|
149
160
|
pasteExpandHintText(),
|
|
150
161
|
activeSelectionHintText() ?? externalEditorHintText(),
|
|
151
162
|
);
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
return
|
|
163
|
+
const styled = hint ? safeThemeFg(uiTheme, "muted", hint) : undefined;
|
|
164
|
+
const parts = [vimStatus, editorStatus, styled].filter((part): part is string => Boolean(part));
|
|
165
|
+
return parts.length > 0 ? parts.join(" ⋅ ") : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Statuses configured with `placement: "editor"`, joined for the metadata row.
|
|
170
|
+
*
|
|
171
|
+
* The row is one muted line like the hints beside it, so the per-status colours
|
|
172
|
+
* and icons (footer features) are not applied here; `colorMode: "original"`
|
|
173
|
+
* text keeps whatever the extension already styled it with.
|
|
174
|
+
*/
|
|
175
|
+
function editorStatusText(
|
|
176
|
+
config: PolishedTuiConfig,
|
|
177
|
+
uiTheme: Theme,
|
|
178
|
+
statuses: ReadonlyMap<string, string> | undefined,
|
|
179
|
+
): string | undefined {
|
|
180
|
+
if (!statuses || statuses.size === 0) return undefined;
|
|
181
|
+
const segments = collectExtensionStatusSegments(statuses, config).editor;
|
|
182
|
+
if (segments.length === 0) return undefined;
|
|
183
|
+
return segments
|
|
184
|
+
.map((segment) =>
|
|
185
|
+
segment.colorMode === "original" ? segment.text : safeThemeFg(uiTheme, "muted", segment.text),
|
|
186
|
+
)
|
|
187
|
+
.join(" ⋅ ");
|
|
155
188
|
}
|
|
156
189
|
|
|
157
190
|
function composeMetadataLine(left: string, right: string | undefined, width: number): string {
|
|
@@ -280,6 +313,7 @@ function renderPolishedFrame({
|
|
|
280
313
|
modelMeta,
|
|
281
314
|
thinkingLevel,
|
|
282
315
|
rightStatus,
|
|
316
|
+
editorStatus,
|
|
283
317
|
splitBaseFrame,
|
|
284
318
|
}: PolishedFrameOptions): string[] {
|
|
285
319
|
if (width <= 2) return clampRenderedLines(baseRendered, width);
|
|
@@ -330,7 +364,7 @@ function renderPolishedFrame({
|
|
|
330
364
|
uiTheme,
|
|
331
365
|
config,
|
|
332
366
|
);
|
|
333
|
-
const status = composeRightStatus(rightStatus, uiTheme);
|
|
367
|
+
const status = composeRightStatus(rightStatus, editorStatus, uiTheme);
|
|
334
368
|
const copyFriendlyMeta = composeMetadataLine(meta, status, Math.max(0, width - 1));
|
|
335
369
|
const railedMeta = composeMetadataLine(meta, status, innerWidth);
|
|
336
370
|
|
|
@@ -381,6 +415,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
381
415
|
private readonly getThinkingLevel: () => string | undefined;
|
|
382
416
|
private readonly getConfig: () => PolishedTuiConfig;
|
|
383
417
|
private readonly uiTheme: Theme;
|
|
418
|
+
private readonly getExtensionStatuses: (() => ReadonlyMap<string, string>) | undefined;
|
|
384
419
|
|
|
385
420
|
constructor(
|
|
386
421
|
tui: TUI,
|
|
@@ -390,6 +425,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
390
425
|
getConfig: () => PolishedTuiConfig,
|
|
391
426
|
getModelMeta: () => EditorMeta,
|
|
392
427
|
getThinkingLevel: () => string | undefined,
|
|
428
|
+
getExtensionStatuses?: () => ReadonlyMap<string, string>,
|
|
393
429
|
) {
|
|
394
430
|
super(tui, theme, keybindings, { paddingX: 0 });
|
|
395
431
|
this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
|
|
@@ -397,6 +433,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
397
433
|
this.getConfig = getConfig;
|
|
398
434
|
this.getModelMeta = getModelMeta;
|
|
399
435
|
this.getThinkingLevel = getThinkingLevel;
|
|
436
|
+
this.getExtensionStatuses = getExtensionStatuses;
|
|
400
437
|
}
|
|
401
438
|
|
|
402
439
|
render(width: number): string[] {
|
|
@@ -412,11 +449,14 @@ export class PolishedEditor extends CustomEditor {
|
|
|
412
449
|
const result = renderPolishedFrame({
|
|
413
450
|
width,
|
|
414
451
|
baseRendered: rendered,
|
|
452
|
+
// SAFETY: this PolishedEditor is constructed with the internals the
|
|
453
|
+
// renderer needs; the widening crosses an interface boundary only.
|
|
415
454
|
autocompleteSource: this as unknown as AutocompleteEditorInternals,
|
|
416
455
|
uiTheme: this.uiTheme,
|
|
417
456
|
config,
|
|
418
457
|
modelMeta,
|
|
419
458
|
thinkingLevel: this.getThinkingLevel(),
|
|
459
|
+
editorStatus: editorStatusText(config, this.uiTheme, this.getExtensionStatuses?.()),
|
|
420
460
|
});
|
|
421
461
|
return result;
|
|
422
462
|
}
|
|
@@ -427,6 +467,8 @@ export class PolishedEditor extends CustomEditor {
|
|
|
427
467
|
}
|
|
428
468
|
// Also resolvable under the pre-rename key, so a pi-zentui wrapper reading a
|
|
429
469
|
// pi-starline-built base still finds the splitter.
|
|
470
|
+
// SAFETY: LEGACY_SPLIT_POLISHED_FRAME is our own Symbol key; the cast only
|
|
471
|
+
// widens the receiver to assign a symbol-keyed method on the prototype.
|
|
430
472
|
(PolishedEditor.prototype as unknown as Record<PropertyKey, unknown>)[LEGACY_SPLIT_POLISHED_FRAME] =
|
|
431
473
|
PolishedEditor.prototype[SPLIT_POLISHED_FRAME];
|
|
432
474
|
|
|
@@ -437,6 +479,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
437
479
|
private readonly getConfig: () => PolishedTuiConfig,
|
|
438
480
|
private readonly getModelMeta: () => EditorMeta,
|
|
439
481
|
private readonly getThinkingLevel: () => string | undefined,
|
|
482
|
+
private readonly getExtensionStatuses?: () => ReadonlyMap<string, string>,
|
|
440
483
|
) {}
|
|
441
484
|
|
|
442
485
|
get focused(): boolean {
|
|
@@ -534,6 +577,7 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
534
577
|
modelMeta,
|
|
535
578
|
thinkingLevel: this.getThinkingLevel(),
|
|
536
579
|
rightStatus: vimStatus,
|
|
580
|
+
editorStatus: editorStatusText(config, this.uiTheme, this.getExtensionStatuses?.()),
|
|
537
581
|
splitBaseFrame: (
|
|
538
582
|
this.base[SPLIT_POLISHED_FRAME] ?? this.base[LEGACY_SPLIT_POLISHED_FRAME]
|
|
539
583
|
)?.bind(this.base),
|
|
@@ -588,10 +632,17 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
588
632
|
return this.base.getLines?.() ?? this.base.getText().split("\n");
|
|
589
633
|
}
|
|
590
634
|
|
|
635
|
+
// Deliberate pass-through of the base editor's duck-typed contract: Pi
|
|
636
|
+
// returns a {line, col} cursor object, so narrowing here would assert a
|
|
637
|
+
// type this wrapper cannot guarantee.
|
|
638
|
+
// pi-lens-ignore: no-unknown-returns
|
|
591
639
|
getCursor(): unknown {
|
|
592
640
|
return this.base.getCursor?.();
|
|
593
641
|
}
|
|
594
642
|
|
|
643
|
+
// Deliberate pass-through of the base editor's duck-typed contract; callers
|
|
644
|
+
// narrow the value with their own runtime checks.
|
|
645
|
+
// pi-lens-ignore: no-unknown-returns
|
|
595
646
|
getMode(): unknown {
|
|
596
647
|
return this.base.getMode?.();
|
|
597
648
|
}
|
|
@@ -606,6 +657,8 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
606
657
|
}
|
|
607
658
|
// Also resolvable under the pre-rename key, so a pi-zentui wrapper reading a
|
|
608
659
|
// pi-starline-built base still finds the splitter.
|
|
660
|
+
// SAFETY: LEGACY_SPLIT_POLISHED_FRAME is our own Symbol key; the cast only
|
|
661
|
+
// widens the receiver to assign a symbol-keyed method on the prototype.
|
|
609
662
|
(WrappedPolishedEditor.prototype as unknown as Record<PropertyKey, unknown>)[
|
|
610
663
|
LEGACY_SPLIT_POLISHED_FRAME
|
|
611
664
|
] = WrappedPolishedEditor.prototype[SPLIT_POLISHED_FRAME];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xzzpig/pi-starline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,11 +28,6 @@
|
|
|
28
28
|
"@earendil-works/pi-tui": ">=0.84.4",
|
|
29
29
|
"@xzzpig/pi-mouse-events": ">=0.1.0 <2"
|
|
30
30
|
},
|
|
31
|
-
"peerDependenciesMeta": {
|
|
32
|
-
"@xzzpig/pi-mouse-events": {
|
|
33
|
-
"optional": true
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
31
|
"pi": {
|
|
37
32
|
"extensions": [
|
|
38
33
|
"./extensions"
|
|
@@ -50,7 +45,12 @@
|
|
|
50
45
|
"@types/node": "^26.0.0",
|
|
51
46
|
"typescript": "^7.0.2",
|
|
52
47
|
"vitest": "^4.1.8",
|
|
53
|
-
"@xzzpig/pi-mouse-events": "0.1.
|
|
48
|
+
"@xzzpig/pi-mouse-events": "0.1.1"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@xzzpig/pi-mouse-events": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"lint": "biome check .",
|