@xzzpig/pi-starline 0.1.0 → 0.2.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 +31 -27
- package/extensions/starline/config.ts +0 -17
- package/extensions/starline/index.ts +12 -0
- package/extensions/starline/mouse/capabilities.ts +36 -36
- package/extensions/starline/mouse/editor-caret.ts +3 -3
- package/extensions/starline/mouse/index.ts +163 -272
- package/extensions/starline/mouse/selection-state.ts +17 -39
- package/extensions/starline/mouse/transcript-copy.ts +1 -1
- package/extensions/starline/prototype-patch-registry.ts +1 -2
- package/extensions/starline/ui.ts +1 -1
- package/package.json +58 -59
- package/extensions/starline/mouse/word-select.ts +0 -145
|
@@ -1,50 +1,28 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The selection hint is derived state, not owned state.
|
|
5
5
|
*
|
|
6
|
-
* Pi
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Pi 0.84.4 owns "select without copying" (`fullscreenCopyOnSelect`), so
|
|
7
|
+
* Starline no longer intercepts the release to arm a pending copy — the hint
|
|
8
|
+
* is computed on demand from the live renderer instead: an active selection
|
|
9
|
+
* that the renderer is not auto-copying. `selectionHintText` is the pure
|
|
10
|
+
* rendering of that state; `index.ts` gathers the pieces.
|
|
10
11
|
*/
|
|
11
|
-
export class SelectionPendingState {
|
|
12
|
-
private characters = 0;
|
|
13
|
-
/**
|
|
14
|
-
* `keyText("app.editor.external")`, resolved at arm time, when the pending
|
|
15
|
-
* selection is the editor's. Editor selections cannot grow past the visible
|
|
16
|
-
* window (there is no drag-scroll), so the hint points at the external
|
|
17
|
-
* editor as the way to act on the whole draft.
|
|
18
|
-
*/
|
|
19
|
-
private externalEditorKey: string | undefined;
|
|
20
|
-
|
|
21
|
-
arm(characters: number, externalEditorKey?: string): void {
|
|
22
|
-
this.characters = characters > 0 ? characters : 0;
|
|
23
|
-
this.externalEditorKey = this.characters > 0 ? externalEditorKey : undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
clear(): void {
|
|
27
|
-
this.characters = 0;
|
|
28
|
-
this.externalEditorKey = undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get pending(): { characters: number; externalEditorKey?: string } | undefined {
|
|
32
|
-
return this.characters > 0
|
|
33
|
-
? { characters: this.characters, externalEditorKey: this.externalEditorKey }
|
|
34
|
-
: undefined;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
12
|
export function selectionHintText(
|
|
39
|
-
|
|
13
|
+
characters: number,
|
|
14
|
+
copyKey: string,
|
|
15
|
+
externalEditorKey: string | undefined,
|
|
40
16
|
editorName: string | null = null,
|
|
41
17
|
): string | null {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
18
|
+
if (characters <= 0) return null;
|
|
19
|
+
const noun = characters === 1 ? "character" : "characters";
|
|
20
|
+
const base =
|
|
21
|
+
copyKey.length > 0
|
|
22
|
+
? `${characters} ${noun} selected, ${copyKey} to copy`
|
|
23
|
+
: `${characters} ${noun} selected`;
|
|
24
|
+
return externalEditorKey
|
|
25
|
+
? `${base} ⋅ ${externalEditorKey} to edit in ${editorName ?? "$EDITOR"}`
|
|
48
26
|
: base;
|
|
49
27
|
}
|
|
50
28
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Strips decoration out of a transcript selection before it reaches the
|
|
3
3
|
* clipboard.
|
|
4
4
|
*
|
|
5
|
-
* Pi's `
|
|
5
|
+
* Pi's `copyActiveSelectionToClipboard` copies *rendered* rows. In the transcript
|
|
6
6
|
* those rows carry chrome that two components paint around their content:
|
|
7
7
|
*
|
|
8
8
|
* - Starline's own user messages (`user-message.ts`): a full-width `─` rule
|
|
@@ -143,7 +143,7 @@ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset:
|
|
|
143
143
|
*/
|
|
144
144
|
function composeRightStatus(vimStatus: string | undefined, uiTheme: Theme): string | undefined {
|
|
145
145
|
// A live selection hint already spells the external editor out ("N
|
|
146
|
-
// characters selected, ctrl+
|
|
146
|
+
// characters selected, ctrl+x to copy ⋅ ctrl+g to edit in $EDITOR"), so it
|
|
147
147
|
// takes precedence; the always-on hint only speaks when nothing else does.
|
|
148
148
|
const hint = composeHints(
|
|
149
149
|
pasteExpandHintText(),
|
package/package.json
CHANGED
|
@@ -1,60 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
2
|
+
"name": "@xzzpig/pi-starline",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Andy8647/pi-starline.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Andy8647/pi-starline#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Andy8647/pi-starline/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi-extension",
|
|
18
|
+
"pi-theme",
|
|
19
|
+
"starship",
|
|
20
|
+
"tui"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"extensions"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@earendil-works/pi-ai": ">=0.84.4",
|
|
27
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
28
|
+
"@earendil-works/pi-tui": ">=0.84.4"
|
|
29
|
+
},
|
|
30
|
+
"pi": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./extensions"
|
|
33
|
+
],
|
|
34
|
+
"image": "https://raw.githubusercontent.com/Andy8647/pi-starline/main/assets/starline.png"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "^2.5.0",
|
|
41
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
43
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
44
|
+
"@types/node": "^26.0.0",
|
|
45
|
+
"typescript": "^7.0.2",
|
|
46
|
+
"vitest": "^4.1.8"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"lint": "biome check .",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"pi:dev": "${PI_BIN:-/opt/homebrew/bin/pi} --no-extensions -e ./extensions/starline/index.ts",
|
|
53
|
+
"pi:install-local": "${PI_BIN:-/opt/homebrew/bin/pi} install ./ -l",
|
|
54
|
+
"fmt": "biome format --write .",
|
|
55
|
+
"fmt:check": "biome format --check .",
|
|
56
|
+
"verify": "npm run lint && npm run typecheck && npm run test",
|
|
57
|
+
"pack:check": "npm pack --dry-run"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which columns a double click on one line should select.
|
|
3
|
-
*
|
|
4
|
-
* Ported from `getWordSelection` in pi commit `56419be51`
|
|
5
|
-
* (`feat/word-selection-path-chars`, `packages/tui/src/tui-alt-screen.ts`),
|
|
6
|
-
* filed upstream as issue #7746. The installed Pi build's own
|
|
7
|
-
* `getWordSelection` predates that patch, so this substitutes Pi's whole
|
|
8
|
-
* word-selection rule rather than layering path handling on top of it — see
|
|
9
|
-
* `index.ts` for where that substitution happens and why it's the intended
|
|
10
|
-
* scope, not creep.
|
|
11
|
-
*
|
|
12
|
-
* This is a pure function of one already-plain (no ANSI) line and a terminal
|
|
13
|
-
* column; the caller in `index.ts` is what reads Pi's live source line and
|
|
14
|
-
* strips escape sequences before calling in, matching the split
|
|
15
|
-
* `getWordSelection`/`getSelectionSourceLine` already have upstream.
|
|
16
|
-
*/
|
|
17
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
-
|
|
19
|
-
const wordSegmenter = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Characters that join two words into one for double-click selection.
|
|
23
|
-
*
|
|
24
|
-
* `Intl.Segmenter` already keeps `_` and `.` inside a word, so `foo_bar` and
|
|
25
|
-
* `file.ts` survive on their own. It breaks on `/` and `-`, which is what
|
|
26
|
-
* takes a path or a kebab-case identifier apart. Terminals keep these inside
|
|
27
|
-
* a word so paths and URLs select whole; this set is what makes fullscreen's
|
|
28
|
-
* own double-click agree with them.
|
|
29
|
-
*/
|
|
30
|
-
const WORD_JOINERS = new Set(["/", "-"]);
|
|
31
|
-
|
|
32
|
-
type WordSegment = {
|
|
33
|
-
start: number;
|
|
34
|
-
end: number;
|
|
35
|
-
isWordLike: boolean;
|
|
36
|
-
isJoiner: boolean;
|
|
37
|
-
isSpace: boolean;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** `segments[index]`, guaranteed — every call site here only ever passes an index it just found in range. */
|
|
41
|
-
function segmentAt(segments: readonly WordSegment[], index: number): WordSegment {
|
|
42
|
-
const segment = segments[index];
|
|
43
|
-
if (segment === undefined) {
|
|
44
|
-
throw new RangeError(`word-select: no segment at index ${index}`);
|
|
45
|
-
}
|
|
46
|
-
return segment;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function collectWordSegments(line: string): WordSegment[] {
|
|
50
|
-
const segments: WordSegment[] = [];
|
|
51
|
-
let start = 0;
|
|
52
|
-
for (const segment of wordSegmenter.segment(line)) {
|
|
53
|
-
const text = segment.segment;
|
|
54
|
-
const end = start + visibleWidth(text);
|
|
55
|
-
segments.push({
|
|
56
|
-
start,
|
|
57
|
-
end,
|
|
58
|
-
isWordLike: segment.isWordLike === true,
|
|
59
|
-
// Joiners arrive one per segment, so `//` in a URL is two of these.
|
|
60
|
-
isJoiner: text.length > 0 && [...text].every((char) => WORD_JOINERS.has(char)),
|
|
61
|
-
isSpace: text.length > 0 && text.trim().length === 0,
|
|
62
|
-
});
|
|
63
|
-
start = end;
|
|
64
|
-
}
|
|
65
|
-
return segments;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Walk back over a run of joiners that opens a word, as in `/usr` or `--flag`.
|
|
70
|
-
*
|
|
71
|
-
* The run only counts as part of the word when nothing but whitespace
|
|
72
|
-
* precedes it, which is what separates a leading separator from an infix one.
|
|
73
|
-
* In `http://a.com` the `//` is preceded by `:`, so the scheme stays out.
|
|
74
|
-
*/
|
|
75
|
-
function openingJoinerRun(segments: WordSegment[], first: number): number {
|
|
76
|
-
let index = first;
|
|
77
|
-
while (segments[index - 1]?.isJoiner) index--;
|
|
78
|
-
if (index === first) return first;
|
|
79
|
-
const before = segments[index - 1];
|
|
80
|
-
return before === undefined || before.isSpace ? index : first;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Grow a word segment across joiners that have a word on both sides, plus any
|
|
85
|
-
* separator run that opens it.
|
|
86
|
-
*
|
|
87
|
-
* Anchoring on word-like segments is what keeps this conservative: in
|
|
88
|
-
* `http://a.com/b` the `:` is not a joiner, so the run stops there instead of
|
|
89
|
-
* swallowing the scheme, and a dash used as prose punctuation in `a - b` has
|
|
90
|
-
* whitespace around it rather than a word.
|
|
91
|
-
*/
|
|
92
|
-
function expandOverWordJoiners(
|
|
93
|
-
segments: WordSegment[],
|
|
94
|
-
index: number,
|
|
95
|
-
): { start: number; end: number } {
|
|
96
|
-
const anchor = segmentAt(segments, index);
|
|
97
|
-
if (anchor.isJoiner) {
|
|
98
|
-
// Clicking a separator should take the word it belongs to. Prefer the
|
|
99
|
-
// word on the left so an infix `/` grows the whole path; failing that,
|
|
100
|
-
// step over the run to the word it opens, so clicking the `/` of
|
|
101
|
-
// `/usr/bin` works.
|
|
102
|
-
if (segments[index - 1]?.isWordLike && segments[index + 1]?.isWordLike) {
|
|
103
|
-
return expandOverWordJoiners(segments, index - 1);
|
|
104
|
-
}
|
|
105
|
-
if (!segments[index - 1]?.isWordLike) {
|
|
106
|
-
let next = index;
|
|
107
|
-
while (segments[next]?.isJoiner) next++;
|
|
108
|
-
if (segments[next]?.isWordLike) return expandOverWordJoiners(segments, next);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (!anchor.isWordLike) return { start: anchor.start, end: anchor.end };
|
|
112
|
-
|
|
113
|
-
let first = index;
|
|
114
|
-
while (first >= 2 && segments[first - 1]?.isJoiner && segments[first - 2]?.isWordLike) {
|
|
115
|
-
first -= 2;
|
|
116
|
-
}
|
|
117
|
-
first = openingJoinerRun(segments, first);
|
|
118
|
-
// A trailing run gets no such treatment: nothing follows it to make it
|
|
119
|
-
// part of the word, and `trailing- x` reading as `trailing-` would be
|
|
120
|
-
// wrong.
|
|
121
|
-
let last = index;
|
|
122
|
-
while (segments[last + 1]?.isJoiner && segments[last + 2]?.isWordLike) {
|
|
123
|
-
last += 2;
|
|
124
|
-
}
|
|
125
|
-
return { start: segmentAt(segments, first).start, end: segmentAt(segments, last).end };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* The column range a double click at `column` on `line` should select, in
|
|
130
|
-
* the same column units as Pi's own `SelectionRange`. `undefined` when
|
|
131
|
-
* `column` lands past the end of the line — the caller calls through to Pi's
|
|
132
|
-
* own selection in that case rather than inventing a range.
|
|
133
|
-
*/
|
|
134
|
-
export function wordRangeAt(
|
|
135
|
-
line: string,
|
|
136
|
-
column: number,
|
|
137
|
-
): { start: number; end: number } | undefined {
|
|
138
|
-
const segments = collectWordSegments(line);
|
|
139
|
-
for (let index = 0; index < segments.length; index++) {
|
|
140
|
-
const segment = segmentAt(segments, index);
|
|
141
|
-
if (column < segment.start || column >= segment.end) continue;
|
|
142
|
-
return expandOverWordJoiners(segments, index);
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|