@gajae-code/tui 0.11.11 → 0.12.1

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/src/terminal.ts CHANGED
@@ -8,6 +8,33 @@ const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
8
8
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
9
9
  const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
10
10
 
11
+ /**
12
+ * Capability-probe reply shapes that only this layer solicits (OSC 11 background
13
+ * color, the Mode 2031 appearance DSR, and the Kitty keyboard-flags report).
14
+ * These are terminal-to-host replies and are NEVER legitimate user input, so a
15
+ * reply that arrives outside its pending-query window is dropped defensively.
16
+ *
17
+ * DA1 is deliberately absent: `Tui` issues its own DA1 request for the sixel
18
+ * probe and consumes that reply downstream.
19
+ */
20
+ export const PROBE_REPLY_PATTERNS: ReadonlyArray<{ name: string; issuedProbe: string; pattern: RegExp }> = [
21
+ {
22
+ name: "osc11-background",
23
+ issuedProbe: "\x1b]11;?\x07",
24
+ pattern: /^\x1b\]11;rgba?:[0-9a-fA-F]{1,4}\/[0-9a-fA-F]{1,4}\/[0-9a-fA-F]{1,4}(?:\x07|\x1b\\)$/,
25
+ },
26
+ { name: "mode2031-dsr", issuedProbe: "\x1b[?2031h", pattern: /^\x1b\[\?997;[12]n$/ },
27
+ { name: "kitty-flags", issuedProbe: "\x1b[?u", pattern: /^\x1b\[\?\d+u$/ },
28
+ ];
29
+
30
+ /** True when `sequence` is one of the probe replies above. */
31
+ export function isUnsolicitedProbeReply(sequence: string): boolean {
32
+ for (const entry of PROBE_REPLY_PATTERNS) {
33
+ if (entry.pattern.test(sequence)) return true;
34
+ }
35
+ return false;
36
+ }
37
+
11
38
  /**
12
39
  * Whether GJC may reprogram the keyboard with enhanced input protocols
13
40
  * (the Kitty keyboard protocol and the xterm modifyOtherKeys fallback).
@@ -222,6 +249,10 @@ export class ProcessTerminal implements Terminal {
222
249
  #privateCsiResponseBuffer = "";
223
250
  #pendingDa1Sentinels = 0;
224
251
  #osc11PollTimer?: Timer;
252
+ // Bounds the OSC 11 / DA1 pending-query window so a dropped or mangled reply
253
+ // (multiplexer, TERM=dumb host) cannot latch #osc11Pending forever and freeze
254
+ // stdin.
255
+ #osc11QueryWatchdog?: Timer;
225
256
  #mode2031DebounceTimer?: Timer;
226
257
  #progressTimer?: ReturnType<typeof setInterval>;
227
258
  #mouseEnabled = false;
@@ -318,6 +349,7 @@ export class ProcessTerminal implements Terminal {
318
349
  // When the terminal reports a change, we re-query OSC 11 to get the
319
350
  // actual background color (following Neovim convention) with 100ms debounce.
320
351
  this.#safeWrite("\x1b[?2031h");
352
+ this.#stdinBuffer?.noteProbeIssued();
321
353
 
322
354
  // Start periodic OSC 11 re-query for terminals without Mode 2031
323
355
  // (Warp, Alacritty, WezTerm, iTerm2). Self-disables once Mode 2031 fires.
@@ -415,10 +447,11 @@ export class ProcessTerminal implements Terminal {
415
447
  // flush timeout elapses mid-sequence, the prefix `\x1b[?<digits>` arrives as
416
448
  // one event and the tail `;...<terminator>` arrives as individual character
417
449
  // events that would otherwise leak into the prompt as keystrokes. See #1238.
418
- if (
419
- this.#privateCsiResponseBuffer ||
420
- (privateCsiPartialPattern.test(sequence) && this.#pendingDa1Sentinels > 0)
421
- ) {
450
+ // Reassembly is keyed on the reply's shape, not on `#pendingDa1Sentinels`:
451
+ // replies the terminal still owed after a counter reset (stop()/start()
452
+ // around a foreground command) otherwise leaked into the editor one
453
+ // character at a time. No keystroke can produce this prefix.
454
+ if (this.#privateCsiResponseBuffer || privateCsiPartialPattern.test(sequence)) {
422
455
  if (this.#privateCsiResponseBuffer && sequence.startsWith("\x1b")) {
423
456
  // New escape arrived mid-reassembly — abandon partial and re-process the new sequence.
424
457
  this.#privateCsiResponseBuffer = "";
@@ -491,7 +524,7 @@ export class ProcessTerminal implements Terminal {
491
524
  // Accumulate fragments until the BEL/ST terminator arrives, then parse once.
492
525
  // If a new escape sequence arrives (not the ST terminator), abort buffering
493
526
  // and forward it as normal input so user keystrokes are never swallowed.
494
- if (this.#osc11Pending && (this.#osc11ResponseBuffer || sequence.startsWith("\x1b]11;"))) {
527
+ if (this.#osc11ResponseBuffer || sequence.startsWith("\x1b]11;")) {
495
528
  if (this.#osc11ResponseBuffer && sequence.startsWith("\x1b") && sequence !== "\x1b\\") {
496
529
  // New escape sequence arrived mid-buffer — not an OSC 11 continuation.
497
530
  this.#osc11ResponseBuffer = "";
@@ -499,12 +532,25 @@ export class ProcessTerminal implements Terminal {
499
532
  } else {
500
533
  this.#osc11ResponseBuffer += sequence;
501
534
  const osc11Match = this.#osc11ResponseBuffer.match(osc11ResponsePattern);
502
- if (!osc11Match) return;
503
- const [, rHex, gHex, bHex] = osc11Match;
504
- this.#osc11Pending = false;
505
- this.#osc11ResponseBuffer = "";
506
- this.#handleOsc11Response(rHex!, gHex!, bHex!);
507
- return;
535
+ if (osc11Match) {
536
+ const [, rHex, gHex, bHex] = osc11Match;
537
+ this.#osc11Pending = false;
538
+ this.#osc11ResponseBuffer = "";
539
+ this.#clearOsc11QueryWatchdog();
540
+ this.#handleOsc11Response(rHex!, gHex!, bHex!);
541
+ return;
542
+ }
543
+ // Bound the reassembly buffer. A real reply is <= ~25 bytes; if the
544
+ // terminator is dropped or mangled (multiplexer, TERM=dumb) an unbounded
545
+ // buffer swallows every following keystroke and freezes input. Past the
546
+ // cap, abandon reassembly and let the sequence fall through as input.
547
+ if (this.#osc11ResponseBuffer.length > 64) {
548
+ this.#osc11Pending = false;
549
+ this.#osc11ResponseBuffer = "";
550
+ this.#clearOsc11QueryWatchdog();
551
+ } else {
552
+ return;
553
+ }
508
554
  }
509
555
  }
510
556
 
@@ -520,6 +566,13 @@ export class ProcessTerminal implements Terminal {
520
566
  }, 100);
521
567
  return;
522
568
  }
569
+ // Defensive backstop. A capability-probe reply reaching this point arrived
570
+ // outside its pending-query window, so none of the handlers above consumed
571
+ // it. These shapes are never user input, and paste content never reaches
572
+ // this handler, so dropping is always safe.
573
+ if (isUnsolicitedProbeReply(sequence)) {
574
+ return;
575
+ }
523
576
  if (this.#inputHandler) {
524
577
  this.#inputHandler(sequence);
525
578
  }
@@ -562,6 +615,39 @@ export class ProcessTerminal implements Terminal {
562
615
  this.#pendingDa1Sentinels++;
563
616
  this.#safeWrite("\x1b]11;?\x07"); // OSC 11 query (BEL terminated)
564
617
  this.#safeWrite("\x1b[c"); // DA1 sentinel
618
+ this.#stdinBuffer?.noteProbeIssued();
619
+ this.#armOsc11QueryWatchdog();
620
+ }
621
+
622
+ /**
623
+ * OSC 11 pending-query watchdog. If neither the OSC 11 reply nor its DA1
624
+ * sentinel comes back (dropped by a multiplexer or a TERM=dumb host),
625
+ * #osc11Pending / #pendingDa1Sentinels latch forever: #queryBackgroundColor
626
+ * stops re-querying and the reassembly branch swallows keystrokes.
627
+ * Force-resolve the cycle after a bounded wait so the state machine self-heals.
628
+ */
629
+ #armOsc11QueryWatchdog(): void {
630
+ this.#clearOsc11QueryWatchdog();
631
+ this.#osc11QueryWatchdog = setTimeout(() => {
632
+ this.#osc11QueryWatchdog = undefined;
633
+ if (this.#dead) return;
634
+ if (!this.#osc11Pending && this.#pendingDa1Sentinels === 0) return;
635
+ this.#osc11Pending = false;
636
+ this.#osc11ResponseBuffer = "";
637
+ this.#pendingDa1Sentinels = 0;
638
+ if (this.#osc11QueryQueued && !this.#dead) {
639
+ this.#osc11QueryQueued = false;
640
+ this.#startOsc11Query();
641
+ }
642
+ }, 1000);
643
+ this.#osc11QueryWatchdog.unref?.();
644
+ }
645
+
646
+ #clearOsc11QueryWatchdog(): void {
647
+ if (this.#osc11QueryWatchdog) {
648
+ clearTimeout(this.#osc11QueryWatchdog);
649
+ this.#osc11QueryWatchdog = undefined;
650
+ }
565
651
  }
566
652
  /**
567
653
  * Parse an OSC 11 background color response and compute BT.601 luminance.
@@ -631,6 +717,7 @@ export class ProcessTerminal implements Terminal {
631
717
  return;
632
718
  }
633
719
  this.#safeWrite("\x1b[?u");
720
+ this.#stdinBuffer?.noteProbeIssued();
634
721
  // Windows Terminal and conhost do not implement the Kitty keyboard
635
722
  // protocol, so the query above never activates it there. They do honor the
636
723
  // modifyOtherKeys fallback below — but that mode breaks Windows CJK/Hangul