@linxiraos/pi-tui 1.0.4 → 1.0.6
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 +18 -5
- package/dist/types/autocomplete.d.ts +116 -0
- package/dist/types/bracketed-paste.d.ts +51 -0
- package/dist/types/components/box.d.ts +31 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +166 -0
- package/dist/types/components/image.d.ts +165 -0
- package/dist/types/components/input.d.ts +25 -0
- package/dist/types/components/loader.d.ts +25 -0
- package/dist/types/components/markdown.d.ts +92 -0
- package/dist/types/components/scroll-view.d.ts +62 -0
- package/dist/types/components/select-list.d.ts +69 -0
- package/dist/types/components/settings-list.d.ts +123 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +89 -0
- package/dist/types/components/text.d.ts +28 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/deccara.d.ts +49 -0
- package/dist/types/desktop-notify.d.ts +52 -0
- package/dist/types/editor-component.d.ts +38 -0
- package/dist/types/fuzzy.d.ts +48 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/keybindings.d.ts +197 -0
- package/dist/types/keys.d.ts +210 -0
- package/dist/types/kill-ring.d.ts +20 -0
- package/dist/types/kitty-graphics.d.ts +76 -0
- package/dist/types/latex-block.d.ts +8 -0
- package/dist/types/latex-to-unicode.d.ts +50 -0
- package/dist/types/loop-watchdog.d.ts +49 -0
- package/dist/types/mouse.d.ts +67 -0
- package/dist/types/stdin-buffer.d.ts +60 -0
- package/dist/types/symbols.d.ts +25 -0
- package/dist/types/terminal-capabilities.d.ts +324 -0
- package/dist/types/terminal.d.ts +175 -0
- package/dist/types/tmux.d.ts +6 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +480 -0
- package/dist/types/utils.d.ts +112 -0
- package/package.json +70 -69
- package/src/keys.ts +1 -1
- package/src/loop-watchdog.ts +32 -6
- package/src/terminal.ts +15 -8
- package/src/tui.ts +70 -16
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Ellipsis, type ExtractSegmentsResult, type SliceResult } from "@linxiraos/pi-natives";
|
|
2
|
+
export { Ellipsis } from "@linxiraos/pi-natives";
|
|
3
|
+
export { DEFAULT_TAB_WIDTH } from "@linxiraos/pi-utils";
|
|
4
|
+
export type HangulCompatibilityJamoWidth = "platform" | "unicode" | 1 | 2;
|
|
5
|
+
export declare function getHangulCompatibilityJamoWidth(): HangulCompatibilityJamoWidth;
|
|
6
|
+
export declare function getWidthConfigEpoch(): number;
|
|
7
|
+
/** Publish exact per-line visible widths for a rendered lines array. */
|
|
8
|
+
export declare function publishLineWidths(lines: readonly string[], widths: readonly number[]): void;
|
|
9
|
+
/** Exact per-line visible widths for an unchanged `lines` array under the current width config. */
|
|
10
|
+
export declare function getPublishedLineWidths(lines: readonly string[]): readonly number[] | undefined;
|
|
11
|
+
export declare function setHangulCompatibilityJamoWidth(width: HangulCompatibilityJamoWidth): boolean;
|
|
12
|
+
export declare function resetHangulCompatibilityJamoWidthForTests(): void;
|
|
13
|
+
export type TextSizingScale = 1 | 2 | 3;
|
|
14
|
+
export type TextSizingVerticalAlign = "top" | "bottom" | "center";
|
|
15
|
+
export type TextSizingHorizontalAlign = "left" | "right" | "center";
|
|
16
|
+
export interface TextSizingOptions {
|
|
17
|
+
scale?: TextSizingScale;
|
|
18
|
+
widthCells?: number;
|
|
19
|
+
verticalAlign?: TextSizingVerticalAlign;
|
|
20
|
+
horizontalAlign?: TextSizingHorizontalAlign;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Encode a plain-text span using Kitty's OSC 66 text-sizing protocol. The TUI
|
|
24
|
+
* emits only safe UTF-8 payloads and ST terminators so its ANSI parser and the
|
|
25
|
+
* terminal agree on span boundaries.
|
|
26
|
+
*/
|
|
27
|
+
export declare function encodeTextSized(text: string, options?: TextSizingOptions): string;
|
|
28
|
+
export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean | null): SliceResult;
|
|
29
|
+
export declare function truncateToWidth(text: string, maxWidth: number, ellipsisKind?: Ellipsis | null | "", pad?: boolean | null): string;
|
|
30
|
+
export declare function wrapTextWithAnsi(text: string, width: number): string[];
|
|
31
|
+
export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter: boolean): ExtractSegmentsResult;
|
|
32
|
+
export declare function replaceTabs(text: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Returns a string of n spaces. Uses a pre-allocated buffer for efficiency.
|
|
35
|
+
*/
|
|
36
|
+
export declare function padding(n: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Get the shared grapheme segmenter instance.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getSegmenter(): Intl.Segmenter;
|
|
41
|
+
/**
|
|
42
|
+
* Visible width of a string in terminal columns, excluding ANSI/OSC escapes.
|
|
43
|
+
*
|
|
44
|
+
* `Bun.stringWidth` does the heavy lifting (UAX#11 width tables + ANSI/OSC
|
|
45
|
+
* stripping); this adds the two corrections it omits — tabs (expanded to
|
|
46
|
+
* `tabWidth` cells) and OSC 66 text-sizing payloads (scaled by `s=`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function visibleWidth(str: string): number;
|
|
49
|
+
/**
|
|
50
|
+
* True when a row carries a Kitty OSC 66 text-sizing span (`\x1b]66;…`).
|
|
51
|
+
* Scaled spans must bypass wrapping/padding and, when scaled up, reserve the
|
|
52
|
+
* terminal rows their multicell glyphs flow into.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isOsc66Line(line: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Largest `s=` scale among the OSC 66 spans in a line (1 when none is scaled).
|
|
57
|
+
* A scale-`s` heading occupies `s` terminal rows, so the `s - 1` blank rows
|
|
58
|
+
* beneath it are the glyph's lower half and must never be erased or overdrawn.
|
|
59
|
+
*/
|
|
60
|
+
export declare function osc66MaxScale(line: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* Normalize text for terminal output without changing logical editor content.
|
|
63
|
+
* Some terminals render precomposed Thai/Lao AM vowels inconsistently during
|
|
64
|
+
* differential repaint. Their compatibility decompositions have the same cell
|
|
65
|
+
* width but avoid stale-cell artifacts in terminal renderers.
|
|
66
|
+
*/
|
|
67
|
+
export declare function normalizeTerminalOutput(str: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a character is whitespace.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isWhitespaceChar(char: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Check if a character is punctuation.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isPunctuationChar(char: string): boolean;
|
|
76
|
+
export type WordNavKind = "whitespace" | "delimiter" | "cjk" | "word" | "other";
|
|
77
|
+
/**
|
|
78
|
+
* Coarse Unicode-aware character classification for word navigation (Option/Alt + Left/Right).
|
|
79
|
+
* This intentionally avoids language-specific word segmentation for predictability across scripts.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getWordNavKind(grapheme: string): WordNavKind;
|
|
82
|
+
export declare function isWordNavJoiner(grapheme: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Move the cursor one "word" to the left using Unicode-aware coarse navigation.
|
|
85
|
+
*
|
|
86
|
+
* Returns a new cursor index in the range [0, text.length].
|
|
87
|
+
*/
|
|
88
|
+
export declare function moveWordLeft(text: string, cursor: number): number;
|
|
89
|
+
/**
|
|
90
|
+
* Move the cursor one "word" to the right using Unicode-aware coarse navigation.
|
|
91
|
+
*
|
|
92
|
+
* Returns a new cursor index in the range [0, text.length].
|
|
93
|
+
*/
|
|
94
|
+
export declare function moveWordRight(text: string, cursor: number): number;
|
|
95
|
+
/**
|
|
96
|
+
* Apply background color to a line, padding to full width.
|
|
97
|
+
*
|
|
98
|
+
* @param line - Line of text (may contain ANSI codes)
|
|
99
|
+
* @param width - Total width to pad to
|
|
100
|
+
* @param bgFn - Background color function
|
|
101
|
+
* @returns Line with background applied and padded to width
|
|
102
|
+
*/
|
|
103
|
+
export declare function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
|
|
106
|
+
*
|
|
107
|
+
* @param strict - If true, exclude wide chars at boundary that would extend past the range
|
|
108
|
+
*/
|
|
109
|
+
export declare function sliceByColumn(line: string, startCol: number, length: number, strict?: boolean): string;
|
|
110
|
+
export declare function setTuiTight(tight: boolean): void;
|
|
111
|
+
export declare function isTuiTight(): boolean;
|
|
112
|
+
export declare function getPaddingX(basePadding: number): number;
|
package/package.json
CHANGED
|
@@ -1,69 +1,70 @@
|
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@linxiraos/pi-tui",
|
|
4
|
+
"version": "1.0.6",
|
|
5
|
+
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
|
+
"homepage": "https://linxira-os.github.io",
|
|
7
|
+
"author": "Can Boluk",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"Mario Zechner"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
|
|
15
|
+
"directory": "packages/tui"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/can1357/oh-my-pi/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"tui",
|
|
22
|
+
"terminal",
|
|
23
|
+
"ui",
|
|
24
|
+
"text-editor",
|
|
25
|
+
"differential-rendering",
|
|
26
|
+
"typescript",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"main": "./src/index.ts",
|
|
30
|
+
"types": "./dist/types/index.d.ts",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"check": "biome check . && bun run check:types",
|
|
33
|
+
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
34
|
+
"lint": "biome lint .",
|
|
35
|
+
"test": "bun test --parallel test/*.test.ts",
|
|
36
|
+
"fix": "biome check --write --unsafe .",
|
|
37
|
+
"fmt": "biome format --write ."
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@linxiraos/pi-natives": "1.0.2",
|
|
41
|
+
"@linxiraos/pi-utils": "1.0.6"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"ghostty-web": "^0.4.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"bun": ">=1.3.14"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src",
|
|
51
|
+
"README.md",
|
|
52
|
+
"CHANGELOG.md",
|
|
53
|
+
"dist/types"
|
|
54
|
+
],
|
|
55
|
+
"exports": {
|
|
56
|
+
".": {
|
|
57
|
+
"types": "./dist/types/index.d.ts",
|
|
58
|
+
"import": "./src/index.ts"
|
|
59
|
+
},
|
|
60
|
+
"./*": {
|
|
61
|
+
"types": "./dist/types/*.d.ts",
|
|
62
|
+
"import": "./src/*.ts"
|
|
63
|
+
},
|
|
64
|
+
"./components/*": {
|
|
65
|
+
"types": "./dist/types/components/*.d.ts",
|
|
66
|
+
"import": "./src/components/*.ts"
|
|
67
|
+
},
|
|
68
|
+
"./*.js": "./src/*.ts"
|
|
69
|
+
}
|
|
70
|
+
}
|
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 = {
|
package/src/loop-watchdog.ts
CHANGED
|
@@ -6,10 +6,12 @@ export interface LoopWatchdogOptions {
|
|
|
6
6
|
intervalMs?: number;
|
|
7
7
|
/** A tick later than this past its deadline counts as a block. Default 250. */
|
|
8
8
|
thresholdMs?: number;
|
|
9
|
-
/** Overshoot beyond this
|
|
9
|
+
/** Overshoot beyond this is suppressed only when the process burned negligible CPU. Default 60_000. */
|
|
10
10
|
sleepMs?: number;
|
|
11
11
|
/** Monotonic clock source; injectable for tests. Default `performance.now`. */
|
|
12
12
|
now?: () => number;
|
|
13
|
+
/** Process CPU time in ms; injectable for tests. Default `process.cpuUsage`. */
|
|
14
|
+
cpuNow?: () => number;
|
|
13
15
|
/** Timer source; injectable for tests. Default `setTimeout`. */
|
|
14
16
|
schedule?: (cb: () => void, ms: number) => LoopWatchdogTimer;
|
|
15
17
|
}
|
|
@@ -23,6 +25,15 @@ interface LoopWatchdogTimer {
|
|
|
23
25
|
cancel?(): void;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Fraction of a missed interval that may be process CPU time while the gap is
|
|
30
|
+
* still treated as system sleep. Keep this near zero: cgroup throttling and
|
|
31
|
+
* scheduler contention can make a CPU-bound loop consume far less CPU than wall
|
|
32
|
+
* time. One percent allows a little measurement/background jitter while erring
|
|
33
|
+
* toward reporting a severe stall instead of hiding it.
|
|
34
|
+
*/
|
|
35
|
+
const CPU_BUSY_RATIO = 0.01;
|
|
36
|
+
|
|
26
37
|
/**
|
|
27
38
|
* Always-on event-loop lag probe. Each tick is scheduled `intervalMs` ahead of
|
|
28
39
|
* a recorded deadline; a tick that fires `thresholdMs` past its deadline means
|
|
@@ -35,18 +46,23 @@ interface LoopWatchdogTimer {
|
|
|
35
46
|
* The handle is `unref`'d so the probe never keeps the process alive, and stop()
|
|
36
47
|
* cancels the armed timer when the handle exposes `cancel` (the default
|
|
37
48
|
* `setTimeout` handle does, via `clearTimeout`). The `#generation` guard remains
|
|
38
|
-
* as a fallback for injected handles that cannot cancel.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
49
|
+
* as a fallback for injected handles that cannot cancel.
|
|
50
|
+
*
|
|
51
|
+
* A long overshoot is classified by CPU time rather than by duration. System
|
|
52
|
+
* sleep and a CPU-bound wedge both produce an arbitrarily large gap, so duration
|
|
53
|
+
* alone cannot tell them apart, and suppressing on duration discards exactly the
|
|
54
|
+
* worst stalls. Only a gap the process spent negligible CPU on is treated as
|
|
55
|
+
* sleep. CPU accounting is process-wide, so worker activity errs toward logging.
|
|
42
56
|
*/
|
|
43
57
|
export class LoopWatchdog {
|
|
44
58
|
#intervalMs: number;
|
|
45
59
|
#thresholdMs: number;
|
|
46
60
|
#sleepMs: number;
|
|
47
61
|
#now: () => number;
|
|
62
|
+
#cpuNow: () => number;
|
|
48
63
|
#schedule: (cb: () => void, ms: number) => LoopWatchdogTimer;
|
|
49
64
|
#expected = 0;
|
|
65
|
+
#expectedCpu = 0;
|
|
50
66
|
#wasBlocked = false;
|
|
51
67
|
#running = false;
|
|
52
68
|
// Bumped by stop(); each scheduled tick captures the generation it was armed
|
|
@@ -60,6 +76,12 @@ export class LoopWatchdog {
|
|
|
60
76
|
this.#thresholdMs = options.thresholdMs ?? 250;
|
|
61
77
|
this.#sleepMs = options.sleepMs ?? 60_000;
|
|
62
78
|
this.#now = options.now ?? (() => performance.now());
|
|
79
|
+
this.#cpuNow =
|
|
80
|
+
options.cpuNow ??
|
|
81
|
+
(() => {
|
|
82
|
+
const usage = process.cpuUsage();
|
|
83
|
+
return (usage.user + usage.system) / 1000;
|
|
84
|
+
});
|
|
63
85
|
this.#schedule =
|
|
64
86
|
options.schedule ??
|
|
65
87
|
((cb, ms) => {
|
|
@@ -86,6 +108,7 @@ export class LoopWatchdog {
|
|
|
86
108
|
#armTick(): void {
|
|
87
109
|
const generation = this.#generation;
|
|
88
110
|
this.#expected = this.#now() + this.#intervalMs;
|
|
111
|
+
this.#expectedCpu = this.#cpuNow();
|
|
89
112
|
this.#handle = this.#schedule(() => this.#tick(generation), this.#intervalMs);
|
|
90
113
|
this.#handle.unref?.();
|
|
91
114
|
}
|
|
@@ -93,17 +116,20 @@ export class LoopWatchdog {
|
|
|
93
116
|
#tick(generation: number): void {
|
|
94
117
|
if (!this.#running || generation !== this.#generation) return;
|
|
95
118
|
const blockedMs = this.#now() - this.#expected;
|
|
119
|
+
const cpuMs = this.#cpuNow() - this.#expectedCpu;
|
|
96
120
|
// Consume the recent phase every tick (block or not) so attribution is
|
|
97
121
|
// scoped to the just-elapsed interval and never carries a stale phase
|
|
98
122
|
// forward to a later, phase-less block.
|
|
99
123
|
const phase = takeRecentLoopPhase();
|
|
100
124
|
if (blockedMs > this.#thresholdMs) {
|
|
101
|
-
if (blockedMs > this.#sleepMs) {
|
|
125
|
+
if (blockedMs > this.#sleepMs && cpuMs < blockedMs * CPU_BUSY_RATIO) {
|
|
126
|
+
// A long gap the process did not spend CPU on: it was suspended.
|
|
102
127
|
this.#wasBlocked = false;
|
|
103
128
|
} else if (!this.#wasBlocked) {
|
|
104
129
|
this.#wasBlocked = true;
|
|
105
130
|
logger.warn("ui.loop-blocked", {
|
|
106
131
|
blockedMs: Math.round(blockedMs),
|
|
132
|
+
cpuMs: Math.round(cpuMs),
|
|
107
133
|
phase: phase ?? "unknown",
|
|
108
134
|
});
|
|
109
135
|
}
|
package/src/terminal.ts
CHANGED
|
@@ -945,10 +945,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
945
945
|
// flush timeout elapses mid-sequence, the prefix `\x1b[?<digits>` arrives as
|
|
946
946
|
// one event and the tail `;...<terminator>` arrives as individual character
|
|
947
947
|
// events that would otherwise leak into the prompt as keystrokes. See #1238.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
)
|
|
948
|
+
// A private CSI (`\x1b[?…`) is a terminal->host report, never a keystroke, so
|
|
949
|
+
// reassembly stays armed for the whole session — not just while a probe
|
|
950
|
+
// sentinel is outstanding — otherwise a reply that lands after the sentinel
|
|
951
|
+
// FIFO drains (slow SSH/PTY links) leaks its tail into the composer (#8542).
|
|
952
|
+
if (this.#privateCsiResponseBuffer || privateCsiPartialPattern.test(sequence)) {
|
|
952
953
|
if (this.#privateCsiResponseBuffer && sequence.startsWith("\x1b")) {
|
|
953
954
|
// New escape arrived mid-reassembly — abandon partial and re-process the new sequence.
|
|
954
955
|
this.#privateCsiResponseBuffer = "";
|
|
@@ -1038,10 +1039,16 @@ export class ProcessTerminal implements Terminal {
|
|
|
1038
1039
|
}
|
|
1039
1040
|
|
|
1040
1041
|
// DA1 response: swallow our sentinel reply regardless of whether an
|
|
1041
|
-
// earlier capability-specific response already succeeded.
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1044
|
-
|
|
1042
|
+
// earlier capability-specific response already succeeded. `CSI ? … c` is
|
|
1043
|
+
// exclusively a terminal->host report, so it is swallowed even with no
|
|
1044
|
+
// outstanding sentinel — a reply that arrives after the FIFO drains (slow
|
|
1045
|
+
// SSH/PTY links) must never reach the composer as literal text (#8542).
|
|
1046
|
+
if (da1ResponsePattern.test(sequence)) {
|
|
1047
|
+
const owner = this.#da1SentinelOwners.shift();
|
|
1048
|
+
if (!owner) {
|
|
1049
|
+
// Late/unowned reply: nothing to resolve, just drop the bytes.
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1045
1052
|
switch (owner.kind) {
|
|
1046
1053
|
case "osc11": {
|
|
1047
1054
|
if (this.#osc11Pending) {
|
package/src/tui.ts
CHANGED
|
@@ -1772,13 +1772,76 @@ export class TUI extends Container {
|
|
|
1772
1772
|
markers.length = keep;
|
|
1773
1773
|
}
|
|
1774
1774
|
|
|
1775
|
+
/**
|
|
1776
|
+
* Replace the cursor markers contributed by one fixed-size root segment.
|
|
1777
|
+
* The ledger stays sorted by absolute frame row; entries belonging to later
|
|
1778
|
+
* siblings move as one tail and otherwise retain their identity.
|
|
1779
|
+
*/
|
|
1780
|
+
#replaceFrameCursorMarkers(startRow: number, lines: readonly string[]): void {
|
|
1781
|
+
const markers = this.#frameCursorMarkers;
|
|
1782
|
+
const endRow = startRow + lines.length;
|
|
1783
|
+
let intervalStart = 0;
|
|
1784
|
+
while (intervalStart < markers.length && markers[intervalStart]!.row < startRow) intervalStart++;
|
|
1785
|
+
let intervalEnd = intervalStart;
|
|
1786
|
+
while (intervalEnd < markers.length && markers[intervalEnd]!.row < endRow) intervalEnd++;
|
|
1787
|
+
|
|
1788
|
+
let nextCount = 0;
|
|
1789
|
+
for (let row = 0; row < lines.length; row++) {
|
|
1790
|
+
if (lines[row]!.includes(CURSOR_MARKER)) nextCount++;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const previousCount = intervalEnd - intervalStart;
|
|
1794
|
+
const delta = nextCount - previousCount;
|
|
1795
|
+
const previousLength = markers.length;
|
|
1796
|
+
if (delta > 0) {
|
|
1797
|
+
markers.length = previousLength + delta;
|
|
1798
|
+
for (let index = previousLength - 1; index >= intervalEnd; index--) {
|
|
1799
|
+
markers[index + delta] = markers[index]!;
|
|
1800
|
+
}
|
|
1801
|
+
} else if (delta < 0) {
|
|
1802
|
+
for (let index = intervalEnd; index < previousLength; index++) {
|
|
1803
|
+
markers[index + delta] = markers[index]!;
|
|
1804
|
+
}
|
|
1805
|
+
markers.length = previousLength + delta;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const reusableCount = Math.min(previousCount, nextCount);
|
|
1809
|
+
let markerSlot = intervalStart;
|
|
1810
|
+
for (let row = 0; row < lines.length; row++) {
|
|
1811
|
+
const line = lines[row]!;
|
|
1812
|
+
const markerIndex = line.indexOf(CURSOR_MARKER);
|
|
1813
|
+
if (markerIndex === -1) continue;
|
|
1814
|
+
const absoluteRow = startRow + row;
|
|
1815
|
+
const col = visibleWidth(line.slice(0, markerIndex));
|
|
1816
|
+
if (markerSlot < intervalStart + reusableCount) {
|
|
1817
|
+
const marker = markers[markerSlot]!;
|
|
1818
|
+
marker.row = absoluteRow;
|
|
1819
|
+
marker.col = col;
|
|
1820
|
+
} else {
|
|
1821
|
+
markers[markerSlot] = { row: absoluteRow, col };
|
|
1822
|
+
}
|
|
1823
|
+
markerSlot++;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/** Strip every internal cursor sentinel from one rendered row. */
|
|
1828
|
+
#stripCursorMarkers(line: string, markerIndex = line.indexOf(CURSOR_MARKER)): string {
|
|
1829
|
+
if (markerIndex === -1) return line;
|
|
1830
|
+
let stripped = line;
|
|
1831
|
+
while (markerIndex !== -1) {
|
|
1832
|
+
stripped = stripped.slice(0, markerIndex) + stripped.slice(markerIndex + CURSOR_MARKER.length);
|
|
1833
|
+
markerIndex = stripped.indexOf(CURSOR_MARKER, markerIndex);
|
|
1834
|
+
}
|
|
1835
|
+
return stripped;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1775
1838
|
/**
|
|
1776
1839
|
* Append one row to the composed frame, stripping CURSOR_MARKER occurrences
|
|
1777
1840
|
* (internal sentinels that must never reach the terminal, the committed
|
|
1778
1841
|
* prefix, or the resync audit) and recording the first marker's position.
|
|
1779
1842
|
*/
|
|
1780
1843
|
#ingestFrameRow(line: string): void {
|
|
1781
|
-
|
|
1844
|
+
const markerIndex = line.indexOf(CURSOR_MARKER);
|
|
1782
1845
|
if (markerIndex === -1) {
|
|
1783
1846
|
this.#composedFrame.push(line);
|
|
1784
1847
|
return;
|
|
@@ -1787,12 +1850,7 @@ export class TUI extends Container {
|
|
|
1787
1850
|
row: this.#composedFrame.length,
|
|
1788
1851
|
col: visibleWidth(line.slice(0, markerIndex)),
|
|
1789
1852
|
});
|
|
1790
|
-
|
|
1791
|
-
while (markerIndex !== -1) {
|
|
1792
|
-
stripped = stripped.slice(0, markerIndex) + stripped.slice(markerIndex + CURSOR_MARKER.length);
|
|
1793
|
-
markerIndex = stripped.indexOf(CURSOR_MARKER, markerIndex);
|
|
1794
|
-
}
|
|
1795
|
-
this.#composedFrame.push(stripped);
|
|
1853
|
+
this.#composedFrame.push(this.#stripCursorMarkers(line, markerIndex));
|
|
1796
1854
|
}
|
|
1797
1855
|
|
|
1798
1856
|
#syncTerminalCursorMode(component: Component | null): void {
|
|
@@ -2558,12 +2616,6 @@ export class TUI extends Container {
|
|
|
2558
2616
|
this.requestComponentRender(component);
|
|
2559
2617
|
return;
|
|
2560
2618
|
}
|
|
2561
|
-
for (const line of nextLines) {
|
|
2562
|
-
if (line.includes(CURSOR_MARKER)) {
|
|
2563
|
-
this.requestComponentRender(component);
|
|
2564
|
-
return;
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
2619
|
|
|
2568
2620
|
let firstChanged = -1;
|
|
2569
2621
|
let lastChanged = -1;
|
|
@@ -2571,8 +2623,9 @@ export class TUI extends Container {
|
|
|
2571
2623
|
for (let i = 0; i < nextLines.length; i++) {
|
|
2572
2624
|
const frameRow = segment.start + i;
|
|
2573
2625
|
const raw = nextLines[i]!;
|
|
2574
|
-
const
|
|
2575
|
-
this.#
|
|
2626
|
+
const composed = this.#stripCursorMarkers(raw);
|
|
2627
|
+
const prepared = this.#prepareLine(composed, width);
|
|
2628
|
+
this.#composedFrame[frameRow] = composed;
|
|
2576
2629
|
this.#preparedMeta[frameRow] = prepared;
|
|
2577
2630
|
this.#preparedFrame[frameRow] = prepared.line;
|
|
2578
2631
|
if (previousWindow[screenStart + i] === prepared.line) continue;
|
|
@@ -2580,7 +2633,8 @@ export class TUI extends Container {
|
|
|
2580
2633
|
if (firstChanged === -1) firstChanged = i;
|
|
2581
2634
|
lastChanged = i;
|
|
2582
2635
|
}
|
|
2583
|
-
|
|
2636
|
+
this.#replaceFrameCursorMarkers(segment.start, nextLines);
|
|
2637
|
+
segment.lines = nextLines;
|
|
2584
2638
|
this.#preparedValidRows = Math.max(this.#preparedValidRows, segment.start + nextLines.length);
|
|
2585
2639
|
this.#renderStablePrefixRows = Math.min(this.#renderStablePrefixRows, segment.start);
|
|
2586
2640
|
|