This repository has no description
0

Configure Feed

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

Expose PtyHandle.readWrappedFlags (xterm isWrapped per row)

Nathan Herald (Apr 17, 2026, 4:51 PM +0200) d584e4e4 8e801067

+104 -3
+1
CHANGELOG.md
··· 4 4 5 5 ### Client API 6 6 - Add `env?: Record<string, string>` to `SpawnDaemonOptions` and `ServerOptions` — use this to spawn a session child with a verbatim environment (no inheritance from the daemon's `process.env`, no allow-list). `PTY_SESSION` is always injected on top so nesting detection and `pty exec` keep working. Mutually exclusive with `isolateEnv` / `extraEnv` (throws at daemon startup if both are passed). Requested by the pty-layout dev to spawn a launcher shell with a shim `tmux` on `PATH`, a custom `TMUX` marker, and a filter-tag env var. 7 + - Add `PtyHandle.readWrappedFlags(scrollOffset?): boolean[]` — per-row flags aligned with `readCells`, where `true` means the row continues the previous row because xterm wrapped a long line (not because the child emitted `\n`). Intended for consumers reconstructing logical lines from a visually-multi-row text selection (e.g., copying a wrapped URL to the clipboard without a spurious newline). Passes through xterm.js's `IBufferLine.isWrapped`; works for both `createPty` embedded terminals and `attachPty` remote sessions since the serialize/replay path preserves wrap state. 7 8 8 9 ## 0.9.0 9 10
+30
src/tui/builders.ts
··· 366 366 return grid; 367 367 } 368 368 369 + /** Read per-row `isWrapped` flags aligned with `readXtermCells(scrollOffset)`. 370 + * A `true` at index `r` means that row continues the previous row because 371 + * xterm wrapped a long line (not because the child emitted `\n`). Used by 372 + * consumers reconstructing logical lines from a visually-multi-row 373 + * selection — e.g., copying a wrapped URL without a spurious newline. */ 374 + function readXtermWrappedFlags( 375 + terminal: any, 376 + rows: number, 377 + scrollOffset: number = 0, 378 + ): boolean[] { 379 + const buf = terminal.buffer.active; 380 + const baseY = buf.baseY as number; 381 + const startLine = Math.max(0, baseY - scrollOffset); 382 + const flags: boolean[] = new Array(rows); 383 + for (let r = 0; r < rows; r++) { 384 + const lineIdx = startLine + r; 385 + const line = lineIdx < buf.length ? buf.getLine(lineIdx) : null; 386 + flags[r] = !!(line?.isWrapped); 387 + } 388 + return flags; 389 + } 390 + 369 391 export function createPty( 370 392 command: string, 371 393 args: string[] = [], ··· 471 493 472 494 readCells(scrollOffset?: number) { 473 495 return readXtermCells(terminal, handle.rows, handle.cols, scrollOffset ?? 0); 496 + }, 497 + 498 + readWrappedFlags(scrollOffset?: number) { 499 + return readXtermWrappedFlags(terminal, handle.rows, scrollOffset ?? 0); 474 500 }, 475 501 476 502 kill() { ··· 607 633 608 634 readCells(scrollOffset?: number) { 609 635 return readXtermCells(terminal, handle.rows, handle.cols, scrollOffset ?? 0); 636 + }, 637 + 638 + readWrappedFlags(scrollOffset?: number) { 639 + return readXtermWrappedFlags(terminal, handle.rows, scrollOffset ?? 0); 610 640 }, 611 641 612 642 kill() {
+13
src/tui/nodes.ts
··· 239 239 * @param scrollOffset Lines to scroll back into history (0 = live viewport). 240 240 */ 241 241 readCells(scrollOffset?: number): { char: string; fg: [number, number, number] | null; bg: [number, number, number] | null; bold: boolean; dim: boolean; italic: boolean; underline: boolean }[][]; 242 + /** 243 + * Read per-row "wrapped" flags aligned with the rows returned by 244 + * `readCells(scrollOffset)`. A `true` at index `r` means row `r` 245 + * continues from row `r-1` because the terminal wrapped a long line 246 + * rather than the child emitting a real newline — the same signal 247 + * xterm.js exposes via `IBufferLine.isWrapped`. 248 + * 249 + * Intended use: reconstructing logical lines from a multi-row text 250 + * selection (e.g., copying a wrapped URL to the clipboard without a 251 + * spurious `\n` in the middle). 252 + * @param scrollOffset Lines to scroll back into history (0 = live viewport). 253 + */ 254 + readWrappedFlags(scrollOffset?: number): boolean[]; 242 255 /** Current PTY dimensions. */ 243 256 cols: number; 244 257 rows: number;
+5 -3
tests/integration.test.ts
··· 1049 1049 } 1050 1050 }); 1051 1051 1052 - // Send items with 200ms delay between each 1053 - const { send } = await import("../src/client.ts"); 1054 - send({ name, data: ["A", "B", "C"], delayMs: 200 }); 1052 + // Send items with 200ms delay between each. Use the programmatic 1053 + // `sendData` (Promise-returning, no process.exit) — the CLI `send` 1054 + // shape is for binaries, not tests. 1055 + const { sendData } = await import("../src/connection.ts"); 1056 + void sendData({ name, data: ["A", "B", "C"], delayMs: 200 }); 1055 1057 1056 1058 // Wait for all items to arrive 1057 1059 await new Promise((r) => setTimeout(r, 1000));
+55
tests/pty-handle.test.ts
··· 177 177 }); 178 178 }); 179 179 180 + // --- readWrappedFlags --- 181 + 182 + describe("readWrappedFlags", () => { 183 + it("returns one flag per visible row", () => { 184 + const h = spawn("cat", [], { rows: 10, cols: 40 }); 185 + const flags = h.readWrappedFlags(); 186 + expect(flags).toHaveLength(10); 187 + expect(flags.every((f) => typeof f === "boolean")).toBe(true); 188 + }); 189 + 190 + it("marks continuation rows as wrapped when a long line overflows", async () => { 191 + // 120-char line at 40-col terminal → wraps to 3 rows. First row is 192 + // not wrapped (it's the start of the line); the next two rows are 193 + // continuations and should both be flagged. 194 + const h = spawn("bash", ["-c", "printf '%0.sa' {1..120}; sleep 5"], { 195 + rows: 12, cols: 40, 196 + }); 197 + await waitFor(h, () => h.readWrappedFlags().some((f) => f === true)); 198 + const flags = h.readWrappedFlags(); 199 + // First row: start of the logical line, not a wrap continuation. 200 + expect(flags[0]).toBe(false); 201 + // Next two rows: wrapped continuations. 202 + expect(flags[1]).toBe(true); 203 + expect(flags[2]).toBe(true); 204 + // Remaining empty rows after the wrapped content: not wrapped. 205 + expect(flags[3]).toBe(false); 206 + }); 207 + 208 + it("short lines followed by a newline produce no wrapped flags", async () => { 209 + const h = spawn("bash", ["-c", "printf 'short\\n'; sleep 5"], { 210 + rows: 8, cols: 40, 211 + }); 212 + await waitFor(h, () => h.cursorRow >= 1); 213 + const flags = h.readWrappedFlags(); 214 + expect(flags.every((f) => f === false)).toBe(true); 215 + }); 216 + 217 + it("scrollOffset shifts the window in sync with readCells", async () => { 218 + // Emit many short lines to push earlier content into scrollback, 219 + // then read with a nonzero offset and confirm flags array length 220 + // still matches readCells rows. 221 + const h = spawn("bash", ["-c", "for i in $(seq 1 30); do echo line $i; done; sleep 5"], { 222 + rows: 10, cols: 40, scrollback: 100, 223 + }); 224 + await waitFor(h, () => h.bufferLength > h.rows); 225 + const flags0 = h.readWrappedFlags(0); 226 + const cells0 = h.readCells(0); 227 + expect(flags0.length).toBe(cells0.length); 228 + 229 + const flags5 = h.readWrappedFlags(5); 230 + const cells5 = h.readCells(5); 231 + expect(flags5.length).toBe(cells5.length); 232 + }); 233 + }); 234 + 180 235 // --- scrollback --- 181 236 182 237 describe("scrollback", () => {