This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Add alternateScreen and kittyKeyboardFlags to PtyHandle

Hosts embedding PtyHandle (e.g. pty-layout) now have read-only access to
two modes they need to proxy to the outer terminal: whether the child is
in the alternate screen buffer (for scroll-as-arrow-keys fallback), and
the kitty keyboard protocol flag stack (so Shift+Enter and other
modified keys can be forwarded to the focused pane).

alternateScreen reads xterm's buffer.active.type directly. The serialize
addon already captures the alt buffer and emits the mode switch on
replay, so no server-side getModePrefix change is needed.

Nathan Herald (Apr 15, 2026, 11:12 PM +0200) 1112ad4e 5281cfbf

+106
+1
CHANGELOG.md
··· 21 21 - Export `extractFilterTags` and `matchesAllTags` from `@myobie/pty/client` so third-party tools (e.g., pty-relay) can accept and apply the same `--filter-tag key=value` syntax 22 22 - Add optional `tags?: Record<string, string>` on remote session entries so pty-relay can surface tags in `ls --json` and have the interactive TUI filter remote sessions by them 23 23 - Add `launcher?: { command: string; args?: string[] }` to `SpawnDaemonOptions` so non-Node callers (Bun, Deno) can route the detached daemon launch through a Node binary — the daemon needs Node to load the `node-pty` native addon (closes #17) 24 + - Add `PtyHandle.alternateScreen: boolean` and `PtyHandle.kittyKeyboardFlags: number[]` so hosts like pty-layout can proxy alternate-screen state and kitty keyboard protocol enable/disable to the outer terminal (for Shift+Enter and friends in the focused pane) 24 25 25 26 ### Project files 26 27 - `pty up` now removes tags that were removed from `pty.toml` — toml-managed tag keys are tracked in a `ptyfile.tags` meta tag so manually-added tags (set via `pty tag`) are preserved
+41
src/tui/builders.ts
··· 409 409 }, 410 410 ); 411 411 412 + // Track kitty keyboard protocol flag stack 413 + const kittyKeyboardStack: number[] = []; 414 + terminal.parser.registerCsiHandler( 415 + { prefix: ">", final: "u" }, 416 + (params: any) => { 417 + const p = params[0]; 418 + kittyKeyboardStack.push(typeof p === "number" ? p : p[0]); 419 + return false; 420 + }, 421 + ); 422 + terminal.parser.registerCsiHandler( 423 + { prefix: "<", final: "u" }, 424 + () => { 425 + kittyKeyboardStack.pop(); 426 + return false; 427 + }, 428 + ); 429 + 412 430 const childEnv: Record<string, string> = { 413 431 ...(process.env as Record<string, string>), 414 432 ...opts?.env, ··· 477 495 get cursorRow() { return terminal.buffer.active.cursorY; }, 478 496 get cursorCol() { return terminal.buffer.active.cursorX; }, 479 497 get mouseMode() { return mouseMode; }, 498 + get alternateScreen() { return terminal.buffer.active.type === "alternate"; }, 499 + get kittyKeyboardFlags() { return [...kittyKeyboardStack]; }, 480 500 get scrollback() { return scrollbackSize; }, 481 501 get bufferLength() { return terminal.buffer.active.length; }, 482 502 get baseY() { return terminal.buffer.active.baseY; }, ··· 539 559 return false; 540 560 }, 541 561 ); 562 + 563 + // Track kitty keyboard protocol flag stack 564 + const kittyKeyboardStack: number[] = []; 565 + terminal.parser.registerCsiHandler( 566 + { prefix: ">", final: "u" }, 567 + (params: any) => { 568 + const p = params[0]; 569 + kittyKeyboardStack.push(typeof p === "number" ? p : p[0]); 570 + return false; 571 + }, 572 + ); 573 + terminal.parser.registerCsiHandler( 574 + { prefix: "<", final: "u" }, 575 + () => { 576 + kittyKeyboardStack.pop(); 577 + return false; 578 + }, 579 + ); 580 + 542 581 const socketPath = getSocketPath(name); 543 582 544 583 // Connect to the daemon socket ··· 592 631 get cursorRow() { return terminal.buffer.active.cursorY; }, 593 632 get cursorCol() { return terminal.buffer.active.cursorX; }, 594 633 get mouseMode() { return mouseMode; }, 634 + get alternateScreen() { return terminal.buffer.active.type === "alternate"; }, 635 + get kittyKeyboardFlags() { return [...kittyKeyboardStack]; }, 595 636 get scrollback() { return scrollbackSize; }, 596 637 get bufferLength() { return terminal.buffer.active.length; }, 597 638 get baseY() { return terminal.buffer.active.baseY; },
+6
src/tui/nodes.ts
··· 258 258 readonly cursorCol: number; 259 259 /** Whether the child process has enabled mouse tracking (modes 1000/1002/1003). */ 260 260 readonly mouseMode: boolean; 261 + /** Whether the child is using the alternate screen buffer (mode 1049). */ 262 + readonly alternateScreen: boolean; 263 + /** Stack of kitty keyboard protocol flags pushed by the child (CSI > N u 264 + * pushes, CSI < u pops). Empty array means the protocol is not active. 265 + * Returned as a defensive copy — mutating it has no effect on the PTY. */ 266 + readonly kittyKeyboardFlags: number[]; 261 267 /** Configured scrollback line count. */ 262 268 readonly scrollback: number; 263 269 /** Total lines in the buffer (viewport + scrollback history). */
+58
tests/pty-handle.test.ts
··· 119 119 }); 120 120 }); 121 121 122 + // --- alternateScreen --- 123 + 124 + describe("alternateScreen", () => { 125 + it("is false by default (primary buffer)", () => { 126 + const h = spawn("cat"); 127 + expect(h.alternateScreen).toBe(false); 128 + }); 129 + 130 + it("becomes true when child enters alternate screen (mode 1049)", async () => { 131 + const h = spawn("bash", ["-c", "printf '\\x1b[?1049h'; sleep 10"]); 132 + await waitFor(h, () => h.alternateScreen === true); 133 + expect(h.alternateScreen).toBe(true); 134 + }); 135 + 136 + it("becomes false when child leaves alternate screen", async () => { 137 + const h = spawn("bash", ["-c", "printf '\\x1b[?1049h'; sleep 0.1; printf '\\x1b[?1049l'; sleep 10"]); 138 + await waitFor(h, () => h.alternateScreen === true); 139 + await waitFor(h, () => h.alternateScreen === false); 140 + expect(h.alternateScreen).toBe(false); 141 + }); 142 + }); 143 + 144 + // --- kittyKeyboardFlags --- 145 + 146 + describe("kittyKeyboardFlags", () => { 147 + it("is empty by default", () => { 148 + const h = spawn("cat"); 149 + expect(h.kittyKeyboardFlags).toEqual([]); 150 + }); 151 + 152 + it("tracks a single pushed flag", async () => { 153 + const h = spawn("bash", ["-c", "printf '\\x1b[>7u'; sleep 10"]); 154 + await waitFor(h, () => h.kittyKeyboardFlags.length > 0); 155 + expect(h.kittyKeyboardFlags).toEqual([7]); 156 + }); 157 + 158 + it("tracks multiple nested pushes", async () => { 159 + const h = spawn("bash", ["-c", "printf '\\x1b[>1u\\x1b[>15u'; sleep 10"]); 160 + await waitFor(h, () => h.kittyKeyboardFlags.length === 2); 161 + expect(h.kittyKeyboardFlags).toEqual([1, 15]); 162 + }); 163 + 164 + it("pops the most recent flag on CSI < u", async () => { 165 + // Sequences land together — just wait for the final state after the pop. 166 + const h = spawn("bash", ["-c", "printf '\\x1b[>1u\\x1b[>15u\\x1b[<u'; sleep 10"]); 167 + await waitFor(h, () => h.kittyKeyboardFlags.length === 1); 168 + expect(h.kittyKeyboardFlags).toEqual([1]); 169 + }); 170 + 171 + it("returns a defensive copy (mutation does not affect internal state)", async () => { 172 + const h = spawn("bash", ["-c", "printf '\\x1b[>7u'; sleep 10"]); 173 + await waitFor(h, () => h.kittyKeyboardFlags.length > 0); 174 + const snapshot = h.kittyKeyboardFlags; 175 + snapshot.push(999); 176 + expect(h.kittyKeyboardFlags).toEqual([7]); 177 + }); 178 + }); 179 + 122 180 // --- scrollback --- 123 181 124 182 describe("scrollback", () => {