@f5xc-salesdemos/pi-tui 19.30.3 → 19.30.4
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/package.json +3 -3
- package/src/stdin-buffer.ts +52 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/pi-tui",
|
|
4
|
-
"version": "19.30.
|
|
4
|
+
"version": "19.30.4",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@f5xc-salesdemos/pi-natives": "19.30.
|
|
41
|
-
"@f5xc-salesdemos/pi-utils": "19.30.
|
|
40
|
+
"@f5xc-salesdemos/pi-natives": "19.30.4",
|
|
41
|
+
"@f5xc-salesdemos/pi-utils": "19.30.4",
|
|
42
42
|
"marked": "^17.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/src/stdin-buffer.ts
CHANGED
|
@@ -22,6 +22,28 @@ const ESC = "\x1b";
|
|
|
22
22
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
23
23
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
24
24
|
|
|
25
|
+
// How long after flushing an incomplete escape we will still stitch a late
|
|
26
|
+
// continuation back onto it. Generous because the same render stall that split
|
|
27
|
+
// the keypress also delays processing of its second half.
|
|
28
|
+
const ESCAPE_CONTINUATION_WINDOW_MS = 2000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Does `combined` (a just-flushed incomplete-escape prefix joined with the next
|
|
32
|
+
* chunk) look like a fragmented CSI/SS3/OSC/DCS/APC sequence being stitched back
|
|
33
|
+
* together — as opposed to an Escape keypress followed by ordinary typed text?
|
|
34
|
+
* Only the former should be reassembled.
|
|
35
|
+
*/
|
|
36
|
+
function isEscapeContinuation(combined: string): boolean {
|
|
37
|
+
if (combined.length < 2 || !combined.startsWith(ESC)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Only CSI (ESC [). That is the family that fragments as keyboard input —
|
|
41
|
+
// arrows, function keys, modifyOtherKeys (ESC[27;5;99~), kitty (ESC[99;5u).
|
|
42
|
+
// OSC/DCS/APC are string-terminated terminal *responses*, not fragmented
|
|
43
|
+
// keypresses, and must never be glued onto unrelated input.
|
|
44
|
+
return combined[1] === "[";
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
/**
|
|
26
48
|
* Check if a string is a complete escape sequence or needs more data
|
|
27
49
|
*/
|
|
@@ -252,6 +274,11 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
252
274
|
readonly #timeoutMs: number;
|
|
253
275
|
#pasteMode: boolean = false;
|
|
254
276
|
#pasteBuffer: string = "";
|
|
277
|
+
// An incomplete escape sequence we flushed on timeout, kept so a continuation
|
|
278
|
+
// arriving in the next read (a keypress fragmented across stdin reads by an
|
|
279
|
+
// extreme render stall) can be reassembled instead of leaking its tail as text.
|
|
280
|
+
#pendingEscape: string | null = null;
|
|
281
|
+
#pendingEscapeAt = 0;
|
|
255
282
|
|
|
256
283
|
constructor(options: StdinBufferOptions = {}) {
|
|
257
284
|
super();
|
|
@@ -279,6 +306,22 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
279
306
|
str = data;
|
|
280
307
|
}
|
|
281
308
|
|
|
309
|
+
// If we recently flushed an incomplete escape (its hold window expired before
|
|
310
|
+
// the rest of the keypress arrived), stitch this continuation back onto it so
|
|
311
|
+
// it parses as one sequence. The lone ESC was already delivered as a harmless
|
|
312
|
+
// Escape; here we recover the CSI/SS3 tail that would otherwise leak as text.
|
|
313
|
+
if (this.#pendingEscape !== null) {
|
|
314
|
+
const prefix = this.#pendingEscape;
|
|
315
|
+
this.#pendingEscape = null;
|
|
316
|
+
if (
|
|
317
|
+
!str.startsWith(ESC) && // a bare tail, not a fresh escape sequence of its own
|
|
318
|
+
Date.now() - this.#pendingEscapeAt < ESCAPE_CONTINUATION_WINDOW_MS &&
|
|
319
|
+
isEscapeContinuation(prefix + str)
|
|
320
|
+
) {
|
|
321
|
+
str = prefix + str;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
282
325
|
if (str.length === 0 && this.#buffer.length === 0) {
|
|
283
326
|
this.emit("data", "");
|
|
284
327
|
return;
|
|
@@ -353,6 +396,14 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
353
396
|
for (const sequence of flushed) {
|
|
354
397
|
this.emit("data", sequence);
|
|
355
398
|
}
|
|
399
|
+
|
|
400
|
+
// Remember a flushed incomplete escape so a continuation arriving in
|
|
401
|
+
// the next read can be reassembled (see process()).
|
|
402
|
+
const last = flushed[flushed.length - 1];
|
|
403
|
+
if (last !== undefined && last.startsWith(ESC)) {
|
|
404
|
+
this.#pendingEscape = last;
|
|
405
|
+
this.#pendingEscapeAt = Date.now();
|
|
406
|
+
}
|
|
356
407
|
}, this.#timeoutMs);
|
|
357
408
|
}
|
|
358
409
|
}
|
|
@@ -380,6 +431,7 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
|
|
|
380
431
|
this.#buffer = "";
|
|
381
432
|
this.#pasteMode = false;
|
|
382
433
|
this.#pasteBuffer = "";
|
|
434
|
+
this.#pendingEscape = null;
|
|
383
435
|
}
|
|
384
436
|
|
|
385
437
|
getBuffer(): string {
|