fix(tui): allowed remote raw backspace opt-in

Windows Terminal identity is commonly lost across SSH and container hops,
leaving only the ambiguous raw 0x08 byte. Added the conservative
PI_TUI_RAW_BACKSPACE_IS_CTRL=1 opt-in so those sessions can map it to
ctrl+backspace without changing the default for terminals where 0x08 means
plain Backspace.

Restored the public isWindowsTerminalSession and matchesRawBackspace exports
and route the parser wrappers through matchesRawBackspace. Documented the
runtime flag and covered local WT, remote opt-in, SSH, and 0x7f behavior.

Fixes #6782
This commit is contained in:
roboomp
2026-07-27 10:27:47 +00:00
parent 6460ba042a
commit dda720af41
4 changed files with 53 additions and 20 deletions
+1
View File
@@ -407,6 +407,7 @@ These are read as runtime signals; they are usually set by the terminal/OS rathe
| ------------------------- | ------------------------------------------------------------------------------------- |
| `PI_NOTIFICATIONS` | `off` / `0` / `false` suppress desktop notifications |
| `PI_TUI_WRITE_LOG` | If set, logs TUI writes to file |
| `PI_TUI_RAW_BACKSPACE_IS_CTRL` | If `1`, interprets raw `0x08` as Ctrl+Backspace instead of Backspace; use when SSH/container hops hide a Windows Terminal client |
| `PI_HARDWARE_CURSOR` | If `1`, enables hardware cursor mode |
| `PI_NO_SYNC_OUTPUT` | If set (any non-empty value), disables DEC 2026 synchronized-output wrappers while keeping TUI autowrap guards |
| `PI_NO_DECCARA` | If set (truthy), disables Kitty DECCARA rectangular-SGR background fills (forces padded-string rendering) |
+1 -1
View File
@@ -5,7 +5,7 @@
### Fixed
- 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)).
- Restored the Windows Terminal raw `0x08` → `ctrl+backspace` disambiguation (`WT_SESSION` set, `SSH_*` unset) in the `matchesKey`/`parseKey` seam, replacing dead helpers that were no longer reachable once matching moved to the native parser ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
- 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)).
## [17.1.4] - 2026-07-26
+14 -14
View File
@@ -29,23 +29,25 @@ import {
// Platform Detection
// =============================================================================
function isWindowsTerminalSession(): boolean {
/** Whether the local process is running directly under Windows Terminal. */
export function isWindowsTerminalSession(): boolean {
return (
Boolean(process.env.WT_SESSION) && !process.env.SSH_CONNECTION && !process.env.SSH_CLIENT && !process.env.SSH_TTY
);
}
/**
* Windows Terminal encodes Ctrl+Backspace as the raw `0x08` (BS) byte and plain
* Backspace as `0x7f` (DEL), unlike terminals that send an explicit CSI-u /
* modifyOtherKeys sequence. The native parser has no environment access and
* reports both bytes as `backspace`, so the ambiguous `0x08` is remapped to
* `ctrl+backspace` here — the one layer where `WT_SESSION` is observable.
* Returns `undefined` for every other input so explicit encodings are untouched.
* Match ambiguous legacy Backspace bytes against an expected modifier mask.
*
* Windows Terminal encodes Ctrl+Backspace as raw `0x08` (BS) and plain
* Backspace as `0x7f` (DEL). Remote/container sessions lose terminal identity,
* so `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` explicitly opts into the same mapping.
*/
function windowsTerminalBackspaceOverride(data: string): KeyId | undefined {
if (data !== "\x08") return undefined;
return isWindowsTerminalSession() ? "ctrl+backspace" : undefined;
export function matchesRawBackspace(data: string, expectedModifier: number): boolean {
if (data === "\x7f") return expectedModifier === 0;
if (data !== "\x08") return false;
const rawBackspaceIsCtrl = process.env.PI_TUI_RAW_BACKSPACE_IS_CTRL === "1" || isWindowsTerminalSession();
return rawBackspaceIsCtrl ? expectedModifier === 4 : expectedModifier === 0;
}
// =============================================================================
@@ -540,8 +542,7 @@ function matchesKeypadKey(data: string, keyId: KeyId): boolean | undefined {
* @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
*/
export function matchesKey(data: string, keyId: KeyId): boolean {
const wtOverride = windowsTerminalBackspaceOverride(data);
if (wtOverride !== undefined) return wtOverride === keyId;
if (matchesRawBackspace(data, 4)) return keyId === "ctrl+backspace";
return matchesKeypadKey(data, keyId) ?? matchesKeyNative(data, keyId, kittyProtocolActive);
}
@@ -554,7 +555,6 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
* @param data - Raw input data from terminal
*/
export function parseKey(data: string): string | undefined {
const wtOverride = windowsTerminalBackspaceOverride(data);
if (wtOverride !== undefined) return wtOverride;
if (matchesRawBackspace(data, 4)) return "ctrl+backspace";
return decodeKittyKeypadText(data) ?? parseKeyNative(data, kittyProtocolActive) ?? undefined;
}
+37 -5
View File
@@ -1,5 +1,12 @@
import { describe, expect, it } from "bun:test";
import { extractPrintableText, matchesKey, parseKey, setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
import {
extractPrintableText,
isWindowsTerminalSession,
matchesKey,
matchesRawBackspace,
parseKey,
setKittyProtocolActive,
} from "@oh-my-pi/pi-tui/keys";
describe("matchesKey", () => {
it("matches ctrl+letter sequences", () => {
@@ -177,8 +184,8 @@ describe("parseKey", () => {
});
});
describe("Windows Terminal raw 0x08 backspace disambiguation", () => {
const envKeys = ["WT_SESSION", "SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"] as const;
describe("Raw 0x08 backspace disambiguation", () => {
const envKeys = ["WT_SESSION", "SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY", "PI_TUI_RAW_BACKSPACE_IS_CTRL"] as const;
function withEnv(overrides: Partial<Record<(typeof envKeys)[number], string>>, run: () => void): void {
const saved: Record<string, string | undefined> = {};
for (const key of envKeys) {
@@ -201,12 +208,18 @@ describe("Windows Terminal raw 0x08 backspace disambiguation", () => {
// Windows Terminal sends 0x08 for Ctrl+Backspace; the native parser reports
// plain `backspace` because it cannot see the environment.
withEnv({ WT_SESSION: "1" }, () => {
expect(isWindowsTerminalSession()).toBe(true);
expect(matchesRawBackspace("\x08", 4)).toBe(true);
expect(matchesRawBackspace("\x08", 0)).toBe(false);
expect(parseKey("\x08")).toBe("ctrl+backspace");
expect(matchesKey("\x08", "ctrl+backspace")).toBe(true);
expect(matchesKey("\x08", "backspace")).toBe(false);
});
// Outside Windows Terminal 0x08 stays plain backspace.
withEnv({}, () => {
expect(isWindowsTerminalSession()).toBe(false);
expect(matchesRawBackspace("\x08", 0)).toBe(true);
expect(matchesRawBackspace("\x08", 4)).toBe(false);
expect(parseKey("\x08")).toBe("backspace");
expect(matchesKey("\x08", "backspace")).toBe(true);
expect(matchesKey("\x08", "ctrl+backspace")).toBe(false);
@@ -215,12 +228,31 @@ describe("Windows Terminal raw 0x08 backspace disambiguation", () => {
it("does not apply the heuristic when WT_SESSION is forwarded over SSH", () => {
withEnv({ WT_SESSION: "1", SSH_CONNECTION: "1.2.3.4 5 6.7.8.9 22" }, () => {
expect(isWindowsTerminalSession()).toBe(false);
expect(parseKey("\x08")).toBe("backspace");
});
});
it("leaves 0x7f as plain backspace regardless of Windows Terminal", () => {
withEnv({ WT_SESSION: "1" }, () => {
it("supports an explicit opt-in when remote/container sessions lose terminal identity", () => {
withEnv(
{
PI_TUI_RAW_BACKSPACE_IS_CTRL: "1",
SSH_CONNECTION: "1.2.3.4 5 6.7.8.9 22",
},
() => {
expect(isWindowsTerminalSession()).toBe(false);
expect(matchesRawBackspace("\x08", 4)).toBe(true);
expect(matchesRawBackspace("\x08", 0)).toBe(false);
expect(parseKey("\x08")).toBe("ctrl+backspace");
expect(matchesKey("\x08", "ctrl+backspace")).toBe(true);
},
);
});
it("leaves 0x7f as plain backspace regardless of Windows Terminal or opt-in", () => {
withEnv({ WT_SESSION: "1", PI_TUI_RAW_BACKSPACE_IS_CTRL: "1" }, () => {
expect(matchesRawBackspace("\x7f", 0)).toBe(true);
expect(matchesRawBackspace("\x7f", 4)).toBe(false);
expect(parseKey("\x7f")).toBe("backspace");
expect(matchesKey("\x7f", "backspace")).toBe(true);
expect(matchesKey("\x7f", "ctrl+backspace")).toBe(false);