@linxiraos/pi-tui 1.1.7 → 1.1.9
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/CHANGELOG.md +2 -7
- package/package.json +64 -70
- package/src/autocomplete.ts +1 -1
- package/src/components/composer/band.ts +6 -2
- package/src/components/editor.ts +13 -6
- package/src/components/image.ts +31 -24
- package/src/components/markdown.ts +79 -10
- package/src/components/select-list.ts +1 -0
- package/src/components/spacer.ts +1 -1
- package/src/components/text.ts +1 -1
- package/src/deccara.ts +2 -0
- package/src/keys.ts +1 -1
- package/src/kitty-graphics.ts +9 -6
- package/src/latex-block.ts +1 -0
- package/src/stdin-buffer.ts +28 -5
- package/src/terminal-capabilities.ts +22 -21
- package/src/terminal-multiplexer.ts +24 -0
- package/src/terminal.ts +173 -85
- package/src/tui.ts +198 -29
- package/THIRD-PARTY-NOTICES.txt +0 -22909
- package/dist/types/autocomplete.d.ts +0 -127
- package/dist/types/bracketed-paste.d.ts +0 -51
- package/dist/types/components/box.d.ts +0 -33
- package/dist/types/components/cancellable-loader.d.ts +0 -23
- package/dist/types/components/composer/band.d.ts +0 -2
- package/dist/types/components/composer/borderless.d.ts +0 -8
- package/dist/types/components/composer/box.d.ts +0 -2
- package/dist/types/components/composer/claude.d.ts +0 -8
- package/dist/types/components/composer/field.d.ts +0 -3
- package/dist/types/components/composer/index.d.ts +0 -10
- package/dist/types/components/composer/pi.d.ts +0 -2
- package/dist/types/components/composer/rail.d.ts +0 -3
- package/dist/types/components/composer/registry.d.ts +0 -19
- package/dist/types/components/composer/rule.d.ts +0 -7
- package/dist/types/components/composer/types.d.ts +0 -96
- package/dist/types/components/editor.d.ts +0 -219
- package/dist/types/components/image.d.ts +0 -164
- package/dist/types/components/input.d.ts +0 -27
- package/dist/types/components/loader.d.ts +0 -33
- package/dist/types/components/markdown.d.ts +0 -100
- package/dist/types/components/scroll-view.d.ts +0 -64
- package/dist/types/components/select-list.d.ts +0 -80
- package/dist/types/components/settings-list.d.ts +0 -133
- package/dist/types/components/spacer.d.ts +0 -13
- package/dist/types/components/tab-bar.d.ts +0 -91
- package/dist/types/components/text.d.ts +0 -29
- package/dist/types/components/truncated-text.d.ts +0 -12
- package/dist/types/debug-server.d.ts +0 -27
- package/dist/types/deccara.d.ts +0 -49
- package/dist/types/desktop-notify.d.ts +0 -52
- package/dist/types/editor-component.d.ts +0 -38
- package/dist/types/fuzzy.d.ts +0 -48
- package/dist/types/index.d.ts +0 -34
- package/dist/types/keybindings.d.ts +0 -202
- package/dist/types/keys.d.ts +0 -210
- package/dist/types/kill-ring.d.ts +0 -20
- package/dist/types/kitty-graphics.d.ts +0 -76
- package/dist/types/latex-block.d.ts +0 -8
- package/dist/types/latex-to-unicode.d.ts +0 -41
- package/dist/types/loop-watchdog.d.ts +0 -49
- package/dist/types/mouse.d.ts +0 -67
- package/dist/types/stdin-buffer.d.ts +0 -60
- package/dist/types/symbols.d.ts +0 -25
- package/dist/types/terminal-capabilities.d.ts +0 -364
- package/dist/types/terminal.d.ts +0 -226
- package/dist/types/tmux.d.ts +0 -6
- package/dist/types/ttyid.d.ts +0 -9
- package/dist/types/tui.d.ts +0 -371
- package/dist/types/utils.d.ts +0 -112
package/CHANGELOG.md
CHANGED
|
@@ -2,14 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [1.1.
|
|
5
|
+
## [1.1.9] - 2026-09-05
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
## [1.1.6] - 2026-08-30
|
|
10
|
-
|
|
11
|
-
- 同步上游 OMP v18.0.10(`33cc6b9a043a`)。
|
|
12
|
-
- 同步上游 OMP v18.0.9(`cc14e04f075d`)。
|
|
7
|
+
- Sidebar gutter: provider-render frames now honor the reserved main width and paint the right gutter column (sidebar was dead code in production since the frame-provider refactor); overlay close clears the painted column.
|
|
13
8
|
|
|
14
9
|
## [1.1.3] - 2026-08-25
|
|
15
10
|
|
package/package.json
CHANGED
|
@@ -1,72 +1,66 @@
|
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"./components/*": {
|
|
67
|
-
"types": "./dist/types/components/*.d.ts",
|
|
68
|
-
"import": "./src/components/*.ts"
|
|
69
|
-
},
|
|
70
|
-
"./*.js": "./src/*.ts"
|
|
71
|
-
}
|
|
2
|
+
"exports": {
|
|
3
|
+
"./*.js": "./src/*.ts",
|
|
4
|
+
"./components/*": {
|
|
5
|
+
"import": "./src/components/*.ts",
|
|
6
|
+
"types": "./src/components/*.ts"
|
|
7
|
+
},
|
|
8
|
+
"./*": {
|
|
9
|
+
"import": "./src/*.ts",
|
|
10
|
+
"types": "./src/*.ts"
|
|
11
|
+
},
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./src/index.ts",
|
|
14
|
+
"types": "./src/index.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"bun": ">=1.3.14"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"kitty-vt-wasm": "^0.2.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@linxiraos/pi-utils": "1.1.9",
|
|
30
|
+
"@linxiraos/pi-natives": "1.1.9"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "bun test --parallel test/*.test.ts",
|
|
34
|
+
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
35
|
+
"check": "tsgo -p tsconfig.json --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"types": "./src/index.ts",
|
|
38
|
+
"main": "./src/index.ts",
|
|
39
|
+
"keywords": [
|
|
40
|
+
"tui",
|
|
41
|
+
"terminal",
|
|
42
|
+
"ui",
|
|
43
|
+
"text-editor",
|
|
44
|
+
"differential-rendering",
|
|
45
|
+
"typescript",
|
|
46
|
+
"cli"
|
|
47
|
+
],
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/Linxira-OS/linxira-zeta/issues"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"directory": "packages/tui",
|
|
53
|
+
"url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
|
|
54
|
+
"type": "git"
|
|
55
|
+
},
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"contributors": [
|
|
58
|
+
"Mario Zechner"
|
|
59
|
+
],
|
|
60
|
+
"author": "Stencil Labs, Inc.",
|
|
61
|
+
"homepage": "https://linxira-os.github.io/zeta/",
|
|
62
|
+
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
63
|
+
"version": "1.1.9",
|
|
64
|
+
"name": "@linxiraos/pi-tui",
|
|
65
|
+
"type": "module"
|
|
72
66
|
}
|
package/src/autocomplete.ts
CHANGED
|
@@ -382,7 +382,7 @@ function buildSlashCommandCompletions(
|
|
|
382
382
|
// Equal text-match scores fall back to usage frequency, then to the
|
|
383
383
|
// stable registry order.
|
|
384
384
|
.sort((a, b) => b.score - a.score || b.usage - a.usage)
|
|
385
|
-
.map(({ score:
|
|
385
|
+
.map(({ score: _score, usage: _usage, ...rest }) => rest)
|
|
386
386
|
);
|
|
387
387
|
}
|
|
388
388
|
|
|
@@ -24,9 +24,13 @@ export const bandComposerStyle: ComposerStyle = {
|
|
|
24
24
|
return 0;
|
|
25
25
|
},
|
|
26
26
|
|
|
27
|
-
renderTop(ctx: ComposerChromeContext): string
|
|
27
|
+
renderTop(ctx: ComposerChromeContext): string {
|
|
28
28
|
const { topBorder, width } = ctx;
|
|
29
|
-
|
|
29
|
+
// The band always owns one chrome row (`verticalChrome: 1`): keep it
|
|
30
|
+
// reserved while the status line has nothing to show yet — the startup
|
|
31
|
+
// prepaint mounts the editor before the session-aware status line
|
|
32
|
+
// attaches — so the band fills in later without shifting the layout.
|
|
33
|
+
if (!topBorder?.content) return "";
|
|
30
34
|
// The band builder already sizes its groups + gauge to the full width;
|
|
31
35
|
// truncation only guards against a stale provider during resize.
|
|
32
36
|
return topBorder.width > width ? truncateToWidth(topBorder.content, width) : topBorder.content;
|
package/src/components/editor.ts
CHANGED
|
@@ -467,6 +467,12 @@ export interface EditorTextAssistProvider {
|
|
|
467
467
|
type HistoryCursorAnchor = "start" | "end";
|
|
468
468
|
type AutocompleteRequest = { kind: "regular"; explicitTab: boolean } | { kind: "force" };
|
|
469
469
|
|
|
470
|
+
/** What the paste transport knew about the input burst before the editor inserts the payload. */
|
|
471
|
+
export interface PasteOptions {
|
|
472
|
+
/** A submit keypress arrived in the same input burst and will be dispatched right after the paste. */
|
|
473
|
+
submitAfterPaste?: boolean;
|
|
474
|
+
}
|
|
475
|
+
|
|
470
476
|
export class Editor implements Component, Focusable {
|
|
471
477
|
#state: EditorState = {
|
|
472
478
|
lines: [""],
|
|
@@ -581,8 +587,9 @@ export class Editor implements Component, Focusable {
|
|
|
581
587
|
* the editor inserts nothing and records no undo state, leaving insertion to the host (e.g. a
|
|
582
588
|
* "wrap in a code block / XML / attach as file" menu for very large pastes), which re-inserts
|
|
583
589
|
* via {@link insertPaste} or {@link insertText}. Return `false` (or leave unset) for the
|
|
584
|
-
* default collapse-to-marker behavior. `lineCount` is the sanitized paste's line count
|
|
585
|
-
|
|
590
|
+
* default collapse-to-marker behavior. `lineCount` is the sanitized paste's line count;
|
|
591
|
+
* `options` carries what the paste transport knew about the burst. */
|
|
592
|
+
onLargePaste?: (text: string, lineCount: number, options: PasteOptions) => boolean;
|
|
586
593
|
onAutocompleteCancel?: () => void;
|
|
587
594
|
disableSubmit: boolean = false;
|
|
588
595
|
|
|
@@ -2148,8 +2155,8 @@ export class Editor implements Component, Focusable {
|
|
|
2148
2155
|
}
|
|
2149
2156
|
|
|
2150
2157
|
/** Apply terminal paste semantics to text from non-bracketed paste transports. */
|
|
2151
|
-
pasteText(text: string): void {
|
|
2152
|
-
this.#handlePaste(text);
|
|
2158
|
+
pasteText(text: string, options: PasteOptions = {}): void {
|
|
2159
|
+
this.#handlePaste(text, options);
|
|
2153
2160
|
}
|
|
2154
2161
|
|
|
2155
2162
|
/** Insert `content` as a collapsed `[Paste #N]` marker (stored for expansion on submit via
|
|
@@ -2289,7 +2296,7 @@ export class Editor implements Component, Focusable {
|
|
|
2289
2296
|
}
|
|
2290
2297
|
}
|
|
2291
2298
|
|
|
2292
|
-
#handlePaste(pastedText: string): void {
|
|
2299
|
+
#handlePaste(pastedText: string, options: PasteOptions = {}): void {
|
|
2293
2300
|
let filteredText = this.#sanitizePastedText(pastedText);
|
|
2294
2301
|
|
|
2295
2302
|
// If pasting a file path (starts with /, ~, or .) and the character before
|
|
@@ -2311,7 +2318,7 @@ export class Editor implements Component, Focusable {
|
|
|
2311
2318
|
// Let the host intercept marker-sized pastes (e.g. the large-paste menu). When it takes
|
|
2312
2319
|
// over, the editor inserts nothing and records no undo state — the host re-inserts via
|
|
2313
2320
|
// `insertPaste`/`insertText` once the user chooses.
|
|
2314
|
-
if (isMarkerSized && this.onLargePaste?.(filteredText, pastedLines.length)) {
|
|
2321
|
+
if (isMarkerSized && this.onLargePaste?.(filteredText, pastedLines.length, options)) {
|
|
2315
2322
|
return;
|
|
2316
2323
|
}
|
|
2317
2324
|
|
package/src/components/image.ts
CHANGED
|
@@ -73,9 +73,9 @@ function nextImageIdSeed(): number {
|
|
|
73
73
|
*
|
|
74
74
|
* The budget keeps the most recent `cap` images live and demotes older ones to
|
|
75
75
|
* their text fallback. Demotion needs a full redraw (so off-screen rows are
|
|
76
|
-
* rewritten) plus an explicit graphics purge of the demoted ids
|
|
77
|
-
* reports display order via {@link observe}
|
|
78
|
-
*
|
|
76
|
+
* rewritten) plus an explicit graphics purge of the demoted ids. {@link Image}
|
|
77
|
+
* reports display order via {@link observe}; when that reveals a stricter split,
|
|
78
|
+
* the TUI repeats the pass before emitting its terminal frame.
|
|
79
79
|
*
|
|
80
80
|
* `cap <= 0` disables budgeting: every image stays a live graphic.
|
|
81
81
|
*/
|
|
@@ -87,6 +87,8 @@ export class ImageBudget {
|
|
|
87
87
|
#idToKey = new Map<number, string>();
|
|
88
88
|
/** Display-order image ids observed during the in-flight pass. */
|
|
89
89
|
#passIds: number[] = [];
|
|
90
|
+
/** Per-id suppression decision from the first observation in this pass. */
|
|
91
|
+
#passSuppression = new Map<number, boolean>();
|
|
90
92
|
/**
|
|
91
93
|
* Suppress threshold reflected in the frame currently on the terminal: images
|
|
92
94
|
* at display indices `[0, #onTerminal)` are shown as text there.
|
|
@@ -104,7 +106,7 @@ export class ImageBudget {
|
|
|
104
106
|
/** Image ids whose data is believed to be loaded in the terminal's store. */
|
|
105
107
|
#transmitted = new Set<number>();
|
|
106
108
|
/** Transmit sequences (full base64) to write once, before this frame's placements. */
|
|
107
|
-
#pendingTransmits
|
|
109
|
+
#pendingTransmits = new Map<number, string>();
|
|
108
110
|
// True while the in-flight pass is a partial/throwaway pass (the
|
|
109
111
|
// non-multiplexer resize viewport fast path) that walks only the visible
|
|
110
112
|
// tail, bottom-up. Such a pass cannot derive display order from observe()
|
|
@@ -185,6 +187,7 @@ export class ImageBudget {
|
|
|
185
187
|
*/
|
|
186
188
|
beginPass(stable = false): void {
|
|
187
189
|
this.#passIds.length = 0;
|
|
190
|
+
this.#passSuppression.clear();
|
|
188
191
|
this.#stablePass = stable;
|
|
189
192
|
this.#applyingReset = !stable && this.#cap > 0 && this.#planned > this.#onTerminal;
|
|
190
193
|
}
|
|
@@ -199,47 +202,49 @@ export class ImageBudget {
|
|
|
199
202
|
* (`#suppressedIds`) keyed by id — order- and partiality-independent.
|
|
200
203
|
*/
|
|
201
204
|
observe(imageId: number): boolean {
|
|
205
|
+
const existing = this.#passSuppression.get(imageId);
|
|
206
|
+
if (existing !== undefined) return existing;
|
|
202
207
|
if (this.#stablePass) {
|
|
203
208
|
const suppressed = this.#cap > 0 && this.#suppressedIds.has(imageId);
|
|
209
|
+
this.#passSuppression.set(imageId, suppressed);
|
|
204
210
|
if (suppressed) this.#forgetKeyForId(imageId);
|
|
205
211
|
return suppressed;
|
|
206
212
|
}
|
|
207
213
|
const index = this.#passIds.length;
|
|
208
214
|
this.#passIds.push(imageId);
|
|
209
215
|
const suppressed = this.#cap > 0 && index < this.#planned;
|
|
216
|
+
this.#passSuppression.set(imageId, suppressed);
|
|
210
217
|
if (suppressed) this.#forgetKeyForId(imageId);
|
|
211
218
|
return suppressed;
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
/**
|
|
215
|
-
* End a render pass. Returns true when
|
|
216
|
-
*
|
|
217
|
-
* {@link takePurgeIds}.
|
|
222
|
+
* End a render pass. Returns true when the pass discovered a stricter budget
|
|
223
|
+
* and must be repeated before its terminal frame is emitted.
|
|
218
224
|
*/
|
|
219
225
|
endPass(): boolean {
|
|
220
226
|
const total = this.#passIds.length;
|
|
221
227
|
this.#lastTotal = total;
|
|
222
|
-
let reset = false;
|
|
223
228
|
if (this.#applyingReset) {
|
|
224
229
|
for (let i = this.#onTerminal; i < this.#planned && i < total; i++) {
|
|
225
230
|
const id = this.#passIds[i];
|
|
226
|
-
|
|
227
|
-
//
|
|
231
|
+
// A transmit queued by a discarded discovery pass never reached
|
|
232
|
+
// the terminal, so cancel it instead of transmitting then purging.
|
|
233
|
+
if (!this.#pendingTransmits.delete(id)) this.#purgeIds.push(id);
|
|
228
234
|
this.#transmitted.delete(id);
|
|
229
235
|
this.#deletePlacementState(id);
|
|
230
236
|
this.#forgetKeyForId(id);
|
|
231
237
|
}
|
|
232
238
|
this.#onTerminal = this.#planned;
|
|
233
239
|
this.#applyingReset = false;
|
|
234
|
-
reset = true;
|
|
235
240
|
}
|
|
236
|
-
this.#reconcile(total);
|
|
241
|
+
const retry = this.#reconcile(total);
|
|
237
242
|
// Snapshot the committed display-order suppression by id: the prefix
|
|
238
243
|
// [0, #onTerminal) is what the terminal currently shows as text. Partial
|
|
239
244
|
// passes replay this per id (see #stablePass) instead of re-deriving it
|
|
240
245
|
// from a reversed, tail-only walk.
|
|
241
246
|
this.#suppressedIds = new Set(this.#passIds.slice(0, this.#onTerminal));
|
|
242
|
-
return
|
|
247
|
+
return retry;
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
/** Image ids to delete from the terminal this frame; clears the pending set. */
|
|
@@ -256,7 +261,7 @@ export class ImageBudget {
|
|
|
256
261
|
const ids = [...this.#transmitted];
|
|
257
262
|
this.#transmitted.clear();
|
|
258
263
|
this.#purgeIds = [];
|
|
259
|
-
this.#pendingTransmits
|
|
264
|
+
this.#pendingTransmits.clear();
|
|
260
265
|
this.#keyToId.clear();
|
|
261
266
|
this.#idToKey.clear();
|
|
262
267
|
this.#placementState.clear();
|
|
@@ -393,12 +398,12 @@ export class ImageBudget {
|
|
|
393
398
|
enqueueTransmit(imageId: number, sequence: string): void {
|
|
394
399
|
if (this.#transmitted.has(imageId)) return;
|
|
395
400
|
this.#transmitted.add(imageId);
|
|
396
|
-
this.#pendingTransmits.
|
|
401
|
+
this.#pendingTransmits.set(imageId, sequence);
|
|
397
402
|
}
|
|
398
403
|
|
|
399
404
|
/** Whether a frame has image data queued but not yet written to the terminal. */
|
|
400
405
|
hasPendingTransmits(): boolean {
|
|
401
|
-
return this.#pendingTransmits.
|
|
406
|
+
return this.#pendingTransmits.size > 0;
|
|
402
407
|
}
|
|
403
408
|
|
|
404
409
|
/**
|
|
@@ -410,7 +415,7 @@ export class ImageBudget {
|
|
|
410
415
|
get quiescent(): boolean {
|
|
411
416
|
return (
|
|
412
417
|
this.#lastTotal === 0 &&
|
|
413
|
-
this.#pendingTransmits.
|
|
418
|
+
this.#pendingTransmits.size === 0 &&
|
|
414
419
|
this.#purgeIds.length === 0 &&
|
|
415
420
|
this.#planned === this.#onTerminal
|
|
416
421
|
);
|
|
@@ -418,9 +423,9 @@ export class ImageBudget {
|
|
|
418
423
|
|
|
419
424
|
/** Transmit sequences to write before this frame's placements; clears the queue. */
|
|
420
425
|
takeTransmits(): readonly string[] {
|
|
421
|
-
if (this.#pendingTransmits.
|
|
422
|
-
const sequences = this.#pendingTransmits;
|
|
423
|
-
this.#pendingTransmits
|
|
426
|
+
if (this.#pendingTransmits.size === 0) return EMPTY_TRANSMITS;
|
|
427
|
+
const sequences = [...this.#pendingTransmits.values()];
|
|
428
|
+
this.#pendingTransmits.clear();
|
|
424
429
|
return sequences;
|
|
425
430
|
}
|
|
426
431
|
|
|
@@ -433,9 +438,9 @@ export class ImageBudget {
|
|
|
433
438
|
* re-emit together; keeps no base64 in budget state (the transmit-once design).
|
|
434
439
|
*/
|
|
435
440
|
forgetTransmitted(): void {
|
|
436
|
-
if (this.#transmitted.size === 0 && this.#pendingTransmits.
|
|
441
|
+
if (this.#transmitted.size === 0 && this.#pendingTransmits.size === 0) return;
|
|
437
442
|
this.#transmitted.clear();
|
|
438
|
-
this.#pendingTransmits
|
|
443
|
+
this.#pendingTransmits.clear();
|
|
439
444
|
}
|
|
440
445
|
|
|
441
446
|
#forgetKeyForId(id: number): void {
|
|
@@ -445,15 +450,16 @@ export class ImageBudget {
|
|
|
445
450
|
if (this.#keyToId.get(key) === id) this.#keyToId.delete(key);
|
|
446
451
|
}
|
|
447
452
|
|
|
448
|
-
#reconcile(total: number):
|
|
453
|
+
#reconcile(total: number): boolean {
|
|
449
454
|
const desired = this.#cap > 0 ? Math.max(0, total - this.#cap) : 0;
|
|
450
455
|
if (desired === this.#planned) {
|
|
451
456
|
// Budget relaxed without a stricter frame (cap raised or images
|
|
452
457
|
// removed): surviving graphics are untouched and re-exposed rows
|
|
453
458
|
// repaint normally, so just track the looser threshold.
|
|
454
459
|
if (this.#planned < this.#onTerminal) this.#onTerminal = this.#planned;
|
|
455
|
-
return;
|
|
460
|
+
return false;
|
|
456
461
|
}
|
|
462
|
+
const retry = desired > this.#onTerminal;
|
|
457
463
|
this.#planned = desired;
|
|
458
464
|
// More images must be demoted than the terminal shows: schedule the purge +
|
|
459
465
|
// full-redraw frame. Fewer: no ghosts to clear, so just catch the tracking
|
|
@@ -461,6 +467,7 @@ export class ImageBudget {
|
|
|
461
467
|
// render is needed to apply the new threshold.
|
|
462
468
|
if (desired <= this.#onTerminal) this.#onTerminal = desired;
|
|
463
469
|
this.#requestRender();
|
|
470
|
+
return retry;
|
|
464
471
|
}
|
|
465
472
|
}
|
|
466
473
|
|
|
@@ -137,7 +137,7 @@ function normalizeHtmlEntitiesForTerminal(raw: string): string {
|
|
|
137
137
|
if (Number.isFinite(value) && value >= 0 && value <= 0x10ffff) {
|
|
138
138
|
try {
|
|
139
139
|
return String.fromCodePoint(value);
|
|
140
|
-
} catch
|
|
140
|
+
} catch {
|
|
141
141
|
// Fallback to empty string or original if invalid codepoint
|
|
142
142
|
}
|
|
143
143
|
}
|
|
@@ -261,7 +261,7 @@ function normalizeHtmlForTerminal(
|
|
|
261
261
|
}
|
|
262
262
|
lastIndex = index + tag.length;
|
|
263
263
|
|
|
264
|
-
const isClosing =
|
|
264
|
+
const isClosing = tag.startsWith("</");
|
|
265
265
|
const isSelfClosing = /\/\s*>$/.test(tag);
|
|
266
266
|
|
|
267
267
|
switch (name) {
|
|
@@ -794,7 +794,7 @@ export function urlTokenPossible(src: string): boolean {
|
|
|
794
794
|
}
|
|
795
795
|
if (i === 0) return false;
|
|
796
796
|
if (i >= URL_GATE_EMAIL_SCAN_LIMIT) return true; // over-long run: give up conservatively
|
|
797
|
-
return src.charCodeAt(i) === 64 /* @
|
|
797
|
+
return src.charCodeAt(i) === 64; /* @ */
|
|
798
798
|
}
|
|
799
799
|
|
|
800
800
|
// Setext-underline pre-gate for marked's `lheading` rule. The rule's lazy body
|
|
@@ -1131,7 +1131,7 @@ function listMayContinueAt(text: string, tailStart: number, listRaw: string): bo
|
|
|
1131
1131
|
// bare newline, or end-of-input (which appends can still extend).
|
|
1132
1132
|
if (i >= n) return true;
|
|
1133
1133
|
const after = text.charCodeAt(i);
|
|
1134
|
-
return after === 0x20 /* space */ || after === 0x09 /* tab */ || after === 0x0a /* \n
|
|
1134
|
+
return after === 0x20 /* space */ || after === 0x09 /* tab */ || after === 0x0a; /* \n */
|
|
1135
1135
|
}
|
|
1136
1136
|
|
|
1137
1137
|
const NO_BLOCK_BOUNDARY = { end: 0, count: 0 } as const;
|
|
@@ -1254,6 +1254,52 @@ function lexDocument(text: string): Token[] {
|
|
|
1254
1254
|
return lexWindowed(text);
|
|
1255
1255
|
}
|
|
1256
1256
|
|
|
1257
|
+
/** A hyperlink as the renderer sees it: inline `[text](href)`, `<autolink>`, bare GFM URL, or reference link. */
|
|
1258
|
+
export interface MarkdownLink {
|
|
1259
|
+
/** Visible link text (equals `href` for autolinks and bare URLs). */
|
|
1260
|
+
text: string;
|
|
1261
|
+
/** Destination exactly as marked resolved it (references resolved, no normalization). */
|
|
1262
|
+
href: string;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* Every link token in `text`, in document order, from the same configured
|
|
1267
|
+
* lexer the renderer uses — so fenced code, code spans, escapes, reference
|
|
1268
|
+
* definitions and the GFM autolink rules agree with what is drawn on screen.
|
|
1269
|
+
* Duplicate hrefs are kept; callers decide how to fold them.
|
|
1270
|
+
*/
|
|
1271
|
+
export function extractMarkdownLinks(text: string): MarkdownLink[] {
|
|
1272
|
+
const links: MarkdownLink[] = [];
|
|
1273
|
+
const walk = (tokens: readonly Token[] | undefined): void => {
|
|
1274
|
+
if (!tokens) return;
|
|
1275
|
+
for (const token of tokens) {
|
|
1276
|
+
if (token.type === "link") {
|
|
1277
|
+
const link = token as Tokens.Link;
|
|
1278
|
+
if (typeof link.href === "string" && link.href.length > 0) {
|
|
1279
|
+
links.push({
|
|
1280
|
+
text: typeof link.text === "string" && link.text.length > 0 ? link.text : link.href,
|
|
1281
|
+
href: link.href,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
// Containers: paragraphs, emphasis, lists, blockquotes, table cells.
|
|
1287
|
+
const any = token as {
|
|
1288
|
+
tokens?: Token[];
|
|
1289
|
+
items?: Token[];
|
|
1290
|
+
header?: Array<{ tokens?: Token[] }>;
|
|
1291
|
+
rows?: Array<Array<{ tokens?: Token[] }>>;
|
|
1292
|
+
};
|
|
1293
|
+
walk(any.tokens);
|
|
1294
|
+
walk(any.items);
|
|
1295
|
+
if (any.header) for (const cell of any.header) walk(cell.tokens);
|
|
1296
|
+
if (any.rows) for (const row of any.rows) for (const cell of row) walk(cell.tokens);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
walk(lexDocument(text));
|
|
1300
|
+
return links;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1257
1303
|
/** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
|
|
1258
1304
|
export function clearRenderCache(): void {
|
|
1259
1305
|
renderCache.clear();
|
|
@@ -1300,6 +1346,15 @@ export interface HighlightStreamSession {
|
|
|
1300
1346
|
push(chunk: string): string;
|
|
1301
1347
|
}
|
|
1302
1348
|
|
|
1349
|
+
/** Collect distinct hyperlink destinations using the renderer's Markdown grammar, excluding images and code. */
|
|
1350
|
+
export function getMarkdownLinkUrls(text: string): string[] {
|
|
1351
|
+
const urls = new Set<string>();
|
|
1352
|
+
markdownParser.walkTokens(markdownParser.lexer(text), token => {
|
|
1353
|
+
if (token.type === "link" && typeof token.href === "string") urls.add(token.href);
|
|
1354
|
+
});
|
|
1355
|
+
return [...urls];
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1303
1358
|
/**
|
|
1304
1359
|
* Theme functions for markdown elements.
|
|
1305
1360
|
* Each function takes text and returns styled text with ANSI codes.
|
|
@@ -1308,6 +1363,8 @@ export interface MarkdownTheme {
|
|
|
1308
1363
|
heading: (text: string) => string;
|
|
1309
1364
|
link: (text: string) => string;
|
|
1310
1365
|
linkUrl: (text: string) => string;
|
|
1366
|
+
/** Resolve the OSC 8 destination without changing visible text; undefined preserves the authored URL. */
|
|
1367
|
+
resolveLink?: (href: string) => string | undefined;
|
|
1311
1368
|
code: (text: string) => string;
|
|
1312
1369
|
codeBlock: (text: string) => string;
|
|
1313
1370
|
codeBlockBorder: (text: string) => string;
|
|
@@ -2355,8 +2412,11 @@ export class Markdown implements Component {
|
|
|
2355
2412
|
}
|
|
2356
2413
|
|
|
2357
2414
|
const recorder: TailRenderRecorder = {
|
|
2415
|
+
// [suppressed] render-cache length preallocation
|
|
2358
2416
|
rows: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2417
|
+
// [suppressed] render-cache length preallocation
|
|
2359
2418
|
raws: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2419
|
+
// [suppressed] render-cache length preallocation
|
|
2360
2420
|
nextTypes: new Array(tokens.length - spliceEnd).fill(undefined),
|
|
2361
2421
|
};
|
|
2362
2422
|
const fresh = this.#renderContentLines(tokens, spliceEnd, tokens.length, contentWidth, signature, recorder);
|
|
@@ -2369,8 +2429,11 @@ export class Markdown implements Component {
|
|
|
2369
2429
|
// `start`), so a mostly-frozen document allocates only for the
|
|
2370
2430
|
// unfrozen tail instead of the whole token list every frame.
|
|
2371
2431
|
const tailCount = tokens.length - start;
|
|
2432
|
+
// [suppressed] render-cache length preallocation
|
|
2372
2433
|
const rows: (readonly string[] | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2434
|
+
// [suppressed] render-cache length preallocation
|
|
2373
2435
|
const raws: (string | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2436
|
+
// [suppressed] render-cache length preallocation
|
|
2374
2437
|
const nextTypes: (string | undefined)[] = new Array(tailCount).fill(undefined);
|
|
2375
2438
|
if (cache !== undefined && cache.tokenStart === start) {
|
|
2376
2439
|
for (let i = start; i < Math.min(cache.cachedThrough, spliceEnd); i++) {
|
|
@@ -3155,17 +3218,22 @@ export class Markdown implements Component {
|
|
|
3155
3218
|
markHtmlItemWhenContent(token.text);
|
|
3156
3219
|
const linkText = this.#renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
3157
3220
|
const styledLinkText = this.#theme.link(this.#theme.underline(linkText));
|
|
3158
|
-
const
|
|
3159
|
-
|
|
3221
|
+
const href = typeof token.href === "string" ? token.href : "";
|
|
3222
|
+
const target = (href && this.#theme.resolveLink?.(href)) || href;
|
|
3223
|
+
const clickableLinkText = formatHyperlink(styledLinkText, target);
|
|
3224
|
+
// If link text matches href, only show the link once. A missing
|
|
3225
|
+
// href (malformed/partial link token) renders as plain link text
|
|
3226
|
+
// instead of crashing the renderer or emitting an empty "()"
|
|
3227
|
+
// (issue #10283).
|
|
3160
3228
|
// Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes
|
|
3161
3229
|
// For mailto: links, strip the prefix before comparing (autolinked emails have
|
|
3162
3230
|
// text="foo@bar.com" but href="mailto:foo@bar.com")
|
|
3163
|
-
const hrefForComparison =
|
|
3164
|
-
if (token.text ===
|
|
3231
|
+
const hrefForComparison = href.startsWith("mailto:") ? href.slice(7) : href;
|
|
3232
|
+
if (!href || token.text === href || token.text === hrefForComparison)
|
|
3165
3233
|
result += clickableLinkText + stylePrefix;
|
|
3166
3234
|
else {
|
|
3167
|
-
const styledLinkUrl = this.#theme.linkUrl(`(${
|
|
3168
|
-
result += `${clickableLinkText} ${formatHyperlink(styledLinkUrl,
|
|
3235
|
+
const styledLinkUrl = this.#theme.linkUrl(`(${href})`);
|
|
3236
|
+
result += `${clickableLinkText} ${formatHyperlink(styledLinkUrl, target)}${stylePrefix}`;
|
|
3169
3237
|
}
|
|
3170
3238
|
break;
|
|
3171
3239
|
}
|
|
@@ -3464,6 +3532,7 @@ export class Markdown implements Component {
|
|
|
3464
3532
|
let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
3465
3533
|
|
|
3466
3534
|
if (minCellsWidth > availableForCells) {
|
|
3535
|
+
// [suppressed] column-width allocation
|
|
3467
3536
|
minColumnWidths = new Array(numCols).fill(1);
|
|
3468
3537
|
const remaining = availableForCells - numCols;
|
|
3469
3538
|
|
|
@@ -207,6 +207,7 @@ export class SelectList implements Component, MouseRoutable {
|
|
|
207
207
|
// every count is 1, so visualTotal == #filteredItems and overflow falls
|
|
208
208
|
// back to the original `N > maxVisible` predicate exactly.
|
|
209
209
|
const conservativeRowWidth = Math.max(0, width - 1);
|
|
210
|
+
// [suppressed] length preallocation
|
|
210
211
|
const rowCounts = new Array<number>(this.#filteredItems.length);
|
|
211
212
|
let visualTotal = 0;
|
|
212
213
|
for (let i = 0; i < this.#filteredItems.length; i++) {
|
package/src/components/spacer.ts
CHANGED
|
@@ -20,7 +20,6 @@ export class Spacer implements Component {
|
|
|
20
20
|
this.#lines = lines;
|
|
21
21
|
this.#cached = undefined;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
23
|
invalidate(): void {
|
|
25
24
|
// No cached state to invalidate currently
|
|
26
25
|
}
|
|
@@ -28,6 +27,7 @@ export class Spacer implements Component {
|
|
|
28
27
|
render(_width: number): readonly string[] {
|
|
29
28
|
let cached = this.#cached;
|
|
30
29
|
if (cached === undefined) {
|
|
30
|
+
// [suppressed] cached line allocation
|
|
31
31
|
cached = new Array(this.#lines).fill("");
|
|
32
32
|
this.#cached = cached;
|
|
33
33
|
}
|
package/src/components/text.ts
CHANGED
|
@@ -168,7 +168,7 @@ export class Text implements Component {
|
|
|
168
168
|
|
|
169
169
|
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
|
170
170
|
if (resultWidths !== undefined) {
|
|
171
|
-
//
|
|
171
|
+
// [suppressed] line-width allocation
|
|
172
172
|
const emptyWidths = new Array<number>(emptyLines.length).fill(width);
|
|
173
173
|
publishLineWidths(result, [...emptyWidths, ...resultWidths, ...emptyWidths]);
|
|
174
174
|
}
|
package/src/deccara.ts
CHANGED
|
@@ -239,7 +239,9 @@ export interface DeccaraPlan {
|
|
|
239
239
|
*/
|
|
240
240
|
export function planDeccaraFills(lines: string[], width: number, firstScreenRow = 0): DeccaraPlan {
|
|
241
241
|
const n = lines.length;
|
|
242
|
+
// [suppressed] length preallocation
|
|
242
243
|
const texts: string[] = new Array(n);
|
|
244
|
+
// [suppressed] length preallocation
|
|
243
245
|
const candidates: (FillCandidate | null)[] = new Array(n);
|
|
244
246
|
|
|
245
247
|
for (let k = 0; k < n; k++) {
|
package/src/keys.ts
CHANGED
|
@@ -198,7 +198,7 @@ export type KeyId = BaseKey | ModifiedKeyId<BaseKey>;
|
|
|
198
198
|
* modifier methods return precisely-typed concatenations (e.g. `Key.ctrl("c")`
|
|
199
199
|
* is `"ctrl+c"`, not just `string`). This mirrors the upstream
|
|
200
200
|
* `@mariozechner/pi-tui` `Key` export verbatim so plugins built against any
|
|
201
|
-
* scope alias (`@mariozechner`, `@earendil-works`, `@
|
|
201
|
+
* scope alias (`@mariozechner`, `@earendil-works`, `@linxiraos`) keep working
|
|
202
202
|
* once the specifier shim remaps them to this package.
|
|
203
203
|
*/
|
|
204
204
|
export const Key = {
|