@oh-my-pi/pi-tui 17.1.4 → 17.1.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.6] - 2026-07-27
6
+
7
+ ### Fixed
8
+
9
+ - Fixed omp dying with an uncaught `setRawMode failed with errno: 2` instead of exiting 129 when the terminal disconnects. A recycled terminal pane revokes the pty, so the raw-mode restore in `stop()` hit an fd that is no longer a tty; the throw escaped `#markTerminalDisconnected()` and preempted its own SIGHUP. Terminal teardown on the disconnect path is now best-effort, matching `emergencyTerminalRestore()`, so the exit added in [#5837](https://github.com/can1357/oh-my-pi/pull/5837) always runs.
10
+ - Fixed the multi-line prompt editor bypassing the keybindings registry for word/line delete and yank: `ctrl+backspace` (a declared default of `tui.editor.deleteWordBackward`) never fired and `keybindings.yml` remaps of `deleteWordBackward`, `deleteWordForward`, `deleteToLineStart`, `deleteToLineEnd`, `yank`, and `yankPop` were ignored, because those actions were matched with hardcoded chords instead of `keybindings.matches(...)` like cursor motion and the single-line `Input` already do ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
11
+ - Restored the Windows Terminal raw `0x08` → `ctrl+backspace` disambiguation (`WT_SESSION` set, `SSH_*` unset) by routing the exported `matchesRawBackspace` helper through the `matchesKey`/`parseKey` seam. Remote SSH/container sessions where terminal identity is unavailable can opt in with `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
12
+ - Fixed plain Backspace deleting a whole word inside tmux/GNU screen/Zellij panes launched from Windows Terminal: multiplexers inherit `WT_SESSION` but emit raw `0x08` for plain Backspace, so the automatic raw-backspace → `ctrl+backspace` heuristic misfired. The heuristic now skips multiplexer sessions (`TMUX`/`STY`/`ZELLIJ` or `TERM` starting with `tmux`/`screen`); `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` remains the explicit opt-in everywhere ([#6784](https://github.com/can1357/oh-my-pi/pull/6784)).
13
+
5
14
  ## [17.1.4] - 2026-07-26
6
15
 
7
16
  ### Fixed
@@ -18,18 +18,19 @@
18
18
  * - isKittyProtocolActive() - Query global Kitty protocol state
19
19
  */
20
20
  import type { KeyEventType } from "@oh-my-pi/pi-natives";
21
- declare function isWindowsTerminalSession(): boolean;
21
+ /** Whether the local process is running directly under Windows Terminal. */
22
+ export declare function isWindowsTerminalSession(): boolean;
22
23
  /**
23
- * Raw 0x08 (BS) is ambiguous in legacy terminals.
24
+ * Match ambiguous legacy Backspace bytes against an expected modifier mask.
24
25
  *
25
- * - Windows Terminal uses it for Ctrl+Backspace.
26
- * - Some legacy terminals and tmux setups send it for plain Backspace.
27
- *
28
- * Prefer explicit Kitty / CSI-u / modifyOtherKeys sequences whenever they are
29
- * available. Fall back to a Windows Terminal heuristic only for raw BS bytes.
26
+ * Windows Terminal encodes Ctrl+Backspace as raw `0x08` (BS) and plain
27
+ * Backspace as `0x7f` (DEL). Remote/container sessions lose terminal identity,
28
+ * and multiplexers (tmux/screen/Zellij) inherit `WT_SESSION` while emitting
29
+ * raw `0x08` for plain Backspace themselves, so the automatic heuristic is
30
+ * limited to direct Windows Terminal sessions. `PI_TUI_RAW_BACKSPACE_IS_CTRL=1`
31
+ * explicitly opts into the mapping everywhere.
30
32
  */
31
- declare function matchesRawBackspace(data: string, expectedModifier: number): boolean;
32
- export { isWindowsTerminalSession, matchesRawBackspace };
33
+ export declare function matchesRawBackspace(data: string, expectedModifier: number): boolean;
33
34
  /**
34
35
  * Set the global Kitty keyboard protocol state.
35
36
  * Called by ProcessTerminal after detecting protocol support.
@@ -206,3 +207,4 @@ export declare function matchesKey(data: string, keyId: KeyId): boolean;
206
207
  * @param data - Raw input data from terminal
207
208
  */
208
209
  export declare function parseKey(data: string): string | undefined;
210
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.1.4",
4
+ "version": "17.1.6",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.1.4",
41
- "@oh-my-pi/pi-utils": "17.1.4",
40
+ "@oh-my-pi/pi-natives": "17.1.6",
41
+ "@oh-my-pi/pi-utils": "17.1.6",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
@@ -1327,38 +1327,31 @@ export class Editor implements Component, Focusable {
1327
1327
  }
1328
1328
 
1329
1329
  // Continue with rest of input handling
1330
- // Ctrl+K - Delete to end of line
1331
- if (matchesKey(data, "ctrl+k")) {
1330
+ // Delete to end of line
1331
+ if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
1332
1332
  this.#deleteToEndOfLine();
1333
1333
  }
1334
- // Ctrl+U - Delete to start of line
1335
- else if (matchesKey(data, "ctrl+u")) {
1334
+ // Delete to start of line
1335
+ else if (kb.matches(data, "tui.editor.deleteToLineStart")) {
1336
1336
  this.#deleteToStartOfLine();
1337
1337
  }
1338
- // Ctrl+W - Delete word backwards
1339
- else if (matchesKey(data, "ctrl+w")) {
1338
+ // Delete word backward. Registry defaults cover ctrl+w, alt+backspace,
1339
+ // ctrl+backspace, and super+alt+backspace (Ghostty on macOS reports
1340
+ // Option+Backspace as super+alt — kitty mod 11, see #2064).
1341
+ else if (kb.matches(data, "tui.editor.deleteWordBackward")) {
1340
1342
  this.#deleteWordBackwards();
1341
1343
  }
1342
- // Option/Alt+Backspace - Delete word backwards.
1343
- // Ghostty on macOS reports Option+Backspace as super+alt (kitty mod 11) see #2064.
1344
- else if (matchesKey(data, "alt+backspace") || matchesKey(data, "super+alt+backspace")) {
1345
- this.#deleteWordBackwards();
1346
- }
1347
- // Option/Alt+D and Option+Delete - Delete word forwards. Same Ghostty quirk applies.
1348
- else if (
1349
- matchesKey(data, "alt+d") ||
1350
- matchesKey(data, "alt+delete") ||
1351
- matchesKey(data, "super+alt+d") ||
1352
- matchesKey(data, "super+alt+delete")
1353
- ) {
1344
+ // Delete word forward. Registry defaults cover alt+d/alt+delete and their
1345
+ // super+alt variants for the same Ghostty quirk.
1346
+ else if (kb.matches(data, "tui.editor.deleteWordForward")) {
1354
1347
  this.#deleteWordForwards();
1355
1348
  }
1356
- // Ctrl+Y - Yank from kill ring
1357
- else if (matchesKey(data, "ctrl+y")) {
1349
+ // Yank from kill ring
1350
+ else if (kb.matches(data, "tui.editor.yank")) {
1358
1351
  this.#yankFromKillRing();
1359
1352
  }
1360
- // Alt+Y - Yank-pop (cycle kill ring)
1361
- else if (matchesKey(data, "alt+y")) {
1353
+ // Yank-pop (cycle kill ring)
1354
+ else if (kb.matches(data, "tui.editor.yankPop")) {
1362
1355
  this.#yankPop();
1363
1356
  }
1364
1357
  // Ctrl+A - Move to start of line
package/src/keys.ts CHANGED
@@ -24,35 +24,38 @@ import {
24
24
  parseKey as parseKeyNative,
25
25
  parseKittySequence as parseKittySequenceNative,
26
26
  } from "@oh-my-pi/pi-natives";
27
+ import { isInsideTerminalMultiplexer } from "./terminal-capabilities";
27
28
 
28
29
  // =============================================================================
29
30
  // Platform Detection
30
31
  // =============================================================================
31
32
 
32
- function isWindowsTerminalSession(): boolean {
33
+ /** Whether the local process is running directly under Windows Terminal. */
34
+ export function isWindowsTerminalSession(): boolean {
33
35
  return (
34
36
  Boolean(process.env.WT_SESSION) && !process.env.SSH_CONNECTION && !process.env.SSH_CLIENT && !process.env.SSH_TTY
35
37
  );
36
38
  }
37
39
 
38
40
  /**
39
- * Raw 0x08 (BS) is ambiguous in legacy terminals.
41
+ * Match ambiguous legacy Backspace bytes against an expected modifier mask.
40
42
  *
41
- * - Windows Terminal uses it for Ctrl+Backspace.
42
- * - Some legacy terminals and tmux setups send it for plain Backspace.
43
- *
44
- * Prefer explicit Kitty / CSI-u / modifyOtherKeys sequences whenever they are
45
- * available. Fall back to a Windows Terminal heuristic only for raw BS bytes.
43
+ * Windows Terminal encodes Ctrl+Backspace as raw `0x08` (BS) and plain
44
+ * Backspace as `0x7f` (DEL). Remote/container sessions lose terminal identity,
45
+ * and multiplexers (tmux/screen/Zellij) inherit `WT_SESSION` while emitting
46
+ * raw `0x08` for plain Backspace themselves, so the automatic heuristic is
47
+ * limited to direct Windows Terminal sessions. `PI_TUI_RAW_BACKSPACE_IS_CTRL=1`
48
+ * explicitly opts into the mapping everywhere.
46
49
  */
47
- function matchesRawBackspace(data: string, expectedModifier: number): boolean {
50
+ export function matchesRawBackspace(data: string, expectedModifier: number): boolean {
48
51
  if (data === "\x7f") return expectedModifier === 0;
49
52
  if (data !== "\x08") return false;
50
- // On Windows Terminal, 0x08 = Ctrl+Backspace. On others, it's plain Backspace.
51
- return isWindowsTerminalSession() ? expectedModifier === 4 : expectedModifier === 0;
53
+ const rawBackspaceIsCtrl =
54
+ process.env.PI_TUI_RAW_BACKSPACE_IS_CTRL === "1" ||
55
+ (isWindowsTerminalSession() && !isInsideTerminalMultiplexer(process.env));
56
+ return rawBackspaceIsCtrl ? expectedModifier === 4 : expectedModifier === 0;
52
57
  }
53
58
 
54
- export { isWindowsTerminalSession, matchesRawBackspace };
55
-
56
59
  // =============================================================================
57
60
  // Global Kitty Protocol State
58
61
  // =============================================================================
@@ -545,6 +548,7 @@ function matchesKeypadKey(data: string, keyId: KeyId): boolean | undefined {
545
548
  * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
546
549
  */
547
550
  export function matchesKey(data: string, keyId: KeyId): boolean {
551
+ if (matchesRawBackspace(data, 4)) return keyId === "ctrl+backspace";
548
552
  return matchesKeypadKey(data, keyId) ?? matchesKeyNative(data, keyId, kittyProtocolActive);
549
553
  }
550
554
 
@@ -557,5 +561,6 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
557
561
  * @param data - Raw input data from terminal
558
562
  */
559
563
  export function parseKey(data: string): string | undefined {
564
+ if (matchesRawBackspace(data, 4)) return "ctrl+backspace";
560
565
  return decodeKittyKeypadText(data) ?? parseKeyNative(data, kittyProtocolActive) ?? undefined;
561
566
  }
package/src/terminal.ts CHANGED
@@ -1462,9 +1462,15 @@ export class ProcessTerminal implements Terminal {
1462
1462
  // where Ctrl+D could close the parent shell over SSH.
1463
1463
  process.stdin.pause();
1464
1464
 
1465
- // Restore raw mode state
1466
- if (process.stdin.setRawMode) {
1467
- process.stdin.setRawMode(this.#wasRaw);
1465
+ // Restore raw mode state. On a disconnected terminal (pane recycled, ssh
1466
+ // dropped) the fd is no longer a tty and Bun's node:tty shim throws; there
1467
+ // is nothing left to restore, and throwing would abort the caller. On a
1468
+ // live terminal the failure still surfaces - swallowing it would silently
1469
+ // leave stdin in raw mode.
1470
+ try {
1471
+ process.stdin.setRawMode?.(this.#wasRaw);
1472
+ } catch (err) {
1473
+ if (!this.#dead) throw err;
1468
1474
  }
1469
1475
  this.#stdoutErrorCleanup?.();
1470
1476
  this.#stdoutErrorCleanup = undefined;
@@ -1482,7 +1488,14 @@ export class ProcessTerminal implements Terminal {
1482
1488
  const disconnectHandler = this.#disconnectHandler;
1483
1489
  this.#disconnectHandler = undefined;
1484
1490
  if (!disconnectHandler) return;
1485
- disconnectHandler();
1491
+ // The handler tears the TUI down against a terminal that is already gone,
1492
+ // so any step in it can fail. Swallow that: the exit below is the whole
1493
+ // point of this method and must not be preempted by teardown noise.
1494
+ try {
1495
+ disconnectHandler();
1496
+ } catch (handlerErr) {
1497
+ logger.error("Terminal disconnect handler failed; exiting anyway", { err: handlerErr });
1498
+ }
1486
1499
 
1487
1500
  if (process.platform === "win32") {
1488
1501
  void postmortem.quit(129);