@f5xc-salesdemos/pi-tui 19.30.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/pi-tui",
4
- "version": "19.30.2",
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.2",
41
- "@f5xc-salesdemos/pi-utils": "19.30.2",
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": {
@@ -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 {