pi-libtui 0.3.2
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/LICENSE +21 -0
- package/README.md +205 -0
- package/package.json +55 -0
- package/src/activity-presentation.ts +75 -0
- package/src/appearance.ts +632 -0
- package/src/background-surface.ts +125 -0
- package/src/color/palette.ts +186 -0
- package/src/color/preview.ts +157 -0
- package/src/color/resolver.ts +75 -0
- package/src/color/theme.ts +531 -0
- package/src/component-stack.ts +247 -0
- package/src/content/markdown-code.ts +60 -0
- package/src/content/terminal-text.ts +135 -0
- package/src/content/text.ts +105 -0
- package/src/controls/action-panel.ts +420 -0
- package/src/controls/dialog-button-bar.ts +287 -0
- package/src/controls/multi-select.ts +467 -0
- package/src/controls/picker-panel.ts +447 -0
- package/src/controls/screen-icon-actions.ts +134 -0
- package/src/controls/searchable-select.ts +394 -0
- package/src/controls/select-box.ts +224 -0
- package/src/controls/selectable-list.ts +433 -0
- package/src/controls/selection-action-bar.ts +382 -0
- package/src/controls/semantic-input.ts +23 -0
- package/src/controls/tab.ts +189 -0
- package/src/cursor.ts +288 -0
- package/src/decoration/editor-pills.ts +156 -0
- package/src/decoration/glyphs.ts +326 -0
- package/src/decoration/pointer-interaction.ts +123 -0
- package/src/decoration/powerline-pill.ts +104 -0
- package/src/decoration/status.ts +185 -0
- package/src/decoration/transient-pill.ts +176 -0
- package/src/diff/index.ts +21 -0
- package/src/diff/model.ts +76 -0
- package/src/diff/parse.ts +310 -0
- package/src/diff/pierre-syntax.ts +86 -0
- package/src/diff/render.ts +504 -0
- package/src/diff/structured.ts +264 -0
- package/src/diff/view.ts +75 -0
- package/src/editor/chrome.ts +341 -0
- package/src/editor/composition.ts +53 -0
- package/src/editor/layout.ts +81 -0
- package/src/editor/presentation.ts +35 -0
- package/src/editor/protocol.ts +188 -0
- package/src/editor.ts +41 -0
- package/src/extension.ts +30 -0
- package/src/folding.ts +192 -0
- package/src/host/cursor-bridge.ts +210 -0
- package/src/host/editor-bridge.ts +148 -0
- package/src/host/extension-host.ts +231 -0
- package/src/host/mouse-bridge.ts +212 -0
- package/src/host/mouse-dispatch.ts +400 -0
- package/src/host/mouse-input.ts +85 -0
- package/src/host/mouse-targets.ts +238 -0
- package/src/host/pi-layout-adapter.ts +298 -0
- package/src/host/pi-native-selection.ts +227 -0
- package/src/host/split-pane-bridge.ts +720 -0
- package/src/host/transcript-bridge.ts +236 -0
- package/src/index.ts +334 -0
- package/src/line-layout.ts +6 -0
- package/src/motion.ts +1409 -0
- package/src/mouse/events.ts +55 -0
- package/src/mouse/fullscreen-layout.ts +260 -0
- package/src/mouse/registry.ts +348 -0
- package/src/mouse.ts +37 -0
- package/src/native-binary.ts +189 -0
- package/src/overlay/anchored.ts +91 -0
- package/src/overlay/detail-card.ts +158 -0
- package/src/overlay/dialog.ts +266 -0
- package/src/overlay/floating.ts +106 -0
- package/src/overlay/framed-editor.ts +236 -0
- package/src/overlay/fullscreen.ts +170 -0
- package/src/overlay/hover-tooltip.ts +109 -0
- package/src/overlay/modal-mount.ts +118 -0
- package/src/panels.ts +207 -0
- package/src/render-cache.ts +80 -0
- package/src/render-epoch.ts +48 -0
- package/src/request-animation.ts +160 -0
- package/src/scrollbar.ts +27 -0
- package/src/selection.ts +148 -0
- package/src/split-pane.ts +232 -0
- package/src/status-presentation.ts +411 -0
- package/src/stream.ts +543 -0
- package/src/syntax.ts +316 -0
- package/src/terminal/bridge-client.ts +243 -0
- package/src/terminal/embedding.ts +6 -0
- package/src/terminal/output.ts +151 -0
- package/src/terminal/projection.ts +362 -0
- package/src/terminal/pty-host.ts +263 -0
- package/src/terminal/pty-pane.ts +308 -0
- package/src/terminal-colors.ts +334 -0
- package/src/tool/action.ts +170 -0
- package/src/tool/activity.ts +500 -0
- package/src/tool/call-preview.ts +44 -0
- package/src/tool/disclosure-action.ts +89 -0
- package/src/tool/expanded-region-viewport.ts +103 -0
- package/src/tool/index.ts +26 -0
- package/src/tool/output.ts +271 -0
- package/src/tool/transcript.ts +150 -0
- package/src/tool/view-region.ts +581 -0
- package/test/appearance.test.ts +183 -0
- package/test/bounded-stream.test.ts +150 -0
- package/test/bridge-client.test.ts +42 -0
- package/test/color-preview.test.ts +23 -0
- package/test/component-stack.test.ts +206 -0
- package/test/components.test.ts +1165 -0
- package/test/content-text.test.ts +21 -0
- package/test/cursor-bridge.test.ts +151 -0
- package/test/cursor.test.ts +147 -0
- package/test/editor-chrome.test.ts +119 -0
- package/test/editor-layout.test.ts +30 -0
- package/test/editor-registry.test.ts +203 -0
- package/test/editor-theme.test.ts +33 -0
- package/test/editor-token-pill.test.ts +72 -0
- package/test/floating-overlay.test.ts +57 -0
- package/test/folding.test.ts +147 -0
- package/test/harmonious-theme.test.ts +23 -0
- package/test/interaction.test.ts +104 -0
- package/test/key-icons.test.ts +62 -0
- package/test/markdown-code.test.ts +24 -0
- package/test/modal-overlay-mount.test.ts +94 -0
- package/test/motion.test.ts +649 -0
- package/test/mouse/bridge.test.ts +1302 -0
- package/test/mouse/extension.test.ts +205 -0
- package/test/mouse/registry.test.ts +140 -0
- package/test/mouse/screen-decoration.test.ts +167 -0
- package/test/native-binary.test.ts +52 -0
- package/test/palette.test.ts +32 -0
- package/test/panels.test.ts +74 -0
- package/test/pointer-components.test.ts +151 -0
- package/test/progress.test.ts +54 -0
- package/test/pty-host.test.ts +126 -0
- package/test/pty-pane.test.ts +62 -0
- package/test/public-surface.test.ts +75 -0
- package/test/render-cache.test.ts +25 -0
- package/test/render-epoch.test.ts +119 -0
- package/test/request-animation.test.ts +193 -0
- package/test/screen-icon-actions.test.ts +69 -0
- package/test/select-box.test.ts +86 -0
- package/test/select-pointer.test.ts +369 -0
- package/test/selectable-list.test.ts +258 -0
- package/test/selection/registry.test.ts +44 -0
- package/test/selection-action-bar.test.ts +204 -0
- package/test/split-pane-bridge.test.ts +944 -0
- package/test/split-pane.test.ts +134 -0
- package/test/status-presentation.test.ts +39 -0
- package/test/syntax.test.ts +59 -0
- package/test/terminal-colors.test.ts +115 -0
- package/test/terminal-embedding.test.ts +12 -0
- package/test/terminal-projection.test.ts +312 -0
- package/test/theme-colors.test.ts +175 -0
- package/test/tool-output.test.ts +160 -0
- package/test/tool-transcript.test.ts +1190 -0
- package/test/unified-diff.test.ts +691 -0
- package/themes/harmonious.json +61 -0
- package/tsconfig.json +16 -0
package/src/stream.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { terminalSequenceEnd, terminalSequenceStart } from "./content/terminal-text.ts";
|
|
2
|
+
|
|
3
|
+
const ANSI_RESET = "\x1b[0m";
|
|
4
|
+
|
|
5
|
+
/** Snapshot of a bounded append-only stream. */
|
|
6
|
+
export interface BoundedStreamSnapshot {
|
|
7
|
+
/** Display-safe retained output, including an omission marker when bounded. */
|
|
8
|
+
readonly text: string;
|
|
9
|
+
/** Stable prefix retained from the beginning of the stream. */
|
|
10
|
+
readonly head: string;
|
|
11
|
+
/** Recent suffix retained from the end of the stream. */
|
|
12
|
+
readonly tail: string;
|
|
13
|
+
/** The current unterminated line, independently bounded to the stream budget. */
|
|
14
|
+
readonly partialLine: string;
|
|
15
|
+
readonly truncated: boolean;
|
|
16
|
+
readonly omittedBytes: number;
|
|
17
|
+
readonly totalBytes: number;
|
|
18
|
+
readonly lineCount: number;
|
|
19
|
+
/** True after `replaceFinal` supplied the authoritative value. */
|
|
20
|
+
readonly authoritative: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BoundedStreamOptions {
|
|
24
|
+
/** Maximum source bytes retained between the head and tail. */
|
|
25
|
+
readonly maxBytes: number;
|
|
26
|
+
/** Fraction of retained bytes reserved for the stable head. Defaults to one half. */
|
|
27
|
+
readonly headShare?: number;
|
|
28
|
+
/** Builds the marker inserted between a truncated head and tail. */
|
|
29
|
+
readonly omissionMarker?: (omittedBytes: number) => string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_MARKER = (omittedBytes: number): string => `\n… ${omittedBytes} bytes omitted …\n`;
|
|
33
|
+
const TAIL_ROLL_BYTES = 256 * 1_024;
|
|
34
|
+
const MAX_ANSI_PARSER_CHARACTERS = 4_096;
|
|
35
|
+
|
|
36
|
+
interface AnsiPending {
|
|
37
|
+
parser: string;
|
|
38
|
+
characters: number;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Incrementally retains the useful edges of a text stream without retaining
|
|
44
|
+
* its unbounded middle. Appends cost O(chunk + configured budget), independent
|
|
45
|
+
* of the total stream size.
|
|
46
|
+
*/
|
|
47
|
+
export class BoundedStreamBuffer {
|
|
48
|
+
private readonly maxBytes: number;
|
|
49
|
+
private readonly headBytes: number;
|
|
50
|
+
private readonly tailBytes: number;
|
|
51
|
+
private readonly omissionMarker: (omittedBytes: number) => string;
|
|
52
|
+
private exactValue = "";
|
|
53
|
+
private exactBytes = 0;
|
|
54
|
+
private truncated = false;
|
|
55
|
+
private headValue = "";
|
|
56
|
+
private tailValue = "";
|
|
57
|
+
private tailPending = "";
|
|
58
|
+
private tailPendingBytes = 0;
|
|
59
|
+
private tailStart = 0;
|
|
60
|
+
private tailValueBytes = 0;
|
|
61
|
+
private tailAnsiPending: AnsiPending = { parser: "", characters: 0, bytes: 0 };
|
|
62
|
+
private partialValue = "";
|
|
63
|
+
private partialPending = "";
|
|
64
|
+
private partialPendingBytes = 0;
|
|
65
|
+
private partialStart = 0;
|
|
66
|
+
private partialBytes = 0;
|
|
67
|
+
private partialHasAnsi = false;
|
|
68
|
+
private partialAnsiPending: AnsiPending = { parser: "", characters: 0, bytes: 0 };
|
|
69
|
+
private bytes = 0;
|
|
70
|
+
private newlines = 0;
|
|
71
|
+
private endsWithNewline = false;
|
|
72
|
+
private hasAnsi = false;
|
|
73
|
+
private isAuthoritative = false;
|
|
74
|
+
private pendingHighSurrogate = "";
|
|
75
|
+
private byteDecoder = new TextDecoder();
|
|
76
|
+
|
|
77
|
+
constructor(options: BoundedStreamOptions) {
|
|
78
|
+
if (!Number.isFinite(options.maxBytes) || options.maxBytes < 1) {
|
|
79
|
+
throw new RangeError("maxBytes must be a positive finite number");
|
|
80
|
+
}
|
|
81
|
+
this.maxBytes = Math.floor(options.maxBytes);
|
|
82
|
+
const headShare = Math.min(1, Math.max(0, options.headShare ?? 0.5));
|
|
83
|
+
this.headBytes = Math.floor(this.maxBytes * headShare);
|
|
84
|
+
this.tailBytes = this.maxBytes - this.headBytes;
|
|
85
|
+
this.omissionMarker = options.omissionMarker ?? DEFAULT_MARKER;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Append a decoded string or a raw UTF-8 chunk. */
|
|
89
|
+
append(chunk: string | Uint8Array): BoundedStreamSnapshot {
|
|
90
|
+
this.isAuthoritative = false;
|
|
91
|
+
if (typeof chunk === "string") {
|
|
92
|
+
const decodedBytes = this.byteDecoder.decode();
|
|
93
|
+
if (decodedBytes) this.appendDecoded(decodedBytes);
|
|
94
|
+
this.appendStringChunk(chunk);
|
|
95
|
+
} else {
|
|
96
|
+
this.flushPendingSurrogate();
|
|
97
|
+
const decoded = this.byteDecoder.decode(chunk, { stream: true });
|
|
98
|
+
if (decoded) this.appendDecoded(decoded);
|
|
99
|
+
}
|
|
100
|
+
return this.snapshot();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Flush an incomplete UTF-8 sequence or dangling UTF-16 surrogate. */
|
|
104
|
+
flush(): BoundedStreamSnapshot {
|
|
105
|
+
this.flushPendingSurrogate();
|
|
106
|
+
const decoded = this.byteDecoder.decode();
|
|
107
|
+
if (decoded) this.appendDecoded(decoded);
|
|
108
|
+
return this.snapshot();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Replace all speculative stream chunks with the authoritative final text. */
|
|
112
|
+
replaceFinal(value: string | Uint8Array): BoundedStreamSnapshot {
|
|
113
|
+
this.reset();
|
|
114
|
+
const decoded = typeof value === "string" ? replaceDanglingSurrogates(value) : new TextDecoder().decode(value);
|
|
115
|
+
this.appendDecoded(decoded);
|
|
116
|
+
this.isAuthoritative = true;
|
|
117
|
+
return this.snapshot();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clear(): void {
|
|
121
|
+
this.reset();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
snapshot(): BoundedStreamSnapshot {
|
|
125
|
+
const partialValue = this.partialValue;
|
|
126
|
+
const partialPending = this.partialPending;
|
|
127
|
+
const partialStart = this.partialStart;
|
|
128
|
+
const partialHasAnsi = this.partialHasAnsi;
|
|
129
|
+
const partialAnsiPendingCharacters = this.partialAnsiPending.characters;
|
|
130
|
+
let partial: string | undefined;
|
|
131
|
+
const partialText = () => {
|
|
132
|
+
if (partial !== undefined) return partial;
|
|
133
|
+
const source = retainedSuffix(partialValue, partialStart, partialPending);
|
|
134
|
+
partial =
|
|
135
|
+
partialHasAnsi && partialAnsiPendingCharacters > 0
|
|
136
|
+
? source.slice(0, Math.max(0, source.length - partialAnsiPendingCharacters))
|
|
137
|
+
: source;
|
|
138
|
+
return partial;
|
|
139
|
+
};
|
|
140
|
+
if (!this.truncated) {
|
|
141
|
+
const pendingCharacters = this.partialAnsiPending.characters;
|
|
142
|
+
const exact =
|
|
143
|
+
pendingCharacters > 0
|
|
144
|
+
? this.exactValue.slice(0, Math.max(0, this.exactValue.length - pendingCharacters))
|
|
145
|
+
: this.exactValue;
|
|
146
|
+
return {
|
|
147
|
+
text: exact,
|
|
148
|
+
head: exact,
|
|
149
|
+
tail: "",
|
|
150
|
+
get partialLine() {
|
|
151
|
+
return partialText();
|
|
152
|
+
},
|
|
153
|
+
truncated: false,
|
|
154
|
+
omittedBytes: 0,
|
|
155
|
+
totalBytes: this.bytes,
|
|
156
|
+
lineCount: this.lineCount(),
|
|
157
|
+
authoritative: this.isAuthoritative,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const tailValue = this.tailValue;
|
|
162
|
+
const tailPending = this.tailPending;
|
|
163
|
+
const tailStart = this.tailStart;
|
|
164
|
+
const tailSourceBytes = this.tailValueBytes;
|
|
165
|
+
const tailAnsiPendingCharacters = this.tailAnsiPending.characters;
|
|
166
|
+
const tailVisibleBytes = Math.max(0, tailSourceBytes - this.tailAnsiPending.bytes);
|
|
167
|
+
const totalBytes = this.bytes;
|
|
168
|
+
const head = this.headValue;
|
|
169
|
+
const headLength = byteLength(head);
|
|
170
|
+
const hasAnsi = this.hasAnsi;
|
|
171
|
+
const omissionMarker = this.omissionMarker;
|
|
172
|
+
let tail: string | undefined;
|
|
173
|
+
let omittedBytes: number | undefined;
|
|
174
|
+
let marker: string | undefined;
|
|
175
|
+
let text: string | undefined;
|
|
176
|
+
const tailText = () => {
|
|
177
|
+
if (tail === undefined) {
|
|
178
|
+
const source = retainedSuffix(tailValue, tailStart, tailPending);
|
|
179
|
+
tail =
|
|
180
|
+
hasAnsi && tailAnsiPendingCharacters > 0
|
|
181
|
+
? source.slice(0, Math.max(0, source.length - tailAnsiPendingCharacters))
|
|
182
|
+
: source;
|
|
183
|
+
}
|
|
184
|
+
return tail;
|
|
185
|
+
};
|
|
186
|
+
const omitted = () => {
|
|
187
|
+
if (omittedBytes === undefined) {
|
|
188
|
+
omittedBytes = Math.max(0, totalBytes - headLength - tailVisibleBytes);
|
|
189
|
+
}
|
|
190
|
+
return omittedBytes;
|
|
191
|
+
};
|
|
192
|
+
const omission = () => {
|
|
193
|
+
if (marker === undefined) marker = omissionMarker(omitted());
|
|
194
|
+
return marker;
|
|
195
|
+
};
|
|
196
|
+
return {
|
|
197
|
+
get text() {
|
|
198
|
+
const suffix = tailText();
|
|
199
|
+
const gap = omission();
|
|
200
|
+
if (text === undefined) {
|
|
201
|
+
text = hasAnsi ? `${head}${ANSI_RESET}${gap}${ANSI_RESET}${suffix}${ANSI_RESET}` : `${head}${gap}${suffix}`;
|
|
202
|
+
}
|
|
203
|
+
return text;
|
|
204
|
+
},
|
|
205
|
+
head,
|
|
206
|
+
get tail() {
|
|
207
|
+
return tailText();
|
|
208
|
+
},
|
|
209
|
+
get partialLine() {
|
|
210
|
+
return partialText();
|
|
211
|
+
},
|
|
212
|
+
truncated: true,
|
|
213
|
+
get omittedBytes() {
|
|
214
|
+
return omitted();
|
|
215
|
+
},
|
|
216
|
+
totalBytes: this.bytes,
|
|
217
|
+
lineCount: this.lineCount(),
|
|
218
|
+
authoritative: this.isAuthoritative,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private appendStringChunk(chunk: string): void {
|
|
223
|
+
let decoded = `${this.pendingHighSurrogate}${chunk}`;
|
|
224
|
+
this.pendingHighSurrogate = "";
|
|
225
|
+
if (decoded.length > 0 && isHighSurrogate(decoded.charCodeAt(decoded.length - 1))) {
|
|
226
|
+
this.pendingHighSurrogate = decoded.at(-1) ?? "";
|
|
227
|
+
decoded = decoded.slice(0, -1);
|
|
228
|
+
}
|
|
229
|
+
if (decoded) this.appendDecoded(decoded);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private appendDecoded(decoded: string): void {
|
|
233
|
+
if (!decoded) return;
|
|
234
|
+
const chunkBytes = byteLength(decoded);
|
|
235
|
+
this.bytes += chunkBytes;
|
|
236
|
+
this.newlines += countNewlines(decoded);
|
|
237
|
+
this.endsWithNewline = decoded.endsWith("\n");
|
|
238
|
+
this.hasAnsi ||= terminalSequenceStart(decoded) !== undefined;
|
|
239
|
+
|
|
240
|
+
this.appendPartial(decoded);
|
|
241
|
+
|
|
242
|
+
if (!this.truncated) {
|
|
243
|
+
if (this.exactBytes + chunkBytes <= this.maxBytes) {
|
|
244
|
+
this.exactValue += decoded;
|
|
245
|
+
this.exactBytes += chunkBytes;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const candidate = this.exactValue + decoded;
|
|
249
|
+
this.headValue = ansiSafePrefix(candidate, this.headBytes);
|
|
250
|
+
this.tailValue = ansiAlignedSuffix(candidate, this.tailBytes);
|
|
251
|
+
this.tailPending = "";
|
|
252
|
+
this.tailPendingBytes = 0;
|
|
253
|
+
this.tailStart = 0;
|
|
254
|
+
this.tailValueBytes = byteLength(this.tailValue);
|
|
255
|
+
this.tailAnsiPending = advanceAnsiPending({ parser: "", characters: 0, bytes: 0 }, this.tailValue);
|
|
256
|
+
this.exactValue = "";
|
|
257
|
+
this.exactBytes = 0;
|
|
258
|
+
this.truncated = true;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.tailPending += decoded;
|
|
263
|
+
this.tailPendingBytes += chunkBytes;
|
|
264
|
+
this.tailValueBytes += chunkBytes;
|
|
265
|
+
this.tailAnsiPending = advanceAnsiPending(this.tailAnsiPending, decoded);
|
|
266
|
+
if (this.tailValueBytes > this.tailBytes) {
|
|
267
|
+
this.trimTail(this.tailValueBytes - this.tailBytes);
|
|
268
|
+
}
|
|
269
|
+
if (this.tailStart >= TAIL_ROLL_BYTES || this.tailPendingBytes >= TAIL_ROLL_BYTES) {
|
|
270
|
+
this.compactTail();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private trimTail(excessBytes: number): void {
|
|
275
|
+
let removedBytes = 0;
|
|
276
|
+
let sequenceStart = terminalSequenceStart(this.tailValue, this.tailStart);
|
|
277
|
+
while (removedBytes < excessBytes) {
|
|
278
|
+
if (this.tailStart >= this.tailValue.length) {
|
|
279
|
+
this.compactTail();
|
|
280
|
+
sequenceStart = terminalSequenceStart(this.tailValue, this.tailStart);
|
|
281
|
+
if (!this.tailValue) break;
|
|
282
|
+
}
|
|
283
|
+
const start = this.tailStart;
|
|
284
|
+
const code = this.tailValue.codePointAt(start) ?? 0xfffd;
|
|
285
|
+
if (sequenceStart === start) {
|
|
286
|
+
let end = terminalSequenceEnd(this.tailValue, start)?.end;
|
|
287
|
+
if (end === undefined && this.tailPending) {
|
|
288
|
+
this.compactTail();
|
|
289
|
+
sequenceStart = terminalSequenceStart(this.tailValue, this.tailStart);
|
|
290
|
+
end = terminalSequenceEnd(this.tailValue, this.tailStart)?.end;
|
|
291
|
+
}
|
|
292
|
+
if (end === undefined) {
|
|
293
|
+
removedBytes += byteLengthRange(this.tailValue, this.tailStart, this.tailValue.length);
|
|
294
|
+
this.tailStart = this.tailValue.length;
|
|
295
|
+
sequenceStart = undefined;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
removedBytes += byteLengthRange(this.tailValue, this.tailStart, end);
|
|
299
|
+
this.tailStart = end;
|
|
300
|
+
sequenceStart = terminalSequenceStart(this.tailValue, this.tailStart);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
removedBytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
304
|
+
this.tailStart += code > 0xffff ? 2 : 1;
|
|
305
|
+
if (sequenceStart !== undefined && this.tailStart > sequenceStart) {
|
|
306
|
+
sequenceStart = terminalSequenceStart(this.tailValue, this.tailStart);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this.tailValueBytes = Math.max(0, this.tailValueBytes - removedBytes);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private compactTail(): void {
|
|
313
|
+
this.tailValue = retainedSuffix(this.tailValue, this.tailStart, this.tailPending);
|
|
314
|
+
this.tailPending = "";
|
|
315
|
+
this.tailPendingBytes = 0;
|
|
316
|
+
this.tailStart = 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private flushPendingSurrogate(): void {
|
|
320
|
+
if (!this.pendingHighSurrogate) return;
|
|
321
|
+
this.pendingHighSurrogate = "";
|
|
322
|
+
this.appendDecoded("\uFFFD");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private appendPartial(decoded: string): void {
|
|
326
|
+
const finalNewline = decoded.lastIndexOf("\n");
|
|
327
|
+
const chunk = finalNewline >= 0 ? decoded.slice(finalNewline + 1) : decoded;
|
|
328
|
+
if (finalNewline >= 0) {
|
|
329
|
+
this.partialValue = chunk;
|
|
330
|
+
this.partialPending = "";
|
|
331
|
+
this.partialPendingBytes = 0;
|
|
332
|
+
this.partialStart = 0;
|
|
333
|
+
this.partialBytes = byteLength(chunk);
|
|
334
|
+
this.partialHasAnsi = terminalSequenceStart(chunk) !== undefined;
|
|
335
|
+
this.partialAnsiPending = advanceAnsiPending({ parser: "", characters: 0, bytes: 0 }, chunk);
|
|
336
|
+
} else if (chunk) {
|
|
337
|
+
const chunkBytes = byteLength(chunk);
|
|
338
|
+
this.partialPending += chunk;
|
|
339
|
+
this.partialPendingBytes += chunkBytes;
|
|
340
|
+
this.partialBytes += chunkBytes;
|
|
341
|
+
this.partialHasAnsi ||= terminalSequenceStart(chunk) !== undefined;
|
|
342
|
+
this.partialAnsiPending = advanceAnsiPending(this.partialAnsiPending, chunk);
|
|
343
|
+
}
|
|
344
|
+
if (this.partialBytes > this.maxBytes) this.trimPartial(this.partialBytes - this.maxBytes);
|
|
345
|
+
if (this.partialStart >= TAIL_ROLL_BYTES || this.partialPendingBytes >= TAIL_ROLL_BYTES) {
|
|
346
|
+
this.compactPartial();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private trimPartial(excessBytes: number): void {
|
|
351
|
+
let removedBytes = 0;
|
|
352
|
+
let sequenceStart = terminalSequenceStart(this.partialValue, this.partialStart);
|
|
353
|
+
while (removedBytes < excessBytes) {
|
|
354
|
+
if (this.partialStart >= this.partialValue.length) {
|
|
355
|
+
this.compactPartial();
|
|
356
|
+
sequenceStart = terminalSequenceStart(this.partialValue, this.partialStart);
|
|
357
|
+
if (!this.partialValue) break;
|
|
358
|
+
}
|
|
359
|
+
const code = this.partialValue.codePointAt(this.partialStart) ?? 0xfffd;
|
|
360
|
+
if (sequenceStart === this.partialStart) {
|
|
361
|
+
let end = terminalSequenceEnd(this.partialValue, this.partialStart)?.end;
|
|
362
|
+
if (end === undefined && this.partialPending) {
|
|
363
|
+
this.compactPartial();
|
|
364
|
+
sequenceStart = terminalSequenceStart(this.partialValue, this.partialStart);
|
|
365
|
+
end = terminalSequenceEnd(this.partialValue, this.partialStart)?.end;
|
|
366
|
+
}
|
|
367
|
+
if (end === undefined) {
|
|
368
|
+
removedBytes += byteLengthRange(this.partialValue, this.partialStart, this.partialValue.length);
|
|
369
|
+
this.partialStart = this.partialValue.length;
|
|
370
|
+
sequenceStart = undefined;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
removedBytes += byteLengthRange(this.partialValue, this.partialStart, end);
|
|
374
|
+
this.partialStart = end;
|
|
375
|
+
sequenceStart = terminalSequenceStart(this.partialValue, this.partialStart);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
removedBytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
379
|
+
this.partialStart += code > 0xffff ? 2 : 1;
|
|
380
|
+
if (sequenceStart !== undefined && this.partialStart > sequenceStart) {
|
|
381
|
+
sequenceStart = terminalSequenceStart(this.partialValue, this.partialStart);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this.partialBytes = Math.max(0, this.partialBytes - removedBytes);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private compactPartial(): void {
|
|
388
|
+
this.partialValue = retainedSuffix(this.partialValue, this.partialStart, this.partialPending);
|
|
389
|
+
this.partialPending = "";
|
|
390
|
+
this.partialPendingBytes = 0;
|
|
391
|
+
this.partialStart = 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private lineCount(): number {
|
|
395
|
+
if (this.bytes === 0) return 0;
|
|
396
|
+
return this.newlines + (this.endsWithNewline ? 0 : 1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private reset(): void {
|
|
400
|
+
this.exactValue = "";
|
|
401
|
+
this.exactBytes = 0;
|
|
402
|
+
this.truncated = false;
|
|
403
|
+
this.headValue = "";
|
|
404
|
+
this.tailValue = "";
|
|
405
|
+
this.tailPending = "";
|
|
406
|
+
this.tailPendingBytes = 0;
|
|
407
|
+
this.tailStart = 0;
|
|
408
|
+
this.tailValueBytes = 0;
|
|
409
|
+
this.tailAnsiPending = { parser: "", characters: 0, bytes: 0 };
|
|
410
|
+
this.partialValue = "";
|
|
411
|
+
this.partialPending = "";
|
|
412
|
+
this.partialPendingBytes = 0;
|
|
413
|
+
this.partialStart = 0;
|
|
414
|
+
this.partialBytes = 0;
|
|
415
|
+
this.partialHasAnsi = false;
|
|
416
|
+
this.partialAnsiPending = { parser: "", characters: 0, bytes: 0 };
|
|
417
|
+
this.bytes = 0;
|
|
418
|
+
this.newlines = 0;
|
|
419
|
+
this.endsWithNewline = false;
|
|
420
|
+
this.hasAnsi = false;
|
|
421
|
+
this.isAuthoritative = false;
|
|
422
|
+
this.pendingHighSurrogate = "";
|
|
423
|
+
this.byteDecoder = new TextDecoder();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function replaceDanglingSurrogates(value: string): string {
|
|
428
|
+
let result = "";
|
|
429
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
430
|
+
const code = value.charCodeAt(index);
|
|
431
|
+
if (isHighSurrogate(code)) {
|
|
432
|
+
const next = value.charCodeAt(index + 1);
|
|
433
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
434
|
+
result += value.slice(index, index + 2);
|
|
435
|
+
index += 1;
|
|
436
|
+
} else result += "\uFFFD";
|
|
437
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) result += "\uFFFD";
|
|
438
|
+
else result += value[index];
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isHighSurrogate(code: number): boolean {
|
|
444
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function countNewlines(text: string): number {
|
|
448
|
+
let count = 0;
|
|
449
|
+
for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) === 10) count += 1;
|
|
450
|
+
return count;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function byteLength(text: string): number {
|
|
454
|
+
return Buffer.byteLength(text, "utf8");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function utf8Prefix(text: string, budget: number): string {
|
|
458
|
+
if (budget <= 0) return "";
|
|
459
|
+
const buffer = Buffer.from(text, "utf8");
|
|
460
|
+
if (buffer.length <= budget) return text;
|
|
461
|
+
let end = budget;
|
|
462
|
+
while (end > 0 && (buffer[end]! & 0xc0) === 0x80) end -= 1;
|
|
463
|
+
return buffer.subarray(0, end).toString("utf8");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function utf8Suffix(text: string, budget: number): string {
|
|
467
|
+
if (budget <= 0) return "";
|
|
468
|
+
const buffer = Buffer.from(text, "utf8");
|
|
469
|
+
if (buffer.length <= budget) return text;
|
|
470
|
+
let start = buffer.length - budget;
|
|
471
|
+
while (start < buffer.length && (buffer[start]! & 0xc0) === 0x80) start += 1;
|
|
472
|
+
return buffer.subarray(start).toString("utf8");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function byteLengthRange(text: string, start: number, end: number): number {
|
|
476
|
+
let bytes = 0;
|
|
477
|
+
for (let index = start; index < end; ) {
|
|
478
|
+
const code = text.codePointAt(index) ?? 0xfffd;
|
|
479
|
+
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
480
|
+
index += code > 0xffff ? 2 : 1;
|
|
481
|
+
}
|
|
482
|
+
return bytes;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function retainedSuffix(value: string, start: number, pending: string): string {
|
|
486
|
+
const retained = start === 0 ? value : value.slice(start);
|
|
487
|
+
if (!retained) return pending;
|
|
488
|
+
if (!pending) return retained;
|
|
489
|
+
return retained + pending;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function advanceAnsiPending(previous: AnsiPending, chunk: string): AnsiPending {
|
|
493
|
+
if (!previous.parser && terminalSequenceStart(chunk) === undefined) return previous;
|
|
494
|
+
const source = previous.parser + chunk;
|
|
495
|
+
for (let sequenceIndex = terminalSequenceStart(source); sequenceIndex !== undefined; ) {
|
|
496
|
+
const end = terminalSequenceEnd(source, sequenceIndex)?.end;
|
|
497
|
+
if (end === undefined) {
|
|
498
|
+
const continued = previous.characters > 0 && sequenceIndex === 0;
|
|
499
|
+
const pending = source.slice(sequenceIndex);
|
|
500
|
+
return {
|
|
501
|
+
parser: boundAnsiParser(pending),
|
|
502
|
+
characters: continued ? previous.characters + chunk.length : pending.length,
|
|
503
|
+
bytes: continued ? previous.bytes + byteLength(chunk) : byteLength(pending),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
sequenceIndex = terminalSequenceStart(source, end);
|
|
507
|
+
}
|
|
508
|
+
return { parser: "", characters: 0, bytes: 0 };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function boundAnsiParser(pending: string): string {
|
|
512
|
+
if (pending.length <= MAX_ANSI_PARSER_CHARACTERS) return pending;
|
|
513
|
+
const prefixLength = pending.charCodeAt(0) === 0x1b ? 2 : 1;
|
|
514
|
+
return pending.slice(0, prefixLength) + pending.slice(-(MAX_ANSI_PARSER_CHARACTERS - prefixLength));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function ansiSafePrefix(text: string, budget: number): string {
|
|
518
|
+
let prefix = utf8Prefix(text, budget);
|
|
519
|
+
for (let sequenceIndex = terminalSequenceStart(prefix); sequenceIndex !== undefined; ) {
|
|
520
|
+
const end = terminalSequenceEnd(prefix, sequenceIndex)?.end;
|
|
521
|
+
if (end === undefined) {
|
|
522
|
+
prefix = prefix.slice(0, sequenceIndex);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
sequenceIndex = terminalSequenceStart(prefix, end);
|
|
526
|
+
}
|
|
527
|
+
return prefix;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function ansiAlignedSuffix(text: string, budget: number): string {
|
|
531
|
+
let suffix = utf8Suffix(text, budget);
|
|
532
|
+
let start = text.length - suffix.length;
|
|
533
|
+
for (let sequenceIndex = terminalSequenceStart(text); sequenceIndex !== undefined && sequenceIndex < start; ) {
|
|
534
|
+
const end = terminalSequenceEnd(text, sequenceIndex)?.end;
|
|
535
|
+
if (end === undefined) return "";
|
|
536
|
+
if (end > start) {
|
|
537
|
+
start = end;
|
|
538
|
+
suffix = text.slice(start);
|
|
539
|
+
}
|
|
540
|
+
sequenceIndex = terminalSequenceStart(text, end);
|
|
541
|
+
}
|
|
542
|
+
return suffix;
|
|
543
|
+
}
|