Public: anyone can read this; the crew can change it.

forked from trevato/cs · proposes

The terminal boots headlessly: src/tty.ts takes its streams, and a session is under test #175

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`src/tty.ts` is now a seam: `run(io)` takes an input that can go raw, an output that takes strings, a resize source, an exit, an open database and the environment as one plain object typed by the members it uses — the way `src/client/main.ts` types its host — and `process` and `openDatabase()` appear only in the last lines, under `import.meta.main`, so importing the file touches no real terminal.…

shipped production is serving it deploy ae07c2d

opened this

src/tty.ts is now a seam: run(io) takes an input that can go raw, an output that takes strings, a resize source, an exit, an open database and the environment as one plain object typed by the members it uses — the way src/client/main.ts types its host — and process and openDatabase() appear only in the last lines, under import.meta.main, so importing the file touches no real terminal. Behaviour on the pty is unchanged except in one place: a draw that throws now restores raw mode, clears, prints the terminal stopped: … and exits 1, instead of leaving a raw shell behind.

src/tty.test.ts runs whole sessions headlessly against plain objects and a real SQLite database under a temporary directory: the first paint, tab cycling all three tabs, j/k stopping at both ends, r picking up a row written behind the session's back, x removing the match under the cursor with the note that names it, q leaving — and the terminal handed back exactly once on q, on ctrl-c and on a throw, asserted against the recording stream. FORGE_APP_SSH is printed verbatim when set and the session says the terminal is off when it is not, with the test checking no address is invented in its place.

The escaping test found a real hole and it is fixed: a name out of the database went into the line of ANSI unescaped, so a player called \x1b[2J cleared the screen of anyone looking at the leaderboard. src/terminal/screen.ts gains printable() — every control character becomes a middle dot, one character for one so the fixed columns still count — and every string the screen did not write itself (name, timestamp, note, environment pill, ssh command) goes through it; screen() and press() keep their signatures and all their old tests. What I did not do: no games tab (#84), no identity check on the delete (#40), and no width-aware layout — the columns are screen.ts's and do not move, so the 20- and 200-column test pins that neither extreme throws and neither changes the layout.

bun test passes (767 tests), and I ran FORGE_APP_DATA=./data bun run src/tty.ts by hand over a pipe: it paints, redraws on a key, and q clears the screen and exits 0.

🤖 Generated with Claude Code

Closes #109

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs