@tn3270/gui 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +50 -2
- package/app/BUNDLED +1 -0
- package/app/atlas.bin +0 -0
- package/app/atlas.json +1 -0
- package/app/connect.html +27 -0
- package/app/connectBoot.js +64 -0
- package/app/connectBoot.js.map +7 -0
- package/app/connectPreload.cjs +19 -0
- package/app/connectPreload.cjs.map +7 -0
- package/app/icon.png +0 -0
- package/app/index.html +6 -0
- package/app/keyboard.html +10 -0
- package/app/keyboardBoot.js +799 -0
- package/app/keyboardBoot.js.map +7 -0
- package/app/keyboardPreload.cjs +18 -0
- package/app/keyboardPreload.cjs.map +7 -0
- package/app/keypad.html +10 -0
- package/app/keypadBoot.js +501 -0
- package/app/keypadBoot.js.map +7 -0
- package/app/keypadPreload.cjs +18 -0
- package/app/keypadPreload.cjs.map +7 -0
- package/app/main.js +11808 -0
- package/app/main.js.map +7 -0
- package/app/package.json +9 -0
- package/app/preload.cjs +37 -0
- package/app/preload.cjs.map +7 -0
- package/app/renderer.js +559 -0
- package/app/renderer.js.map +7 -0
- package/app/transfer.html +15 -0
- package/app/transferBoot.js +531 -0
- package/app/transferBoot.js.map +7 -0
- package/app/transferPreload.cjs +33 -0
- package/app/transferPreload.cjs.map +7 -0
- package/app/ui.css +843 -0
- package/bin/launch.d.mts +10 -0
- package/bin/launch.mjs +56 -0
- package/bin/tn3270-gui.mjs +97 -0
- package/package.json +29 -4
package/app/main.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/main.ts", "../../core/src/constants.ts", "../../core/src/devname.ts", "../../core/src/newenviron.ts", "../../core/src/address.ts", "../../core/src/codepages/cp037.ts", "../../core/src/codepage.ts", "../../core/src/palette.ts", "../../core/src/render.ts", "../../core/src/trace.ts", "../../core/src/telnet.ts", "../../core/src/screen.ts", "../../core/src/bind.ts", "../../core/src/stream/sf.ts", "../../core/src/stream/parse.ts", "../../core/src/stream/execute.ts", "../../core/src/queryreply.ts", "../../core/src/termtype.ts", "../../core/src/geometry.ts", "../../core/src/inbound.ts", "../../core/src/oia.ts", "../../core/src/keyboard.ts", "../../core/src/tn3270e.ts", "../../core/src/session.ts", "../../core/src/ft/cut.ts", "../../core/src/ft/frames.ts", "../../core/src/ft/detect.ts", "../../core/src/ft/transfer.ts", "../../core/src/ft/dftFrames.ts", "../../core/src/ft/dft.ts", "../../frontend/src/hostspec.ts", "../../frontend/src/tls.ts", "../../frontend/src/oversizeFlag.ts", "../../frontend/src/geometryChoice.ts", "../../frontend/src/session.ts", "../../frontend/src/keymap.ts", "../../frontend/src/keypad.ts", "../../frontend/src/bindings.ts", "../../frontend/src/keypadView.ts", "../../frontend/src/actions.ts", "../../frontend/src/paste.ts", "../../frontend/src/pasteAction.ts", "../../frontend/src/palette.ts", "../../frontend/src/transfer.ts", "../../frontend/src/transferRun.ts", "../../frontend/src/transferForm.ts", "../../frontend/src/keyboard3279.ts", "../../node-files/src/index.ts", "../../canvas/src/cg.ts", "../../canvas/src/drawlist.ts", "../../canvas/src/blit.ts", "../../canvas/src/keys.ts", "../../canvas/src/assets.ts", "../../canvas/src/selection.ts", "../src/args.ts", "../src/connectForm.ts", "../src/connectDialog.ts", "../src/measure.ts", "../src/keyspec.ts", "../src/menu.ts", "../src/transferWindow.ts", "../src/appRoot.ts", "../src/electronSwitches.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n app, BrowserWindow, clipboard, dialog, globalShortcut, ipcMain, Menu, screen,\n} from 'electron';\nimport { fileURLToPath } from 'node:url';\nimport { writeFileSync, readFileSync } from 'node:fs';\nimport { createHash } from 'node:crypto';\nimport { dirname } from 'node:path';\nimport {\n resolve, OversizeError, TerminalTypeError, KNOWN_MODELS,\n type GeometrySetting, type Session, type Size,\n} from '@tn3270/core';\nimport {\n applyAction, applyGeometryChoice, applyPaste, describeConnectError, describeTlsError, resolveScheme,\n startTransfer, transferCommand, DEFAULT_TLS, type Action, type TlsRemedy,\n} from '@tn3270/frontend';\nimport { nodeTransferFiles } from '@tn3270/node-files';\nimport { drawList, blankColumns, extractText, readAtlas } from '@tn3270/canvas';\nimport { buildSession, printGeometryNotices, UsageError, type GuiArgs } from './args.js';\nimport { connectModels, parseConnectSeam, planStartup, submitConnect } from './connectDialog.js';\nimport { measureAuto, windowScale } from './measure.js';\nimport { parseKeySpec, type KeySpec } from './keyspec.js';\nimport { buildMenuTemplate, modelMenuStates, applyChoiceStates, type GeometryMenu } from './menu.js';\nimport { createTransferController } from './transferWindow.js';\nimport { layoutFor } from './appRoot.js';\nimport { stripElectronSwitches } from './electronSwitches.js';\n\n/**\n * Electron main: the Session, the socket and the window live here.\n *\n * CTRL-] QUITS, AND THAT IS NOT A STYLE CHOICE. Ctrl-C is the Clear AID -- a 3270 user\n * needs it constantly to dismiss VM's MORE... state -- so the usual instinct for escaping\n * cannot be the way out, and an undocumented alternative is no alternative. The TUI has\n * the same binding; `BINDING_INTENT` in `@tn3270/frontend` records it for both.\n *\n * ## THREE THINGS THAT ARE NOT NEGOTIABLE ON THIS BOX\n *\n * Measured 2026-08-28, written up in docs/live-testing.md under *Electron headless\n * re-verification*:\n *\n * - `--no-sandbox`: Chromium's sandbox wants privileges this box does not grant.\n * - `--disable-gpu`: there is no GL at all (`GLX is not present`), and WITHOUT this a\n * `show: false` window HANGS rather than failing -- a stall, not an error, the same shape\n * as the TLS trap.\n * - `useContentSize: true`, below: `capturePage()` returns the CONTENT area, so a 400x200\n * window otherwise yields a 400x173 image and every screenshot golden silently depends on\n * window chrome height, which will not reproduce on a Mac.\n *\n * ## FAILURES GO IN THE WINDOW, NOT ONLY TO stderr\n *\n * Someone who double-clicked a `.app` never sees a console. `describeTlsError` exists so\n * every TLS failure names the flag that fixes it -- and against Hercules the common case is\n * a plaintext host, where a default-TLS attempt HANGS rather than refusing, so the message\n * pointing at `-insecure` is the difference between a diagnosis and a mystery.\n */\nconst here = dirname(fileURLToPath(import.meta.url));\n/**\n * Where this run's documents, preloads and atlas are. EVERY file this process hands to Electron\n * goes through `layout`, never through `join(here, ...)` -- the unbundled `dist/` and the esbuild\n * bundle `app/` disagree about where the HTML lives, and one call site left on `join` would be a\n * window that loads in the monorepo and is blank in the shipped app. `appRoot.ts` has the rule.\n */\nconst layout = layoutFor(here);\n\n// `ELECTRON_SWITCHES` and the `--user-data-dir=` rule: electronSwitches.ts.\n\n/**\n * The test seams, read ONCE for the whole process.\n *\n * THREE PLACES ASK \"IS THE KEYS SEAM ACTIVE?\" -- the action log's privacy gate,\n * `maybeSendKeys` and `quitIfKeysOnly`. When each re-read `process.env` with its own inline\n * emptiness check they agreed only by coincidence: a later edit to what counts as empty\n * (treating `' '` as unset, say) would have moved one gate and left the others, and one of\n * them is the gate that keeps a typed password off stdout. The environment cannot change\n * under a running process, so reading it once is not a cache with an invalidation problem --\n * the action log already relied on exactly that.\n *\n * EMPTY STRING MEANS ABSENT throughout, so `TN3270_GUI_SHOT=` behaves as not setting it.\n */\nconst SEAM = Object.freeze({\n /** Chord specs for `maybeSendKeys`, comma-separated Accelerator names. */\n keys: process.env['TN3270_GUI_KEYS'] ?? '',\n /** A recorded trace to paint instead of dialling a host. */\n replay: process.env['TN3270_GUI_REPLAY'] ?? '',\n /** Where `maybeCapture` writes its PNG. */\n shot: process.env['TN3270_GUI_SHOT'] ?? '',\n /** How long to let the host paint before capturing. */\n shotMs: Number(process.env['TN3270_GUI_SHOT_MS'] ?? '2500'),\n /** How long to let the host paint before typing -- a FLOOR applies; see `maybeSendKeys`. */\n keysMs: Number(process.env['TN3270_GUI_KEYS_MS'] ?? '1200'),\n /**\n * A FOURTH TEST SEAM: `TN3270_GUI_URL=http://127.0.0.1:PORT/?t=TOKEN` loads a URL instead of\n * this package's own `index.html`, and creates NO `Session` at all.\n *\n * It exists so the WEB gateway's served page can be driven by a real browser with real key\n * events, which is the one thing no test in `packages/web` can reach: vitest has no DOM, and\n * `bridgecore.test.ts` deliberately injects a fake socket. Pointing this Electron shell at the\n * gateway exercises the served `bridge.js`, the WebSocket hop and the renderer's own `keydown`\n * listener in one path.\n *\n * WHY IT SKIPS EVERYTHING ELSE: in this mode the PAGE's bridge owns the protocol. A `Session`\n * here would be a second, unrelated 3270 connection whose frames nothing displays, and the\n * atlas would be sent over an IPC channel the served page does not listen on. So this returns\n * before argv parsing, before `buildSession` and before `readAtlas` -- and takes no host\n * argument, which is also what keeps it unable to dial anything.\n */\n url: process.env['TN3270_GUI_URL'] ?? '',\n /**\n * `TN3270_GUI_SIZE=720x350` sets the CONTENT size, and it is meaningful only alongside `url`.\n *\n * On the normal path the window sizes itself from the first draw list, because main computes that\n * list. In URL mode main sees no frames at all -- the page does -- so the window would stay at its\n * 800x600 default, `renderer.ts` would center a 720x350 drawing inside it, and a capture would\n * differ from the GUI golden by a black border alone. That is a difference in the HARNESS's\n * geometry rather than in anything drawn, which is the least interesting reason for a golden to\n * fail. `browser-shot.mjs` reads the size out of the golden PNG itself, so the golden defines the\n * geometry rather than a constant repeated somewhere else.\n */\n size: process.env['TN3270_GUI_SIZE'] ?? '',\n /**\n * A FIFTH TEST SEAM: `TN3270_GUI_CLICKS='PF1,PA2,SysRq'` clicks keypad buttons by LABEL.\n *\n * Labels, not coordinates. A coordinate list would be a second copy of the layout, and it would\n * pass while the layout was wrong -- which is the one thing this seam exists to catch. Main asks\n * the renderer for the button's center and delivers a real `mouseDown`/`mouseUp` pair through\n * `sendInputEvent`, so the click enters at the top of Chromium's input pipeline exactly as\n * `TN3270_GUI_KEYS` does for keys.\n *\n * IT DOES NOT IMPLY THE KEYPAD, and the plan for it said it did -- so this is written down rather\n * than left as an omission. `clicks.mjs` shows the keypad with `TN3270_GUI_KEYS=Ctrl+K`, which\n * proves the chord and the click path in one run, and a second on-switch here would be wrong.\n * So the caller opens the keypad; this seam only clicks.\n *\n * **STALE AS OF 2026-10-06 AND REWRITTEN IN TASK 8 OF THE KEYPAD WORK.** The description below\n * this point was written for the CANVAS keypad: a per-window `showKeypad` flag, a `list.keypad`\n * region, and `__tn3270ButtonCenter` returning canvas coordinates for `hitTestAt`. None of that\n * exists in this front end any more -- `Ctrl-K` opens a `BrowserWindow` of real HTML controls,\n * and a DOM button has no canvas coordinates to aim at. The seam becomes a DOM query by label,\n * keeping the by-LABEL property deliberately, since a coordinate list would be a second copy of\n * the layout that passes while the layout is wrong. Marked rather than silently deleted so the\n * reasoning that survives the rewrite is not lost with the mechanism that does not.\n */\n clicks: process.env['TN3270_GUI_CLICKS'] ?? '',\n /**\n * AN EIGHTH TEST SEAM: `TN3270_GUI_SELECT='0,0,0,9'` drags from one cell to another and copies.\n *\n * `top,left,bottom,right` in CELLS, NOT PIXELS, for exactly the reason `TN3270_GUI_CLICKS` takes\n * labels: a pixel list would be a second copy of the geometry that would pass while the geometry\n * was wrong. Main asks the RENDERER to turn cells into viewport pixels\n * (`__tn3270CellCenter`, the inverse of the renderer's own `cellAt`), then drives real Chromium\n * mouse events through them -- so `mousedown`, `mousemove`, `cellAt`, the highlight,\n * `__tn3270Copy`, `sendAction`, the IPC hop, `extractText` and `clipboard.writeText` are ALL\n * under test. A seam that called `extractText` directly would skip precisely the plumbing that\n * has never run.\n *\n * THIS IS THE ONLY COVER THE SELECTION GESTURE HAS. `renderer.ts` throws at module load outside\n * a browser and the barrel does not export it, so no vitest file can execute a line of the\n * gesture -- the same argument the chord and click seams won.\n */\n select: process.env['TN3270_GUI_SELECT'] ?? '',\n /**\n * A SIXTH TEST SEAM: `TN3270_GUI_TRANSFER='localFile=/tmp/f,hostFile=A.B,submit'` drives the transfer\n * window without a mouse.\n *\n * Comma-separated steps, applied in order, so one variable expresses a whole scenario and the\n * harness needs no IPC of its own. Like `TN3270_GUI_CLICKS` it names WHAT to do rather than where\n * to click: a coordinate list would be a second copy of the layout and would pass while the\n * layout was wrong.\n *\n * ITS PRESENCE IS WHAT OPENS THE WINDOW, and the plan for this seam had an `open` STEP instead --\n * which nothing would have consumed. Its loop skipped `open` with \"already open\" while nothing in\n * this file had opened anything, so the whole scenario would have printed nothing at all. One\n * fact, one place.\n *\n * A STEP THIS FILE DOES NOT RECOGNISE IS REPORTED, not ignored, the same way `clicks: NO BUTTON`\n * reports a label that is not in the layout: a typo would otherwise present as a field that never\n * got set, which reads as a broken form.\n */\n transfer: process.env['TN3270_GUI_TRANSFER'] ?? '',\n /**\n * What the native file dialog should return, INSTEAD OF SHOWING.\n *\n * Separate from `transfer` above because it substitutes a MODAL, and a modal nobody can click\n * does not fail -- it HANGS, which is the failure shape this project has met in four other\n * places. Empty means show the real dialog.\n *\n * ONE VALUE FOR BOTH DIRECTIONS, which is honest about what it is: a stub for a chooser, not a\n * model of one. A scenario that needed an Open and a Save to answer differently would need two\n * variables, and no scenario does.\n */\n transferPath: process.env['TN3270_GUI_TRANSFER_PATH'] ?? '',\n /**\n * A SEVENTH TEST SEAM: `TN3270_GUI_KEYS='wait:CP READ,l,o,g,o,n,Enter'` makes a `wait:TEXT`\n * step in the KEYS list block until the host has painted TEXT on the screen.\n *\n * ## WHY THE KEYS SEAM COULD NOT DRIVE A LIVE HOST WITHOUT THIS\n *\n * `maybeSendKeys` waits a FIXED `keysMs` and then sends each chord 150ms apart. That is right\n * for a replay, which paints synchronously, and wrong for a host: `docs/live-testing.md`'s own\n * rule is that every step waits for TEXT THE HOST SENT, because the keyboard is locked until\n * the host writes and anything typed early is correctly REFUSED. The failure is a stall rather\n * than an error -- the keys all \"succeed\", the screen never advances, and the run burns its\n * timeout looking like a broken client. `live-drive.py` has driven the TUI this way since\n * 2026-08-25; the GUI is the one front end that had no equivalent, which is why no transfer had\n * ever been driven from its window against a real host.\n *\n * ## IT IS A STEP IN THE KEYS LIST, NOT A SEPARATE VARIABLE\n *\n * A second variable would have to express WHICH key each wait precedes, i.e. re-encode the\n * ordering the keys list already has. Reusing the list keeps one ordering in one place --\n * the same argument `SEAM.transfer`'s docstring makes for putting a whole scenario in one\n * variable.\n *\n * ## HOW LONG, AND WHAT A TIMEOUT MEANS\n *\n * `TN3270_GUI_WAIT_MS` per wait, default 40000 to match `live-drive.py`'s `STEP_TIMEOUT`.\n * **A WAIT THAT TIMES OUT FAILS THE RUN** -- it does not proceed hopefully. A seam that gave up\n * quietly and typed anyway would produce exactly the stall-dressed-as-success this seam exists\n * to remove, and the subsequent keys would be refused by a locked keyboard with nothing saying\n * why.\n *\n * ## WHAT IT MATCHES ON, AND THE ONE THING IT MUST NOT PRINT\n *\n * The RESOLVED screen text (`resolve(snapshot).map(c => c.text)`), which is what the operator\n * sees -- not the draw list, and not the OIA. Matching is a plain substring on a\n * whitespace-collapsed copy, because a 3270 screen pads with blanks and a phrase can straddle a\n * field boundary.\n *\n * **`|` SEPARATES ALTERNATIVES, and the first one found wins.** `wait:CP READ|VM/370` is needed\n * rather than convenient: VM has TWO legal entry states and which one appears is not under the\n * harness's control. A fresh connection paints the `VM/370` banner; a HELD account **reconnects\n * and lands at `CP READ` with no banner at all** (`docs/live-testing.md`: \"a held account\n * reconnects and lands you at `CP READ`\"). MEASURED on the first live run of\n * `live-transfer.py`, which timed out on `VM/370` for exactly this reason after its own previous\n * attempt had left CMSUSER held. `live-drive.py` has always passed a LIST of needles per step\n * for the same reason; this is that, in one string.\n *\n * A `|` in a needle is therefore not matchable, which costs nothing: these are host prompts.\n *\n * **THE SEARCH TEXT IS ECHOED; THE SCREEN IS NEVER ECHOED.** A wait for `PASSWORD` is fine to\n * print -- the harness chose it -- but the screen at that moment contains whatever the operator\n * typed, and on the next screen it contains the LOGMSG of a live system. `live-drive.py` writes\n * its panels to `/tmp` with a warning that they may contain the password, and nothing of the\n * kind belongs on this process's stdout.\n */\n waitMs: Number(process.env['TN3270_GUI_WAIT_MS'] ?? '40000'),\n /**\n * A NINTH TEST SEAM: `TN3270_GUI_CONNECT='{\"host\":\"127.0.0.1:3270\",\"model\":\"3278-2-E\",\"tls\":false}'`\n * fills the startup Connect dialog and presses Connect.\n *\n * THROUGH THE DOM, not through `submitConnect`: main sets the page's own input values and clicks\n * its own Connect button with `executeJavaScript`, so the boot file's real submit listener, the\n * preload and the `connect:submit` handler all run -- a seam that called the handler directly\n * would skip exactly the plumbing that has never run. Read only when the dialog opens, i.e. a\n * launch with no host; with a host it is ignored, which is itself a fact the harness can check.\n *\n * It prints `connect: dialog shown`, `connect: submitted`, and `connect: error <text>` if the\n * dialog's error line has text once the submit has settled. The text is the PARSER'S message\n * about the host the harness chose, never anything an operator typed, so it is safe to print.\n *\n * EMPTY MEANS ABSENT. Malformed JSON is refused by name (`parseConnectSeam`).\n */\n connect: process.env['TN3270_GUI_CONNECT'] ?? '',\n});\n\n/** The `wait:` prefix on a `TN3270_GUI_KEYS` step. Exported for the test that pins the seam. */\nexport const WAIT_PREFIX = 'wait:';\n\n/**\n * The `kbd:` prefix: deliver this chord to the KEYBOARD WINDOW instead of the canvas.\n *\n * WHY THE SEAM NEEDED A SECOND TARGET AT ALL. Every `sendInputEvent` in this file went to the\n * canvas `win`, so no harness could deliver a keystroke to a CHILD window -- and the child windows\n * run a DIFFERENT keydown path: `canvas/src/forwardKeys.ts`'s `forwardKeys`, not `renderer.ts`'s\n * listener. That gap is why `Ctrl-Shift-C` with the keyboard window focused could reach\n * `applyAction` and freeze the main process with every test in the suite green. A guard for that\n * freeze is only real if something can aim the chord where the freeze lives.\n *\n * A STEP PREFIX AND NOT A NEW ENVIRONMENT VARIABLE, following `wait:`: the ORDER matters here --\n * the keyboard window has to be opened by a `Ctrl+B` earlier in the same list -- and two variables\n * cannot express an interleaving. One list, read left to right, is the only shape that can.\n *\n * Exported for the test that pins it, exactly as `WAIT_PREFIX` is.\n */\nexport const KBD_PREFIX = 'kbd:';\n\n/**\n * How much of the screen a timed-out step prints.\n *\n * **1900, i.e. essentially the whole 24x80 buffer, because 240 WAS NOT ENOUGH AND THAT COST A\n * DIAGNOSIS.** The TSO failure of 2026-10-02 was narrowed to \"`X` does not exit this ISPF\", and the\n * one thing that would have named the real exit command -- the panel's own option list -- sat PAST\n * THE CUT in every capture. Four TK5 userids were spent on hypotheses that one full panel would have\n * settled, and each failed run strands an account.\n *\n * A screen is 1920 cells and the text is whitespace-collapsed before slicing, so this is almost\n * always the entire panel; the cap stays only so a pathological 43x80 paint cannot dump 3440\n * characters into one log line. **Not unbounded, and not small.** The rule it encodes: a diagnostic\n * that truncates the evidence is a diagnostic that sends the reader guessing.\n */\nconst SCREEN_DUMP_CHARS = 1900;\n\n/**\n * Set while a `TN3270_GUI_TRANSFER` scenario is mid-flight, so `quitIfKeysOnly` does not cut it off.\n *\n * ## THE RACE THIS CLOSES, MEASURED AGAINST VM/CMS\n *\n * `Ctrl+t` in a KEYS list opens the transfer window through the `action` handler, so\n * `driveTransferWindow` runs INSIDE `maybeSendKeys`'s loop -- and `quitIfKeysOnly` fires as soon as\n * that loop ends. The log showed the ordering plainly: `submit -> ok=true status=transferring`,\n * then `keys: sent ...`, and NO `done ->` line at all. **So a real transfer against a real host was\n * started and then killed by the harness's own quit**, twice, and the byte comparison could never\n * have passed.\n *\n * `driveTransferWindow`'s own docstring predicted a race here and guessed the wrong direction: it\n * reasoned that folding its quit into `quitIfKeysOnly` would let whichever path arrived first quit\n * \"with the scenario still running\". That is exactly what happened -- but from the OTHER side, with\n * `quitIfKeysOnly` as the one that arrived first. A flag is the smallest thing that orders the two,\n * and it is read ONLY by the quit helpers, so nothing about the product path depends on it.\n */\nlet transferScenarioRunning = false;\n\n/**\n * Set when a seam has decided to END the process, so nothing starts new async work afterwards.\n *\n * ## THE FAILURE THIS REMOVES, MEASURED TWICE ON TK5\n *\n * A timed-out `wait:` step calls `app.exit`, and `Ctrl+t` earlier in the same keys list has already\n * put `openTransferWindow` in flight -- so `loadFile` loses its renderer mid-load and rejects with\n * `ERR_FAILED (-2) loading transfer.html`. Catching at the `action` handler fixed one route;\n * `maybeOpenTransferWindow` then started a SECOND window on the way out and rejected again.\n *\n * **Why it matters beyond tidiness:** that rejection is the last thing in the log, so it buries the\n * `keys: TIMED OUT` line that says what actually went wrong -- and on TSO a run that dies mid-logon\n * never reaches its own logoff, which STRANDS A USERID recoverable only from the MVS operator\n * console. A misleading log is expensive when the diagnosis costs a userid.\n */\nlet shuttingDown = false;\n\n/** The connect a failure came from, when it came from one. */\ninterface ConnectTarget {\n readonly host: string;\n readonly port: number;\n /** Whether the session dialled TLS. A plaintext failure is NOT described as a TLS one. */\n readonly tls: boolean;\n readonly remedy: TlsRemedy;\n}\n\n/**\n * Turn any startup failure into something a person can act on.\n *\n * ONLY A TLS SESSION'S FAILURE IS DESCRIBED AS TLS. Node attaches a `code` to socket failures as\n * well as TLS ones, and this once sent every coded error through `describeTlsError` -- so with TLS\n * off, a host that was merely down read \"TLS connection to 127.0.0.1:1 failed (ECONNREFUSED)\", and a\n * double-clicker went looking for a TLS setting that was not on. `describeConnectError` says what\n * happened instead.\n */\nfunction explain(err: unknown, target?: ConnectTarget): string {\n if (err instanceof UsageError) return err.message;\n if (err instanceof TerminalTypeError) return err.message;\n if (err instanceof OversizeError) return err.message;\n if (err instanceof Error) {\n const code = (err as { code?: string }).code;\n if (code !== undefined && target !== undefined) {\n return target.tls\n ? describeTlsError(code, target.host, target.port, target.remedy)\n : describeConnectError(code, target.host, target.port, err.message);\n }\n return err.message;\n }\n return String(err);\n}\n\napp.whenReady().then(async () => {\n /*\n macOS ignores a window's `icon`: the Dock shows the .app bundle's icon, which electron-builder\n makes from the same PNG. An UNPACKAGED run (`tn3270-gui` from npm, or `electron dist/main.js`)\n has no bundle of ours, so it would show Electron's icon -- set it explicitly there. `app.dock`\n exists only on macOS, which keeps this free of a `process.platform` branch.\n */\n if (!app.isPackaged) app.dock?.setIcon(layout.icon);\n const win = new BrowserWindow({\n icon: layout.icon,\n width: 800,\n height: 600,\n // See the note above: this is what makes capturePage() match the window we asked for.\n useContentSize: true,\n backgroundColor: '#000000',\n webPreferences: {\n contextIsolation: true,\n nodeIntegration: false,\n /**\n * NO PRELOAD IN URL MODE, and this was MEASURED as a hard failure rather than reasoned.\n *\n * `preload.cts` calls `contextBridge.exposeInMainWorld('tn3270', ...)`, which defines a\n * NON-WRITABLE property on `window`. The gateway's own served `bridge.js` then does\n * `window.tn3270 = createBridge(...)` and dies with \"Cannot assign to read only property\n * 'tn3270' of object '#<Window>'\" -- so the page loads, receives keys, and does nothing.\n *\n * The two bridges are alternatives, never both: Electron's supplies the four functions over\n * IPC, the gateway's supplies them over a WebSocket. A real browser has no preload at all, so\n * this asymmetry belongs to the test shell and not to the served page.\n */\n // `.cjs`, compiled from preload.cts: an ESM preload cannot load. See that file.\n ...(process.env['TN3270_GUI_URL'] ? {} : { preload: layout.preload('preload.cjs') }),\n },\n });\n // Renderer console and load failures forwarded to stdout. Without this a renderer that\n // throws produces a BLANK WINDOW and no explanation anywhere -- which is exactly how the\n // first screenshot came out, and it is indistinguishable from a drawing bug.\n win.webContents.on('console-message', (_e, level, message, line, sourceId) => {\n process.stdout.write(`renderer[${level}] ${sourceId}:${line} ${message}\\n`);\n });\n win.webContents.on('did-fail-load', (_e, code, desc, url) => {\n process.stdout.write(`renderer failed to load ${url}: ${code} ${desc}\\n`);\n });\n\n // UNBUNDLED, that HTML pulls its renderer from `../canvas/dist/renderer.js`, which ASSUMES `gui`\n // and `canvas` stay siblings on disk: true in the workspace, false in an asar. This comment once\n // predicted that packaging would have to copy or rewrite that path, and 2026-11-06 it did:\n // `scripts/bundle.mjs` builds each page's module self-contained into `app/`, rewrites every\n // `<script src>` to `./<name>.js`, strips the import maps, and REFUSES to write a document that\n // still references `../` or a file `app/` lacks. `layout.document` picks whichever copy this run\n // has. The failure, if either side drifts, is still a blank window with a `did-fail-load` line\n // above it -- which is why the bundle checks and does not leave it to a harness.\n // The URL seam branches HERE, before anything reads argv or builds a Session -- see SEAM.url.\n if (SEAM.url !== '') {\n if (SEAM.size !== '') {\n const [w, h] = SEAM.size.split('x').map(Number);\n if (Number.isInteger(w) && Number.isInteger(h) && w! > 0 && h! > 0) {\n win.setContentSize(w!, h!);\n } else {\n // Refused by name rather than ignored: a silently-dropped size produces a golden mismatch\n // that reads as a rendering change, which is the diagnosis this seam exists to avoid.\n process.stderr.write(`TN3270_GUI_SIZE must be WxH, not ${JSON.stringify(SEAM.size)}\\n`);\n app.exit(2);\n return;\n }\n }\n await win.loadURL(SEAM.url);\n globalShortcut.register('Control+]', () => { app.quit(); });\n await maybeSendKeys(win);\n /**\n * CALLED IN URL MODE NOW, WHICH IT WAS NOT BEFORE, and the reason it could not be is gone.\n *\n * The old `TN3270_GUI_CLICKS` asked the RENDERER for a keypad button's canvas coordinates, and\n * in URL mode the served page owns its own renderer -- main sees no frames, so there was\n * nothing to ask and an untested call site would have been a claim this file had not earned.\n *\n * The seam is a DOM QUERY now, which works identically in both modes: the keypad is an overlay\n * in the page either way. So this is the ONLY cover for the web gateway's keypad actually\n * being clickable -- `browser-clicks.mjs` drives it, and nothing else in the repo loads that\n * overlay in a browser at all.\n */\n await maybeSendClicks(win);\n await maybeCapture(win);\n await quitIfKeysOnly();\n return;\n }\n\n await win.loadFile(layout.document('index.html'));\n globalShortcut.register('Control+]', () => { app.quit(); });\n\n const fail = (message: string): void => {\n process.stderr.write(`${message}\\n`);\n // A window closed while a connect was in flight has no renderer to tell; sending to it THROWS,\n // and from a promise's catch that is an unhandled rejection. stderr above still has it.\n if (!win.isDestroyed()) win.webContents.send('error-message', message);\n };\n\n const argv = stripElectronSwitches(process.argv.slice(2));\n const plan = planStartup(argv, SEAM);\n if (plan.kind === 'fail') {\n fail(explain(plan.err));\n await maybeCapture(win, 1);\n return;\n }\n if (plan.kind === 'session') {\n await startSession(win, fail, plan.args, 'flag');\n return;\n }\n // No host: ask for one. The session, if any, starts from the dialog's submit handler.\n await openConnectDialog(win, fail, argv);\n});\n\n/**\n * THE STARTUP CONNECT DIALOG, for a launch with no host -- a double-clicked app, typically. Spec\n * docs/superpowers/specs/2026-11-05-packaging-design.md section 2.\n *\n * MODAL OVER THE TERMINAL WINDOW, which stays up behind it: the window is where a failed connect is\n * shown afterwards, exactly as for a command-line launch, and the dialog does NOT reopen (File >\n * Connect... is milestone B).\n *\n * ## REGISTERED ONCE, ON THIS PATH ONLY\n *\n * The two `connect:` handlers are registered here and nowhere else, and this function runs at most\n * once per process -- from `app.whenReady()`, which nothing re-enters. `ipcMain.handle` THROWS on a\n * second registration, which is the rule `startSession`'s own handlers live under too.\n *\n * `started` IS THE ONE-SESSION GUARD. `startSession` registers handlers and must run once; the page\n * disables Connect while a submit is in flight, but a page is not where a once-only rule should\n * live, so main refuses a second start itself.\n *\n * ## QUIT, ESCAPE, CLOSE ALL QUIT THE APP -- BUT ONLY BEFORE A SUBMIT SUCCEEDED\n *\n * Closing the dialog leaves the terminal window open with nothing in it and nothing to connect it\n * to, so `closed` quits. A SUCCESSFUL submit closes the dialog too, and that close must NOT quit:\n * `started` is set before `close()` and the `closed` handler checks it.\n */\nasync function openConnectDialog(\n win: BrowserWindow, fail: (message: string) => void, commandLine: readonly string[],\n): Promise<void> {\n let seamForm;\n try {\n seamForm = parseConnectSeam(SEAM.connect);\n } catch (err) {\n fail(explain(err));\n await maybeCapture(win, 1);\n return;\n }\n\n let started = false;\n const cw = new BrowserWindow({\n icon: layout.icon,\n width: 420,\n height: 240,\n useContentSize: true,\n title: 'Connect',\n parent: win,\n modal: true,\n resizable: false,\n minimizable: false,\n maximizable: false,\n webPreferences: {\n contextIsolation: true,\n nodeIntegration: false,\n // `.cjs`, compiled from connectPreload.cts: an ESM preload cannot load.\n preload: layout.preload('connectPreload.cjs'),\n },\n });\n // The deprecated five-argument form, matching the other windows -- see the keypad's note.\n cw.webContents.on('console-message', (_e, level, message, line, sourceId) => {\n process.stdout.write(`connect[${level}] ${sourceId}:${line} ${message}\\n`);\n });\n cw.webContents.on('did-fail-load', (_e, code, desc, url) => {\n process.stdout.write(`connect window failed to load ${url}: ${code} ${desc}\\n`);\n });\n cw.on('closed', () => { if (!started) app.quit(); });\n\n ipcMain.handle('connect:models', () => connectModels());\n ipcMain.handle('connect:submit', async (_e, form: unknown) => {\n if (started) return { error: 'already connecting' };\n const r = submitConnect(form, commandLine);\n if (!r.ok) return { error: r.error };\n started = true;\n // AFTER the reply, not before it: closing the window that is awaiting this `invoke` would\n // leave its promise to a destroyed renderer. `setImmediate` lets the reply go out first.\n setImmediate(() => {\n if (!cw.isDestroyed()) cw.close();\n // The terminal window can be gone by now (closed in the gap): nothing to start a session in.\n if (win.isDestroyed()) return;\n startSession(win, fail, r.args, 'dialog').catch((err: unknown) => {\n fail(explain(err));\n });\n });\n return { ok: true };\n });\n ipcMain.on('connect:quit', () => { app.quit(); });\n\n await cw.loadFile(layout.document('connect.html'));\n process.stdout.write('connect: dialog shown\\n');\n if (seamForm !== undefined) await driveConnectDialog(cw, seamForm);\n}\n\n/**\n * `TN3270_GUI_CONNECT`'s driver: type the form into the dialog's own controls and press its own\n * Connect, then report what the dialog's error line says. See `SEAM.connect`.\n *\n * WAITS FOR CONNECT TO BE ENABLED first, because the page disables it until the model list has\n * arrived -- clicking a disabled button does nothing at all, which would read as a broken dialog.\n * And then for the submit to settle: either the window closes (accepted) or Connect is enabled\n * again (refused, with the reason in the error line).\n *\n * A THROW HERE WOULD BE A HANG, so it is caught: this runs inside `app.whenReady()`'s promise.\n */\nasync function driveConnectDialog(\n cw: BrowserWindow, form: { host: string; model: string; tls: boolean },\n): Promise<void> {\n const until = async (expr: string): Promise<boolean> => {\n const deadline = Date.now() + 10_000;\n while (Date.now() < deadline) {\n if (cw.isDestroyed()) return false;\n if (await cw.webContents.executeJavaScript(expr) === true) return true;\n await new Promise((r) => setTimeout(r, 50));\n }\n return false;\n };\n try {\n if (!await until(\"document.querySelector('#connect').disabled === false\")) {\n process.stdout.write('connect: Connect never became enabled\\n');\n return;\n }\n await cw.webContents.executeJavaScript(`(() => {\n const f = ${JSON.stringify(form)};\n document.querySelector('#host').value = f.host;\n document.querySelector('#model').value = f.model;\n document.querySelector('#tls').checked = f.tls;\n document.querySelector('#connect').click();\n })()`);\n process.stdout.write('connect: submitted\\n');\n // Settled = the window closed (accepted) or Connect came back (refused).\n await until(\"document.querySelector('#connect').disabled === false\");\n if (cw.isDestroyed()) return;\n const text = await cw.webContents.executeJavaScript(\n \"document.querySelector('#connect-error').textContent\") as string;\n if (text !== '') process.stdout.write(`connect: error ${text}\\n`);\n } catch (err) {\n if (!cw.isDestroyed()) process.stdout.write(`connect: DRIVE FAILED ${explain(err)}\\n`);\n }\n}\n\n/**\n * Everything after the arguments are known: the session, the atlas, the windows, the menus, the IPC\n * handlers, and the connect (or the replay). ONE PATH for a host given on the command line and, from\n * the next commit, one typed into the startup Connect dialog -- so the dialog cannot build a session\n * the command line would not.\n *\n * CALLED AT MOST ONCE PER PROCESS, and that is load-bearing: it registers `ipcMain.handle` channels\n * and an `app.on('before-quit')` hook, and the first would THROW on a second call (see the note at\n * `transferWin`). Moved out of `app.whenReady()` verbatim; the module-level indentation is the same.\n */\nasync function startSession(\n win: BrowserWindow, fail: (message: string) => void, args: GuiArgs, remedy: TlsRemedy,\n): Promise<void> {\n // Resolved once: a scheme is fixed for the process, and re-resolving per frame would put a\n // string lookup in the paint path.\n const scheme = args.scheme ?? resolveScheme();\n\n /**\n * `-oversize auto`'s measure, which the session calls at EVERY connect and never before\n * (`buildSession`, args.ts): the display's work area at the scale `fit()` below would pick for\n * the model (`measure.ts`). Both inputs are read when called. The atlas is read further down,\n * after the session exists (the spec's \"Why auto is measured at connect\"), and before the\n * connect at the end of this function -- a failed read returns from here first -- but the\n * measure does not rely on that ordering: no atlas, or a display Electron cannot report, is\n * no measurement, which `resolveGeometry` turns into the model and a notice. Never a throw,\n * which would fail the connect over a window size.\n */\n let atlas: ReturnType<typeof readAtlas> | undefined;\n const measure = (setting: GeometrySetting | undefined): Size | undefined => {\n let area;\n try {\n area = screen.getPrimaryDisplay().workAreaSize;\n } catch {\n area = undefined;\n }\n return measureAuto(setting, area, atlas === undefined\n ? undefined\n : { width: atlas.geometry.cellWidth, height: atlas.geometry.cellHeight });\n };\n\n let session;\n try {\n session = buildSession(args, measure);\n } catch (err) {\n fail(explain(err));\n await maybeCapture(win, 1);\n return;\n }\n // Before any connect, so the first connect's notice is printed too -- see the function.\n printGeometryNotices(session, process.stderr);\n\n /**\n * The atlas is read HERE and shipped over IPC, not fetched by the renderer.\n *\n * Two reasons, both found by running it: `fetch` on a `file://` URL is blocked in\n * Chromium, and a packaged app's resources live inside an asar archive that only the main\n * process can read. Sending it once at startup avoids both and keeps the renderer with no\n * filesystem access at all.\n */\n /**\n * GUARDED like the two startup failures above, because a missing atlas is a REAL user's\n * broken install and not only a developer's half-built tree.\n *\n * `readAtlas` throws synchronously, and this runs inside `app.whenReady().then(async ...)`\n * where an uncaught throw is an unhandled rejection: MEASURED, that produced a warning on\n * stderr and then a BLANK WINDOW that sat until the harness timeout -- console-only\n * diagnosis, which is exactly what the rule at the top of this file forbids. A scoped\n * `npm run build -w @tn3270/gui` is enough to reach it; `assets.ts` records why.\n */\n try {\n // `layout.atlasDir` is undefined unbundled (canvas's own `assetDir()`), and `app/` in the bundle\n // -- stated, not left to `assetDir()` landing there by coincidence. See `appRoot.ts`.\n atlas = readAtlas(layout.atlasDir);\n } catch (err) {\n fail(explain(err));\n await maybeCapture(win, 1);\n return;\n }\n const { geometry, coverage } = atlas;\n const blank = [...blankColumns(coverage, geometry)];\n win.webContents.send('atlas', { geometry, coverage, blank });\n\n /**\n * The KEYPAD window, created on first request and reused after that.\n *\n * ## IT REPLACED A `showKeypad` BOOLEAN AND A DRAW-LIST REGION\n *\n * Until 2026-10-06 the keypad was drawn INTO the canvas: a per-window flag here, a `keypad`\n * region in the `DrawList`, and `fit()` growing the window to make room. The user judged that\n * version \"ugly\" (3270 glyphs blitted through x3270's own atlas, so the buttons were characters\n * rather than buttons) and \"modal in an annoying way\" -- and explicitly NOT a problem for\n * taking up screen space, which is what ruled out merely shrinking it. The medium was the\n * problem, so it is real HTML controls now. Task 9 deletes the canvas side.\n *\n * ## NON-MODAL BY CONSTRUCTION, WHICH IS THE COMPLAINT ANSWERED STRUCTURALLY\n *\n * A separate window has no reason to steal the terminal's keyboard, so it can simply be left\n * open beside the screen. An in-canvas overlay had to own the display it was drawn on.\n *\n * ## NO PERSISTENCE, CONFIRMED ON ITS MERITS AND NOT AS A HOLDING POSITION\n *\n * x3270 remembers through its `keypadOn` resource (\"Turn on pop-up keypad at start-up\",\n * `x3270/x3270.c:264`) and we deliberately do not. The user's words: \"making the user reopen\n * the keypad on each new application start, if they need it, is fine.\" **This is explicitly NOT\n * on the list of things a future preferences store should bring** -- reopening is one keystroke,\n * and a keypad that reappears unbidden costs window space to an operator who may not want it\n * this session. Recorded so the weaker reading (\"wait for preferences\") cannot invite someone to\n * add it the moment a preferences store exists.\n *\n * The transfer window below is this one's precedent in every structural respect; read it first.\n */\n let keypadWin: BrowserWindow | undefined;\n const openKeypadWindow = async (): Promise<void> => {\n // SHOW AND FOCUS AN EXISTING ONE rather than building a second. Same as the transfer window,\n // and here it is also what makes `Ctrl-K` idempotent: the chord OPENS, it does not toggle, so\n // pressing it twice must not stack two windows.\n if (keypadWin !== undefined && !keypadWin.isDestroyed()) {\n keypadWin.show();\n keypadWin.focus();\n return;\n }\n const kw = new BrowserWindow({\n icon: layout.icon,\n width: 560,\n height: 480,\n // As the terminal window does, so the content area is the size asked for.\n useContentSize: true,\n title: 'Keypad',\n // A CHILD of the terminal window so it travels with it, but NOT modal -- the whole point is\n // that it can be left up while the operator works on the screen behind it.\n parent: win,\n modal: false,\n webPreferences: {\n contextIsolation: true,\n nodeIntegration: false,\n // `.cjs`, compiled from keypadPreload.cts: an ESM preload cannot load, and when it fails\n // the bridge silently never appears -- a window whose 48 buttons all do nothing.\n preload: layout.preload('keypadPreload.cjs'),\n },\n });\n keypadWin = kw;\n // THE DEPRECATED FIVE-ARGUMENT FORM, matching the other two windows rather than\n // electron.d.ts's preferred shape. Electron 44 declares both; `level` is a NUMBER here and a\n // STRING in the new form, and the harnesses filter stdout on the literal `renderer[3]`. One\n // spelling across all three windows, one upgrade to do when the level finally moves.\n kw.webContents.on('console-message', (_e, level, message, line, sourceId) => {\n process.stdout.write(`keypad[${level}] ${sourceId}:${line} ${message}\\n`);\n });\n // WITHOUT THIS A FAILED LOAD IS A BLANK WINDOW AND NOTHING ELSE. Measured on the transfer\n // window, where `ERR_FAILED (-2) loading transfer.html` was the line that explained it.\n kw.webContents.on('did-fail-load', (_e, code, desc, url) => {\n process.stdout.write(`keypad window failed to load ${url}: ${code} ${desc}\\n`);\n });\n // CLEARED ON CLOSE, so the next `Ctrl-K` builds a fresh one instead of calling `show()` on a\n // destroyed object. The transfer window learned this the hard way -- see the recorded\n // closing-a-parent-skips-the-child-close finding.\n kw.on('closed', () => { keypadWin = undefined; });\n await kw.loadFile(layout.document('keypad.html'));\n /**\n * REPORT WHAT THE WINDOW ACTUALLY CONTAINS, once, after the load resolves.\n *\n * THE ONLY THING THAT CAN SEE A BLANK KEYPAD. `loadFile` resolving means the document\n * parsed; it says nothing about whether the module graph resolved, whether the preload\n * installed its bridge, or whether `createKeypadUi` ran -- and all three failures produce the\n * SAME window: a title, a heading, and no buttons. `did-fail-load` above catches a 404 on the\n * document itself and nothing deeper.\n *\n * So this counts the buttons in the DOM and prints the number. `clicks.mjs` reads the line to\n * bail early with a diagnosis instead of reporting 48 mysterious `NO BUTTON`s, and it is also\n * how this window was first proved to work at all.\n *\n * UNGATED, deliberately. It carries a COUNT and no screen content, so unlike the action log\n * it cannot leak what an operator typed -- which is the test every ungated write in this file\n * has to pass. One line per window open is not noise worth gating.\n *\n * A THROW HERE WOULD BE A HANG, so it is caught: this runs inside `app.whenReady()`'s promise\n * chain, where an unhandled rejection means `quitIfKeysOnly` never fires and the client sits\n * until the harness's timeout -- which reads as a broken client rather than a broken probe.\n */\n try {\n const count = await kw.webContents.executeJavaScript(\n 'document.querySelectorAll(\\'button[data-label]\\').length');\n process.stdout.write(`keypad: buttons=${String(count)}\\n`);\n } catch (err) {\n process.stdout.write(`keypad: PROBE FAILED ${explain(err)}\\n`);\n }\n };\n\n /**\n * The FULL-KEYBOARD window, `Ctrl-B`, a second surface beside the keypad.\n *\n * ## THE SAME SHAPE AS `openKeypadWindow` ABOVE, DELIBERATELY AND ALMOST LINE FOR LINE\n *\n * Read that one first: every structural decision here is its, for its reasons -- lazy, reused,\n * a non-modal child of the terminal window, `closed` clearing the handle, a `console-message`\n * forward, a `did-fail-load` forward, and a button-count probe after the load resolves. Only\n * the differences are commented below, so that a reader can diff the two and find them.\n *\n * ## WHY A SECOND WINDOW RATHER THAN ONE SURFACE WITH TWO MODES\n *\n * The user asked for BOTH -- *\"I like Ctrl-K for the quick special-keys overlay like we have\n * now. Maybe Ctrl-B for the full keyboard?\"* -- and may later keep only one. Two windows means\n * dropping either is a deletion rather than an untangling, and neither depends on the other\n * existing. The web gateway makes the same call for the same reason (`web/src/bridge.ts`).\n *\n * ## THE SIZE IS MEASURED IN A REAL WINDOW, AND MY ARITHMETIC WAS WRONG\n *\n * I first wrote 760x300 from `ui.css`'s own note that the lower band is 24 key units at a 28px\n * pitch, so ~672px. **BOTH NUMBERS WERE WRONG**, and loading the document said so (2026-10-23,\n * content widths swept 700..900 against the rendered boxes):\n *\n * content 760 -> the numpad REFLOWS BELOW the main block; root is 732x350\n * content 780 -> the numpad sits BESIDE it; root is 752x222\n *\n * THE REFLOW THRESHOLD IS BETWEEN 760 AND 780, so 760 opened the window in the narrow layout --\n * the tall stacked one the container query exists to provide for a cramped browser tab, not the\n * arrangement this keyboard is supposed to show. And the HEIGHT I picked was 300 against a real\n * 350 for the layout I accidentally selected: too short for either.\n *\n * 820x280 IS THE SHIPPED PAIR, with ~40px of margin over the 780 threshold rather than sitting\n * on it -- the threshold is a container query against THIS element's width, and the padding and\n * scrollbar between the window and that element are exactly the kind of thing that differs by\n * platform. 280 clears the measured 222 plus the `h1` and the page padding.\n *\n * `useContentSize` MAKES THESE THE CONTENT BOX rather than the frame, so the numbers mean what\n * they say on every platform's window decorations. A window the operator then shrinks merely\n * reflows and scrolls, which is correct -- this is a starting size, not a constraint.\n *\n * ## NOT RESIZED TO FIT AND NOT REMEMBERED, BOTH ON PURPOSE\n *\n * The keypad window's note settles persistence for this surface too, on the user's own words:\n * reopening is one keystroke, and a window that reappears unbidden costs screen space to an\n * operator who may not want it this session. Explicitly NOT on the list for a future\n * preferences store.\n */\n let keyboardWin: BrowserWindow | undefined;\n const openKeyboardWindow = async (): Promise<void> => {\n // SHOW AND FOCUS AN EXISTING ONE rather than building a second, which is what makes `Ctrl-B`\n // idempotent: the chord OPENS, it does not toggle, so pressing it twice must not stack two.\n if (keyboardWin !== undefined && !keyboardWin.isDestroyed()) {\n keyboardWin.show();\n keyboardWin.focus();\n return;\n }\n const kbw = new BrowserWindow({\n icon: layout.icon,\n width: 820,\n height: 280,\n useContentSize: true,\n title: 'Keyboard',\n parent: win,\n modal: false,\n webPreferences: {\n contextIsolation: true,\n nodeIntegration: false,\n // `.cjs`, compiled from keyboardPreload.cts: an ESM preload cannot load, and when it fails\n // the bridge silently never appears -- a window whose 118 caps all do nothing.\n preload: layout.preload('keyboardPreload.cjs'),\n },\n });\n keyboardWin = kbw;\n // THE DEPRECATED FIVE-ARGUMENT FORM, matching the other three windows rather than\n // electron.d.ts's preferred shape -- see the keypad window's note. One spelling across all\n // four windows, one upgrade to do when the level finally moves.\n kbw.webContents.on('console-message', (_e, level, message, line, sourceId) => {\n process.stdout.write(`keyboard[${level}] ${sourceId}:${line} ${message}\\n`);\n });\n kbw.webContents.on('did-fail-load', (_e, code, desc, url) => {\n process.stdout.write(`keyboard window failed to load ${url}: ${code} ${desc}\\n`);\n });\n kbw.on('closed', () => { keyboardWin = undefined; });\n await kbw.loadFile(layout.document('keyboard.html'));\n /**\n * REPORT WHAT THE WINDOW ACTUALLY CONTAINS, once, after the load resolves.\n *\n * THE ONLY THING THAT CAN SEE AN EMPTY KEYBOARD, and this surface has MORE ways to be empty\n * than the keypad does: `loadFile` resolving means the document parsed, and says nothing\n * about whether the import map answered all four specifiers, whether the preload installed\n * its bridge, or whether `createKeyboardUi` ran. All of those produce the SAME window -- a\n * title, a heading, and nothing else.\n *\n * `button[data-key]` AND NOT `button[data-label]`, which is the keypad's attribute:\n * `keyboardUi.ts` sets `data-key` to the cap's NAME, because several caps' legends are glyphs\n * and one is the spacebar whose label is empty. Counting the wrong attribute would report 0\n * on a perfectly good window.\n *\n * UNGATED, like the keypad's: it carries a COUNT and no screen content, so it cannot leak\n * what an operator typed.\n *\n * A THROW HERE WOULD BE A HANG, so it is caught -- this runs inside `app.whenReady()`'s\n * promise chain, where an unhandled rejection means `quitIfKeysOnly` never fires and the\n * client sits until the harness's timeout.\n */\n try {\n const count = await kbw.webContents.executeJavaScript(\n 'document.querySelectorAll(\\'button[data-key]\\').length');\n process.stdout.write(`keyboard: buttons=${String(count)}\\n`);\n } catch (err) {\n process.stdout.write(`keyboard: PROBE FAILED ${explain(err)}\\n`);\n }\n };\n\n /**\n * The transfer window, created on first request and reused after that.\n *\n * LAZY because most sessions never transfer a file, and an Electron window costs a renderer\n * process. Reused rather than recreated so that reopening mid-transfer shows the RUNNING\n * state instead of a fresh form -- two submits would interleave two machines' frames on one\n * screen.\n *\n * ## ONE PER PROCESS, WHICH IS WHY THE HANDLERS BELOW ARE SAFE\n *\n * Everything in this block runs inside `app.whenReady().then(...)`, and that callback is\n * registered exactly ONCE at module scope (line 151). There is no `second-instance` or\n * `activate` handler in this file, so nothing re-enters it -- which matters because\n * `ipcMain.handle` THROWS on a second registration for the same channel (\"Attempted to\n * register a second handler for 'transfer:browse'\") and `app.on('before-quit')` would\n * accumulate a listener per entry. Both are therefore correct HERE and would be bugs if this\n * callback ever became re-entrant; a second window per process needs the three handlers and\n * the quit hook hoisted to module scope, keyed on which window asked.\n */\n let transferWin: BrowserWindow | undefined;\n let transfer: ReturnType<typeof createTransferController> | undefined;\n\n const openTransferWindow = async (): Promise<void> => {\n if (transferWin !== undefined && !transferWin.isDestroyed()) {\n transferWin.show();\n transferWin.focus();\n return;\n }\n const tw = new BrowserWindow({\n icon: layout.icon,\n width: 460,\n height: 420,\n useContentSize: true,\n title: 'File Transfer',\n // A CHILD of the terminal window so it travels with it, but NOT modal: the operator may\n // want to look at the screen behind it, and a 3270 transfer is typed at a command prompt\n // they may need to see.\n parent: win,\n modal: false,\n webPreferences: {\n contextIsolation: true,\n nodeIntegration: false,\n // `.cjs`, compiled from transferPreload.cts: an ESM preload cannot load.\n preload: layout.preload('transferPreload.cjs'),\n },\n });\n transferWin = tw;\n // Same forwarding as the main window: a renderer that throws otherwise produces a window\n // that does nothing with no explanation anywhere.\n //\n // THE DEPRECATED FIVE-ARGUMENT FORM, MATCHING THE MAIN WINDOW'S HANDLER ABOVE RATHER THAN\n // electron.d.ts's preferred shape. Electron 44 still declares both -- `(details, level,\n // message, line, sourceId)` with the last four marked `@deprecated` -- and `level` is a\n // NUMBER there, where the `details.level` of the new form is a STRING ('info' | 'warning' |\n // 'error' | 'debug'). That difference is not cosmetic for the harnesses: `shot.mjs`,\n // `keys.mjs` and `clicks.mjs` all filter stdout on the literal `renderer[3]`, and each\n // records that the numeric level is deprecated and will drift silently on an upgrade. Using\n // the new shape here would print `transfer[error]` while the main window printed\n // `renderer[3]` -- two spellings of one thing in one stream, for a window whose log nothing\n // greps yet. One spelling, one upgrade to do when the level finally moves.\n tw.webContents.on('console-message', (_e, level, message, line, sourceId) => {\n process.stdout.write(`transfer[${level}] ${sourceId}:${line} ${message}\\n`);\n });\n tw.webContents.on('did-fail-load', (_e, code, desc, url) => {\n process.stdout.write(`transfer window failed to load ${url}: ${code} ${desc}\\n`);\n });\n\n transfer = createTransferController({\n send: (channel, payload) => {\n if (!tw.isDestroyed()) tw.webContents.send(channel, payload);\n },\n // THE WINDOW, NOT THE BUTTON, and the dep is named `focusWindow` for that reason -- see its\n // docstring. `webContents.focus()` moves focus to no particular control; the form's only\n // enabled control while a transfer runs IS Cancel, because `setRunning(true)` disables\n // everything else, so bringing the window forward is the whole of the remedy. `show()`\n // first, because a close attempt can come from a window that is behind the terminal.\n focusWindow: () => { tw.show(); tw.focus(); },\n openDialog: async () => {\n // THE STUB SUBSTITUTES THE WHOLE DIALOG, never merely its default. A real modal under Xvfb\n // has nobody to click it and would HANG the harness -- a stall, not an error.\n if (SEAM.transferPath !== '') return SEAM.transferPath;\n // `tw` AS THE FIRST ARGUMENT IS WHAT MAKES THIS WINDOW-MODAL (the `showOpenDialog(window,\n // options)` overload, electron.d.ts:7850), which is what closes the interleaving recorded\n // in the AS BUILT notes: a parentless dialog leaves Start clickable while it is open, and\n // a late resolution then lands mid-transfer. `transferUi.ts`'s post-await `isRunning`\n // check is the primary guard for that and stays -- this is the half that stops the race\n // being reachable at all.\n const r = await dialog.showOpenDialog(tw, {\n title: 'Send which file?',\n properties: ['openFile'],\n });\n return r.canceled ? undefined : r.filePaths[0];\n },\n saveDialog: async () => {\n // Same stub, same reason as `openDialog` above.\n if (SEAM.transferPath !== '') return SEAM.transferPath;\n const r = await dialog.showSaveDialog(tw, { title: 'Receive into which file?' });\n // `filePath`, SINGULAR, and a STRING rather than an optional one: `SaveDialogReturnValue`\n // (electron.d.ts:23629-23637) gives '' when the dialog was canceled, where\n // `OpenDialogReturnValue` gives a `filePaths` array. So the `canceled` check is what\n // distinguishes them, and returning '' would put an empty path in the form's Local file\n // field -- erasing whatever the operator had typed, which `browseLocal` treats `undefined`\n // as specifically meaning it must not do.\n return r.canceled ? undefined : r.filePath;\n },\n startTransfer: (opts) => startTransfer({ ...opts, session, files: nodeTransferFiles }),\n buildCommand: (keywords) => transferCommand(keywords),\n });\n\n // THE CLOSE GUARD. `preventDefault` on `close` is what makes an accidental Cmd-W or red\n // button unable to abandon a running transfer; Cancel is the only route out.\n tw.on('close', (e) => {\n if (transfer?.shouldPreventClose() === true) {\n e.preventDefault();\n transfer.onCloseAttempt();\n }\n });\n tw.on('closed', () => {\n // GUARDED ON IDENTITY, because `closed` can arrive after a replacement window has already\n // been built: `openTransferWindow` only reuses a window that is not destroyed, so a\n // destroyed-but-not-yet-notified window would otherwise clear the NEW window's controller\n // and leave `transfer` undefined while a form sat on screen -- every submit answered 'the\n // transfer window is not open'. Nothing reaches `transfer = undefined` in that case now.\n if (transferWin !== tw) return;\n /**\n * CANCEL BEFORE CLEARING -- THE FOURTH INSTANCE OF THE TEARDOWN BUG, AND IT IS REACHABLE.\n *\n * `closed` can mean \"the window was destroyed out from under a LIVE transfer\", not only\n * \"nothing was running\". MEASURED in real Electron: closing a child window's PARENT destroys\n * the child and fires the child's `closed` WITHOUT EVER FIRING ITS `close`. So an operator\n * who closes the terminal window mid-transfer never reaches the close guard above at all.\n *\n * WHY `before-quit` IS NOT SUFFICIENT ON ITS OWN, which is the non-obvious part: the order\n * is `closed` -> this handler -> `window-all-closed` -> `app.quit()` -> `before-quit`. So by\n * the time the quit hook runs, `transfer` is ALREADY `undefined` and it has nothing left to\n * cancel. Observed: `cancel() was called 0 time(s)`, with `session.dft` still set and the\n * host waiting for a frame that never comes -- the spec's \"session.dft set with the window\n * gone\", reached by a different door than the identity guard above is watching.\n *\n * NOT A DOUBLE CANCEL on the ordinary paths. A `closed` that follows a REFUSED close can\n * only happen after the transfer ended -- `onDone` cleared `run` -- so `shutdown` finds\n * nothing and does nothing. Cmd-Q genuinely does reach `before-quit` first (verified order:\n * `before-quit` -> child `close` -> PREVENTED -> `closed`), and `shutdown` is idempotent, so\n * the two hooks overlapping costs nothing. `TransferRun.cancel` is itself idempotent too\n * (`transferRun.ts:388`).\n */\n // `'windowGone'` AND NOT `'sessionLost'`: the form this reason decides about has already been\n // destroyed, so there is nothing on screen to tell. `shutdown`'s own docstring has the split.\n transfer?.shutdown('windowGone');\n transferWin = undefined;\n transfer = undefined;\n });\n\n await tw.loadFile(layout.document('transfer.html'));\n await driveTransferWindow(tw, win, session);\n };\n\n /**\n * THE SIXTH SEAM'S ENTRY POINT: open the transfer window because `TN3270_GUI_TRANSFER` is set.\n *\n * A WRAPPER RATHER THAN A CALL AT EACH SITE, so the \"is the seam active?\" question is asked in one\n * place. `main.ts`'s own `SEAM` docstring records why that matters: three places once asked \"is\n * the keys seam active?\" with their own inline emptiness checks, agreed only by coincidence, and\n * one of them was the gate keeping a typed password off stdout.\n *\n * CALLED FROM THE REPLAY AND LIVE PATHS, after the input seams and before `maybeCapture`. Not from\n * the URL branch: there is no `Session` there, no `openTransferWindow` in scope, and\n * `TN3270_GUI_CLICKS` is ignored in that mode for the same reason -- an untested call site is a\n * claim this file has not earned.\n *\n * AFTER the keys, which matters for composition rather than for this seam alone: `keys.mjs` sends\n * a real Ctrl+T, and that opens the window through the `action` handler. Reaching here afterwards\n * finds the window already open, `openTransferWindow` shows and focuses it, and `driveTransferWindow`\n * is NOT re-entered -- it runs from the `loadFile` path only. A chord run therefore cannot be\n * driven by this seam, which is correct: `keys.mjs` sets no `TN3270_GUI_TRANSFER`.\n */\n const maybeOpenTransferWindow = async (): Promise<void> => {\n if (SEAM.transfer === '') return;\n // NOTHING NEW ONCE A SEAM HAS GIVEN UP: see `shuttingDown`. Opening a window while the process\n // is exiting produces an ERR_FAILED rejection that buries the line explaining the real failure.\n if (shuttingDown) return;\n await openTransferWindow();\n };\n\n /**\n * ONE REGISTRATION PER PROCESS, for the reason given at `transferWin` above.\n *\n * Registered OUT HERE rather than inside `openTransferWindow`, which is the difference between\n * working and crashing: `ipcMain.handle` throws on a second handler for the same channel, so\n * registering them per window would die the second time the operator closed the form and\n * pressed Ctrl-T again -- and that throw would come out of `openTransferWindow`'s promise,\n * i.e. as an unhandled rejection with no window, which is this file's worst failure shape.\n *\n * They tolerate a MISSING controller rather than assuming one, because these channels outlive\n * any particular window: the preload is only loaded by the transfer window, so in practice\n * nothing can call them while it is closed, but a handler that threw on the way to finding out\n * would reject the renderer's `invoke` with a stack trace instead of an answer.\n */\n ipcMain.handle('transfer:browse', async (_e, direction: string) =>\n transfer === undefined ? undefined : transfer.browse(direction));\n ipcMain.handle('transfer:submit', async (_e, keywords: readonly string[]) =>\n transfer === undefined\n ? { ok: false, error: 'the transfer window is not open' }\n : transfer.submit(keywords));\n ipcMain.on('transfer:cancel', () => { transfer?.requestCancel(); });\n\n /**\n * A QUIT CANCELS A RUNNING TRANSFER RATHER THAN BEING BLOCKED BY IT.\n *\n * The window refuses a close while running; it must not make the application unquittable.\n * Canceling here is what tells the host to leave transfer mode -- walking away leaves its\n * program waiting for a frame that never comes.\n *\n * ONE LISTENER, for the same scope reason as the handlers above: `app.on` accumulates, and a\n * per-window registration would cancel the same transfer once per window ever opened.\n * `shutdown` is safe to reach with nothing running, and swallows a `cancel` that throws --\n * which it can, on a dropped session -- because a throw here would be the unquittable app this\n * hook exists to prevent.\n */\n app.on('before-quit', () => { transfer?.shutdown('quit'); });\n\n /**\n * Compute the DRAW LIST here and send that, rather than sending the snapshot.\n *\n * `drawList` needs core's palette and code page, and a browser cannot resolve a bare\n * specifier like `@tn3270/core` without a bundler -- measured: the renderer failed with\n * \"Failed to resolve module specifier\". Doing it in main means the renderer imports only\n * its own relative modules, which is also less for it to know. It already owns no\n * protocol state, so a dropped or coalesced frame costs a repaint and never correctness.\n */\n /**\n * Grow the window to hold the whole screen, once we know how big the screen is.\n *\n * NOT COSMETIC. The screen geometry is not known until the host has spoken -- VM sends\n * Erase/Write Alternate and a model 4 becomes 43 rows -- and 44 rows of 14px is 616px,\n * which does not fit the 600px default. The first live run against VM CLIPPED the bottom\n * 16px, losing the entire OIA row: the host's own data and our status line, silently.\n *\n * The TUI refuses to draw rather than clip, because it cannot resize a terminal. A window\n * CAN resize, so it does -- and the scale follows the design's rule: the largest integer\n * multiple whose screen fits within 80% of the display work area, minimum 1.\n *\n * The rule itself is `windowScale` in measure.ts, which `-oversize auto`'s `measureDisplay` also\n * calls, so an auto-sized screen is drawn at the scale the model would have been; there is one\n * copy of the 0.8, and `measure.test.ts` tests it.\n */\n let sized = '';\n const fit = (list: { width: number; height: number }): void => {\n const key = `${list.width}x${list.height}`;\n if (key === sized) return; // most frames change nothing\n sized = key;\n const scale = windowScale(list, screen.getPrimaryDisplay().workAreaSize);\n win.setContentSize(list.width * scale, list.height * scale);\n };\n\n const send = (): void => {\n const snapshot = session.screen.snapshot();\n const oia = session.oia.toText();\n const list = drawList(\n snapshot, resolve(snapshot), geometry, scheme, oia === '' ? undefined : oia,\n );\n fit(list);\n win.webContents.send('frame', list);\n };\n session.on('screen', send);\n session.on('connect', send);\n session.on('disconnect', send);\n\n /**\n * THE APPLICATION MENU, and the app's first -- `setApplicationMenu` was never called before\n * copy/paste needed somewhere to live. Installed HERE, after `send` exists, because Paste needs\n * it to repaint.\n *\n * ## ITS ITEMS ARE INERT WHILE THE TRANSFER WINDOW HAS FOCUS\n *\n * AN ACCELERATOR REGISTERED HERE FIRES REGARDLESS OF WHICH WINDOW IS FOCUSED, so with the\n * transfer window open a `Cmd-V` would paste into the SESSION BEHIND IT -- the operator would\n * watch their clipboard land in a 3270 field while typing into an HTML form. That window's\n * fields are HTML inputs and get Chromium's native clipboard behavior for free, so the right\n * answer is to DECLINE rather than to forward. Same family as the recorded\n * closing-a-parent-skips-the-child-close finding: two windows, one global mechanism, and the\n * second window is the one nobody remembers.\n *\n * `isDestroyed()` BEFORE `isFocused()`, in that order: calling a method on a destroyed\n * BrowserWindow throws, and a closed transfer window leaves `transferWin` set.\n */\n const transferHasFocus = (): boolean => {\n const tw = transferWin;\n return tw !== undefined && !tw.isDestroyed() && tw.isFocused();\n };\n // `[...]` AND NOT A CAST, AND NO CAST AT ALL BEYOND IT: `buildMenuTemplate` returns a `readonly`\n // array and `buildFromTemplate` wants a mutable one, which is a real difference rather than a\n // nuisance -- Electron is free to sort or splice the array it is handed. A spread gives it its\n // own copy and keeps the template immutable on our side.\n //\n // THE REST TYPECHECKS STRUCTURALLY, which is the point of `menu.ts` not importing Electron:\n // `MenuItemTemplate` is assignable to `MenuItemConstructorOptions` because its `role` is a\n // LITERAL union rather than `string`. It was `string` first, and that does not overlap with\n // Electron's 40-name union at all -- TypeScript refuses even an `as` between them -- so the\n // alternative was an `as unknown as` double cast right here, which would have switched off\n // checking on the whole template to paper over one field.\n /**\n * THE MODEL MENU'S INPUTS, read off the session every time rather than cached: the setting\n * changes on every accepted choice and `connected` on every connect and disconnect.\n *\n * UNDEFINED FOR A LEGACY SESSION (`--terminal-type`), which therefore gets NO Model menu at all:\n * its terminal type was fixed at construction, `setGeometry` refuses it outright, and a menu of\n * eight models that all refuse would be eight controls that cannot work. `geometrySetting` is\n * the test, the same one the CLI's `Set()` makes.\n *\n * `startupFixed` is the `-oversize <cols>x<rows>` from argv, if that is what was given: the\n * only fixed size the menu can offer, because its choices are picked, never typed (the spec).\n */\n const startupOversize = args.oversize;\n const startupFixed: Size | undefined = startupOversize?.kind === 'fixed'\n ? { rows: startupOversize.rows, cols: startupOversize.cols } : undefined;\n const geometryMenu = (): GeometryMenu | undefined => {\n const current = session.geometrySetting;\n if (current === undefined) return undefined;\n return {\n // CONNECTING TOO: `setGeometry` refuses during an attempt in flight (`isConnecting`), which\n // has already resolved the setting it will advertise.\n models: Object.keys(KNOWN_MODELS), current,\n connected: session.isConnected() || session.isConnecting(),\n // For the Disconnect item only, and the web gateway's spelling for its reason: `isConnecting()`\n // is still true during the 'connect' event itself (`web/src/geometryChoices.ts`).\n connecting: session.isConnecting() && !session.isConnected(),\n ...(startupFixed !== undefined ? { startupFixed } : {}),\n };\n };\n /**\n * THE MENU IS MUTATED BY ID, NEVER REBUILT. `MenuItem.checked` and `.enabled` are writable\n * (\"This property can be dynamically changed\", electron.d.ts), and rebuilding the application\n * menu to change two booleans would re-create the macOS app menu and every accelerator in it.\n *\n * CALLED AFTER EVERY CLICK, ACCEPTED OR REFUSED: Electron checks a clicked radio item itself,\n * before our handler runs, so a refused choice would otherwise be left looking chosen.\n * `enabled` is written on EVERY item; `checked` only on the one true item of each group, which\n * clears the rest -- `applyChoiceStates` (menu.ts) records the measurement: assigning `false`\n * to a radio item's `checked` CHECKS it. `modelMenuStates` is the same function the template\n * was built from, so the menu cannot drift from the session.\n *\n * ON FOUR EVENTS. 'geometry' is the first thing `connect()` emits, before its socket opens and\n * with `isConnecting()` already true, so it disables the items for an attempt in flight;\n * 'connectSettled' re-enables them when an attempt FAILS, which emits nothing else; 'connect'\n * and 'disconnect' cover the rest.\n */\n const refreshModelMenu = (): void => {\n const g = geometryMenu();\n const menu = Menu.getApplicationMenu();\n if (g === undefined || menu === null) return;\n applyChoiceStates(modelMenuStates(g), (id) => menu.getMenuItemById(id));\n };\n for (const ev of ['geometry', 'connect', 'connectSettled', 'disconnect'] as const) {\n session.on(ev, refreshModelMenu);\n }\n Menu.setApplicationMenu(Menu.buildFromTemplate(\n [...buildMenuTemplate(process.platform, {\n /**\n * ONE MODEL-MENU CHOICE, applied at the next connect. NOT gated on focus: it changes a\n * setting, not the screen, so it is the same harmless request from any window.\n *\n * `applyGeometryChoice` (`@tn3270/frontend`, shared with the web gateway) owns the\n * order-dependence rule -- a model the current oversize refuses is applied with oversize\n * None, and the returned notice says so. Every other refusal (`disconnect first`; `wait for\n * the connection attempt to finish`; an oversize below the model) throws, and goes where\n * every other failure in this window goes.\n *\n * THE RESET NOTICE IS NOT A FAILURE, AND STILL GOES THROUGH `fail()`: this window has no\n * other channel. `error-message` is the only one the preload exposes (`preload.cts`); the\n * renderer's softer notice painter is `onCopyFailed`, which only the web bridge provides.\n * Adding a notice channel is a preload, renderer and bridge change for one line of text;\n * the red painter at least shows it, and the next frame wipes it like any message.\n */\n onGeometry: (choice) => {\n const current = session.geometrySetting;\n if (current === undefined) return;\n try {\n const notice = applyGeometryChoice(current, choice, (next) => { session.setGeometry(next); });\n if (notice !== undefined) fail(notice);\n } catch (err) {\n fail(explain(err));\n }\n refreshModelMenu();\n },\n /**\n * THE MODEL MENU'S DISCONNECT, 2026-11-05: drop the host session so the choices free up,\n * rather than waiting for the host to drop the line. Enabled only while connected (menu.ts);\n * the 'disconnect' it emits runs `refreshModelMenu` and repaints, like a host-side close.\n * Enter or Clear then reconnects with the new choice (`reconnectInstead`). A\n * `--terminal-type` session has no Model menu and so no Disconnect item: accepted, since it\n * has no choices to free.\n */\n onDisconnect: () => { session.disconnect(); },\n /**\n * OPEN THE KEYPAD, and NOT gated on which window has focus -- unlike Copy and Paste below.\n *\n * The gate exists for those two because they act on the SESSION, so firing one while the\n * transfer window has focus would paste into the 3270 screen behind an HTML form. Opening a\n * window is not that: it is the same harmless thing to ask for from anywhere, and an\n * operator who has the transfer window up and wants the keypad should get it rather than\n * silent nothing.\n *\n * `openKeypadWindow` shows-and-focuses an existing window, so a second invocation brings it\n * forward rather than stacking one. Rejection CAUGHT for the measured reason given at the\n * `toggleKeypad` intercept: `void` on a rejecting promise is what creates an unhandled one.\n */\n onKeypad: () => {\n openKeypadWindow().catch((err: unknown) => {\n process.stdout.write(`keypad window failed to open: ${explain(err)}\\n`);\n });\n },\n /**\n * OPEN THE FULL KEYBOARD, and ungated on focus for the reason given just above: opening a\n * window is the same harmless request from anywhere, unlike Copy and Paste which act on the\n * SESSION and are gated so they cannot fire into the 3270 screen behind an HTML form.\n *\n * Rejection CAUGHT, not voided -- `void` on a rejecting promise is what creates an unhandled\n * one, measured on the transfer window.\n */\n onKeyboard: () => {\n openKeyboardWindow().catch((err: unknown) => {\n process.stdout.write(`keyboard window failed to open: ${explain(err)}\\n`);\n });\n },\n onCopy: () => {\n if (transferHasFocus()) return;\n // THROUGH THE RENDERER, because the SELECTION lives there -- main knows the screen but not\n // what is selected on it. The global returns false when nothing is selected, which is not\n // an error worth reporting: the operator pressed Copy with no selection.\n void win.webContents.executeJavaScript('window.__tn3270Copy && window.__tn3270Copy()');\n },\n /**\n * ASYNC, BECAUSE ELECTRON 44's CLIPBOARD IS PROMISE-BASED AND THERE IS NO SYNC ALTERNATIVE.\n * `clipboard.readText(): Promise<string>` and `writeText(): Promise<void>`, modeled on the\n * W3C `navigator.clipboard` API (`electron.d.ts:7002`, `:7012`); the old synchronous forms\n * this feature's plan assumed are gone from the typings entirely. Nothing is lost -- a menu\n * click is already asynchronous from the session's point of view -- but it must be AWAITED\n * rather than voided, or a paste would read an empty string and silently do nothing.\n *\n * The `catch` is what keeps a clipboard failure from becoming an unhandled rejection:\n * measured on this project's transfer window, `void` on a rejecting promise is what CREATES\n * one. A clipboard the OS refuses (no X selection owner, a locked Wayland portal) must\n * report and leave the session alone.\n */\n onPaste: () => {\n if (transferHasFocus()) return;\n void (async (): Promise<void> => {\n const text = await clipboard.readText();\n if (text === '') return;\n /**\n * THE SHARED HELPER, 2026-10-23: the wording and the thresholds now live in\n * `frontend/src/pasteAction.ts` so the web gateway cannot drift from them. The rules\n * themselves are unchanged -- remedy first, because the status line TRUNCATES and this\n * project has already shipped a message whose only actionable phrase fell past the cut.\n * A silently half-applied paste is the worst outcome available here: the operator cannot\n * see how much of a dataset name arrived, and a short one may be a VALID different name.\n */\n const r = applyPaste(session, text);\n if (r.warning !== undefined) win.webContents.send('error-message', r.warning);\n // UNCONDITIONALLY, whatever the outcome: a paste is a LOCAL action and emits no `screen`\n // event, so without this the window shows nothing until the host next speaks. Measured on\n // the same path for `tab` in the web gateway, where it cost a 5-second test timeout.\n send();\n })().catch((err: unknown) => {\n // A CLIPBOARD THE OS REFUSES is reportable and recoverable, not fatal: no X selection\n // owner, a Wayland portal that declined. Carries no path, so it is safe to print on an\n // ungated run -- the same rule the transfer window's load failure follows.\n process.stdout.write(`paste failed: ${String(err)}\\n`);\n });\n },\n }, geometryMenu())],\n ));\n /**\n * THE SESSION GOING AWAY UNDERNEATH, which is the fourth of the paths `transferWindow.ts`'s\n * docstring names and the one nothing handled.\n *\n * A repaint alone is not enough, and the gap is not a leak in core: `Session.handleClose` clears\n * its own `dft`, so nothing is left registered down there. It is the CONTROLLER that keeps\n * `run !== undefined` forever, which makes `shouldPreventClose()` permanently true -- so the\n * transfer window refuses EVERY close on a session that is already dead. The only control left is\n * Cancel, and that throws `not connected` (`core/src/session.ts:1513`), which `cancelRun`\n * swallows and reports: an escape by error message rather than by a working control.\n *\n * A SEPARATE LISTENER rather than a line inside `send`, because `send` is the paint path and runs\n * on every frame -- putting teardown in it would mean re-deciding this on each of them. The TUI\n * does the equivalent from its own close path (`tui/src/app.ts:899-900`, `closeTransfer`), so\n * this makes the two front ends agree rather than inventing a rule for this one.\n *\n * `shutdown` and not `requestCancel`: this is the same \"cancel rather than block\" case as a quit,\n * and it must not throw out of an event listener -- `shutdown` swallows a failed cancel, which on\n * a dropped socket is the expected outcome rather than a surprise.\n *\n * ## `'sessionLost'` IS THE ONE REASON THAT STILL HAS SOMEBODY TO TELL\n *\n * AND THE ONE THING CANCELING DOES NOT ACHIEVE BY ITSELF, which cost this branch a defect: at\n * 24x80 the cancel THROWS on its way to the driver's `finish` (`transferRun.ts:438` ->\n * `session.sendAID` -> `not connected`), so `onDone` never fires and the form was left showing\n * 'transferring' with a dead Cancel and a dead Start -- then 30 seconds later the driver's own\n * frame timer said `press Attn or Clear: host may still be transferring`, at a host that was gone.\n * Geometry-dependent, and 24x80 is the broken one. Naming the reason is what lets `shutdown` send\n * an honest ending instead, and bump the generation so that stale timeout cannot contradict it.\n */\n session.on('disconnect', () => { transfer?.shutdown('sessionLost'); });\n\n /**\n * Every action the renderer sends, logged for the chord harness -- and ONLY while BOTH\n * the keys seam and replay mode are active.\n *\n * THE GATE IS A PRIVACY REQUIREMENT, NOT TIDINESS. A `type` action carries the text\n * typed, so logging unconditionally would put a password on stdout in a live session --\n * the same hazard that keeps goldens away from live logons.\n *\n * SEAM PRESENCE ALONE IS NOT ENOUGH -- do not \"simplify\" this back to one variable.\n * `TN3270_GUI_KEYS` has been set against a LIVE host before (docs/live-testing.md,\n * \"Typed input, proved end to end\"), and `keys.ts` builds a `type` action carrying the\n * text for any printable keypress, seam-driven or not -- so the seam variable alone says\n * nothing about what is on screen. REPLAY mode is what actually makes a logged keystroke\n * safe: it cannot reach a host, so nothing typed while it is active can be a live\n * credential. Both variables must be set together, which is how the Task 5 harness and\n * the Task 3 manual smoke test already use them, so this costs no coverage.\n *\n * This is the ONE funnel every renderer action passes through, which is why the harness\n * asserts here rather than on pixels: in replay mode nothing is connected, `sendAID`\n * throws 'not connected', and `applyAction` swallows it, so a PA key has no other\n * observable consequence.\n *\n * Key order in `JSON.stringify(action)` is insertion order, not a stable contract -- a\n * consumer of this log should parse and compare canonically rather than string-diffing\n * the raw line.\n */\n const logActions = SEAM.keys !== '' && SEAM.replay !== '';\n\n ipcMain.on('action', (_e, action: Action) => {\n if (logActions) process.stdout.write(`action: ${JSON.stringify(action)}\\n`);\n // `quit` is THIS front end's business: applyAction throws on it rather than ignoring\n // it, so a front end that forgot this check fails loudly instead of being unquittable.\n if (action.kind === 'quit') { app.quit(); return; }\n /**\n * `Ctrl-K` NOW OPENS A WINDOW instead of toggling a region of the canvas.\n *\n * STILL INTERCEPTED HERE, because `applyAction` still throws on it: showing a keypad is a\n * display decision and this is the front end that owns this display. The ACTION'S NAME is\n * unchanged, which is what lets `canvas/src/keys.ts`, the keypad's own `Ctrl-K` and\n * `keys.mjs` all keep working with no edit -- the kind is the seam, not the mechanism.\n *\n * OPEN, NOT TOGGLE, and that is the user's decision rather than a simplification: the\n * complaint was that the keypad was MODAL and awkward to leave up. Closing it is the window's\n * own close button. A chord that closed it again would rebuild exactly the toggle this work\n * exists to remove -- and `openKeypadWindow` shows-and-focuses an existing window, so pressing\n * the chord twice brings it forward rather than stacking a second one.\n *\n * NO `send()` ANY MORE. The old arm repainted because the keypad lived in the draw list and\n * `fit()` resized the window from it. Nothing about a separate window touches the frame, so a\n * repaint here would be work with no visible effect.\n *\n * THE REJECTION IS CAUGHT, NOT VOIDED. Measured on the transfer window: `void` on a rejecting\n * promise is what CREATES an unhandled rejection, and there it buried a `keys: TIMED OUT` line\n * that said what had actually gone wrong.\n */\n if (action.kind === 'toggleKeypad') {\n openKeypadWindow().catch((err: unknown) => {\n process.stdout.write(`keypad window failed to open: ${explain(err)}\\n`);\n });\n return;\n }\n /**\n * `Ctrl-B`, THE FULL KEYBOARD, intercepted for exactly the reason `toggleKeypad` is:\n * `applyAction` THROWS on it (`frontend/src/actions.ts:75-78`, \"the front end owns its own\n * display\"), so a front end that forgot this arm would die on the keystroke rather than being\n * silently inert -- which is the property that throw exists to give.\n *\n * OPEN, NOT TOGGLE, like the keypad's: the window's own close button closes it, and\n * `openKeyboardWindow` shows-and-focuses an existing one so the chord cannot stack two.\n *\n * ## IT DOES NOT CLOSE THE KEYPAD, AND THAT IS A DELIBERATE DIVERGENCE FROM THE WEB GATEWAY\n *\n * `web/src/bridge.ts` makes opening one surface HIDE the other, and gives the reason: two\n * overlapping OPAQUE overlays over one canvas would be unreadable. **That reason does not\n * exist here.** These are two separate windows the operator can place side by side, and an\n * operator who wants both -- the quick keypad for AIDs and the full keyboard for characters --\n * should get both. Closing one because the other opened would be the MODAL behaviour this\n * whole feature was built to remove, and the user's complaint about the old canvas keypad was\n * precisely that.\n *\n * So the divergence is not an omission: the web rule is a constraint of its medium, and\n * copying it into a medium without that constraint would be copying the symptom.\n *\n * THE REJECTION IS CAUGHT, NOT VOIDED. Measured on the transfer window: `void` on a rejecting\n * promise is what CREATES an unhandled rejection, and there it buried the line that said what\n * had actually gone wrong.\n */\n if (action.kind === 'toggleKeyboard') {\n openKeyboardWindow().catch((err: unknown) => {\n process.stdout.write(`keyboard window failed to open: ${explain(err)}\\n`);\n });\n return;\n }\n /*\n `Alt-M`, THE MODEL/OVERSIZE CHOOSER, 2026-11-04 -- INTERCEPTED NOW, BEFORE IT DOES ANYTHING,\n AND THE ORDER OF THOSE TWO IS THE POINT.\n\n `applyAction` THROWS on `geometryMenu` (the eighth guard in `frontend/src/actions.ts`), and a\n throw in this listener FREEZES THE MAIN PROCESS -- the `requestCopy` note below has the\n measurement. And the kind can arrive from windows this file does not draw the canvas in:\n `canvas/src/forwardKeys.ts`'s `forwardKeys` forwards whatever `actionForKey` returns from the\n KEYPAD AND KEYBOARD WINDOWS with no kind filtering, and `actionForKey` raises this on `Alt-M`\n the moment `keys.ts` binds it. So the arm had to land in the same commit as the binding, and\n `keys.mjs`'s `kbd:Alt+M` case followed by its liveness probe is the witness for that path.\n\n EVERY WINDOW GETS THE SAME ANSWER, deliberately not `requestCopy`'s child-window drop. That\n kind is dropped because a child window holds no selection; a menu needs nothing a child\n window lacks, and `forwardKeys` records the user's rule that ALL CHORDS WORK EVERYWHERE. The\n Model menu is the APPLICATION's, so popping it from the keypad window is as correct as from\n the canvas, and the sender is not consulted.\n\n IT POPS UP THE MODEL MENU'S SUBMENU (Task 13), RELATIVE TO THE MAIN WINDOW `win` and never\n the sender: a popup over the keypad window would be a menu about the session drawn over a\n window that is not the session. `getMenuItemById('model')` finds nothing for a legacy\n (`--terminal-type`) session, which has no Model menu, so the chord does nothing there --\n silently, because there is no remedy to offer short of a restart, and the menu's absence is\n already the statement. Still a `return` before `applyAction` either way.\n */\n if (action.kind === 'geometryMenu') {\n const model = Menu.getApplicationMenu()?.getMenuItemById('model')?.submenu;\n if (model !== undefined && !win.isDestroyed()) {\n model.popup({ window: win });\n // FOR `keys.mjs`, under the action log's own gate: the logged kind above proves the chord\n // ARRIVED, not that a menu opened, and without this line deleting the popup passes every\n // gate. `popup` returns at once on Linux (measured: the next chord's action is logged\n // while the menu is still up), so this line is not a promise that it was dismissed.\n if (logActions) process.stdout.write('menu: popup model\\n');\n }\n return;\n }\n /**\n * INTERCEPTED HERE for the same reason as the three above: `applyAction` THROWS on `copy`,\n * because what a clipboard IS belongs to the front end. This one has an OS clipboard; the\n * gateway has the operator's browser, and refuses the action outright for that reason.\n *\n * THE EXTRACTION HAPPENS ON THIS SIDE AND NOT IN THE RENDERER, and that is forced rather than\n * chosen: a `DrawCell` carries a CG-order atlas glyph and NO character\n * (`canvas/src/drawlist.ts:111-121`), so the renderer has no text to send and sends the\n * RECTANGLE. This side already calls `resolve(snapshot)` every frame in `send` above, which is\n * where `text` and `hidden` both live.\n *\n * `hidden` IS HONORED INSIDE `extractText`, which is the one line in this feature whose\n * failure mode is a leaked credential -- `ResolvedCell.text` is still the real character in a\n * password field. Mutation-verified in `canvas/test/selection.test.ts`.\n *\n * AN EMPTY RESULT DOES NOT TOUCH THE CLIPBOARD. Copying a blank region must not silently wipe\n * whatever the operator had copied from somewhere else; doing nothing is the lesser surprise.\n */\n if (action.kind === 'copy') {\n const snapshot = session.screen.snapshot();\n const text = extractText(resolve(snapshot), snapshot.cols, action.rect);\n if (text !== '') clipboard.writeText(text);\n // THE LENGTH AND NEVER THE TEXT. The action log above is gated behind `--log-actions`\n // precisely because a `type` action carries typed characters; this line is UNGATED and runs\n // against a live host, and a copied rectangle can contain anything on the screen. A count is\n // enough for `select.mjs` to assert against and cannot leak a field.\n process.stdout.write(`copy: ${text.length} chars\\n`);\n return;\n }\n /*\n `requestCopy` IS DROPPED HERE, AND THIS ARM IS REACHABLE -- DO NOT DELETE IT AS DEAD CODE.\n\n WHO ACTUALLY SENDS IT. `canvas/src/renderer.ts` consumes the kind in its own keydown listener\n and turns it into the `copy` above, so the CANVAS window never raises it. The senders are the\n KEYBOARD AND KEYPAD WINDOWS: `canvas/src/forwardKeys.ts`'s `forwardKeys` calls\n `actionForKey(e)` and forwards whatever comes back with NO kind filtering, and\n `keyboardBoot.ts` / `keypadBoot.ts` install it with `sendAction` wired straight to\n `ipcRenderer.send('action', ...)`. Those documents load `keyboardBoot.js` / `keypadBoot.js`\n and NOT `renderer.js`, so the renderer's interception does not cover them. `Ctrl-Shift-C` with\n either window focused reached `applyAction` before this arm existed, which THROWS on the kind\n -- and a throw in this listener does not merely log, it FREEZES THE MAIN PROCESS EVENT LOOP,\n with no `uncaughtException` handler anywhere in this file. Measured as a freeze in the\n previous feature; see the `paste` note below, which carries the same severity for the same\n reason.\n\n A BARE `return` AND NOT A FORWARD TO THE CANVAS WINDOW'S `__tn3270Copy`. A child window holds\n no selection -- the rectangle lives in the canvas renderer's module state -- so there is\n nothing here to copy, and an arm that claimed to copy would be lying. Making the chord work\n from a child window (as `Ctrl-B` self-focuses) is a FEATURE and not part of closing a freeze;\n `web/src/protocol.ts:324` already records that this kind's interception can only be a bare\n return.\n\n THE KIND CANNOT BE SHAPE-CHECKED, which is why the two-branch rule is satisfied differently\n here than for `copy` above: `requestCopy` carries NO FIELDS, so there is nothing to validate\n and a malformed one is indistinguishable from a well-formed one. The two branches are per\n FRONT END rather than per field -- this front end INTERCEPTS the kind, and the web gateway\n REJECTS it at decode (`web/src/protocol.ts:232`) with a per-client `error` frame, because\n there the sender is a remote browser rather than one of our own windows. Both DOM front ends\n are covered, by different mechanisms chosen to suit who can send.\n\n NOTHING IS LOGGED. The action log at the top of this listener already records the kind when\n `--log-actions` is on, which is what the harness asserts against; a second unconditional line\n would say nothing the operator can act on, since the chord's only honest effect in a child\n window is nothing at all.\n */\n if (action.kind === 'requestCopy') return;\n /*\n THERE IS DELIBERATELY NO `paste` ARM HERE, AND ADDING ONE IS NOT THE SAFE CHANGE IT LOOKS\n LIKE. 2026-10-26.\n\n `paste` is the ONE kind `applyAction` throws on that this front end does not intercept, so a\n reader who has just read the arms above -- `quit`, `toggleKeypad`, `toggleKeyboard`,\n `geometryMenu`, `copy` and `requestCopy` -- will notice the gap and reach for one more. (`transferForm` is the\n remaining one and it is BELOW this comment, so a reader counting upward from here finds one\n fewer than the listener has.)\n\n THE ARMS ARE NAMED AND NOT COUNTED, deliberately, and the previous version of this paragraph\n is why: it said \"the FOUR arms above\" and then defended the number -- \"`transferForm` is the\n fifth and it is BELOW this comment, which is why the count here is four and not five.\" Four\n was right until `4d56edb` inserted the `requestCopy` arm directly above this comment, and a\n self-justifying count is worse than a bare one, because a reader who counts five distrusts\n the whole comment. The LIST goes stale loudly -- a missing name is visible -- where a numeral\n goes stale silently.\n\n What makes the gap survivable is that nothing in this app RAISES the kind: Paste is a menu\n item (`menu.ts`) wired to `onPaste` -> `applyPaste` above, which never goes through here.\n\n THE GAP IS GUARDED ON THE OTHER SIDE, by `forwardPaste` in `canvas/src/forwardKeys.ts`\n refusing to install its `paste` listener on `file:` -- this window's own scheme. The one\n consequence worth carrying here is the severity: the throw would land in THIS listener, and\n it does not merely log -- it FREEZES THE MAIN PROCESS EVENT LOOP, with no\n `uncaughtException` handler anywhere in this file to soften it.\n\n AND AN ARM HERE WOULD NOT MERELY BE REDUNDANT, IT WOULD DOUBLE-PASTE, because the Paste\n accelerator and the page's `paste` event both fire for one keystroke. If you want the GUI's\n native `Ctrl+V` to work, the two have to be reconciled first -- see `pasteAccelerator` in\n `menu.ts`, which is where that decision belongs.\n\n **`forwardPaste`'S DOCSTRING IS THE SINGLE HOME FOR BOTH MEASUREMENTS** and for the Electron\n version they were taken on. Cited rather than restated: they were briefly copied into this\n comment and two others, which would have let one Electron upgrade falsify three files at\n once -- the duplication `d4fce66` exists to stop.\n */\n // INTERCEPTED HERE for the same reason as `quit` and `toggleKeypad`: `applyAction` THROWS on\n // it (`frontend/src/actions.ts:57-58`), because a transfer dialog is the front end's own\n // business. A front end that forgot this arm would die on the keystroke rather than being\n // silently inert -- which is the property that throw exists to give.\n //\n /**\n * NOT AWAITED -- this is a fire-and-forget UI action and the listener is deliberately not\n * `async` -- but the rejection IS CAUGHT, and the difference is not cosmetic.\n *\n * MEASURED 2026-10-01, on the first live run of `live-transfer.py`: a bare\n * `void openTransferWindow()` printed\n * `UnhandledPromiseRejectionWarning: Error: ERR_FAILED (-2) loading transfer.html`. The trigger\n * is any failure inside that promise, and the one that fired is worth knowing: the wait seam\n * called `app.exit(1)` on a timed-out `wait:` step while `loadFile` was in flight, so the load\n * was aborted by the shutdown it was racing. An earlier version of this comment claimed `void`\n * AVOIDED an unhandled rejection; `void` on a rejecting promise is what CREATES one, and the\n * `did-fail-load` forwarding it pointed at reports the failure without settling the promise.\n *\n * `catch` and not `await`, because awaiting would make the listener async and delay every\n * other action behind a window load. The handler reports and continues: a transfer window that\n * failed to open must not take the session down with it, and this is the one action whose\n * failure is recoverable by pressing the key again.\n */\n if (action.kind === 'transferForm') {\n openTransferWindow().catch((err: unknown) => {\n /**\n * NOT PREFIXED `transfer window:`, deliberately, and the guard test is what forced the\n * question. That prefix is the SEAM's reporting vocabulary and every line carrying it is\n * required to live inside the gated function -- because those lines carry paths. This one\n * is a real user-facing diagnosis on an UNGATED path: it must print on a normal run, since\n * a window that failed to open is something the operator needs told. Keeping the prefix\n * would have put an ungated write into the census the gate is proved by, which is the leak\n * that test exists to catch; borrowing the vocabulary of a gated channel for an ungated\n * line is how a privacy rule gets quietly widened.\n *\n * It carries no path -- only Electron's own load error -- which is what makes it safe to\n * print unconditionally.\n */\n process.stdout.write(`transfer window failed to open: ${explain(err)}\\n`);\n });\n return;\n }\n applyAction(session, action);\n send();\n });\n\n /**\n * A SECOND TEST SEAM: `TN3270_GUI_REPLAY=<trace>` paints a recorded trace instead of\n * connecting.\n *\n * This is how screenshot goldens are made, and the reason is not convenience. A golden\n * taken from a live logon can contain a typed password -- and goldens live in git\n * forever. It also cannot be byte-reproducible if the host paints a clock, which TK5's\n * VTAM logon panel does. A synthetic trace has neither problem and is identical on every\n * run. `docs/live-testing.md` already warns that traces carry passwords in EBCDIC, so\n * the trace CHOSEN matters as much as the mechanism.\n */\n if (SEAM.replay !== '') {\n // NO GEOMETRY IS RESOLVED HERE: only `connect()` resolves it, so `-oversize` (and `auto`'s\n // measure) does not apply under TN3270_GUI_REPLAY -- the session keeps the model's sizes.\n session.replay(readFileSync(SEAM.replay, 'utf8'));\n send();\n // The session is passed so a `wait:` step works here too -- a replay paints synchronously, so\n // every wait in a replay run matches on its first poll. That is what makes the seam's own\n // guard test runnable without a host.\n //\n // The keyboard-window getter is passed so a `kbd:` step can reach the child window opened by a\n // `Ctrl+B` earlier in the same list -- the path `forwardKeys` runs, which no other seam reaches.\n await maybeSendKeys(win, session, () => keyboardWin);\n // AFTER the keys, always: `clicks.mjs` shows the keypad with a real Ctrl+K, and there is no\n // keypad to click before that chord has been delivered and repainted.\n await maybeSendClicks(win, () => keypadWin);\n // AFTER the clicks and the keys: a selection is cleared by any claimed keystroke and by every\n // host frame, so a drag delivered before them would be wiped before it could be copied.\n await maybeSelect(win);\n await maybeOpenTransferWindow();\n await maybeCapture(win);\n await quitIfKeysOnly();\n return;\n }\n\n try {\n await session.connect(args.host!, args.port ?? 23,\n args.lus !== undefined && args.lus.length > 0 ? { lus: args.lus } : {});\n } catch (err) {\n // Host and port passed in, so a TLS or socket failure names the actual target rather\n // than a placeholder -- 'vm:3270 accepted the connection but never completed a TLS\n // handshake ... use -insecure' is the message that matters against Hercules.\n fail(explain(err, {\n host: args.host!, port: args.port ?? 23,\n tls: (args.tls ?? DEFAULT_TLS).kind !== 'plaintext', remedy,\n }));\n }\n\n // THE LIVE PATH, and the only one where a `wait:` step earns its keep: this is the branch a\n // real host is on, and the one `live-transfer.py` drives.\n await maybeSendKeys(win, session, () => keyboardWin);\n await maybeSendClicks(win, () => keypadWin); // after the keys, for the reason the replay branch gives\n await maybeSelect(win); // and after both, for the reason given there too\n await maybeOpenTransferWindow();\n await maybeCapture(win);\n await quitIfKeysOnly();\n}\n\n/**\n * A THIRD TEST SEAM: `TN3270_GUI_KEYS='Alt+1,Ctrl+A,Enter'` delivers REAL key events.\n *\n * `webContents.sendInputEvent` goes in at the top of Chromium's input pipeline, so this\n * exercises the ONE link nothing else can reach: the renderer's own `keydown` listener,\n * `actionForKey`, the IPC hop and `applyAction`. A seam that injected actions at `ipcMain`\n * instead would skip exactly the part that had never run.\n *\n * SPELLINGS ARE ELECTRON ACCELERATOR NAMES, NOT DOM CODE NAMES -- `1` and `Up`, never\n * `Digit1` or `ArrowUp`, which are delivered as EMPTY events. `parseKeySpec` refuses those\n * by name; see `keyspec.ts` for the measurement.\n *\n * Note that a spelling's CASE IS IGNORED by Chromium: both `A` and `a` deliver `key: 'a'`,\n * so this seam types lowercase unless `Shift+` is given.\n *\n * THE MAC BEHAVIOR HERE IS REASONED, NOT MEASURED. Accelerator names are Chromium's own\n * vocabulary and ought to be platform-independent, but this seam has only ever run on Linux\n * under Xvfb. Treat a Mac disagreement as likely rather than surprising: `Option-1` reports\n * `key === '\u00A1'` there, which is the whole reason `keys.ts` matches the PA keys on `e.code`,\n * and this is the first place to look if a chord goes missing on a laptop.\n *\n * THE COMMITTED HARNESSES deliberately do not type a logon: on VM a completed logon arms the\n * reconnect trap for the next run (docs/live-testing.md), and a golden of a logged-on screen can\n * contain a password. **That is a property of `keys.mjs` and `clicks.mjs`, not a limit of this\n * function** -- `live-transfer.py` DOES log on, because a live transfer cannot happen anywhere but\n * at a logged-on command prompt. It logs off again at the end for the reconnect-trap reason, and\n * it prints no screen text for the password one. The `wait:` step is what makes that possible; see\n * `SEAM.waitMs`.\n *\n * `session` is optional because the `TN3270_GUI_URL` branch has none at all -- in that mode the\n * served page owns the protocol. A `wait:` step there is refused rather than silently skipped,\n * since a wait nobody can satisfy is the stall this seam exists to remove.\n */\n/**\n * Collapse a 3270 screen to something a substring match can be written against.\n *\n * A formatted screen PADS WITH BLANKS and a phrase can straddle a field boundary, so\n * `CP READ` on screen may be `CP READ` in cells. Runs of whitespace become one space and the\n * ends are trimmed, which is the same normalisation `live-drive.py`'s `plain()` applies before its\n * own `wait_for`. Exported for the test, which cannot otherwise reach it.\n */\nexport function screenNeedleText(cells: readonly { readonly text: string }[]): string {\n return cells.map((c) => c.text).join('').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Block until the host has painted `text`, or `SEAM.waitMs` elapses.\n *\n * Returns whether it matched; the CALLER ends the run on false, because only it can drain stdout\n * and pick an exit code.\n *\n * ## IT POLLS RATHER THAN LISTENING, DELIBERATELY\n *\n * `session.on('screen')` would be the event-driven form and is the wrong one here: the text may\n * ALREADY be on screen when the wait begins -- the common case for the first step of a scenario,\n * and for any wait that follows a key the host answered quickly -- and a listener-only wait would\n * then block until the next unrelated repaint, or forever. Polling asks the question the scenario\n * is actually asking (\"is it there yet?\") and answers it immediately when it already is.\n *\n * The 200ms interval is well under any human-visible delay and far above the cost of one\n * `snapshot()` + `resolve()`, which is what the renderer already does on every frame.\n */\nasync function waitForScreen(session: Session, text: string): Promise<string | undefined> {\n // ALTERNATIVES, and an empty one is dropped rather than matching everything: `a|` would\n // otherwise succeed against any screen including a blank one, which is the vacuous-wait failure\n // the whole seam is built to avoid.\n const needles = text.split('|').map((n) => n.replace(/\\s+/g, ' ').trim()).filter((n) => n !== '');\n const deadline = Date.now() + SEAM.waitMs;\n for (;;) {\n const seen = screenNeedleText(resolve(session.screen.snapshot(), {}));\n // RETURNS WHICH ONE MATCHED, so the caller can log it: on VM the difference between seeing\n // `VM/370` and `CP READ` is the difference between a fresh session and a reconnect to a held\n // one, and a run that cannot tell them apart cannot explain its own result afterwards.\n const hit = needles.find((n) => seen.includes(n));\n if (hit !== undefined) return hit;\n if (Date.now() >= deadline) return undefined;\n await new Promise((r) => setTimeout(r, 200));\n }\n}\n\nasync function maybeSendKeys(\n win: BrowserWindow,\n session?: Session,\n /**\n * WHICH WINDOW A `kbd:` STEP TARGETS, for the reason `KBD_PREFIX` and `maybeSendClicks` both\n * give. A GETTER, because the window is opened by a chord earlier in the same list.\n *\n * DEFAULTS TO \"NO SUCH WINDOW\" rather than to `win`: in `TN3270_GUI_URL` mode there is no\n * Electron keyboard window at all (the browser's keyboard is an overlay in the page), so a\n * `kbd:` step there must report its absence rather than quietly become a canvas chord.\n */\n keyboardTarget: () => BrowserWindow | undefined = () => undefined,\n): Promise<void> {\n if (SEAM.keys === '') return;\n /**\n * EVERY spec is parsed before ANY key is delivered, and a refusal EXITS rather than throws.\n *\n * `parseKeySpec` throws by design, and this runs inside `app.whenReady()`'s promise, where\n * a throw is an unhandled rejection: the steps after it -- including `quitIfKeysOnly` --\n * never run. MEASURED: `TN3270_GUI_KEYS=Digit1` printed the refusal and then SAT until the\n * harness timeout killed it, which reads as a broken client rather than as the typo it is.\n * A non-zero exit keeps the diagnosis and loses the hang. Parsing the whole list up front\n * also means a bad third spec cannot half-deliver the first two, which would leave the\n * action log looking like a mapping bug.\n */\n /**\n * A `wait:` STEP IS NOT PARSED AS A CHORD, and it is validated in this same up-front pass so a\n * wait with no session fails before any key goes in -- the whole point of parsing everything\n * first.\n */\n /**\n * A `kbd:` STEP IS A CHORD WITH A DIFFERENT TARGET, so it carries the same parsed `KeySpec` and\n * is validated in this same up-front pass -- `kbd:Digit1` must be refused by name before any key\n * is delivered, exactly as a bare `Digit1` is.\n */\n type Step =\n | { readonly kind: 'chord'; readonly spec: KeySpec; readonly toKeyboard: boolean }\n | { readonly kind: 'wait'; readonly text: string };\n let steps: Step[];\n try {\n steps = SEAM.keys.split(',').map((spec): Step => {\n if (spec.startsWith(KBD_PREFIX)) {\n const chord = spec.slice(KBD_PREFIX.length);\n // REFUSED RATHER THAN TREATED AS A BARE `kbd:`, for the reason the empty wait is: a step\n // that names no key cannot be anything but a typo, and silently sending nothing would make\n // the run report a delivery it never made.\n if (chord.trim() === '') {\n throw new Error(`empty ${KBD_PREFIX} chord in ${JSON.stringify(spec)}`);\n }\n return { kind: 'chord', spec: parseKeySpec(chord), toKeyboard: true };\n }\n if (!spec.startsWith(WAIT_PREFIX)) {\n return { kind: 'chord', spec: parseKeySpec(spec), toKeyboard: false };\n }\n const text = spec.slice(WAIT_PREFIX.length);\n // AN EMPTY WAIT MATCHES EVERY SCREEN INCLUDING A BLANK ONE, so it is a typo rather than a\n // no-op and is refused the way `parseKeySpec` refuses an empty chord.\n if (text.trim() === '') throw new Error(`empty ${WAIT_PREFIX} text in ${JSON.stringify(spec)}`);\n if (session === undefined) {\n throw new Error(\n `${WAIT_PREFIX}${text} needs a session, and TN3270_GUI_URL mode has none: in that mode `\n + 'the served page owns the protocol and this process never sees the screen',\n );\n }\n return { kind: 'wait', text };\n });\n } catch (err) {\n // Drained before exiting, for the reason quitIfKeysOnly spells out: a write to a PIPE is\n // asynchronous, and app.exit would otherwise be free to discard the one line that says\n // what was wrong.\n await new Promise<void>((r) => { process.stderr.write(`${explain(err)}\\n`, () => { r(); }); });\n // 2 is this repo's usage-error code, as cli/src/main.ts and tui/src/main.ts both use it\n // for a bad argument: a misspelled spec is the same kind of mistake, so it exits the same\n // way. And this is the ONE failure that goes to stderr only, against the rule at the top\n // of this file, because the only route to it is an env var somebody deliberately set --\n // nobody who double-clicked a `.app` can reach it, so there is no console-less user to\n // strand.\n app.exit(2);\n return;\n }\n /**\n * Let the paint settle first: typing into a screen that has no field yet proves nothing,\n * and the OIA would rightly refuse the input.\n *\n * `TN3270_GUI_SHOT_MS` IS A FLOOR WHEN A SCREENSHOT IS ALSO BEING TAKEN. Keys used to be\n * sent from inside `maybeCapture`, i.e. always after that deliberately generous wait, and\n * making the seam independent would otherwise have quietly cut a shot run's settle from\n * 2500ms to 1200ms -- a REGRESSION dressed as a refactor, and invisible to a replay smoke\n * test because a replay paints synchronously. `Math.max` also means an explicit\n * `TN3270_GUI_KEYS_MS` still wins whenever it is the larger number.\n *\n * The 1200ms default therefore only has to cover a synchronous replay paint plus startup.\n * A LIVE OR SLOW HOST SHOULD RAISE `TN3270_GUI_KEYS_MS` EXPLICITLY: against VM the first\n * screen can take seconds to arrive, and a chord delivered before it does is an input\n * inhibit, not a failed mapping -- which is a confusing thing to debug from a log.\n */\n const settleMs = SEAM.shot !== '' ? Math.max(SEAM.keysMs, SEAM.shotMs) : SEAM.keysMs;\n await new Promise((r) => setTimeout(r, settleMs));\n for (const step of steps) {\n if (step.kind === 'wait') {\n const hit = await waitForScreen(session!, step.text);\n if (hit === undefined) {\n /**\n * A TIMED-OUT WAIT ENDS THE RUN, and this is the single most important line in the seam.\n *\n * Proceeding would type into a keyboard the host still has locked, so every remaining\n * key would be correctly refused and the run would report that it sent them all -- a\n * stall dressed as success, which is the shape this seam exists to remove rather than to\n * reproduce one layer up.\n *\n * THE SEARCH TEXT IS NAMED AND THE SCREEN IS NOT. What the harness asked for is the\n * harness's own string; what is on screen at that moment may be a LOGMSG or a password\n * the previous step typed. `live-drive.py` keeps its panels in /tmp behind a warning for\n * exactly this reason, and stdout is not /tmp.\n */\n await new Promise<void>((r) => {\n process.stderr.write(\n `keys: TIMED OUT after ${SEAM.waitMs}ms waiting for ${JSON.stringify(step.text)}\\n`,\n () => { r(); },\n );\n });\n shuttingDown = true;\n app.exit(1);\n return;\n }\n // WHICH alternative matched, not the whole spec: for an alternation that is the informative\n // half, and for a single needle the two are the same string.\n process.stdout.write(`keys: saw ${JSON.stringify(hit)}\\n`);\n continue;\n }\n const { keyCode, modifiers } = step.spec;\n // Spread conditionally: an empty `modifiers` array is not the same as absent under\n // exactOptionalPropertyTypes, and the rest of this file builds options the same way.\n const chord = { keyCode, ...(modifiers.length > 0 ? { modifiers: [...modifiers] } : {}) };\n /**\n * WHICH WINDOW THIS CHORD GOES TO, defaulting to the canvas.\n *\n * A `kbd:` step with NO KEYBOARD WINDOW is REPORTED AND SKIPPED rather than silently\n * retargeted at the canvas, and that choice is the whole value of the step: the canvas runs a\n * DIFFERENT keydown path, so a fallback would deliver the chord to the one place that already\n * handles it and the run would pass while proving nothing about the child window. The line is\n * the same shape as `clicks: NO KEYPAD WINDOW` above and exists for the same reason.\n *\n * THE GETTER IS EVALUATED HERE AND NOT AT CALL TIME, matching `maybeSendClicks`: the keyboard\n * window is created by a `Ctrl+B` EARLIER IN THIS SAME LIST, so a window captured when the\n * function was called would always be `undefined`.\n */\n let target = win;\n if (step.toKeyboard) {\n const kbw = keyboardTarget();\n if (kbw === undefined || kbw.isDestroyed()) {\n process.stdout.write(`keys: NO KEYBOARD WINDOW for ${KBD_PREFIX}${keyCode}\\n`);\n continue;\n }\n target = kbw;\n // FOCUSED FIRST, because `forwardKeys` listens on that window's own `window` object: an\n // unfocused window still receives `sendInputEvent` (it goes to the webContents, not through\n // the OS), but focusing it makes the delivery match what an operator does and keeps the\n // chord's own self-focus behavior honest.\n kbw.focus();\n }\n target.webContents.sendInputEvent({ type: 'keyDown', ...chord });\n // `char` is the event that carries a TYPED CHARACTER, so it is sent for a bare key --\n // that is what a printable keystroke does, and `keys.ts` turns it into a `type` action.\n // A real Ctrl- or Alt-held keystroke produces NO char event, so sending one here would\n // simulate a keyboard that does not exist. Harmless today (the renderer listens on\n // `keydown` and the page has no editable node) and live the moment one appears.\n if (modifiers.length === 0) target.webContents.sendInputEvent({ type: 'char', ...chord });\n target.webContents.sendInputEvent({ type: 'keyUp', ...chord });\n await new Promise((r) => setTimeout(r, 150));\n }\n process.stdout.write(`keys: sent ${SEAM.keys}\\n`);\n}\n\n/**\n * A FIFTH TEST SEAM: `TN3270_GUI_CLICKS='PF1,PA2,SysRq'` clicks keypad buttons by LABEL.\n *\n * The click path -- canvas `mousedown`, the primary-button guard, `hitTestAt`, `sendAction`, the IPC\n * hop, `applyAction` -- is plumbing no unit test can reach, for the reason `hittest.ts:53-58` gives:\n * `renderer.ts` throws at module load outside a browser, so the barrel cannot export it. The same\n * argument the chord seam won. `hitTestAt`'s ARITHMETIC is separately unit-tested at scale 3 with a\n * non-zero offset (`canvas/test/keypad.test.ts:297`), so what this seam carries is the wiring, which\n * is provable at any scale -- and just as well, because in native Electron mode the centring offset\n * can never be non-zero at ANY scale: `fit` sets the content size to exactly `list.width * scale` by\n * `list.height * scale`, so `center` returns (0,0) for every model on every display. The SCALE does\n * vary with the display -- `fit` takes 80% of the work area -- and this seam does not care, because\n * it asks the renderer for a point rather than computing one.\n *\n * ASKS THE RENDERER WHERE THE BUTTON IS, rather than carrying coordinates. A coordinate list here\n * would be a second copy of the layout that agreed with itself while the layout was wrong -- the one\n * failure this seam exists to catch. `__tn3270ButtonCenter` returns a point; the CLICK still goes in\n * through Chromium, so nothing about the path under test is bypassed.\n *\n * THE SETTLE IS NOT DECORATION. `clicks.mjs` shows the keypad with a real `Ctrl+K` first, and that\n * action makes the window GROW (`fit` re-sizes from the new draw list: 720x350 to 720x476 for a\n * model 2). Every keypad button is in the pixels that resize adds, so a click delivered before it\n * lands outside the content area entirely and hits nothing. `maybeSendKeys` has already waited\n * `keysMs` before its own first chord; this waits again because the chord that matters is the LAST\n * one, and because a clicks-only run gets no settle from `maybeSendKeys` at all.\n *\n * NOT CALLED FROM THE URL BRANCH, unlike `maybeSendKeys`. In that mode the served page owns the\n * protocol and main sees no frames, so nothing here would be wrong -- `__tn3270ButtonCenter` reads\n * the renderer's own `last` -- but no harness drives it that way and an untested call site is a\n * claim this file has not earned. `TN3270_GUI_CLICKS` is therefore IGNORED alongside\n * `TN3270_GUI_URL`, which fails loudly rather than quietly: no `clicks: sent` line is printed, and\n * `clicks.mjs`'s seam-ran bail is exactly the check for that.\n */\nasync function maybeSendClicks(\n win: BrowserWindow,\n /**\n * WHICH WINDOW HOLDS THE KEYPAD, because the two front ends differ and this is the one seam\n * that has to know.\n *\n * In the BROWSER (`TN3270_GUI_URL` mode) the keypad is an OVERLAY inside the page `win` already\n * shows, so the default -- `win` itself -- is right. In ELECTRON it is a SEPARATE\n * `BrowserWindow` opened by `Ctrl+K`, and querying `win`'s document would find no buttons and\n * report 48 `NO BUTTON` lines while the real keypad sat beside it, fully working.\n *\n * A GETTER AND NOT A WINDOW, because the keypad window does not exist when this function is\n * called: it is created by the `Ctrl+K` that `maybeSendKeys` delivers moments earlier, and a\n * value captured at call time would always be `undefined`. The getter is evaluated after the\n * settle below, by which point the window exists.\n */\n keypadTarget: () => BrowserWindow | undefined = () => win,\n): Promise<void> {\n if (SEAM.clicks === '') return;\n await new Promise((r) => setTimeout(r, SEAM.keysMs));\n const target = keypadTarget() ?? win;\n if (target.isDestroyed()) {\n // REPORTED, not crashed: calling into a destroyed window throws, and the reachable cause is a\n // keypad window that failed to open -- which `keypad window failed to load` already named.\n process.stdout.write('clicks: NO KEYPAD WINDOW\\n');\n return;\n }\n for (const label of SEAM.clicks.split(',')) {\n /**\n * VIEWPORT PIXELS, which is what `sendInputEvent` wants -- and that equality is a property of\n * `gui/index.html`, not of the renderer: `html,body{margin:0}`, `canvas{display:block}` and\n * `overflow:hidden`, so the canvas box starts at the viewport origin and cannot scroll away\n * from it. The renderer computes the point from the canvas's own origin (that is where its\n * `offsetX` is measured from too). Give the page a body margin and every click here misses by\n * it.\n */\n let clicked;\n try {\n /**\n * A DOM QUERY BY LABEL, which replaced canvas coordinates on 2026-10-06.\n *\n * The keypad used to be blitted into the canvas, so clicking a button meant asking the\n * renderer where it was (`__tn3270ButtonCenter`) and delivering a real `mouseDown`/`mouseUp`\n * pair at that point -- the hit-test arithmetic WAS the thing under test. It is real HTML\n * now, so the browser does the hit testing and there are no coordinates to compute.\n *\n * STILL BY LABEL, which is the property worth preserving: a coordinate list would be a\n * second copy of the layout that passes while the layout is wrong. `data-label` carries it,\n * separate from the visible text so that giving the arrows real icons later cannot break\n * this query.\n *\n * `.click()` AND NOT `sendInputEvent`: a DOM button's own `click()` runs the same listener a\n * real press does, through the same `addEventListener('click', ...)` that\n * `keypadUi.test.ts` asserts against. `sendInputEvent` would need the element's viewport\n * rectangle -- reintroducing exactly the geometry this change removes, and with it the\n * possibility of aiming at the wrong place.\n *\n * WHICH DOCUMENT IS SEARCHED DIFFERS BY FRONT END, and the same expression serves both: in\n * the browser the keypad is an OVERLAY in this window's own page, so `document` finds it; in\n * Electron it is a SEPARATE WINDOW, and `clickKeypadButton` below is what reaches that one.\n * This path handles the overlay case.\n */\n clicked = await target.webContents.executeJavaScript(\n `(() => {\n const b = document.querySelector('button[data-label=' + JSON.stringify(${JSON.stringify(label)}) + ']');\n if (b === null || b.offsetParent === null) return false;\n b.click();\n return true;\n })()`,\n ) as boolean;\n } catch (err) {\n /**\n * A REJECTION HERE WOULD OTHERWISE HANG THE PROCESS, which is the exact trap `maybeSendKeys`\n * measured for `parseKeySpec`: a throw inside `app.whenReady()`'s promise is an unhandled\n * rejection, so `quitIfKeysOnly` never runs and the client SITS until the harness's 120s\n * timeout -- which reads as a broken client rather than as the broken page it is.\n *\n * The reachable cause is now a page that threw before building the keypad -- `bridge.js`\n * failing to load, say, which is a 404 away and takes `window.tn3270` with it. Exiting 2\n * keeps the diagnosis and loses the hang.\n */\n await new Promise<void>((r) => {\n process.stdout.write(`clicks: PROBE FAILED ${label}: ${explain(err)}\\n`, () => { r(); });\n });\n app.exit(2);\n return;\n }\n if (!clicked) {\n /**\n * NOT a plumbing failure and reported as its own thing: either the keypad is not SHOWING --\n * `offsetParent` is null for a hidden overlay, which is the common case and means the caller\n * forgot `Ctrl+K` -- or the label is not in the table.\n *\n * `offsetParent === null` IS THE VISIBILITY TEST, deliberately, and it is why a hidden\n * overlay reports rather than silently succeeding: `querySelector` finds buttons inside a\n * `hidden` container perfectly well, and `.click()` on one would fire the handler and send\n * the action with nothing on screen. That would be a harness proving the keypad works while\n * it was invisible.\n */\n process.stdout.write(`clicks: NO BUTTON ${label}\\n`);\n continue;\n }\n await new Promise((r) => setTimeout(r, 120));\n }\n process.stdout.write(`clicks: sent ${SEAM.clicks}\\n`);\n}\n\n/**\n * AN EIGHTH TEST SEAM: `TN3270_GUI_SELECT='0,0,0,9'` drags across cells and copies.\n * `'0,1,0,1,0,5'` adds a pixel offset `dx,dy` to the move and release, for a drag inside one cell.\n *\n * ## WHAT IT COVERS THAT NOTHING ELSE CAN\n *\n * The whole selection gesture lives in `renderer.ts` -- `mousedown`'s selection branch,\n * `mousemove`, `cellAt`, the inverse-video highlight and `__tn3270Copy` -- and NO vitest file can\n * execute a line of it: that module throws at load outside a browser, so `canvas/src/index.ts`\n * deliberately does not export it. Same argument as the chord and click seams, and the same\n * answer.\n *\n * ## IT ASKS THE RENDERER WHERE THE CELL IS\n *\n * `__tn3270CellCenter(row, col)` is the inverse of the renderer's own `cellAt`, so the two must\n * agree or this fails -- a harness carrying its own pixel arithmetic would be testing itself.\n * Cells rather than pixels for the reason `TN3270_GUI_CLICKS` takes labels.\n *\n * ## THE MOVE BETWEEN PRESS AND RELEASE IS THE POINT\n *\n * A press and a release at two points is NOT a drag: `mousemove` is what advances `focus`, so\n * without it this would pass against a renderer that never tracked the drag at all and selected\n * only the anchor cell -- a press that never moved, which the renderer calls a click (its 4 px\n * drag threshold, `movedPastThreshold`), making the whole feature inert. The\n * `mouseMove` carries `button: 'left'` because a move with no button held is not a drag either.\n *\n * REPORTS THE CLIPBOARD ITSELF, read back through Electron, which is the only honest end of this\n * path: everything short of it could pass while the text never reached the OS.\n */\nasync function maybeSelect(win: BrowserWindow): Promise<void> {\n if (SEAM.select === '') return;\n // The same settle as `maybeSendClicks`, and for the same reason: a frame has to have been\n // painted before the renderer has a `last` to measure cells against.\n await new Promise((r) => setTimeout(r, SEAM.keysMs));\n const parts = SEAM.select.split(',').map((n) => Number(n));\n if ((parts.length !== 4 && parts.length !== 6) || parts.some((n) => !Number.isInteger(n))) {\n // REPORTED, not ignored, the same way `clicks: NO BUTTON` is: a typo'd seam value must not\n // read as a failure of the path under test.\n process.stdout.write(`select: BAD SPEC ${SEAM.select}\\n`);\n return;\n }\n const [top, left, bottom, right] = parts as [number, number, number, number];\n // AN OPTIONAL PIXEL OFFSET for the move and the release, from the end cell's centre, 2026-11-03.\n // It is what lets a ONE-CELL case drag at all: with `0,1,0,1` the move would land on the press\n // point, which is a click and selects nothing -- correctly. `0,1,0,1,0,5` moves 5 px down,\n // inside the cell, past the renderer's 4 px drag threshold.\n const dx = parts[4] ?? 0;\n const dy = parts[5] ?? 0;\n let from: { x: number; y: number } | null;\n let to: { x: number; y: number } | null;\n try {\n from = await win.webContents.executeJavaScript(\n `window.__tn3270CellCenter(${top}, ${left})`) as { x: number; y: number } | null;\n to = await win.webContents.executeJavaScript(\n `window.__tn3270CellCenter(${bottom}, ${right})`) as { x: number; y: number } | null;\n } catch (err) {\n // A REJECTION HERE WOULD HANG THE PROCESS, measured on the click seam: a throw inside\n // `app.whenReady()`'s promise is an unhandled rejection, so the quit never runs and the client\n // sits until the harness's timeout -- reading as a broken client rather than a broken\n // renderer. The reachable cause is a renderer that threw before installing the global.\n await new Promise<void>((r) => {\n process.stdout.write(`select: PROBE FAILED ${explain(err)}\\n`, () => { r(); });\n });\n app.exit(2);\n return;\n }\n if (from === null || to === null) {\n process.stdout.write(`select: NO CELL ${SEAM.select}\\n`);\n return;\n }\n win.webContents.sendInputEvent(\n { type: 'mouseDown', x: from.x, y: from.y, button: 'left', clickCount: 1 });\n win.webContents.sendInputEvent(\n { type: 'mouseMove', x: to.x + dx, y: to.y + dy, button: 'left' });\n win.webContents.sendInputEvent(\n { type: 'mouseUp', x: to.x + dx, y: to.y + dy, button: 'left', clickCount: 1 });\n await new Promise((r) => setTimeout(r, 200));\n const copied = await win.webContents.executeJavaScript('window.__tn3270Copy()') as boolean;\n // The copy crosses IPC to main, which extracts and writes the clipboard, so the read below must\n // not race it.\n await new Promise((r) => setTimeout(r, 300));\n const text = await clipboard.readText();\n process.stdout.write(`select: copied=${copied} clipboard=${JSON.stringify(text)}\\n`);\n}\n\n/**\n * A SIXTH TEST SEAM: `TN3270_GUI_TRANSFER='localFile=/tmp/f,hostFile=A.B,submit'` drives the transfer\n * window, and reports what it did.\n *\n * ## WHAT IT COVERS THAT NOTHING ELSE CAN\n *\n * A second `BrowserWindow`, its own preload bridge, `transfer.html`'s IMPORT MAP, every line of\n * `transferBoot.ts`, and the `transfer:submit` hop into the controller. None of that is reachable\n * from vitest: `transferBoot.ts` needs a `document` and there is none under `environment: 'node'`,\n * this file cannot be imported at all, and no fake DOM can fail to resolve a module specifier.\n * `transferUi.ts`'s docstring names this seam's harness as the only thing that can see a\n * blank-window-with-no-error -- which this branch shipped once already, as a TDZ read in\n * `transferBoot.ts`.\n *\n * ## STEPS, NOT COORDINATES, AND THE ECHO IS THE MODEL'S OWN VALUE\n *\n * Each step names a FIELD BY ID and goes through `window.__tn3270SetField`, which calls the same\n * `ui.type` a keystroke does. The line printed back carries `ui.values()[id]` rather than the\n * string this function handed over, so a value the MODEL REFUSED cannot be reported as set -- a\n * non-digit into `Lrecl`, a cycle field, an id that is not in the table. A seam that echoed its own\n * input would pass while the form ignored it.\n *\n * ## EVERY LINE HERE IS GATED, AND THAT IS A PRIVACY RULE RATHER THAN TIDINESS\n *\n * A `localFile=` line carries a PATH, in a window opened in front of a live logged-on session. The\n * same argument gates the action log (see `logActions`) and keeps goldens away from live logons.\n *\n * THE GATE IS THE EARLY RETURN BELOW AND NOTHING ELSE -- one `if` ahead of every write, rather than\n * a condition on each. That is deliberate: a per-line gate is a thing each new line has to\n * remember, and the one that forgets is the one that prints a path. A whole FUNCTION that is not\n * entered is structural. `transferSeam.test.ts` pins it by reading this file as text, since nothing\n * can import it.\n *\n * NOT GATED ON REPLAY MODE, unlike `logActions`, and the asymmetry is reasoned. That gate exists\n * because `TN3270_GUI_KEYS` is used against live hosts and `keys.ts` builds a `type` action\n * carrying typed text for every printable keypress -- so the seam variable alone says nothing about\n * what is on screen. This seam has no such life: it is set by a harness, the paths it prints are\n * the harness's own, and requiring replay would make it unable to ever drive a real transfer by\n * hand -- which is the thing a human would most want it for.\n */\nasync function driveTransferWindow(\n tw: BrowserWindow, win: BrowserWindow, session: Session,\n): Promise<void> {\n if (SEAM.transfer === '') return;\n /**\n * THE WHOLE BODY STAYS IN THIS FUNCTION, deliberately, and an earlier attempt at the\n * `transferScenarioRunning` flag split it into a wrapper plus a `runTransferScenario`. **Five\n * tests in `transferSeam.test.ts` reddened**, and they were right to: they read THIS function's\n * body as text to prove the privacy gate is its first statement and that every\n * `transfer window:` line lives inside it. A wrapper moved every one of those lines outside the\n * function the test inspects, so the gate it checks would have been guarding an empty shell while\n * the printing happened elsewhere. The guard test did its job; the flag is set inline instead.\n */\n transferScenarioRunning = true;\n try {\n await driveSteps(tw, win, session);\n } finally {\n // CLEARED IN A `finally`, so a scenario that throws cannot leave the process unquittable --\n // which would turn one failure mode into a worse one.\n transferScenarioRunning = false;\n }\n /**\n * THE RUN HAS TO END ITSELF, and `quitIfKeysOnly` will not do it: it returns early unless the\n * keys or clicks seam is set, so a transfer-only run reaches the end of `app.whenReady()`'s\n * callback with two windows open and SITS THERE until something kills it. Measured before this\n * line existed: the harness burned its full 120-second timeout, which is the stall-rather-than-\n * fail shape this file keeps writing comments about.\n *\n * NOT FOLDED INTO `quitIfKeysOnly`, and THAT FUNCTION NOW WAITS FOR THIS ONE. It is reached\n * from three places on the main window's paths, all AFTER `maybeCapture`, while this seam runs\n * inside `openTransferWindow` -- from the `action` handler and from Ctrl-T. This paragraph used\n * to warn that folding the two would let whichever arrived first quit \"with the scenario still\n * running\". **That race was real and it fired from the OTHER side:** `quitIfKeysOnly` arrived\n * first and killed a live transfer against VM/CMS mid-flight, so no run could ever print a\n * `done ->` line. Hence `transferScenarioRunning`, which that function spins on.\n *\n * THE DRAIN IS NOT OPTIONAL, and `quitIfKeysOnly` has the measurement: writing to a PIPE is\n * asynchronous, and with no drain output stops at one 64000-byte buffer with the last line GONE,\n * three runs out of three. Everything `driveSteps` prints is what the harness scores, and the\n * `submit ->` line is the LAST of them.\n *\n * A SHOT RUN IS LEFT ALONE, so the two seams compose rather than racing: `maybeCapture` quits\n * when it has its picture, and quitting here first would leave a zero-byte PNG. That is also what\n * makes the privacy check in the AS BUILT notes runnable -- a `TN3270_GUI_SHOT` run with this\n * variable unset prints nothing and exits on its own.\n */\n if (SEAM.shot !== '') return;\n await new Promise<void>((r) => { process.stdout.write('', () => { r(); }); });\n app.quit();\n}\n\n/**\n * Every step of the scenario, and every line it prints.\n *\n * REACHED ONLY FROM `driveTransferWindow`, i.e. only behind that function's seam gate -- which is\n * what keeps the privacy property structural: this function has no gate of its own because it has\n * exactly one caller that does. `transferSeam.test.ts` pins both halves.\n */\nasync function driveSteps(\n tw: BrowserWindow, win: BrowserWindow, session: Session,\n): Promise<void> {\n process.stdout.write('transfer window: opened\\n');\n /**\n * THE FIELD COUNT FIRST, BEFORE ANY STEP RUNS.\n *\n * This is the blank-window check, and it describes the form AS FIRST DRAWN, which is what \"did it\n * render\" means. The plan for this seam printed it LAST, after the submit, where the number\n * describes whatever the submit left behind and a reader has to know which -- and where a\n * scenario that crashed mid-way would print no count at all, losing the one line that says the\n * window is not blank.\n */\n const rows = await tw.webContents.executeJavaScript(\n 'document.querySelectorAll(\"#fields .row\").length',\n ) as number;\n process.stdout.write(`transfer window: fields=${rows}\\n`);\n\n for (const step of SEAM.transfer.split(',')) {\n /**\n * SPLIT ON THE FIRST `=` ONLY, KEEPING THE REMAINDER -- and `split('=', 2)` DOES NOT DO THAT.\n *\n * The plan for this seam used it. JavaScript's second argument is a LIMIT ON THE OUTPUT, not\n * Python's maxsplit: `'path=/tmp/a=b'.split('=', 2)` is `['path', '/tmp/a']`, silently\n * discarding `=b`. Verified under node. So a Windows path, or any value containing `=`, would\n * have been TRUNCATED and the field set to a prefix -- and the echoed line would have agreed\n * with the truncation, because it reports what the model holds. `indexOf` plus `slice` keeps\n * the remainder whole.\n */\n const at = step.indexOf('=');\n const name = at < 0 ? step : step.slice(0, at);\n const value = at < 0 ? undefined : step.slice(at + 1);\n\n if (name === 'submit') {\n const r = await tw.webContents.executeJavaScript('window.__tn3270Submit()') as\n { ok: boolean; status: string };\n // THE FORM'S OWN STATUS LINE, which is what a user would see and what distinguishes a\n // validator complaint from `not in 3270 mode` from 'the transfer window is not open'. `ok`\n // alone is satisfied by all three, i.e. by the form never having been filled in.\n process.stdout.write(`transfer window: submit -> ok=${r.ok} status=${r.status}\\n`);\n continue;\n }\n /**\n * `done` WAITS FOR A RUNNING TRANSFER TO END, and without it no scenario could ever see one.\n *\n * MEASURED against VM/CMS: `submit` returns as soon as the submit was ACCEPTED, so the seam\n * printed `ok=true status=transferring` and then the quit below tore the process down MID\n * TRANSFER. The send had genuinely started -- that was the first transfer this window ever\n * drove against a real host -- and the harness killed it. **Replay mode could not have found\n * this**, because there a submit is always refused and there is nothing to wait for; that is\n * the same blind spot that let the synchronous-completion bug through.\n *\n * Optional rather than implied by `submit`, because a scenario that WANTS to leave a transfer\n * running is the one that tests the close guard -- and that is the whole of roadmap item 3.\n */\n if (name === 'done') {\n const budget = value === undefined ? 120000 : Number(value);\n const r = await tw.webContents.executeJavaScript(\n `window.__tn3270AwaitDone(${JSON.stringify(budget)})`,\n ) as { running: boolean; status: string; timedOut: boolean };\n process.stdout.write(\n `transfer window: done -> running=${r.running} timedOut=${r.timedOut} status=${r.status}\\n`,\n );\n continue;\n }\n /**\n * `host:CMD|AWAIT` TYPES A COMMAND INTO THE SESSION, after the transfers, and it exists for\n * ONE job: **logging off without stranding the account.**\n *\n * ## WHY NOTHING ELSE COULD DO IT\n *\n * `maybeSendKeys` runs to COMPLETION before the transfer window opens, so a `LOGOFF` at the end\n * of the keys list executes before the transfer has started. A separate logoff PROCESS does not\n * work either, because it has no session: it must log on first, and **a held TSO userid is\n * REFUSED** (`IKJ56425I ... IN USE`), so the one case that needs recovering is the one case it\n * cannot reach. Three TK5 userids were stranded exactly this way in 2026-08, and the only\n * recovery is `/c u=<userid>` at the MVS operator console -- Hercules' controlling terminal,\n * which an agent cannot read or type into.\n *\n * So this types into the ALREADY LOGGED-ON session, at the prompt the transfers left it at.\n *\n * ## IT WAITS ON HOST TEXT, LIKE EVERY OTHER STEP HERE\n *\n * `host:logoff|LOGGED OFF` types `logoff`, sends Enter, and waits for `LOGGED OFF` (alternatives\n * with `|` as `wait:` takes them). A logoff that did not take must be VISIBLE, because the cost\n * of missing it is a stranded userid and a manual console step.\n */\n /**\n * SPLIT ON `:` AND NOT `=`, which is why this arm comes BEFORE the `value === undefined` guard\n * and tests `step` rather than `name`. The step parser above splits on the first `=`, so\n * `host:logoff|LOGGED OFF` arrives with `name` holding the whole thing and `value` undefined --\n * measured, it printed `UNKNOWN STEP host:test`. A `:` separator is also what keeps a command\n * containing `=` expressible, which a host command may well be.\n *\n * AND IT CANNOT COLLIDE WITH THE `host` FIELD, whose steps are `host=vm`: one uses `=`, this\n * one `:`, and the field arm below never sees a step starting `host:`.\n */\n /**\n * `drain:TEXT` PRESSES ENTER UNTIL `TEXT` IS GONE FROM THE SCREEN.\n *\n * **`drain:TEXT>TARGET` presses until `TARGET` ARRIVES instead, and that is the form a slow host\n * needs** -- an absent `TEXT` cannot be told apart from a `TEXT` that has not been painted yet,\n * which is what broke every TSO run on 2026-10-02. The implementation below carries the\n * measurement; the short version is that the stopping rule must be the arrival of what you are\n * waiting for, not the absence of what you are dismissing.\n *\n * ## WHY A COUNT OF ENTERS CANNOT WORK, AND THIS COST A STRANDED USERID\n *\n * TSO shows an indeterminate number of more-output prompts after logon -- a welcome banner, then\n * a FORTUNE COOKIE, then the fortune's own `***`. `live-drive.py` has a `DRAIN***` step for\n * exactly this and says why in as many words: *\"A fixed settle then one Enter was not enough:\n * the count is not fixed ... and a settle can fire the Enter before the next screen has even\n * arrived.\"*\n *\n * The GUI harness guessed two Enters, which was not enough: the run timed out waiting for the\n * ISPF panel, died before the transfer window opened, and so never reached its own logoff --\n * **stranding HERC01 with `IKJ56425I LOGON REJECTED, USERID HERC01 IN USE`, recoverable only\n * from the MVS operator console.** A loop that asks the screen is the only form that does not\n * depend on counting.\n *\n * Bounded, because an unbounded loop against a host that never clears the prompt is a hang.\n */\n /**\n * `wait:TEXT` HERE TOO, with the same meaning and the same implementation as in the keys seam.\n *\n * The scenario needs it because the steps that follow type into the SESSION (`host:`, `drain:`),\n * and those must not run before the host is ready -- the same rule that made `wait:` necessary\n * for the keys list. Measured: without it, `host x` typed while the OIA still showed `X Wait`\n * and the ISPF panel had not arrived.\n *\n * A TIMEOUT HERE DOES NOT END THE PROCESS, unlike the keys seam's, and the asymmetry is\n * deliberate: by this point a transfer may be mid-flight and a `app.exit` would abandon it\n * rather than let the scenario's own logoff step run. It REPORTS and continues, and the\n * subsequent steps report their own failures -- which is how the stranded-userid diagnosis\n * stayed legible.\n */\n /**\n * `sample:MS` WATCHES THE STATUS LINE CLIMB, which is the whole of the progress item.\n *\n * Prints each DISTINCT line seen, so the harness can assert the byte counts are monotonic and\n * that the last one is the whole file. A single reading cannot show a count climbing, and the\n * runbook names what that misses: *\"a progress report that silently stops is the failure this\n * window would hide best\"*.\n *\n * **249 BYTES CANNOT EXERCISE THIS** -- it completes in one frame, so there is exactly one line\n * to see. `cancel-transfer.py` measured the sizing for the TUI's equivalent: CUT runs ~15 ms a\n * frame against local Hercules and the codec expands random data 1.727x, so 200 KB is ~185\n * frames and ~2.8 s per direction.\n */\n if (step.startsWith('sample:')) {\n const budget = Number(step.slice('sample:'.length));\n const seen = await tw.webContents.executeJavaScript(\n `window.__tn3270SampleStatus(${JSON.stringify(budget)}, 100)`,\n ) as string[];\n for (const line of seen) process.stdout.write(`transfer window: status ${line}\\n`);\n process.stdout.write(`transfer window: sampled ${seen.length} distinct status line(s)\\n`);\n continue;\n }\n /**\n * `cancel` CLICKS THE REAL BUTTON, mid-flight, and says what it interrupted.\n *\n * Through the button and not through `requestCancel()`, because the button IS the operator's\n * only route out of a running transfer -- the window refuses to close -- so a test that bypassed\n * it would pass while the control was disabled or wired to nothing.\n *\n * `enabled` is reported separately because `click()` on a disabled button is a SILENT no-op, and\n * `statusBefore` because that byte count is the evidence the transfer was genuinely under way.\n * `cancel-transfer.py`: *\"A canceled 200KB transfer that reports 204800 bytes was not\n * canceled.\"*\n */\n if (step === 'cancel') {\n const r = await tw.webContents.executeJavaScript('window.__tn3270ClickCancel()') as\n { enabled: boolean; statusBefore: string };\n process.stdout.write(\n `transfer window: cancel -> enabled=${r.enabled} at ${r.statusBefore}\\n`,\n );\n continue;\n }\n /**\n * `close` ASKS THE WINDOW TO CLOSE AND REPORTS WHETHER IT SURVIVED.\n *\n * Survival is the close guard's whole observable: `tw.on('close')` calls `preventDefault` while\n * `shouldPreventClose()` is true, so a window still alive afterwards is the guard working and a\n * destroyed one is a transfer abandoned mid-frame.\n *\n * **THIS DRIVES ONLY THE WINDOW'S OWN `close`.** The other paths that end this window are\n * caught elsewhere and by different mechanisms -- closing the TERMINAL window never fires this\n * event at all (measured: a parent's destruction fires only `closed`), and Cmd-Q goes through\n * `before-quit`. Both of those END THE PROCESS, so neither can be observed from inside a\n * scenario that has to report afterwards; they stay by-hand items rather than being\n * half-claimed here.\n */\n if (step === 'close') {\n tw.close();\n await new Promise((r) => { setTimeout(r, 700); });\n const alive = !tw.isDestroyed();\n process.stdout.write(`transfer window: close -> survived=${alive}\\n`);\n continue;\n }\n if (step.startsWith(WAIT_PREFIX)) {\n const hit = await waitForScreen(session, step.slice(WAIT_PREFIX.length));\n if (hit === undefined) {\n /**\n * SAY WHAT WAS ON SCREEN INSTEAD, because a timeout that does not is nearly undiagnosable\n * here -- and that cost a whole misdirected diagnosis.\n *\n * A timeout on this path REPORTS AND CONTINUES (deliberately, see above), so every later\n * step runs against whatever screen the host is actually showing. On TSO 2026-10-02 that\n * meant the ISPF wait expired, `drain:***` pressed two blind Enters into a panel that was\n * not the one it was written for, `host x` never reached `READY`, and `IND$FILE` was typed\n * somewhere it does not exist -- producing a transfer with ZERO DFT frames that was recorded\n * as \"no DFT frames are arriving\" at the protocol layer. The whole chain is visible in one\n * line of screen text and was invisible in three lines of `TIMED OUT`.\n *\n * ## IT REDACTS, AND THAT IS NOT PRECAUTIONARY -- THE FIRST RUN PRINTED A REAL PASSWORD\n *\n * My first version of this printed the leading 200 characters on the reasoning that a\n * scenario step runs after the logon, so the screen could only be a host panel. **MEASURED\n * 2026-10-02, and it was wrong: the screen at the timeout was TSO's logon panel with\n * `ENTER CURRENT PASSWORD FOR HERC01- CUL8TR` on it** -- the password ECHOED IN PLAIN TEXT,\n * because that is exactly the failure being diagnosed (the wait expired while the logon was\n * still in progress). The one state a timeout here is most likely to catch is the state the\n * assumption ruled out.\n *\n * So the text after TSO's own prompt is replaced. Matched on the HOST'S WORDING rather than\n * on a field attribute, because the panel echoes it as ordinary text: there is no\n * `hidden` cell to honour here, which is what makes a structural fix unavailable.\n * `docs/live-testing.md` already warns the /tmp log may carry a password; this keeps the one\n * line that is MEANT to be read from being the thing that puts it there.\n */\n const seen = screenNeedleText(resolve(session.screen.snapshot(), {}))\n .replace(/(ENTER CURRENT PASSWORD FOR \\S+)[^!+]*/i, '$1 <redacted> ');\n process.stdout.write(\n `transfer window: wait TIMED OUT for ${JSON.stringify(step.slice(WAIT_PREFIX.length))}`\n + ` -- screen was ${JSON.stringify(seen.slice(0, SCREEN_DUMP_CHARS))}\\n`,\n );\n } else {\n process.stdout.write(`transfer window: saw ${JSON.stringify(hit)}\\n`);\n }\n continue;\n }\n if (step.startsWith('drain:')) {\n /**\n * `drain:PROMPT` or `drain:PROMPT>TARGET` -- press Enter until the host gets somewhere.\n *\n * ## WHY `>TARGET` EXISTS, AND IT IS A MEASURED FIX RATHER THAN A GENERALISATION\n *\n * The one-needle form stops when `PROMPT` is ABSENT, and that is the wrong stopping rule\n * whenever the host has not caught up yet: an absent prompt is indistinguishable from a prompt\n * that has not arrived. **Measured on TK5 2026-10-02, three runs, at both payload sizes:** the\n * screen after the TSO password was the PASSWORD PANEL with `LOGON IN PROGRESS` appended and a\n * box border -- **no `***` anywhere on it** -- so `drain:***` broke on its first poll\n * (`drained \"***\" after 0 Enter(s)`), pressed nothing, and the welcome banner it should have\n * dismissed arrived afterwards with nobody left to press past it. Every later step then ran\n * against the logon: `host x` never reached `READY` and `IND$FILE` was typed at a screen with\n * no such command, producing a transfer with ZERO DFT frames that was recorded as a\n * protocol-layer fault and blamed on the 200 KB size. It was neither.\n *\n * With `>TARGET` the stopping rule becomes the ARRIVAL of what the scenario is waiting for,\n * which is the question actually being asked. `PROMPT` keeps its job -- it is what makes a\n * press WORTHWHILE -- but a screen showing neither now means \"wait longer\", not \"done\".\n * `live-drive.py` gets this right by accident of shape: its `DRAIN***` settles 3s per check and\n * loops 8 times, so it is still pressing when the banner shows up.\n *\n * The one-needle spelling is kept working and unchanged, because `drain:` is a seam other\n * scenarios may use and silently redefining it would break them: with no `>`, `TARGET` is empty\n * and the old absence rule applies exactly as before.\n */\n const spec = step.slice('drain:'.length);\n const gt = spec.indexOf('>');\n const needle = gt < 0 ? spec : spec.slice(0, gt);\n const target = gt < 0 ? '' : spec.slice(gt + 1);\n // SPLIT AND EMPTIES DROPPED, exactly as `waitForScreen` does it: an empty alternative would\n // match every screen including a blank one, which is the vacuous-wait failure this whole seam\n // is built to avoid.\n const targetNeedles = target.split('|')\n .map((n) => n.replace(/\\s+/g, ' ').trim()).filter((n) => n !== '');\n win.show();\n win.focus();\n let pressed = 0;\n let arrived = false;\n for (let poll = 0; poll < 20; poll++) {\n await new Promise((r) => { setTimeout(r, 1500); });\n const seen = screenNeedleText(resolve(session.screen.snapshot(), {}));\n // THE TARGET WINS, and it is tested FIRST: once the panel is up, a further Enter would type\n // into it -- on ISPF that selects an option and navigates away from the panel the next step\n // needs. Checked before the prompt because a screen can carry both while repainting.\n //\n // **`|` ALTERNATIVES, as `wait:` takes them, and the first version's absence of them was a\n // REAL defect rather than a missing nicety.** With a bare substring target the live run\n // reported `NEVER REACHED \"Primary Option\"` while the very next step `saw \"USERID\"` -- the\n // panel HAD arrived, under one of the other names the scenario already lists for it\n // (`Primary Option|USERID|BROWSE`). A one-name target therefore drains to its full 20 polls\n // on a host that is finished, which is a 30-second stall and a log line that contradicts the\n // step after it. The needle and the target must be able to say the same thing.\n if (target !== '' && targetNeedles.some((n) => seen.includes(n))) { arrived = true; break; }\n if (seen.includes(needle)) {\n for (const t of ['keyDown', 'char', 'keyUp'] as const) {\n win.webContents.sendInputEvent({ type: t, keyCode: 'Enter' });\n }\n pressed++;\n continue;\n }\n // NEITHER ON SCREEN. With no target this is the ORIGINAL stopping rule and must stay one, or\n // every existing `drain:` spends 30s before continuing. With a target it means the host is\n // still working, which is the whole case this form exists for -- so keep polling.\n if (target === '') break;\n }\n process.stdout.write(\n `transfer window: drained ${JSON.stringify(needle)} after ${pressed} Enter(s)`\n + (target === '' ? '' : ` -- ${arrived ? 'reached' : 'NEVER REACHED'} ${JSON.stringify(target)}`)\n + '\\n',\n );\n continue;\n }\n if (step.startsWith('host:')) {\n const spec = step.slice('host:'.length);\n const bar = spec.indexOf('|');\n const cmd = bar < 0 ? spec : spec.slice(0, bar);\n const expect = bar < 0 ? '' : spec.slice(bar + 1);\n // FOCUSED FIRST: the transfer window has the keyboard, and `sendInputEvent` goes to the\n // window it is called on -- but a background window's renderer may not be the one Chromium\n // routes to. Showing the main window is what makes the typing land on the session.\n win.show();\n win.focus();\n await new Promise((r) => { setTimeout(r, 400); });\n /**\n * CLEAR BEFORE TYPING, and this is not belt-and-braces -- it is the difference between a\n * command running and vanishing.\n *\n * VM's `MORE...` state silently EATS input, which this project has already recorded as what\n * once swallowed a LOGOFF and left an account logged on. An ABORTED TRANSFER lands there:\n * measured 2026-10-02, a canceled 200 KB send left the screen mid-page and `#cp logoff` typed\n * into it did nothing at all, so the run reported the host had not left transfer mode when in\n * fact the question had never been asked. The account stayed logged on and the next run met\n * the reconnect trap.\n *\n * Ctrl-C is the CLEAR AID here, not an interrupt -- which is the whole reason Ctrl-] is this\n * app's quit binding.\n */\n /**\n * **CLEAR ONLY WHERE IT IS NEEDED, AND THAT CONDITION IS WHY THE TSO STEPS TYPED INTO NOTHING.**\n *\n * Clear is an AID: it erases the screen and the HOST REPAINTS. On VM's `MORE...` that is the\n * whole point (the state eats input, and this step existed because `#cp logoff` typed into it\n * vanished). **On a panel that is already accepting input it is actively harmful** -- measured\n * on TK5 2026-10-03: ISPF's menu was up and unlocked, Clear wiped it, and the characters went\n * out while ISPF was still rebuilding its fields, so `Option ===>` came back EMPTY. The\n * contrast proves it: the FORM's own `IND$FILE` text lands in that same field reliably, and\n * `primeAndType` does it with purely LOCAL operations -- `home()`, `eraseEOF()`, `typeString()`\n * -- sending NO AID and so racing no repaint.\n *\n * So: skip the Clear when the keyboard is free and the screen already has an unprotected field\n * to type into, which is exactly the \"already at a prompt\" case. A locked or unformatted screen\n * still gets it, which keeps the VM `MORE...` behaviour this step was built for.\n */\n const needsClear = session.oia.isInhibited() || !session.screen.isFormatted();\n if (needsClear) {\n for (const t of ['keyDown', 'char', 'keyUp'] as const) {\n win.webContents.sendInputEvent({ type: t, keyCode: 'c', modifiers: ['control'] });\n }\n }\n process.stdout.write(`transfer window: host ${cmd} clear=${needsClear}\\n`);\n /**\n * **WAIT FOR THE KEYBOARD, NOT A FIXED 600ms -- AND THE FIXED WAIT IS WHY EVERY TSO `host:`\n * STEP SILENTLY TYPED NOTHING.**\n *\n * Clear is an AID: it erases the screen AND LOCKS THE KEYBOARD (`X Wait`) until the host\n * repaints. On VM's `MORE...` the answer is instant, which is why 600ms was never caught there.\n * **On TK5's ISPF it is not:** the host has to redraw a full 1900-character panel, and the\n * typing went out while the keyboard was still inhibited -- so `keyboardLock` swallowed every\n * character and the panel came back with `Option ===>` EMPTY. Measured 2026-10-02: the step\n * reported `TIMED OUT waiting for \"READY\"` with the keyboard showing `4 A` and `inhibited=false`\n * BY THE TIME IT GAVE UP, which is the signature of a lock that has since cleared -- the state\n * at the moment of typing is the one that mattered and the one nothing recorded.\n *\n * This is the same fixed-delay-versus-condition mistake `live-drive.py` names\n * (*\"a settle can fire the Enter before the next screen has even arrived\"*) and that `drain:`\n * was just fixed for. A deadline rather than an unbounded wait, because a host that never\n * unlocks must still reach the report instead of hanging the scenario.\n */\n const unlockBy = Date.now() + 20_000;\n while (session.oia.isInhibited() && Date.now() < unlockBy) {\n await new Promise((r) => { setTimeout(r, 200); });\n }\n // A beat after the unlock: the keyboard frees on the host's write, and the FIELDS land with\n // that same write -- typing into the very first tick has no margin for the repaint's own\n // ordering, and this step is typing into a field the panel must already have drawn.\n await new Promise((r) => { setTimeout(r, 400); });\n for (const ch of [...cmd, '\\r']) {\n const keyCode = ch === '\\r' ? 'Enter' : ch === ' ' ? 'Space' : ch;\n /**\n * **UPPERCASE NEEDS `Shift` AND THE `char` EVENT KEPT -- BOTH, AND THAT PAIR IS THE WHOLE FIX.**\n *\n * `sendInputEvent` FOLDS CASE, so a bare `keyCode: 'X'` arrives as `x`. **PROVED OFFLINE, no\n * host and no userid**, by replaying a trace with the action log on: `host:X` printed\n * `action: {\"kind\":\"type\",\"text\":\"x\"}`. Nothing was lost -- every character arrived -- they\n * simply arrived LOWERCASE, which is invisible against any host whose fields fold (VM/CP\n * does) and fatal against one whose do not (TSO's ISPF `Option ===>`, measured, came back\n * empty because `x` is not its `X EXIT`).\n *\n * **MY FIRST ATTEMPT AT THIS FAILED FOR A DIFFERENT REASON AND THAT IS WHY IT IS SPELLED OUT.**\n * It added `modifiers: ['shift']` AND suppressed the `char` event, copying `sendKeys`'s rule\n * that a real Ctrl-/Alt-held keystroke produces no `char`. Live result: the field came back\n * EMPTY with no host complaint at all, i.e. delivery stopped entirely -- strictly worse than\n * the folding it was meant to fix, so it was reverted. **Shift is NOT like Ctrl/Alt here: a\n * real Shift-held letter DOES produce a `char` event**, which is exactly how a keyboard types\n * a capital. Sending the modifier while withholding the `char` simulates a keyboard that\n * cannot.\n *\n * So: modifier for the keyDown/keyUp, and the `char` kept unconditionally. Verified offline\n * the same way the fold was found -- `host:X` must print `text\":\"X\"`.\n */\n const shifted = /^[A-Z]$/.test(ch);\n const chord = { keyCode, ...(shifted ? { modifiers: ['shift' as const] } : {}) };\n win.webContents.sendInputEvent({ type: 'keyDown', ...chord });\n // THE `char` CARRIES THE CHARACTER and is sent for every key, shifted or not. Withholding it\n // on a shifted letter is what broke delivery outright; see above.\n win.webContents.sendInputEvent({ type: 'char', ...chord });\n win.webContents.sendInputEvent({ type: 'keyUp', ...chord });\n await new Promise((r) => { setTimeout(r, 90); });\n }\n if (expect === '') { process.stdout.write(`transfer window: host ${cmd} (no wait)\\n`); continue; }\n const hit = await waitForScreen(session, expect);\n if (hit === undefined) {\n // THE SCREEN HERE TOO, for the reason the `wait:` arm gives: this step also reports and\n // continues, so a `host:` that went nowhere leaves every later step typing into the wrong\n // panel -- and a bare `TIMED OUT waiting for \"READY\"` cannot say whether the command was\n // refused, swallowed, or typed into a screen that has no such command. REDACTED identically;\n // a `host:` step runs after the logon, but that was exactly the assumption that printed a\n // live password from the `wait:` arm, so the same guard applies rather than being re-argued.\n const seen = screenNeedleText(resolve(session.screen.snapshot(), {}))\n .replace(/(ENTER CURRENT PASSWORD FOR \\S+)[^!+]*/i, '$1 <redacted> ');\n // **AND THE OIA, because the screen alone cannot say whether the command was even ACCEPTED.**\n // This step types BLIND -- it checks no keyboard state -- and `drain:`'s own comment records\n // the failure once already (\"`host x` typed while the OIA still showed `X Wait`\"). A panel\n // that looks untouched is produced equally by a command the host ignored and by a keystroke\n // the KEYBOARD refused, and those want opposite fixes: the first is the wrong command, the\n // second is the wrong moment. The OIA is the only thing that tells them apart.\n process.stdout.write(\n `transfer window: host ${cmd} -> TIMED OUT waiting for ${JSON.stringify(expect)}`\n + ` -- OIA ${JSON.stringify(session.oia.toText())}`\n + ` inhibited=${session.oia.isInhibited()}`\n + ` -- screen was ${JSON.stringify(seen.slice(0, SCREEN_DUMP_CHARS))}\\n`,\n );\n } else {\n process.stdout.write(`transfer window: host ${cmd} -> saw ${JSON.stringify(hit)}\\n`);\n }\n continue;\n }\n if (value === undefined) {\n // REPORTED, not ignored, for the reason `SEAM.transfer`'s docstring gives.\n process.stdout.write(`transfer window: UNKNOWN STEP ${step}\\n`);\n continue;\n }\n /**\n * THE FIELD ID IS NOT VALIDATED HERE, and that is the renderer's job rather than an omission.\n * The hooks return what the model holds afterwards, so an id outside `TransferFieldId` comes\n * back as the empty string and the echo below says so -- a line reading `localFil=` is a\n * visible failure, where a list of legal ids in this file would be a second copy of\n * `TRANSFER_FIELDS` that could agree with itself while the form disagreed.\n *\n * ## WHICH HOOK IS CHOSEN BY THE MODEL, NOT BY A LIST KEPT HERE\n *\n * A CYCLE FIELD CANNOT BE SET AS TEXT, and this seam shipped not knowing it: `setFieldText` is\n * a documented no-op on `kind === 'cycle'`, so `direction=send,host=vm` echoed\n * `direction=receive,host=tso`. The echo was honest and the scenario was still wrong -- a\n * RECEIVE sent to a TSO host while the harness believed it had set both. Found by running it\n * against a real host, which is the only thing that could have found it.\n *\n * So the kind is asked of `TRANSFER_FIELDS` -- the model's own table, already in the renderer\n * -- rather than listed here. A second list is exactly the drift that produced the bug: it\n * would agree with itself while the form disagreed, and a field changing kind would break the\n * seam silently.\n */\n const kind = await tw.webContents.executeJavaScript(\n `window.__tn3270FieldKind(${JSON.stringify(name)})`,\n ) as string;\n const hook = kind === 'cycle' ? '__tn3270CycleField' : '__tn3270SetField';\n const held = await tw.webContents.executeJavaScript(\n `window.${hook}(${JSON.stringify(name)}, ${JSON.stringify(value)})`,\n ) as string;\n /**\n * THE ECHO IS CHECKED, not merely printed, and only here -- a text field legitimately holds\n * what it was given, so a mismatch is a REFUSAL (a non-digit into `Lrecl`) and worth seeing;\n * for a cycle field a mismatch means the value is not one the field offers, which a scenario\n * cannot recover from and should not proceed past. Printed either way so the log shows what\n * the form really holds.\n */\n process.stdout.write(`transfer window: ${name}=${held}\\n`);\n if (held !== value) {\n process.stdout.write(\n `transfer window: REFUSED ${name}: asked ${JSON.stringify(value)}, holds ${JSON.stringify(held)}\\n`,\n );\n }\n }\n\n}\n\n/**\n * A keys-only run has to quit itself.\n *\n * `maybeCapture` quits when it has taken its picture, but a chord run takes none -- and\n * without this the process hangs, which reads as a broken client rather than a missing exit.\n *\n * A CLICKS-ONLY RUN IS THE SAME RUN, hence the second half of the condition rather than a second\n * quit path: it also produces only stdout, and `clicks.mjs` reads that stdout with a 120s timeout,\n * so a client that did not quit would burn the timeout and arrive as `error`/SIGTERM -- diagnosable,\n * but as \"the client never ran to completion\" rather than as the missing exit it is. The NAME still\n * says keys because `keys.mjs` and the docs cite it, and a rename would invalidate those citations\n * for no gain; read it as \"an input-seam-only run\".\n *\n * ## THE DRAIN, AND WHY THERE IS NO SLEEP BESIDE IT\n *\n * MEASURED 2026-09-15, because a fixed sleep here would have been exactly the kind of\n * unjustified number this file otherwise refuses. Writing ~5MB to a PIPE and exiting:\n *\n * - with no drain at all, output stops at 64000 bytes -- one pipe buffer -- and the last\n * line is GONE, three runs out of three. So the hazard is real, not folklore.\n * - with `write('', cb)` and exit from the callback, all 5000005 bytes arrive, three out of\n * three. An EMPTY chunk still queues behind the pending ones, which is the property being\n * relied on here.\n *\n * A 200ms sleep after that callback was also tried and removed: the full chord sequence ran\n * 20 times with the callback alone and all 20 kept every `action:` line and the trailing\n * `keys:` line. The callback is the mechanism; the sleep only made it look like one.\n *\n * The seam's own output is a few hundred bytes and would fit the buffer regardless -- the\n * drain earns its place for the run that adds a longer chord list or a slower reader.\n */\nasync function quitIfKeysOnly(): Promise<void> {\n // `select` COUNTS AS A DRIVEN RUN TOO. Without it a selection-only invocation drives the drag,\n // prints its line and then SITS until the harness's 90s timeout -- which reads as a hung client\n // rather than as a seam nobody taught to quit.\n if ((SEAM.keys === '' && SEAM.clicks === '' && SEAM.select === '') || SEAM.shot !== '') return;\n /**\n * A TRANSFER SCENARIO STILL RUNNING OUTRANKS THIS QUIT, and the measurement is at\n * `transferScenarioRunning`: a `Ctrl+t` in the keys list makes `driveTransferWindow` run inside\n * `maybeSendKeys`, so without this the quit arrived while a REAL TRANSFER was mid-flight and the\n * `done ->` line never printed. `driveTransferWindow` does its own quit when it finishes, which\n * is why waiting here is enough and no second quit is needed.\n */\n while (transferScenarioRunning) await new Promise((r) => { setTimeout(r, 200); });\n await new Promise<void>((resolve) => { process.stdout.write('', () => { resolve(); }); });\n app.quit();\n}\n\n/**\n * A TEST SEAM, not a feature: `TN3270_GUI_SHOT=<path>` captures the window and exits.\n *\n * `capturePage()` is a main-process API, so a screenshot harness cannot reach it from\n * outside -- something in here has to take the picture. This is the same instinct as\n * `app.ts` injecting its streams rather than reaching for `process`: the alternative is a\n * renderer nothing can check without a human looking at it.\n *\n * `TN3270_GUI_SHOT_MS` is how long to let the host finish painting first. It defaults\n * generously because a missed frame produces a blank golden, which looks like a rendering\n * bug rather than a timing one. It runs AFTER `maybeSendKeys`, so a run with both seams set\n * still gets its full settle before the capture -- and the keys are in the picture.\n * `maybeSendKeys` ALSO applies this value as a floor on its own wait, so making the keys seam\n * independent did not shorten the settle a screenshot run used to get; the reasoning is\n * there rather than here, next to the `Math.max` that does it.\n *\n * `failExitCode`, WHEN GIVEN, MEANS THIS CAPTURE IS A STARTUP FAILURE'S, not a normal run's:\n * every `fail(...)` call site on the startup path (argv, session, atlas) passes one after\n * painting the error, so a harness running under the shot seam sees the failure as a FAST\n * non-zero exit with the error visible in the PNG, rather than the window sitting open for a\n * real user -- which is correct there -- until the harness's own ~120s timeout expires and\n * reports \"no capture produced\" with no clue why. `app.exit(code)` is used instead of this\n * function's normal `app.quit()` for exactly that reason: `quit()` is graceful and exits 0,\n * which would make a harness that only checks the exit code call a painted-error golden a\n * pass. Never set outside the startup failure paths, so the ordinary success exit (0, via\n * `quit()`) is completely unchanged.\n */\nasync function maybeCapture(win: BrowserWindow, failExitCode?: number): Promise<void> {\n const path = SEAM.shot;\n if (path === '') return;\n await new Promise((r) => setTimeout(r, SEAM.shotMs));\n const image = await win.webContents.capturePage();\n writeFileSync(path, image.toPNG());\n\n /**\n * The hash is of the RAW BITMAP, not of the PNG.\n *\n * Rendering is deterministic -- bitmap glyphs at integer scale with smoothing off have no\n * hinting and no subpixel antialiasing -- but the PNG ENCODER is not part of that promise\n * and can change between Electron versions. Hashing `toBitmap()` compares what was drawn;\n * hashing the file would compare what was drawn AND how it was compressed, and a goldens\n * suite that breaks on an Electron upgrade teaches people to run --update without looking.\n *\n * The PNG is still written, for a human to look at when a hash does differ.\n */\n writeFileSync(`${path}.sha256`, createHash('sha256').update(image.toBitmap()).digest('hex'));\n const { width, height } = image.getSize();\n process.stdout.write(`shot: ${path} ${width}x${height}\\n`);\n if (failExitCode !== undefined) {\n app.exit(failExitCode);\n return;\n }\n app.quit();\n}\n\napp.on('window-all-closed', () => { app.quit(); });\n", "/**\n * Wire constants for the 3270 datastream and the telnet options TN3270 uses.\n *\n * Verified against GA23-0059-07 Appendix F (hexadecimal index), Tables 3-2\n * (WCC), 3-4 (AID), 4-4 (field attributes), 4-6 (attribute types), and 6-1\n * (Query Replies), plus chapter 5 (Read Partition) and chapter 6 (Query\n * Reply), cross-checked against x3270's include/3270ds.h. Do not change a\n * value without checking both sources: the manual's OCR mangles some hex\n * digits in tables, and memory is not a source.\n */\n\n/** Telnet commands (RFC 854), plus EOR (RFC 885). */\nexport const TelnetCmd = {\n EOR: 239,\n SE: 240,\n NOP: 241,\n DM: 242,\n BREAK: 243,\n IP: 244,\n AO: 245,\n AYT: 246,\n EC: 247,\n EL: 248,\n GA: 249,\n SB: 250,\n WILL: 251,\n WONT: 252,\n DO: 253,\n DONT: 254,\n IAC: 255,\n} as const;\n\n/** Telnet options. The first three are what make a session TN3270 (RFC 1576 \u00A73). */\nexport const TelnetOpt = {\n BINARY: 0,\n TERMINAL_TYPE: 24,\n EOR: 25,\n ECHO: 1,\n SUPPRESS_GO_AHEAD: 3,\n TIMING_MARK: 6,\n REGIME_3270: 29,\n TN3270E: 40,\n NEW_ENVIRON: 39,\n} as const;\n\n/** TERMINAL-TYPE subnegotiation codes (RFC 1091). */\nexport const TelnetSubopt = { IS: 0, SEND: 1 } as const;\n\n/**\n * NEW-ENVIRON group codes (RFC 1572), from x3270's include/arpa_telnet.h:122-125.\n *\n * `ESC` is the escape prefix, not a group a host may request: any byte of a NAME or a\n * VALUE that happens to equal one of these four codes is prefixed with `ESC` so the\n * receiver does not read it as a delimiter.\n */\nexport const EnvironGroup = {\n VAR: 0,\n VALUE: 1,\n ESC: 2,\n USERVAR: 3,\n} as const;\n\n/**\n * NEW-ENVIRON qualifiers (RFC 1572), from x3270's include/arpa_telnet.h:117-119.\n *\n * IS AND SEND SHARE THEIR VALUES WITH `TelnetSubopt`, WHICH IS TERMINAL-TYPE's, AND THE\n * TWO ARE NOT THE SAME CONCEPT. Both encode \"here it is\" as 0 and \"give it to me\" as 1,\n * by coincidence of two independent RFCs. Reusing one for the other would work today and\n * would be wrong the first time either changed. The same trap, and the same resolution,\n * as this file's note on XAH.DEFAULT versus XA.RESET.\n *\n * INFO (2) is a host-to-client unsolicited update. We never send it and no trace in\n * x3270's collection sends one to us; it is defined so a received INFO can be traced by\n * name rather than as an unknown byte. x3270 does define it (TELQUAL_INFO,\n * arpa_telnet.h:119) and names it in the `telquals[]` trace table (Common/telnet.c:143,\n * `{ \"IS\", \"SEND\", \"INFO\" }`), used by telnet_new_environ.c to log subnegotiations.\n */\nexport const EnvironQual = {\n IS: 0,\n SEND: 1,\n INFO: 2,\n} as const;\n\n/**\n * TN3270E subnegotiation operations (RFC 2355 \u00A73, rfc2355.txt:317-347), verified\n * against x3270's include/tn3270e.h.\n *\n * THE OPERAND ORDER IS ASYMMETRIC AND MISORDERING IT FAILS SILENTLY. The server\n * sends SEND then DEVICE_TYPE (0x08 0x02); the client replies DEVICE_TYPE then\n * REQUEST (0x02 0x07). x3270 pins the layout at telnet.c:2199, where the test is\n * `sbbuf[2] == TN3270E_OP_DEVICE_TYPE` \u2014 so sbbuf[1] is the verb. Sending\n * 0x02 0x08 was tried while building the test harness: real s3270 logged\n * `DEVICE-TYPE ??8` and then stalled, with no reject and no error.\n */\nexport const Tn3270eOp = {\n ASSOCIATE: 0x00,\n CONNECT: 0x01,\n DEVICE_TYPE: 0x02,\n FUNCTIONS: 0x03,\n IS: 0x04,\n REASON: 0x05,\n REJECT: 0x06,\n REQUEST: 0x07,\n SEND: 0x08,\n} as const;\n\n/**\n * DEVICE-TYPE REJECT reason codes (RFC 2355 \u00A73).\n *\n * `INV_NAME` is the RFC's spelling; x3270 calls the same value\n * TN3270E_REASON_INV_DEVICE_NAME. Same code 3, two names \u2014 the RFC's is used here.\n */\nexport const Tn3270eReason = {\n CONN_PARTNER: 0x00,\n DEVICE_IN_USE: 0x01,\n INV_ASSOCIATE: 0x02,\n INV_NAME: 0x03,\n INV_DEVICE_TYPE: 0x04,\n TYPE_NAME_ERROR: 0x05,\n UNKNOWN_ERROR: 0x06,\n UNSUPPORTED_REQ: 0x07,\n} as const;\n\n/**\n * UNBIND reason codes (x3270's include/tn3270e.h:108-118).\n *\n * RFC 2355 does not enumerate these; x3270 decodes them in `unbind_reason`\n * (Common/telnet.c:2592) and the wire is the authority. THE GAPS ARE REAL \u2014 there is\n * no 0x03-0x06 and no 0x0d, and x3270 names none of them either. An unrecognized\n * reason is reported as unknown rather than guessed at.\n *\n * BIND_FORTHCOMING IS THE OPERATIONALLY INTERESTING ONE: it means another BIND is\n * coming, which is how a host hands a session between applications. A client that\n * treated it as a teardown would drop a session the host intended to keep.\n *\n * NORMAL is 0x01, NOT 0x00, and that matters: a zero-length UNBIND body carries no\n * reason at all, so 0x00 must stay available to mean \"absent\".\n */\nexport const Tn3270eUnbindReason = {\n NORMAL: 0x01,\n BIND_FORTHCOMING: 0x02,\n VR_INOPERATIVE: 0x07,\n RX_INOPERATIVE: 0x08,\n HRESET: 0x09,\n SSCP_GONE: 0x0a,\n VR_DEACTIVATED: 0x0b,\n LU_FAILURE_PERM: 0x0c,\n LU_FAILURE_TEMP: 0x0e,\n CLEANUP: 0x0f,\n BAD_SENSE: 0xfe,\n} as const;\n\n/**\n * Negotiable functions.\n *\n * CONTENTION_RESOLUTION (0x05) is NOT in RFC 2355 \u2014 it is a later extension that\n * x3270 requests anyway (telnet.c:953). We request it for parity; nothing depends\n * on a host granting it. x3270 also defines TN3270E_FUNC_SNA_SENSE = 6, which is\n * deliberately absent here: we neither request nor grant it, and an unrecognized\n * inbound code is dropped from the list per \u00A77.2.2 rather than refused.\n *\n * SCS_CTL_CODES and DATA_STREAM_CTL are printer-session functions by definition\n * (\u00A77.2.2) and belong to the printer stage.\n *\n * BIND_IMAGE IS requested as of this task (tn3270e.ts, REQUESTED_FUNCTIONS). Granted\n * it and sent no BIND, real s3270 never enters 3270 mode at all \u2014 the write is\n * delivered and ignored (telnet.c:2339). That hazard is real and unchanged; what\n * changed is the measurement behind asking anyway \u2014 see tn3270e.ts's comment on\n * REQUESTED_FUNCTIONS for the trace-collection count and the timeout that bounds it.\n */\nexport const Tn3270eFunc = {\n BIND_IMAGE: 0x00,\n DATA_STREAM_CTL: 0x01,\n RESPONSES: 0x02,\n SCS_CTL_CODES: 0x03,\n SYSREQ: 0x04,\n CONTENTION_RESOLUTION: 0x05,\n} as const;\n\n/**\n * Header DATA-TYPE values (RFC 2355 \u00A78.1.1).\n *\n * `DATA_3270` rather than `3270_DATA` only because an identifier cannot start with\n * a digit; the wire name is 3270-DATA. x3270 additionally defines\n * TN3270E_DT_BID = 0x09, which the RFC's list does not reach \u2014 an inbound 0x09 is\n * traced and dropped like any other type we do not implement.\n */\nexport const Tn3270eDataType = {\n DATA_3270: 0x00,\n SCS_DATA: 0x01,\n RESPONSE: 0x02,\n BIND_IMAGE: 0x03,\n UNBIND: 0x04,\n NVT_DATA: 0x05,\n REQUEST: 0x06,\n SSCP_LU_DATA: 0x07,\n PRINT_EOJ: 0x08,\n} as const;\n\n/**\n * Header REQUEST-FLAG (RFC 2355 \u00A78.1.2), meaningful only when DATA-TYPE is REQUEST.\n *\n * THE RFC DEFINES EXACTLY ONE VALUE, AND THIS LIST IS NOT EXHAUSTIVE OF WHAT A HOST\n * MAY SEND. x3270 also has SEND_DATA 0x01, KEYBOARD_RESTORE 0x02 and SIGNAL 0x04.\n * All three are only meaningful on a DATA-TYPE of REQUEST, which we do not handle,\n * so they are omitted rather than defined-and-ignored.\n */\nexport const Tn3270eRequestFlag = { ERR_COND_CLEARED: 0x00 } as const;\n\n/**\n * Header RESPONSE-FLAG (RFC 2355 \u00A78.1.3).\n *\n * The field is OVERLOADED BY DATA-TYPE, which is why two pairs share values. On\n * 3270-DATA or SCS-DATA it says whether the sender wants a response; on RESPONSE it\n * says whether this is a positive or negative one. So POSITIVE_RESPONSE equals\n * NO_RESPONSE (0x00) and NEGATIVE_RESPONSE equals ERROR_RESPONSE (0x01) \u2014 not a\n * copy-paste error. Both spellings are kept so each call site reads as what it\n * means.\n *\n * x3270 has a third RESPONSE value, TN3270E_RSF_SNA_SENSE 0x02, which RFC 2355 does\n * not list among the response flags.\n */\nexport const Tn3270eResponseFlag = {\n NO_RESPONSE: 0x00,\n ERROR_RESPONSE: 0x01,\n ALWAYS_RESPONSE: 0x02,\n POSITIVE_RESPONSE: 0x00,\n NEGATIVE_RESPONSE: 0x01,\n} as const;\n\n/**\n * The one data byte of a RESPONSE message (RFC 2355 \u00A710.4.1, rfc2355.txt:1440-1462).\n *\n * DEVICE_END and COMMAND_REJECT share 0x00 because the meaning comes from\n * RESPONSE-FLAG: under POSITIVE, 0x00 is \"successful completion (when sent by the\n * client, this is equivalent to Device End)\"; under NEGATIVE it is \"an invalid 3270\n * command was received\". OP_CHECK 0x02 is \"an illegal 3270 buffer address or order\n * sequence was received\".\n *\n * The printer senses are omitted as unreachable from a display session: 0x01\n * \"printer is not ready\" (x3270's INTERVENTION_REQUIRED) and 0x03 \"printer is\n * powered off or not connected\" (COMPONENT_DISCONNECTED).\n */\nexport const Tn3270eSense = {\n DEVICE_END: 0x00,\n COMMAND_REJECT: 0x00,\n OP_CHECK: 0x02,\n} as const;\n\n/**\n * 3270 commands, non-SNA/channel encoding. x3270 accepts these as well as the\n * SNA codes below, so we do too.\n *\n * Note WSF (0x11) collides numerically with Order.SBA; they are distinguished\n * by position (command byte vs. order byte), never by value. Note also that\n * Cmd.NOP (0x03) is a 3270 command and has nothing to do with TelnetCmd.NOP.\n */\nexport const Cmd = {\n W: 0x01,\n RB: 0x02,\n NOP: 0x03,\n EW: 0x05,\n RM: 0x06,\n EWA: 0x0d,\n RMA: 0x0e,\n EAU: 0x0f,\n WSF: 0x11,\n} as const;\n\n/** 3270 commands, SNA encoding \u2014 what a TN3270 host normally sends. */\nexport const SnaCmd = {\n RMA: 0x6e,\n EAU: 0x6f,\n EWA: 0x7e,\n W: 0xf1,\n RB: 0xf2,\n WSF: 0xf3,\n EW: 0xf5,\n RM: 0xf6,\n} as const;\n\n/** Buffer orders (Appendix F). SA/SFE/MF are recognized in stage 1 only so\n * they can be skipped by length instead of mis-executed. */\nexport const Order = {\n PT: 0x05,\n GE: 0x08,\n SBA: 0x11,\n EUA: 0x12,\n IC: 0x13,\n SF: 0x1d,\n SA: 0x28,\n SFE: 0x29,\n MF: 0x2c,\n RA: 0x3c,\n} as const;\n\n/**\n * The two format control orders the Dup and Field Mark keys write (Table 4-3,\n * pages.txt:3196-3197: \"DUP Duplicate X'1C'\", \"FM Field Mark X'1E'\"), confirmed\n * against x3270's `include/3270ds.h:364-365` \u2014 `EBC_dup 0x1c`, `EBC_fm 0x1e`.\n *\n * THEY ARE CHARACTERS, NOT AIDS, which is why they live here beside `Order` and\n * not beside the `AID` table a reader might look in first. x3270's Dup and\n * FieldMark actions both call `key_Character(...)` (`Common/kybd.c:2788`,\n * `:2825`), so they go into the buffer and set MDT exactly as a typed letter\n * does. Nothing about them reaches `sendAID`. Table 4-3 is where the manual puts\n * them too, alongside NUL and SUB.\n *\n * Stored in the buffer they display as an overscored asterisk and an overscored\n * semicolon respectively (same table); we render neither yet.\n */\nexport const EBCDIC_DUP = 0x1c;\nexport const EBCDIC_FIELD_MARK = 0x1e;\n\n/**\n * The four EBCDIC bytes of the TEST REQUEST READ heading, which is what the Sys Req\n * (TEST REQ) key puts on the wire on a classic, non-TN3270E session.\n *\n * Values from x3270's `include/3270ds.h`: `EBC_soh 0x01` and `EBC_stx 0x02` at `:356-357`,\n * `EBC_slash 0x61` at `:373`, `EBC_percent 0x6c` at `:375`. In order the heading reads\n * SOH, `%`, `/`, STX \u2014 see `buildReadModified`, which assembles it.\n *\n * THEY ARE NOT AN AID, and this is the counterintuitive part of the whole key. `AID.SYSREQ`\n * (0xf0) is a real architected AID \u2014 GA23-0059-07 Table 3-4 lists \"Test Req and Sys Req F0\"\n * (pages.txt:2017) \u2014 and it is what SELECTS this heading, but it is never itself\n * transmitted for this key. The manual is explicit: the test request read data stream is\n * \"the same as described previously for read-modified operations, EXCLUDING the 3-byte read\n * heading (AID and cursor address)\" (pages.txt:13786-13789). x3270 implements exactly that:\n * `ctlr_read_modified`'s `case AID_SYSREQ` writes these four bytes and `break`s out of the\n * AID switch without ever emitting the AID (`Common/ctlr.c:770-777`).\n *\n * `EBCDIC_PERCENT` IS THE SAME BYTE AS `AID.PA1` \u2014 both 0x6c, and a wire dump cannot tell\n * them apart. Named separately so the code that builds the heading says which it means, and\n * so a reader decoding a capture does not conclude a PA1 went out.\n *\n * There is no ETX here. The manual's BSC form ends in one (pages.txt:13780-13785) because\n * ETX is BSC block framing; the non-SNA form is \"the same as for the BSC environment,\n * except there is no ETX\" (pages.txt:14180-14184), and a telnet record is terminated by\n * IAC EOR instead.\n */\nexport const EBCDIC_SOH = 0x01;\nexport const EBCDIC_PERCENT = 0x6c;\nexport const EBCDIC_SLASH = 0x61;\nexport const EBCDIC_STX = 0x02;\n\n/**\n * Structured field identifiers (SFID), GA23-0059 p. 5-51 and chapter 6.\n *\n * 0x81 is BOTH the Query Reply SFID (inbound \u2014 i.e. sent by us; chapter 6 is\n * \"Inbound Structured Fields\") and, as a QCODE, the Usable Area reply code.\n * They occupy different byte positions and are never interchangeable: SFID is\n * byte 2, QCODE is byte 3.\n */\nexport const Sfid = {\n READ_PARTITION: 0x01,\n QUERY_REPLY: 0x81,\n /**\n * `SF_TRANSFER_DATA` \u2014 DFT file transfer, dispatched by x3270 at `sf.c:175`.\n * Both a host request type and the SFID we send our replies under. A host only\n * sends this if we advertised Query Reply (DDM) 0x95, which `-ddm on` does.\n */\n TRANSFER_DATA: 0xd0,\n} as const;\n\n/** PID value meaning \"this is a query, not a read of partition 0x00-0x7E\". */\nexport const PID_QUERY = 0xff;\n\n/**\n * Read Partition TYPE byte, GA23-0059 p. 5-51 (pages.txt:6350-6355).\n *\n * We answer both query types. The table also lists X'6E' Read Modified All,\n * X'F2' Read Buffer and X'F6' Read Modified \u2014 reads against a REAL partition,\n * which we do not support, so they are absent here on purpose. x3270 handles\n * them (sf.c:304-334) and requires PID 0x00 for each; we have no partitions to\n * read.\n */\nexport const ReadPartitionType = {\n QUERY: 0x02,\n QUERY_LIST: 0x03,\n} as const;\n\n/**\n * Query List REQTYP, GA23-0059 p. 5-51 and p. 6-19.\n *\n * BITS 0-1 OF BYTE 5, not the whole byte: \"an additional parameter, REQTYP\n * (Request Type), bits 0-1 of byte 5 and, / optionally, a list of QCODES\n * starting at byte 6\" (pages.txt:8508-8509, p. 6-19). Bits 2-7 are reserved \u2014\n * the byte table's row reads \"2-7 SFID Reserved\" (pages.txt:6356; that \"SFID\"\n * is OCR damage, the field is unnamed reserved space).\n *\n * The values below are therefore the SHIFTED forms, B'00'/B'01'/B'10' occupying\n * the top two bits: 0x00, 0x40, 0x80. That is exactly how x3270 spells them\n * (include/3270ds.h:118-120):\n *\n * #define SF_RPQ_LIST\t0x00\t// QCODE list\n * #define SF_RPQ_EQUIV\t0x40\t// equivalent+ QCODE list\n * #define SF_RPQ_ALL\t0x80\t// all\n *\n * B'11' (0xC0) is \"Reserved\" (pages.txt:6361) and there is no entry for it:\n * x3270 rejects an unrecognized request type outright (sf.c:301-303, `default:\n * ... return PDS_BAD_CMD`), and so do we.\n *\n * NOTE x3270 compares `buf[5]` against these WITHOUT masking off bits 2-7, so a\n * host setting a reserved low bit lands in its default case and is rejected.\n * We mask instead \u2014 see REQTYP_MASK \u2014 because the manual defines the field as\n * bits 0-1 and says nothing about the other six mattering.\n */\nexport const ReqTyp = {\n QCODE_LIST: 0x00,\n EQUIVALENT: 0x40,\n ALL: 0x80,\n} as const;\n\n/** Bits 0-1 of byte 5. The rest is reserved; see the note on ReqTyp. */\nexport const REQTYP_MASK = 0xc0;\n\n/**\n * Query Reply codes (QCODE), GA23-0059 Table 6-1. Only what we implement.\n *\n * Usable Area and Implicit Partition are legible in that table (pages.txt:8638,\n * :8608). Summary's own code is NOT: its row OCRs as \"Summary Yes X'BO' Yes\n * Yes\" (pages.txt:8635), and 0xB0 is a real QCODE elsewhere (Segment,\n * 3270ds.h:173), so the damage is not self-evident. 0x80 comes from the prose\n * at p. 6-20: \"must support the Summary Query Reply, QCODE = X '80'\"\n * (pages.txt:8568-8569), and from 3270ds.h:136 QR_SUMMARY 0x80.\n */\nexport const Qcode = {\n SUMMARY: 0x80,\n USABLE_AREA: 0x81,\n /**\n * Color. Table 6-1: \"Color Yes X'86' Yes Yes\" (pages.txt:8589) \u2014 that row is\n * OCR-clean, and the unit's own byte table agrees: \"3 QCODE X'86' Identifies\n * this Query Reply as Color\" (pages.txt:9214). x3270's 3270ds.h:142\n * QR_COLOR 0x86.\n *\n * Table 6-1's \"Yes\" in the Query column is what makes `returnedForQuery: true`\n * correct for it; x3270 also gives it a `single_fn` (sf.c:86), which is only\n * reachable from a plain Query or an enumerating Query List.\n */\n COLOR: 0x86,\n /**\n * Highlighting. Table 6-1: \"Highlighting Yes X'87' Yes Yes\"\n * (pages.txt:8605), confirmed by \"3 QCODE X'87' Identifies this Query Reply as\n * Highlighting\" (pages.txt:10342) and 3270ds.h:143 QR_HIGHLIGHTING 0x87.\n */\n HIGHLIGHTING: 0x87,\n /**\n * Distributed Data Management, the advertisement that lets a host choose DFT\n * file transfer instead of CUT.\n *\n * Table 6-1: \"Distributed Data Management Yes X'95' Yes Yes\"\n * (pages.txt:8595), and the unit's own section confirms both the code and the\n * causality: \"The Query Reply (Distributed Data Management) indicates the\n * Distributed Data Management (DDM) subsets supported. When this function is\n * supported, this Query Reply is transmitted inbound in reply to a Read\n * Partition structured field specifying Query or Query List\"\n * (pages.txt:9806-9810). x3270's 3270ds.h:155 QR_DDM 0x95.\n *\n * THIS IS THE ONLY SWITCH BETWEEN CUT AND DFT, and it is ours to throw rather\n * than the host's to offer: the client does not choose a transfer protocol,\n * it declares a capability and the host picks. `ft_cut.c:440` and\n * `ft_dft.c:175` call `ft_running(true)`/`ft_running(false)`, which REPORTS\n * which protocol arrived (read once, `ft.c:556`, to print \"CUT\" or \"DFT\").\n * See docs/superpowers/specs/2026-09-24-dft-file-transfer-design.md.\n */\n DDM: 0x95,\n IMPLICIT_PARTITION: 0xa6,\n /**\n * Null (QCODE 0xFF) \u2014 \"we support none of what you asked for\".\n *\n * NOT a capability, which is why it is not in DEFAULT_CAPABILITIES and why\n * Table 6-1 gives it \"Null No X'FF' No No\" (pages.txt:8612): the only three\n * columns are Query / Query List / Equivalent / All and it is returned by\n * NONE of them as a matter of support. It is returned by exactly one path,\n * REQTYP=QCODE List matching nothing we have. x3270 keeps it in its reply\n * table but pointedly excludes it from every enumeration, with the comment\n * \"QR_NULL must be last in the table\" (sf.c:94) and NSR = NSR_ALL - 1\n * (sf.c:102-103) so the loops stop before it.\n *\n * Value from 3270ds.h:180 `#define QR_NULL 0xff` and from the manual's own\n * byte table, \"3 QCODE X'FF' Identifies this Query Reply as Null\"\n * (pages.txt:10771).\n */\n NULL: 0xff,\n} as const;\n\n/**\n * SFE/MF attribute-type for the basic 3270 field attribute.\n *\n * 0xC0, confirmed twice because the manual's prose example OCRs as X'C8'\n * (pages.txt:2882). The attribute-type table (Table 4-6) reads\n * \"X'CO' 3270 Field attribute\" (pages.txt:3430) \u2014 that O is OCR of a zero \u2014\n * and x3270's include/3270ds.h:230 defines XA_3270 0xc0. Numerically equal to\n * FA.PRINTABLE, which is a coincidence of the architecture, not a relation \u2014\n * do not unify them.\n *\n * This is one row of Table 4-6. The other rows this codebase names are just\n * below, as `XA`/`XAH`/`XAC_DEFAULT`.\n */\nexport const XA_3270 = 0xc0;\n\n/**\n * Extended attribute types, carried by SA (X'28') and as SFE type-value pairs\n * \u2014 the other rows of Table 4-6, of which `XA_3270` above is one row (0xC0).\n * x3270's include/3270ds.h:229 (XA_ALL), 240-250.\n *\n * `RESET` is x3270's XA_ALL, deliberately renamed: \"reset\" names what it\n * DOES (return every character attribute type to its default), where \"all\"\n * names only which types it affects. Every other member below is a\n * mechanical prefix-drop of its x3270 name; this one isn't, on purpose.\n *\n * RESET's defining text is under the heading \"All Character Attributes\":\n * \"All character attributes reset all character attribute types that are\n * specifiable in the SA order to their default value. Attribute types\n * affected are color, highlighting, and character set. The only valid value\n * setting is X'00' ... The attribute type X'00' can appear only in the SA\n * order\" (pages.txt:3449-3456). Separately, pages.txt:2986 says X'00' is the\n * one attribute type \"always supported by the SA order\" \u2014 i.e. every\n * implementation must accept it \u2014 which supports supportedness, not the\n * reset-all definition; don't cite it for the latter.\n *\n * Do not confuse `RESET` (0x00 as a TYPE, meaning reset-all) with\n * `XAC_DEFAULT` below (0x00 as a VALUE under FOREGROUND/BACKGROUND, meaning\n * \"device default color\"). Both appear in the committed TK5 fixture (101\n * FOREGROUND, 12 RESET), so a conflation is not hypothetical.\n *\n * CHARSET is named but deliberately NOT implemented \u2014 it selects Programmable\n * Symbol Sets, which are out of scope. It stays counted by setAttributeIgnored.\n * Table 4-6 also has VALIDATION (0xC1), OUTLINING (0xC2), and TRANSPARENCY\n * (0x46) (3270ds.h:231, 235, 251); those are out of scope too and, unlike\n * CHARSET, not even named here.\n */\nexport const XA = {\n RESET: 0x00,\n HIGHLIGHTING: 0x41,\n FOREGROUND: 0x42,\n CHARSET: 0x43,\n BACKGROUND: 0x45,\n} as const;\n\n/**\n * Highlighting values for `XA.HIGHLIGHTING`. x3270's 3270ds.h:241-246.\n *\n * TRAP: the manual's Chapter 4 highlighting table (pages.txt:3487-3498) lists\n * only FIVE settings \u2014 X'00', X'F0', X'F1', X'F2', X'F4' \u2014 and OMITS X'F8'\n * Intensify entirely. A verifier who checks only that obvious table will find\n * 5 of these 6 values and may wrongly conclude INTENSIFY is wrong. INTENSIFY\n * is confirmed instead by the Query Reply (Highlighting) section, which lists\n * all six including \"X'F8' ... Intensify\" (pages.txt:10310-10325). Same class\n * of trap as the Qcode.SUMMARY OCR note and the XA_3270 mangled-hex note\n * above: check the source that actually settles it, not the first one found.\n */\nexport const XAH = {\n DEFAULT: 0x00,\n NORMAL: 0xf0,\n BLINK: 0xf1,\n REVERSE: 0xf2,\n UNDERSCORE: 0xf4,\n INTENSIFY: 0xf8,\n} as const;\n\n/** Color value meaning \"the device default\", per Query Reply (Color). 3270ds.h:248. */\nexport const XAC_DEFAULT = 0x00;\n\n/**\n * Attention identifiers (Table 3-4).\n *\n * `SYSREQ` IS THE ONE MEMBER THAT IS NEVER TRANSMITTED. Table 3-4 lists it \u2014 \"Test Req and\n * Sys Req F0\" (pages.txt:2017) \u2014 and it is a genuine AID in the sense that it is what\n * `buildReadModified` switches on. But that function turns it into the four-byte TEST\n * REQUEST READ heading (`EBCDIC_SOH`/`EBCDIC_PERCENT`/`EBCDIC_SLASH`/`EBCDIC_STX` above)\n * and drops the AID, exactly as x3270's `ctlr_read_modified` does (`Common/ctlr.c:770-777`).\n * So 0xf0 never reaches the wire for the Sys Req key. Under TN3270E the key is not an AID at\n * all: it is Telnet IAC AO. See `Session.sysreq`.\n */\nexport const AID = {\n NONE: 0x60,\n QREPLY: 0x61,\n ENTER: 0x7d,\n PF1: 0xf1,\n PF2: 0xf2,\n PF3: 0xf3,\n PF4: 0xf4,\n PF5: 0xf5,\n PF6: 0xf6,\n PF7: 0xf7,\n PF8: 0xf8,\n PF9: 0xf9,\n PF10: 0x7a,\n PF11: 0x7b,\n PF12: 0x7c,\n PF13: 0xc1,\n PF14: 0xc2,\n PF15: 0xc3,\n PF16: 0xc4,\n PF17: 0xc5,\n PF18: 0xc6,\n PF19: 0xc7,\n PF20: 0xc8,\n PF21: 0xc9,\n PF22: 0x4a,\n PF23: 0x4b,\n PF24: 0x4c,\n PA1: 0x6c,\n PA2: 0x6e,\n PA3: 0x6b,\n CLEAR: 0x6d,\n SYSREQ: 0xf0,\n SELECT: 0x7e,\n SF: 0x88,\n} as const;\n\n/** PF key number 1-24, at index n-1, to its AID byte. */\nexport const PF_AIDS: readonly number[] = [\n AID.PF1, AID.PF2, AID.PF3, AID.PF4, AID.PF5, AID.PF6,\n AID.PF7, AID.PF8, AID.PF9, AID.PF10, AID.PF11, AID.PF12,\n AID.PF13, AID.PF14, AID.PF15, AID.PF16, AID.PF17, AID.PF18,\n AID.PF19, AID.PF20, AID.PF21, AID.PF22, AID.PF23, AID.PF24,\n];\n\n/** PA key number 1-3, at index n-1, to its AID byte. */\nexport const PA_AIDS: readonly number[] = [AID.PA1, AID.PA2, AID.PA3];\n\n/**\n * The AID byte for PF key `n`, or a `RangeError` naming the range.\n *\n * ## USE THIS RATHER THAN INDEXING THE TABLE, AND THE REASON IS A MEASURED LIVE DEFECT\n *\n * `PF_AIDS[n - 1]!` looks safe and is not. An out-of-range `n` yields `undefined`, the non-null\n * assertion hides it from the compiler, and `buildReadModified` finishes the job:\n * `Uint8Array.from([undefined])` SILENTLY COERCES TO 0. So `{kind:'pf', n:-1}` did not fail --\n * it transmitted **AID 0x00 to a live mainframe**, then locked the local keyboard while\n * `applyAction`'s catch-all swallowed any complaint. Measured on the wire as\n * `[0x00, 0x40, 0x40, 0xff, 0xef]`.\n *\n * Three of the four front ends were safe only by accident, because their `n` comes from a trusted\n * keymap table. The web gateway is the first where a REMOTE party supplies it. Bounding it at that\n * boundary closed the live hole; this closes the CLASS, by making the unchecked index unreachable\n * from the API rather than merely unused today.\n *\n * It throws rather than returning `undefined` deliberately: a caller that must handle `undefined`\n * can still forget, and `sendAID` would take it. A throw from inside `applyAction`'s `try` puts\n * nothing on the wire at all, which is the correct outcome for a number no key can produce.\n *\n * The bound is the table's own length, never a literal 24, so the two cannot drift apart.\n */\nexport function pfAID(n: number): number {\n const aid = Number.isInteger(n) ? PF_AIDS[n - 1] : undefined;\n if (aid === undefined) {\n throw new RangeError(`PF number must be an integer in 1..${PF_AIDS.length}, not ${n}`);\n }\n return aid;\n}\n\n/** The AID byte for PA key `n`, or a `RangeError`. See `pfAID` for why this is not an index. */\nexport function paAID(n: number): number {\n const aid = Number.isInteger(n) ? PA_AIDS[n - 1] : undefined;\n if (aid === undefined) {\n throw new RangeError(`PA number must be an integer in 1..${PA_AIDS.length}, not ${n}`);\n }\n return aid;\n}\n\n/**\n * Every byte that is a legitimate AID, for `sendAID` to check against.\n *\n * The BACKSTOP behind `pfAID`/`paAID`: those two fix the callers that exist, and this makes a bogus\n * AID unsendable by any caller, including one written later. `0x00` -- the exact value the coercion\n * above produced -- is not in this set, and neither is `undefined`.\n *\n * Built from `AID` itself so a new key added there is accepted without a second edit here. That is\n * also why this is a `Set` of the values rather than a hand-written list: a list would be the same\n * drift risk in a new place.\n */\nexport const VALID_AIDS: ReadonlySet<number> = new Set(Object.values(AID));\n\n/**\n * Short-read AIDs send the AID byte ALONE \u2014 no cursor address, no field data.\n * GA23-0059-07: \"During the short-read operation, only an AID byte is\n * transferred to the application program.\" x3270's ctlr_read_modified jumps to\n * rm_done immediately after writing the AID for these four.\n *\n * Read Modified All suppresses this (see inbound.ts), and SELECT is NOT a\n * short read \u2014 it sends cursor but no field data.\n */\nexport function isShortReadAID(aid: number): boolean {\n return aid === AID.CLEAR || aid === AID.PA1 || aid === AID.PA2 || aid === AID.PA3;\n}\n\n/** Field attribute bits (Table 4-4). */\nexport const FA = {\n PRINTABLE: 0xc0,\n PROTECT: 0x20,\n NUMERIC: 0x10,\n INTENSITY: 0x0c,\n INT_NORM_NSEL: 0x00,\n INT_NORM_SEL: 0x04,\n INT_HIGH_SEL: 0x08,\n INT_ZERO_NSEL: 0x0c,\n RESERVED: 0x02,\n MODIFY: 0x01,\n} as const;\n\n/**\n * WCC bits (Table 3-2). The manual numbers bits 0-7 from the most significant\n * end, so its \"bit 7\" (reset MDT) is mask 0x01 and its \"bit 1\" (reset) is 0x40.\n */\nexport const WCC = {\n RESET: 0x40,\n START_PRINTER: 0x08,\n SOUND_ALARM: 0x04,\n KEYBOARD_RESTORE: 0x02,\n RESET_MDT: 0x01,\n} as const;\n\n/**\n * Maps a 6-bit value to its outbound byte in 12-bit address form.\n * Matches x3270's code_table in Common/ctlr.c.\n */\nexport const ADDRESS_CODE_TABLE: readonly number[] = [\n 0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,\n 0xc8, 0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,\n 0x50, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,\n 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,\n 0x60, 0x61, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,\n 0xe8, 0xe9, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,\n 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,\n 0xf8, 0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,\n];\n\n/** Default screen geometry for an IBM-3278-2. */\nexport const MODEL_2 = { rows: 24, cols: 80 } as const;\n/**\n * Alternate screen sizes for models 3, 4 and 5, per x3270 `include/3270ds.h:446-453`.\n *\n * THESE ARE ALTERNATE SIZES ONLY. Every model's DEFAULT size is model 2's 24x80 --\n * x3270 sets `ROWS = defROWS = MODEL_2_ROWS` unconditionally (`ctlr.c:341`) and\n * only `altROWS = maxROWS` varies (`ctlr.c:345`), and `globals.h:196-198` labels\n * the pair \"default (EraseWrite)\" and \"alternate (EraseWriteAlternate)\". So a\n * model number does not change the screen you get on connect; it changes the\n * screen the host may switch you to with Erase/Write Alternate.\n */\nexport const MODEL_3 = { rows: 32, cols: 80 } as const;\nexport const MODEL_4 = { rows: 43, cols: 80 } as const;\nexport const MODEL_5 = { rows: 27, cols: 132 } as const;\n\nexport const TERMINAL_TYPE = 'IBM-3278-2-E';\n", "/**\n * A device-name template that yields a fresh candidate per request.\n *\n * WHY A HOST WANTS THIS: the device name is how some hosts identify a session, and a\n * name already in use is refused -- so the client offers the next candidate. x3270's\n * `devname_success.trc` shows a host asking four times and receiving foo001 through\n * foo004; `devname_failure.trc` shows the same with a single digit, saturating at foo9\n * on the tenth and later requests.\n *\n * THE ONLY STATEFUL PIECE OF THIS FEATURE, deliberately isolated here so the reply\n * builder in `newenviron.ts` can stay pure and be tested against byte arrays.\n *\n * Ported from x3270's `devname_init` (Common/devname.c:42-69) and `devname_next`\n * (Common/devname.c:72-82).\n */\nexport class DeviceName {\n private readonly stem: string;\n private readonly digits: number;\n private readonly max: number;\n private current = 0;\n\n constructor(template: string) {\n // Count TRAILING '=' only: an '=' earlier in the name is part of the name.\n // Mirrors the backward scan in devname_init (devname.c:57-61).\n let digits = 0;\n while (digits < template.length\n && template[template.length - 1 - digits] === '=') digits++;\n this.digits = digits;\n this.stem = digits === 0 ? template : template.slice(0, template.length - digits);\n // 10^digits - 1, matching x3270's `max *= 10` per '=' and then `max - 1`\n // (devname.c:60,65). With no trailing '=', digits is 0 and max is 0, so the\n // `current < max` guard in next() is always false: a template with no '='\n // is a constant, exactly as devname_next returns the unmodified template\n // when sub_length is 0.\n //\n // Bounded to avoid Number.MAX_SAFE_INTEGER overflow on a pathological template\n // (e.g. 20 '=' characters): x3270's `unsigned long max` has the same class of\n // problem in C (silent wraparound), but here we cap at 15 digits (10^15 - 1,\n // well under 2^53) rather than let 10 ** digits silently exceed safe-integer\n // range and produce a max that Number can no longer represent exactly. A\n // template asking for more digits than that is almost certainly a typo, not\n // a real device-name scheme, so we treat additional trailing '=' beyond 15\n // as part of the stem instead of growing the counter further.\n const cappedDigits = Math.min(digits, 15);\n if (cappedDigits !== digits) {\n this.digits = cappedDigits;\n this.stem = template.slice(0, template.length - cappedDigits);\n }\n this.max = this.digits === 0 ? 0 : 10 ** this.digits - 1;\n }\n\n /**\n * The next candidate.\n *\n * Saturates rather than wrapping, as x3270 does (`if (d->current < d->max)`,\n * devname.c:76): re-offering a name the host already refused is the one outcome\n * this mechanism exists to prevent. A template with no '=' is a constant.\n */\n next(): string {\n if (this.digits === 0) return this.stem;\n if (this.current < this.max) this.current++;\n return this.stem + String(this.current).padStart(this.digits, '0');\n }\n}\n", "/**\n * The TELNET NEW-ENVIRON option (RFC 1572), telnet option 39.\n *\n * PURE: this module parses a request body and builds a reply body. It holds no session\n * state, opens no socket and owns no counter -- `DeviceName` in devname.ts is where the\n * one piece of state lives, and it is passed in already resolved. Same division as\n * `tn3270e.ts` (a pure state machine) and `bind.ts` (pure parsers).\n *\n * WHAT A HOST USES IT FOR HERE: a `DEVNAME` USERVAR naming the session. That is a\n * SECOND route to the thing TN3270E's CONNECT does with LU names, and the two are\n * unrelated mechanisms for different hosts -- do not unify them.\n *\n * Reference implementation: x3270's Common/telnet_new_environ.c.\n */\nimport { EnvironGroup, EnvironQual } from './constants.js';\n\n/** One requested entry. An empty `name` means \"every variable in this group\". */\nexport interface EnvironRequest {\n readonly group: number;\n readonly name: string;\n}\n\n/**\n * Parse a SEND request body into the entries requested, in order.\n *\n * Returns null for a malformed body rather than throwing -- the same contract as\n * `decodeHeader` and `parseBind`, and for the same reason: a client cannot correct a\n * host, and an exception here would surface to the operator as a program check the host\n * never caused.\n *\n * THE ORDER IS PRESERVED because the reply must list variables in the order asked; the\n * recorded trace does exactly that with its three uservars.\n *\n * TWO NON-OBVIOUS BEHAVIORS, both from x3270's four-state machine\n * (telnet_new_environ.c:356-470):\n * - A VAR or USERVAR byte ENDS the previous request and begins a new one. There is no\n * length prefix and no delimiter; the group bytes are the delimiters.\n * - AN EMPTY BODY MEANS \"SEND EVERYTHING\" and expands to one request per group\n * (:453-470). Returning an empty list instead would answer such a host with nothing,\n * in a reply that is still well-formed -- so nothing downstream would notice. This is\n * not merely x3270's C: a REAL HOST DOES THIS. `s3270/Test/dbcs-wrap.trc:103-104` has\n * the host send a bare `fffa2701fff0` (SEND, empty body) and x3270's reply at :108\n * dumps every VAR and USERVAR it knows (`lu.trc`, `target.trc` and `korean.trc` show\n * the same pattern). A recorded host, not just the reference source, depends on the\n * empty-body expansion.\n *\n * ON x3270's FOUR STATES VERSUS THIS FUNCTION'S TWO (`out.length === 0` doubling as\n * `EE_BASE`, \"append to `out[out.length-1]!.name`\" doubling as both `EE_VAR` and\n * `EE_NAME`): checked against the source, `EE_VAR`'s default arm (Malloc(1); name[0]=c)\n * and `EE_NAME`'s default arm (Realloc; name[len-1]=c) build the identical byte\n * sequence, and both route a VAR/USERVAR byte and an ESC byte identically\n * (telnet_new_environ.c:397-440) -- `EE_VAR`'s only difference is that a *plain* byte\n * moves it to `EE_NAME`, which is a self-transition from `EE_NAME`'s own point of view.\n * No input distinguishes them; collapsing the two into \"does this request already have\n * a name byte\" changes nothing observable.\n *\n * NOT REPLICATED: x3270's `EE_NAME_ESC` case never transitions back to `EE_NAME` after\n * consuming the escaped byte (no `state = EE_NAME;` anywhere in that arm) -- so in the\n * reference parser, ONE escape inside a name permanently disables the VAR/USERVAR\n * delimiter for the rest of the buffer, silently absorbing what should have been the\n * next request's group byte into the current name. That looks like a latent bug in\n * telnet_new_environ.c, not a behavior worth copying: RFC 1572 defines ESC as escaping\n * exactly the following byte, which is what this function does (`escaped` is cleared\n * right after consuming one literal byte).\n *\n * THIS IS NOT MERELY UNTESTED, IT IS UNREACHABLE: every NEW-ENVIRON SEND in x3270's\n * entire trace collection (`s3270/Test/*.trc`) was grepped for byte 0x02 in a name or\n * value, and none contains one -- every real name on the wire is plain ASCII (`IBMELF`,\n * `IBMAPPLID`, `DEVNAME`, `USER`, `CODEPAGE`, `CHARSET`, `KBDTYPE`). No host anyone has\n * recorded exercises an escape at all, so none can have been built to depend on the\n * sticky `EE_NAME_ESC` state -- this divergence differs from x3270 somewhere no host\n * can reach, not merely somewhere no trace happens to look.\n */\nexport function parseEnvironSend(body: Uint8Array): readonly EnvironRequest[] | null {\n const out: { group: number; name: number[] }[] = [];\n let escaped = false;\n\n for (const c of body) {\n if (escaped) {\n // ESC makes the next byte literal, whatever it is.\n out[out.length - 1]!.name.push(c);\n escaped = false;\n continue;\n }\n if (c === EnvironGroup.VAR || c === EnvironGroup.USERVAR) {\n out.push({ group: c, name: [] });\n continue;\n }\n if (out.length === 0) {\n // A name byte before any group byte: x3270's EE_BASE refuses this.\n return null;\n }\n if (c === EnvironGroup.ESC) {\n escaped = true;\n continue;\n }\n out[out.length - 1]!.name.push(c);\n }\n\n if (out.length === 0) {\n // \"Send everything.\" Both groups, in x3270's order.\n return [\n { group: EnvironGroup.VAR, name: '' },\n { group: EnvironGroup.USERVAR, name: '' },\n ];\n }\n\n // Spreading `r.name` into `fromCharCode` is fine because the caller can never hand us\n // more bytes than the whole subnegotiation held, and `telnet.ts`'s MAX_SUBNEG_BYTES\n // (1024) caps that upstream before this function ever runs -- so there is no\n // pathologically long name here to blow an engine's argument-count limit.\n return out.map((r) => ({\n group: r.group,\n name: String.fromCharCode(...r.name),\n }));\n}\n\n/**\n * Escape a name or value for the wire (RFC 1572; x3270's `escaped_copy`,\n * telnet_new_environ.c:118-129).\n *\n * Any byte equal to one of the four group codes would otherwise be read as a\n * delimiter, so it is prefixed with ESC. The receiver strips the ESC and takes the next\n * byte literally, which is what `parseEnvironSend` does.\n *\n * UNWITNESSED BY ANY HOST: every real device name in x3270's trace collection is plain\n * ASCII, so no recorded exchange exercises this. Implemented anyway, because the\n * alternative is silent corruption on the first host that does something unusual, and\n * because it round-trips against our own parser -- a real property even without a host.\n *\n * Masked to a byte BEFORE the comparison, not after: `charCodeAt` yields a UTF-16 code\n * unit, and U+01FF is 511 rather than any group code but truncates to 0xFF. The same\n * trap telnet.ts's TERMINAL-TYPE reply documents.\n *\n * ITERATES CODE POINTS (`for...of` on a string), NOT CODE UNITS, which matters only if\n * a name ever contains a surrogate pair: `for...of` would yield the whole astral code\n * point as one `ch`, and `ch.charCodeAt(0)` reads only its LEADING surrogate (0xD800-\n * 0xDBFF), truncating to some byte in 0x00-0xDB with no ESC applied even if that byte\n * happens to collide with a group code -- a silent corruption one level below the\n * documented U+01FF truncation. Not worth handling: a device name is ASCII in every\n * known case (see the module-level comment's grep of x3270's trace collection), so no\n * real input reaches this path either way, and `parseEnvironSend`'s own byte-oriented\n * un-escaping has no way to reconstruct a code point that was never sent as bytes in\n * the first place -- the mangling would need fixing on the decode side too if it ever\n * mattered, not just here.\n */\nexport function escapeEnvironBytes(s: string): number[] {\n const out: number[] = [];\n for (const ch of s) {\n const b = ch.charCodeAt(0) & 0xff;\n if (b === EnvironGroup.VAR || b === EnvironGroup.VALUE\n || b === EnvironGroup.ESC || b === EnvironGroup.USERVAR) {\n out.push(EnvironGroup.ESC);\n }\n out.push(b);\n }\n return out;\n}\n\n/**\n * Build the IS reply body for `requests`.\n *\n * Returns the body from the qualifier onward: the caller adds `IAC SB NEW-ENVIRON`\n * ahead of it and `IAC SE` after, and is responsible for IAC doubling -- see the\n * TERMINAL-TYPE reply in telnet.ts for the same division and the RFC 855 reason.\n *\n * THE MISSING-VARIABLE RULE IS THE ONE TO GET RIGHT: a name we do not have is echoed\n * with NO VALUE byte at all (x3270 appends VALUE only `if (value != NULL)`,\n * telnet_new_environ.c:561). That is distinguishable on the wire from a known variable\n * whose value is empty -- name + VALUE + nothing -- and emitting an empty VALUE for an\n * unknown name would claim we have a variable we do not.\n *\n * A WHOLE-GROUP DUMP (`name === ''`) EMITS THE GROUP BYTE PER VARIABLE, not once for\n * the group: x3270's own `name_len == 0` branch puts `vb_appendf(&reply, \"%c\",\n * ereq->group)` INSIDE the `FOREACH_LLIST` over that group's variables\n * (telnet_new_environ.c:532-537), building the same (group, name, VALUE, value) tuple\n * per entry that the single-name branch below it builds for one name. There is no\n * \"dump the group header once\" shape on the wire at all.\n *\n * A WHOLE-GROUP DUMP'S ORDER FOLLOWS THE MAP'S INSERTION ORDER, which callers should\n * treat as significant even though nothing here enforces a particular order: x3270's\n * own list is a `FOREACH_LLIST` over variables in the order `add_environ` inserted them\n * (`environ_init`, telnet_new_environ.c:220-245: USER, then DEVNAME, then IBMELF, then\n * IBMAPPLID, then CODEPAGE/CHARSET/KBDTYPE), and a host that has only ever seen that\n * order from real x3270 has no reason to expect another. `Map` iteration order is\n * insertion order per the ECMAScript spec, so a caller (Task 7) that populates the map\n * in x3270's order gets x3270's wire order for free.\n *\n * WE REQUIRE AN EXACT NAME MATCH, WHICH DIVERGES FROM x3270 DELIBERATELY. Its\n * `find_environ` compares `memcmp(name, e->name, namelen)` without checking that the\n * lengths agree (:159-175: only `namelen` bytes of the stored name are compared), so a\n * request for \"IBM\" matches its stored \"IBMELF\". That answers a question the host did\n * not ask; we do not copy it -- a plain `Map.get` already requires equality.\n */\nexport function buildEnvironIs(\n requests: readonly EnvironRequest[],\n vars: ReadonlyMap<string, string>,\n uservars: ReadonlyMap<string, string>,\n): Uint8Array {\n const out: number[] = [EnvironQual.IS];\n const emit = (group: number, name: string, value: string | undefined): void => {\n out.push(group, ...escapeEnvironBytes(name));\n if (value !== undefined) out.push(EnvironGroup.VALUE, ...escapeEnvironBytes(value));\n };\n\n for (const req of requests) {\n const list = req.group === EnvironGroup.VAR ? vars : uservars;\n if (req.name === '') {\n // The whole group, in the map's insertion order -- see the doc comment above.\n for (const [name, value] of list) emit(req.group, name, value);\n continue;\n }\n emit(req.group, req.name, list.get(req.name));\n }\n return Uint8Array.from(out);\n}\n", "import { ADDRESS_CODE_TABLE } from './constants.js';\n\n/**\n * Thrown when a datastream contains an address we must refuse \u2014 currently only\n * the reserved 10 flag combination. Callers turn this into a program check.\n */\nexport class AddressError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AddressError';\n }\n}\n\n/**\n * Decode a two-byte buffer address.\n *\n * GA23-0059-07: the top two bits of the first byte are flags.\n * 00 -> 14-bit binary address in the remaining 14 bits\n * 01 -> 12-bit coded address (low 6 bits of each byte)\n * 10 -> reserved; receipt rejects the datastream\n * 11 -> 12-bit coded address, same as 01\n */\nexport function decodeAddress(b1: number, b2: number): number {\n const flags = (b1 & 0xc0) >> 6;\n switch (flags) {\n case 0b00:\n return ((b1 & 0x3f) << 8) | (b2 & 0xff);\n case 0b10:\n throw new AddressError(\n `reserved address flag bits 10 in byte 0x${b1.toString(16).padStart(2, '0')}`,\n );\n default:\n // 01 and 11 are both 12-bit coded form.\n return ((b1 & 0x3f) << 6) | (b2 & 0x3f);\n }\n}\n\n/**\n * Encode a buffer address for output.\n *\n * Screens of 4096 cells or fewer use 12-bit coded form; larger ones use 14-bit\n * binary. x3270's ENCODE_BADDR switches on `(ROWS * COLS) > 0x1000`.\n */\nexport function encodeAddress(addr: number, bufferSize: number): Uint8Array {\n const use14Bit = bufferSize > 0x1000;\n const limit = use14Bit ? 0x4000 : 0x1000;\n if (!Number.isInteger(addr) || addr < 0 || addr >= limit) {\n throw new AddressError(`address ${addr} out of range for ${limit}-cell encoding`);\n }\n if (use14Bit) {\n return Uint8Array.of((addr >> 8) & 0x3f, addr & 0xff);\n }\n const hi = ADDRESS_CODE_TABLE[(addr >> 6) & 0x3f]!;\n const lo = ADDRESS_CODE_TABLE[addr & 0x3f]!;\n return Uint8Array.of(hi, lo);\n}\n", "/**\n * CP037 (EBCDIC US/Canada) to Unicode.\n *\n * GENERATED FILE - do not edit by hand.\n * Regenerate with: node tools/gen-cp037.mjs src/codepages/cp037.ts\n */\n\n/** EBCDIC byte -> Unicode code point. */\nexport const CP037_TO_UNICODE: readonly number[] = [\n 0x0000, 0x0001, 0x0002, 0x0003, 0x009c, 0x0009, 0x0086, 0x007f,\n 0x0097, 0x008d, 0x008e, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,\n 0x0010, 0x0011, 0x0012, 0x0013, 0x009d, 0x0085, 0x0008, 0x0087,\n 0x0018, 0x0019, 0x0092, 0x008f, 0x001c, 0x001d, 0x001e, 0x001f,\n 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000a, 0x0017, 0x001b,\n 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x0005, 0x0006, 0x0007,\n 0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,\n 0x0098, 0x0099, 0x009a, 0x009b, 0x0014, 0x0015, 0x009e, 0x001a,\n 0x0020, 0x00a0, 0x00e2, 0x00e4, 0x00e0, 0x00e1, 0x00e3, 0x00e5,\n 0x00e7, 0x00f1, 0x00a2, 0x002e, 0x003c, 0x0028, 0x002b, 0x007c,\n 0x0026, 0x00e9, 0x00ea, 0x00eb, 0x00e8, 0x00ed, 0x00ee, 0x00ef,\n 0x00ec, 0x00df, 0x0021, 0x0024, 0x002a, 0x0029, 0x003b, 0x00ac,\n 0x002d, 0x002f, 0x00c2, 0x00c4, 0x00c0, 0x00c1, 0x00c3, 0x00c5,\n 0x00c7, 0x00d1, 0x00a6, 0x002c, 0x0025, 0x005f, 0x003e, 0x003f,\n 0x00f8, 0x00c9, 0x00ca, 0x00cb, 0x00c8, 0x00cd, 0x00ce, 0x00cf,\n 0x00cc, 0x0060, 0x003a, 0x0023, 0x0040, 0x0027, 0x003d, 0x0022,\n 0x00d8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,\n 0x0068, 0x0069, 0x00ab, 0x00bb, 0x00f0, 0x00fd, 0x00fe, 0x00b1,\n 0x00b0, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070,\n 0x0071, 0x0072, 0x00aa, 0x00ba, 0x00e6, 0x00b8, 0x00c6, 0x00a4,\n 0x00b5, 0x007e, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,\n 0x0079, 0x007a, 0x00a1, 0x00bf, 0x00d0, 0x00dd, 0x00de, 0x00ae,\n 0x005e, 0x00a3, 0x00a5, 0x00b7, 0x00a9, 0x00a7, 0x00b6, 0x00bc,\n 0x00bd, 0x00be, 0x005b, 0x005d, 0x00af, 0x00a8, 0x00b4, 0x00d7,\n 0x007b, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,\n 0x0048, 0x0049, 0x00ad, 0x00f4, 0x00f6, 0x00f2, 0x00f3, 0x00f5,\n 0x007d, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050,\n 0x0051, 0x0052, 0x00b9, 0x00fb, 0x00fc, 0x00f9, 0x00fa, 0x00ff,\n 0x005c, 0x00f7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,\n 0x0059, 0x005a, 0x00b2, 0x00d4, 0x00d6, 0x00d2, 0x00d3, 0x00d5,\n 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,\n 0x0038, 0x0039, 0x00b3, 0x00db, 0x00dc, 0x00d9, 0x00da, 0x009f,\n];\n", "import { CP037_TO_UNICODE } from './codepages/cp037.js';\n\n/** EBCDIC substitute character \u2014 used for anything we cannot map. */\nconst EBCDIC_SUB = 0x3f;\n\n/**\n * A single-byte EBCDIC code page. Table-driven so that CP285, CP297, CP500 and\n * friends are data files rather than code changes.\n */\nexport class CodePage {\n readonly name: string;\n private readonly toUni: readonly number[];\n private readonly fromUni: Map<number, number>;\n\n constructor(name: string, toUnicodeTable: readonly number[]) {\n if (toUnicodeTable.length !== 256) {\n throw new Error(`code page ${name}: table must have 256 entries, got ${toUnicodeTable.length}`);\n }\n for (let b = 0; b < 256; b++) {\n const cp = toUnicodeTable[b]!;\n if (!Number.isInteger(cp) || cp < 0 || cp > 0x10ffff) {\n throw new Error(\n `code page ${name}: entry for byte 0x${b.toString(16).padStart(2, '0')} is not a valid code point: ${cp}`,\n );\n }\n }\n this.name = name;\n this.toUni = toUnicodeTable;\n this.fromUni = new Map();\n // Iterate high to low so the lowest EBCDIC byte is written last and\n // therefore wins, for any Unicode char with more than one representation.\n // Deterministic.\n for (let b = 255; b >= 0; b--) {\n this.fromUni.set(toUnicodeTable[b]!, b);\n }\n }\n\n /** EBCDIC byte to a single-character string. */\n toUnicode(byte: number): string {\n const cp = this.toUni[byte & 0xff];\n return String.fromCodePoint(cp!);\n }\n\n /** A single character to its EBCDIC byte, or the substitute if unmappable. */\n fromUnicode(char: string): number {\n const cp = char.codePointAt(0);\n if (cp === undefined) return EBCDIC_SUB;\n return this.fromUni.get(cp) ?? EBCDIC_SUB;\n }\n\n decode(bytes: Uint8Array): string {\n let out = '';\n for (const b of bytes) out += this.toUnicode(b);\n return out;\n }\n\n encode(text: string): Uint8Array {\n // Normalize to NFC first: decomposed input (e.g. macOS keyboard/paste\n // delivering 'e' + combining acute instead of precomposed 'e-acute')\n // would otherwise expand to two cells for one visible character and\n // shift every following column.\n const chars = Array.from(text.normalize('NFC'));\n const out = new Uint8Array(chars.length);\n for (let i = 0; i < chars.length; i++) out[i] = this.fromUnicode(chars[i]!);\n return out;\n }\n}\n\nexport const cp037 = new CodePage('cp037', CP037_TO_UNICODE);\n\n/** Registry, so a session can select a page by name. */\nexport const CODE_PAGES = new Map<string, CodePage>([['cp037', cp037]]);\n\nexport function getCodePage(name: string): CodePage {\n const cp = CODE_PAGES.get(name);\n if (!cp) throw new Error(`unknown code page: ${name}`);\n return cp;\n}\n", "/**\n * The 3279 color palette: sixteen architected color identifications and the\n * RGB each one renders as.\n *\n * IN CORE BECAUSE IT IS THE ARCHITECTED MEANING, not because it is what gets drawn.\n * This table answers \"which color IS code F1\" and is pinned to GA23-0059 below. What a\n * front end actually paints comes from the scheme registry in `packages/frontend/palette.ts`,\n * where this table is the `3279` scheme's data \u2014 and where the READABLE default lives, since\n * the pure `#0000ff` blue below is close to illegible on black.\n *\n * An earlier version of this comment said the TUI quantised these values and the GUI would\n * fill cells with them. The first half was false from the day the TUI shipped its own table,\n * and the resulting drift is what a user reported on 2026-09-14: two front ends, two blues.\n *\n * ## THE MANUAL'S TABLE IS OCR-DAMAGED \u2014 DO NOT TRANSCRIBE IT LITERALLY\n *\n * Table 4-7 (GA23-0059 p. 4-20, pages.txt:3524-3541) prints `X'FB'` TWICE,\n * for both Black and Purple, and renders the second Neutral (F7) as `X'F?'`.\n * The codes are in fact contiguous 0xF0-0xFF, so Black is 0xF8 and Purple\n * 0xFB \u2014 confirmed two ways: (1) the same table is reprinted, undamaged, in\n * Chapter 6's \"Query Reply (Color)\" section (p. 6-37, pages.txt:9244-9260),\n * where line 9253 reads `Black X'F8'` \u2014 though that reprint went through the\n * same OCR pipeline, so it corroborates rather than independently confirms;\n * and (2) x3270's include/3270ds.h:313-328, whose HOST_COLOR_* run 0..15 in\n * the same order (HOST_COLOR_BLACK == 8, HOST_COLOR_PURPLE == 11), which\n * *is* an independent source. palette.test.ts pins both of the damaged\n * entries.\n *\n * ## THE RGB VALUES ARE OUR OWN CHOICE, DELIBERATELY NOT X3270'S\n *\n * x3270's own default 3279 rendering (c3270/screen.c:213-229, `rgbmap[16]`)\n * uses muted, named-CSS-ish colors: e.g. blue is `0x1e90ff` (dodger blue),\n * turquoise is `0x00ffff`, black is `0x2f4f4f` (dark slate grey \u2014 x3270's own\n * comment there reads \"alas, this may be gray\"). Measured against a standard\n * 16-color ANSI palette, x3270's blue and turquoise both quantise to the\n * same slot, collapsing two of the seven base colors into one. Task 10\n * (terminal quantisation) depends on all seven base colors staying visually\n * distinct at both 16 and 256 colors, so this table instead uses saturated\n * primaries/secondaries (pure red, green, blue, cyan, magenta, yellow, plus\n * black and white) that survive quantisation at both depths. These are a\n * presentation choice, not architecture: the manual specifies which color\n * each code IS, not its exact chromaticity, and a real 3279's phosphors\n * matched none of these precisely \u2014 ours or x3270's.\n *\n * All sixteen RGB triples are pairwise distinct (palette.test.ts), so no two\n * architecturally-different color identifications alias to the same pixel.\n */\n\n/** The seven base 3279 colors, by architected code. Table 4-7. */\nexport const Color = {\n NEUTRAL_BLACK: 0xf0,\n BLUE: 0xf1,\n RED: 0xf2,\n PINK: 0xf3,\n GREEN: 0xf4,\n TURQUOISE: 0xf5,\n YELLOW: 0xf6,\n NEUTRAL_WHITE: 0xf7,\n BLACK: 0xf8,\n DEEP_BLUE: 0xf9,\n ORANGE: 0xfa,\n PURPLE: 0xfb,\n PALE_GREEN: 0xfc,\n PALE_TURQUOISE: 0xfd,\n GREY: 0xfe,\n WHITE: 0xff,\n} as const;\n\n/** A 3279 color identification, 0xF0-0xFF. */\nexport type Color3279 = number;\n\n/**\n * `GREY` AND `'grey'` KEEP THE BRITISH SPELLING DELIBERATELY. DO NOT \"FINISH\" THE\n * AMERICANISATION HERE.\n *\n * Everything else in this repo was moved to US spellings on 2026-09-30, which leaves these two\n * looking like something the sweep missed. They are not: **`grey` is x3270's own canonical\n * spelling**, measured in its source rather than assumed \u2014\n *\n * - `Common/glue.c:1041-1042`: `{ \"Grey\", HOST_COLOR_GREY }` and then\n * `{ \"Gray\", HOST_COLOR_GREY }, /* alias *\\/` \u2014 x3270's own comment marks which is which.\n * - `Common/see.c:310` and `Common/fprint_screen.c:118` both emit `grey`.\n *\n * These names are an INTERFACE to the reference implementation, not prose: this project is\n * conformance-tested against real s3270, and a name that appears in a trace or a comparison has\n * to match what x3270 writes. Americanising it would be a gratuitous divergence in the one place\n * where matching the reference is the whole point. The same reasoning is why the wire constants\n * elsewhere are never \"tidied\" \u2014 see `docs/live-testing.md` on conformance.\n */\nexport const COLOR_NAMES: Readonly<Record<number, string>> = Object.freeze({\n 0xf0: 'neutral-black',\n 0xf1: 'blue',\n 0xf2: 'red',\n 0xf3: 'pink',\n 0xf4: 'green',\n 0xf5: 'turquoise',\n 0xf6: 'yellow',\n 0xf7: 'neutral-white',\n 0xf8: 'black',\n 0xf9: 'deep-blue',\n 0xfa: 'orange',\n 0xfb: 'purple',\n 0xfc: 'pale-green',\n 0xfd: 'pale-turquoise',\n // x3270's spelling, not a missed rename. See the note above.\n 0xfe: 'grey',\n 0xff: 'white',\n});\n\nexport type Rgb = readonly [number, number, number];\n\nexport const PALETTE_3279: Readonly<Record<number, Rgb>> = Object.freeze({\n // Neutral black/white and Black/White are architecturally distinct codes\n // (a host can choose either), so they get distinct RGB despite both being\n // \"black-ish\" or \"white-ish\" -- see the OCR-damage note above.\n 0xf0: [0x1a, 0x1a, 0x1a], // neutral-black: near-black, not pure black\n 0xf1: [0x00, 0x00, 0xff],\n 0xf2: [0xff, 0x00, 0x00],\n 0xf3: [0xff, 0x00, 0xff],\n 0xf4: [0x00, 0xff, 0x00],\n 0xf5: [0x00, 0xff, 0xff],\n 0xf6: [0xff, 0xff, 0x00],\n 0xf7: [0xe0, 0xe0, 0xe0], // neutral-white: near-white, not pure white\n 0xf8: [0x00, 0x00, 0x00], // black: pure black\n 0xf9: [0x00, 0x00, 0x80],\n 0xfa: [0xff, 0x80, 0x00],\n 0xfb: [0x80, 0x00, 0xff],\n 0xfc: [0x80, 0xff, 0x80],\n 0xfd: [0x80, 0xff, 0xff],\n 0xfe: [0x80, 0x80, 0x80],\n 0xff: [0xff, 0xff, 0xff], // white: pure white\n});\n\n/** RGB for a color identification. Throws rather than guessing. */\nexport function colorRgb(code: Color3279): Rgb {\n const rgb = PALETTE_3279[code];\n if (rgb === undefined) {\n throw new RangeError(`0x${code.toString(16)} is not a 3279 color (expected 0xF0-0xFF)`);\n }\n return rgb;\n}\n", "/**\n * Resolve protocol attributes into concrete colors for a renderer.\n *\n * ## WHY THIS IS IN CORE AND NOT IN EACH FRONT END\n *\n * Storage says what the host sent. This says what color a cell IS, and its\n * rules are datastream semantics rather than rendering taste:\n *\n * - A character with no attribute of its own falls back to its FIELD's\n * extended attribute before reaching the base map (pages.txt:3383-3387).\n * - A color VALUE of 0x00 means \"the device default indicated in Query Reply\n * (Color)\" (GA23-0059 p. 4-20, pages.txt:3544-3546) -- NOT black.\n * - 0xF7 is the Neutral color identification, and means \"the color comes\n * from a triple-plane character set\"; with a single-plane or nonloadable set\n * it takes the single color Query Reply (Color) gives for F7\n * (pages.txt:3546-3549). Ours is the identity, so F7 stays F7 -- see the\n * note on that below, which corrects an error in the plan.\n *\n * Reimplemented in the TUI, the GUI and a web front end, those would diverge\n * three ways. So they live here, once, and every front end consumes\n * `ResolvedCell[]`.\n *\n * This module is PURE: no I/O, no Session, no terminal. It takes a snapshot and\n * returns an array. That is what makes every rule above independently testable.\n *\n * ## THE PRECEDENCE, WHICH IS THE WHOLE POINT\n *\n * For each cell, in order:\n *\n * 1. the CHARACTER's own extended attribute (`cell.fg`), if usable\n * 2. the FIELD's extended attribute, read from the cell at the governing\n * field's `attrAddr` -- where `execute.ts` stores an SFE's pairs\n * 3. the base-attribute map, `defaultColor` below\n * 4. `mode3279 === false` overrides all three with green\n *\n * Level 2 is the manual's conflict-resolution rule: \"If there are field\n * attributes in the character buffer and if a character attribute specifies\n * default for any character property (color, highlighting, or character set),\n * the character is displayed using the value of that property established for\n * the field in the extended field attribute. Otherwise, the character attribute\n * overrides the field attribute\" (p. 4-16, pages.txt:3383-3387).\n *\n * x3270 does exactly this two-step, per property, in both of its renderers:\n *\n * if (ea_buf[baddr].fg) { ix = ea_buf[baddr].fg & 0x0f; ... }\n * else if (ea_buf[fa_addr].fg) { ix = ea_buf[fa_addr].fg & 0x0f; ... }\n * else { fg = default_color_from_fa(fa); }\n *\n * (`c3270/screen.c:1139-1150`, with the same shape for bg at :1153-1158 and for\n * gr at :1166-1171; the print path is `fprint_screen.c:754-758` falling back to\n * the `fa_fg` it set from the FA cell at :581-585.) Note that the fallback\n * covers BACKGROUND AND HIGHLIGHTING TOO, not just foreground -- the manual says\n * \"any character property\", and treating it as foreground-only would leave an\n * SFE's reverse-video field flat.\n *\n * The unformatted case is the manual's other bullet: \"If there are no field\n * attributes in the character buffer, that is, the buffer is unformatted, then\n * the character attributes are always used ... If a character attribute\n * specifies default for a particular property, the device default for that\n * property is used\" (pages.txt:3378-3382). Level 2 is vacuous there (no field)\n * and level 3 with an attribute of 0x00 gives green, which IS our device\n * default -- the same value x3270 reports as CAV 0x00 in Query Reply (Color)\n * (`sf.c` `do_qr_color`: `*obptr++ = 0xf0 + HOST_COLOR_GREEN`).\n */\n\nimport { FA, XAH } from './constants.js';\n// CodePage is a CLASS, not an interface, so it is a plain import. Its\n// byte-to-string method is `toUnicode(byte)`.\nimport { cp037, CodePage } from './codepage.js';\nimport { Color, PALETTE_3279, type Color3279 } from './palette.js';\nimport type { ScreenSnapshot } from './screen.js';\n\n/**\n * One cell, ready to draw. Every front end consumes an array of these.\n *\n * ## WHAT THIS DELIBERATELY DOES NOT CARRY\n *\n * GEOMETRY. `resolve` reads `snap.cells` and `snap.fields` and ignores `rows`,\n * `cols` and `cursor`, so a `ResolvedCell[]` is a flat array with no shape: a\n * consumer that needs to know where row 2 starts, or where the cursor is, must\n * keep the snapshot alongside it. That is why `resolve` returns a bare array\n * rather than a wrapper -- the snapshot is already the authority on geometry and\n * duplicating it here would give two things to keep in step.\n *\n * ## FOUR BOOLEANS RATHER THAN ONE TAGGED HIGHLIGHTING VALUE\n *\n * `blink`/`reverse`/`underscore`/`intensify` are mutually exclusive on the wire\n * -- highlighting is \"exclusive ... one and only one of the highlight values can\n * be applied\" (pages.txt:10326-10328) -- so a single `highlight: 'blink' | ...`\n * would model the protocol more tightly and give each front end an\n * exhaustiveness-checked switch instead of four branches.\n *\n * Four booleans anyway, for two reasons. First, the exclusivity is a fact about\n * the WIRE, not about rendering: `intensify` is already documented below as\n * something a renderer may want to OR with its own `FA_IS_HIGH` test, the way\n * x3270 does, and a tagged value makes that combination unrepresentable. A\n * canvas or CSS back end likewise sets bold, inversion and underline as\n * independent switches. Second, the flags are what a renderer needs at the point\n * of use; reconstructing them from a tag is work every front end would repeat.\n *\n * The cost is real and accepted: nothing in the type stops a future edit setting\n * two at once. `render.test.ts` pins the exclusivity instead (\"sets ONLY the flag\n * for the value given\"), which is where the invariant lives.\n */\nexport interface ResolvedCell {\n /**\n * What to draw. A space for nulls and for field-attribute positions.\n *\n * STILL THE REAL CHARACTER WHEN `hidden` IS SET -- this is not pre-redacted,\n * and a front end that draws `text` without checking `hidden` puts a password\n * on screen and into the terminal's scrollback. Suppression is left to the\n * renderer because only it knows what to draw instead (a space, a bullet, a\n * styled run), and because the inbound path must still transmit the real data:\n * a non-display field's \"attribute values ... are transmitted inbound\"\n * (pages.txt:3464-3467). Task 11's renderer has the corresponding test.\n */\n text: string;\n /** Concrete 3279 color identification. Never undefined, never invalid. */\n fg: Color3279;\n bg: Color3279;\n blink: boolean;\n reverse: boolean;\n underscore: boolean;\n /**\n * Highlighting 0xF8 ALONE. Deliberately NOT the base attribute's intensified\n * bit: \"A highlighting property specified by the extended field attribute does\n * not affect the intensify property specified by the field attribute\"\n * (pages.txt:3474-3475), and an intensified FIELD is already carried here as\n * color, by `defaultColor` returning red or white.\n *\n * x3270's equivalent is wider -- `(gr & GR_INTENSIFY) || FA_IS_HIGH(fa)`\n * (c3270/screen.c:1183) -- because curses gives it A_BOLD as its only\n * brightness lever and it uses bold for both. We have real color, so folding\n * the two would make an intensified field bold AND red, double-signaling one\n * protocol fact. A renderer wanting x3270's look can OR in its own test.\n */\n intensify: boolean;\n /**\n * Field intensity 0x0C: a renderer must not draw the text at all. See the\n * warning on `text` above -- this flag is the ONLY thing standing between a\n * password field and the screen.\n */\n hidden: boolean;\n}\n\nexport interface ResolveOptions {\n /**\n * Is this a color device? Defaults to true.\n *\n * When false EVERY cell is green regardless of what the host sent, which is\n * x3270's behavior (`color_from_fa` returns HOST_COLOR_GREEN unconditionally,\n * fprint_screen.c:90-94). A 3278 is monochrome hardware and must not be\n * colorised just because a host sent an attribute it should not have.\n *\n * Applies to BACKGROUND as well as foreground, and x3270 is the reason: its\n * whole color block is inside `if (mode3279 || ...)` (c3270/screen.c:1126),\n * so a mono device reaches neither `ea_buf[baddr].fg` nor `.bg`. Coloring the\n * background of a green-only screen would be worse than useless -- green on\n * blue is what a host meant for a color terminal.\n */\n mode3279?: boolean;\n codePage?: CodePage;\n}\n\n/**\n * The 3279 default color map: which color a cell takes from its base field\n * attribute when neither it nor its field specifies one.\n *\n * x3270's `field_colors[4]` with its `DEFCOLOR_MAP` index\n * (fprint_screen.c:81-88): bit 1 is PROTECT, bit 0 is INT_HIGH_SEL.\n *\n * NO `mode3279` PARAMETER, unlike x3270's `color_from_fa`, which takes the gate\n * inside itself (fprint_screen.c:90-94) because it is the only color path its\n * print renderer has. Here the gate has to sit at the call site instead: it\n * overrides levels 1 and 2 as well, and those never reach this function. An\n * earlier version had the check in BOTH places, so deleting the one here changed\n * nothing and no test could tell \u2014 mutation testing found it. One gate, one\n * place, and `mode3279: false` is now the only thing that can produce green\n * without consulting this table.\n */\nconst DEFAULT_COLORS: readonly Color3279[] = [\n Color.GREEN, // unprotected, normal\n Color.RED, // unprotected, intensified\n Color.BLUE, // protected, normal\n Color.WHITE, // protected, intensified\n];\n\nfunction defaultColor(attr: number): Color3279 {\n // A BIT TEST on FA.INT_HIGH_SEL, not a field comparison against FA.INTENSITY,\n // matching x3270's DEFCOLOR_MAP `(f) & FA_INT_HIGH_SEL) >> 3`\n // (fprint_screen.c:86-87). The two differ for exactly one attribute: intensity\n // 0x0C (hidden) has bit 0x08 set, so THIS classifies a hidden field as\n // intensified and returns red, where `Screen.intensified` (screen.ts:309) and\n // `hidden` below use `=== FA.INTENSITY` comparisons and call it hidden, not\n // intensified. Both are right for their own purpose and the divergence is\n // deliberate: the color a hidden field would have had is moot, because\n // `hidden` tells the renderer not to draw its text at all. x3270 has the same\n // pair of idioms side by side -- FA_IS_HIGH is a field comparison\n // (3270ds.h:211-212) while FA_IS_INTENSE is a bit test (3270ds.h:225-226).\n const index = ((attr & FA.PROTECT) !== 0 ? 2 : 0) | ((attr & FA.INT_HIGH_SEL) !== 0 ? 1 : 0);\n return DEFAULT_COLORS[index]!;\n}\n\n/**\n * Is `code` a color we can actually render?\n *\n * 0x00 is excluded deliberately: it is legal on the wire and means \"device\n * default\", so it must fall through to the NEXT LEVEL rather than being treated\n * as a value. An unrecognized byte falls through the same way -- a malformed\n * attribute from a host must never reach `colorRgb`, which throws.\n *\n * THE 0x00 CHECK IS UNREACHABLE FROM ANY `Screen`-DERIVED SNAPSHOT, AND KEPT ON\n * PURPOSE. `Screen` stores \"unspecified\" as the byte 0 and `cellAt` omits the\n * property when it is zero (see the `fgs`/`bgs`/`grs` comment in screen.ts), so\n * every value that reaches here through a real `Screen` is either absent or\n * non-zero -- an explicit X'00' from an SA order arrives as `undefined`, caught by\n * the clause before this one.\n *\n * Two reasons it stays. First, `resolve` is an EXPORTED PURE FUNCTION of a\n * `ScreenSnapshot`, so a hand-built snapshot is a legitimate input and can carry\n * an explicit zero; the branch is reachable across the module's actual contract,\n * just not through today's only producer. Second, this clause and the palette\n * lookup below encode GENUINELY DIFFERENT RULES that merely coincide: this is the\n * architected meaning of X'00', \"the device default color indicated in the Query\n * Reply (Color) structured field\" (pages.txt:3544-3546), where the lookup means\n * only \"not a byte we can render\". Were `PALETTE_3279` ever to gain a 0x00 entry\n * -- a device-default swatch is an entirely plausible change -- the protocol rule\n * would silently invert into \"0x00 paints that swatch\", overriding a field color\n * the host did set.\n *\n * `render.test.ts` pins all of this through hand-built snapshots, and separately\n * pins the `Screen` collapse itself so the reason those tests bypass `Screen`\n * cannot rot. An earlier version of that block asserted the rule only through\n * `Screen`, which made every assertion true but VACUOUS: instrumentation showed\n * this branch taking zero hits, and mutating it to return black -- the exact error\n * the manual forbids -- left the whole file green.\n *\n * Note this returns a value rather than a boolean, so callers can chain levels\n * with `??` and cannot accidentally use an unusable code.\n *\n * 0xF7 IS USABLE AND IS RETURNED UNCHANGED. The plan drafted `cell.fg === 0xf7 ?\n * Color.WHITE : cell.fg`, and that is wrong twice over. (1) 0xF7 is itself an\n * architected color identification, Neutral, distinct from White 0xFF -- Table\n * 4-7 lists both (pages.txt:3527-3541), palette.ts gives them distinct RGB on\n * purpose, and x3270 keeps them as separate slots HOST_COLOR_NEUTRAL_WHITE (7)\n * and HOST_COLOR_WHITE (15) (3270ds.h:313-328). (2) The rule the draft was\n * reaching for -- \"If a single-plane or nonloadable character set is referenced,\n * the color defaults to the single color specified for the X'F7' value by Query\n * Reply (Color)\" (pages.txt:3547-3549) -- resolves through OUR Query Reply\n * (Color), whose F7 entry is the identity pair F7->F7, exactly as x3270 sends\n * (`sf.c` `do_qr_color`, and Task 7's unit). So the correct single color for F7\n * is F7. The manual's \"defined as White for a display\" (pages.txt:3542-3543)\n * describes what that phosphor LOOKS like, and is not an instruction to\n * substitute the other code. x3270 special-cases F7 nowhere.\n */\nfunction usableColor(code: number | undefined): Color3279 | undefined {\n if (code === undefined || code === 0x00) return undefined;\n return PALETTE_3279[code] !== undefined ? code : undefined;\n}\n\n/**\n * The highlighting values that name an actual highlight, for `usableHighlight`.\n *\n * The architecture defines SIX valid values and this set holds FIVE. From Query\n * Reply (Highlighting), which is the source that lists them all: \"the following\n * attribute values are the only valid values: X'00' X'F0' X'F1' X'F2' X'F4'\n * X'F8'\" (pages.txt:10313-10325). Chapter 4's table omits X'F8' entirely\n * (pages.txt:3485-3498) -- see the TRAP note on `XAH` in constants.ts.\n *\n * X'00' is the omitted one, and it is omitted because it is a FALL-THROUGH rather\n * than a highlight: \"the default action of the device\" (pages.txt:10329-10331).\n * It is valid on the wire and rejected here, which is exactly the split\n * `PALETTE_3279` makes for colors -- 0x00 is a legal color value and not a\n * palette key.\n *\n * NOTE X'F0' Normal IS a member, and that is not an oversight: \"Normal (as\n * determined by the 3270 field attribute)\" (pages.txt:3489-3495) is a positive\n * instruction to show no extended highlighting, so a character set to Normal must\n * OVERRIDE a reverse-video field rather than inherit from it. The difference\n * between F0 and 00 is the whole reason this set exists instead of a range check;\n * `render.test.ts` pins both directions.\n */\nconst HIGHLIGHTS: ReadonlySet<number> = new Set([\n XAH.NORMAL, XAH.BLINK, XAH.REVERSE, XAH.UNDERSCORE, XAH.INTENSIFY,\n]);\n\n/**\n * Is `code` a highlighting value we can act on? The exact analogue of\n * `usableColor`, and deliberately so: all three properties then read as ONE\n * rule and the `??` chain in `resolve` is uniform across them.\n *\n * IT DID NOT USE TO BE, and the divergence was reachable rather than academic.\n * This line was once `(cell.gr ?? 0x00) !== 0x00 ? cell.gr! : ...`, which gates\n * level 1 on NON-ZERO where fg and bg gate it on RENDERABLE. So a garbage byte\n * behaved oppositely one property apart: a cell carrying gr = 0x99 inside a\n * reverse-video field came out with reverse SUPPRESSED, while a cell carrying\n * fg = 0x99 inside a yellow field correctly fell THROUGH to yellow. Same\n * malformed host byte, two different policies.\n *\n * The manual makes falling through the right one, and says so for values as well\n * as types: \"Attribute types and values that are unknown or cannot be maintained\n * and returned inbound by an implementation are rejected. All attribute types and\n * values are checked for validity\" (p. 4-5, pages.txt:2897-2899). A rejected\n * value is one the device never established, so the field's highlighting still\n * stands -- exactly as a rejected color leaves the field's color standing.\n *\n * `XAH.DEFAULT` (0x00) is excluded for the same reason `usableColor` excludes\n * 0x00: it means \"the default action of the device\" (pages.txt:10329-10331), a\n * fall-through rather than a value. And for the same reason as there, THAT CLAUSE\n * IS UNKILLABLE BY TEST TODAY -- `XAH.DEFAULT` is not a member of `HIGHLIGHTS`\n * either, so deleting it changes no behavior, and mutation testing confirmed\n * every test still passes. It is kept on the same grounds: the two lines encode\n * different rules that merely coincide, one architectural (\"X'00' means device\n * default\") and one representational (\"not a highlight we act on\"). Were X'00'\n * ever added to `HIGHLIGHTS` -- and it IS one of the six architecturally valid\n * values, so a future reader completing the set from the manual is a realistic\n * mistake -- this clause is what would stop it overriding a field's highlighting\n * with nothing.\n *\n * WE VALIDATE WHERE X3270 MASKS, and the difference is representational, not a\n * disagreement. `ctlr_add_gr` stores whatever it is handed without checking\n * (ctlr.c), because its SA path has already reduced the byte to a low nibble --\n * `ctlr_add_gr(buffer_addr, *cp & 0x0f)` (ctlr.c:1785) -- to fit its compressed\n * `GR_*` bit field (GR_BLINK 0x01 ... GR_INTENSIFY 0x08, globals.h:375-378).\n * That mask silently turns 0x99 into 0x09, i.e. blink|intensify: two highlights\n * at once, which the architecture forbids on an \"exclusive basis\"\n * (pages.txt:10326-10328). We keep the architected value verbatim, so we must\n * check membership instead. Note x3270 DOES validate the analogous color case\n * at store time, `if ((color & 0xf0) != 0xf0) color = 0` in `ctlr_add_fg` -- so\n * rejecting a malformed value is its instinct too, just at a different layer.\n */\nfunction usableHighlight(code: number | undefined): number | undefined {\n if (code === undefined || code === XAH.DEFAULT) return undefined;\n return HIGHLIGHTS.has(code) ? code : undefined;\n}\n\nexport function resolve(snap: ScreenSnapshot, opts: ResolveOptions = {}): ResolvedCell[] {\n const mode3279 = opts.mode3279 ?? true;\n const codePage = opts.codePage ?? cp037;\n const size = snap.cells.length;\n\n // Which field governs each cell, precomputed as two parallel arrays: the\n // ADDRESS of that field's attribute byte, and the attribute VALUE. Both are\n // needed and neither substitutes for the other -- level 2 reads the extended\n // attributes stored ON the attribute cell, level 3 reads the attribute bits.\n //\n // Built by walking the field list, NOT by calling `fieldAt` per cell:\n // `fieldAt` scans backwards for an attribute and so is O(size) each, making a\n // full sweep O(size^2) -- ~3.7M operations on a 1920-cell buffer, on every\n // host record. The fields partition the ring, so this walk visits each cell\n // exactly once: O(size + fields) overall. Same reasoning as\n // `Screen.forEachCellWithField` and x3270's `current_fa` tracking\n // (ctlr.c:1809-1816).\n //\n // `Int32Array` for the addresses because it is the natural width for a signed\n // index, NOT because Int16Array would overflow -- two earlier versions of this\n // comment claimed a size limit and both were wrong. The largest architected\n // model is 27x132 = 3564 cells (Model 5); the 43-row figure belongs to a device\n // that can do 43 rows OR 132 columns \"but not concurrently\"\n // (pages.txt:11924-11928), so 43x132 is not a geometry any host can drive.\n // Either way it is orders of magnitude inside Int16Array's 32767, and\n // `Screen.attrs` is itself an Int16Array carrying the same -1 sentinel\n // (screen.ts:103) -- so a size-limit argument here would imply a latent bug in\n // screen.ts that does not exist. Int16Array would work; Int32Array is simply\n // the default integer width and costs a few KB on a per-record redraw.\n // `attrOf` holds a byte, so Uint8Array suffices, and\n // 0x00 is a legitimate attribute value (x3270's `START_FIELD(0)` for an SFE\n // with no 0xC0 pair, ctlr.c:1883-1885) -- which is why \"no field\" is signaled\n // by `attrAddrOf[i] < 0` and never by `attrOf[i] === 0`.\n const attrAddrOf = new Int32Array(size).fill(-1);\n const attrOf = new Uint8Array(size);\n for (const f of snap.fields) {\n // From the attribute byte through the field's data: `length + 1` cells, so\n // the FA cell is governed by its own field. That is what makes the FA\n // position resolve to its own extended color, matching x3270's\n // `calc_attrs(baddr, baddr, fa)` (c3270/screen.c:1451). It also makes the\n // wrap-around case fall out: the last field's run continues past the end of\n // the buffer to cell 0, so cells before the first attribute are owned by it.\n //\n // No overlap check, and none is needed: `Screen.makeField` computes `length`\n // by scanning forward to the next attribute, so the runs TILE the ring\n // exactly -- every cell is claimed once and only once. Adding an\n // `if (attrAddrOf[a] < 0)` guard is therefore a no-op, which mutation testing\n // confirmed by leaving every test passing; a brute-force check over 400\n // random layouts (768,000 assignments) found zero double-claims. Unconditional\n // assignment is the honest expression of a partition.\n let a = f.attrAddr;\n for (let n = 0; n <= f.length; n++) {\n attrAddrOf[a] = f.attrAddr;\n attrOf[a] = f.attr;\n a = a + 1 === size ? 0 : a + 1;\n }\n }\n\n // NO SEPARATE SET OF ATTRIBUTE POSITIONS, because the walk above already\n // answers the question. A cell is a field attribute exactly when the field\n // governing it is its own: `attrAddrOf[i] === i`. That follows from the tiling\n // argument -- each field claims `attrAddr` first and then only cells after it,\n // so no other field can write its own address into another cell's slot.\n //\n // An earlier version built `new Set(snap.fields.map(f => f.attrAddr))` and\n // justified it against `snap.fields.some(...)` per cell, which is O(fields) per\n // cell and genuinely bad. But that was arguing against the wrong alternative:\n // the array it needed had already been built one statement earlier. One\n // comparison beats a Set probe, and there is one less structure to keep\n // consistent with the walk.\n\n const out: ResolvedCell[] = new Array(size);\n for (let i = 0; i < size; i++) {\n const cell = snap.cells[i]!;\n const attrAddr = attrAddrOf[i]!;\n // An unformatted buffer has no field: attribute 0x00 (unprotected, normal)\n // is what the base map should see, and `field` stays undefined so level 2 is\n // skipped. x3270 reaches the same place with its `fa` initialized to 0.\n const field = attrAddr >= 0 ? snap.cells[attrAddr]! : undefined;\n const attr = attrAddr >= 0 ? attrOf[i]! : 0x00;\n\n // The four levels, per property. `usableColor` returning undefined for both\n // 0x00 and a malformed byte is what makes `??` express the fall-through.\n const fg = mode3279\n ? usableColor(cell.fg) ?? usableColor(field?.fg) ?? defaultColor(attr)\n : Color.GREEN;\n const bg = mode3279\n ? usableColor(cell.bg) ?? usableColor(field?.bg) ?? Color.NEUTRAL_BLACK\n : Color.NEUTRAL_BLACK;\n\n // Highlighting takes the same two levels through the same idiom, and is NOT\n // gated on mode3279 -- blink, reverse and underscore are things a monochrome\n // 3278 does, and x3270 computes gr outside its color block\n // (c3270/screen.c:1166, after the `if (!mode3279 || ...)` branch closes).\n const gr = usableHighlight(cell.gr) ?? usableHighlight(field?.gr) ?? XAH.DEFAULT;\n\n out[i] = {\n // A field attribute position \"displays as a blank\" and holds no character\n // (Screen.setFieldAttribute nulls it), and a null is a space.\n text: attrAddr === i || cell.ebcdic === 0x00 ? ' ' : codePage.toUnicode(cell.ebcdic),\n fg,\n bg,\n // Equality, not a bit test: highlighting is a one-of VALUE (\"the field can\n // have only one highlighting property ... such as blink or reverse video\n // but not both\", pages.txt:3472-3473), unlike x3270's internal GR_* bit\n // field, which is its own compressed form of the same one-of.\n blink: gr === XAH.BLINK,\n reverse: gr === XAH.REVERSE,\n underscore: gr === XAH.UNDERSCORE,\n intensify: gr === XAH.INTENSIFY,\n hidden: (attr & FA.INTENSITY) === FA.INT_ZERO_NSEL,\n };\n }\n return out;\n}\n", "/**\n * Byte-level session trace.\n *\n * Two jobs: let a human see what went over the wire, and produce a file that\n * can be replayed as a test fixture. The format is deliberately plain text so\n * it diffs and greps:\n *\n * <elapsed> <dir> <hex bytes...> [# annotation]\n *\n * where dir is '<' for received, '>' for sent, '=' for a note, or '+' for a\n * continuation of the previous line's byte run (used when a single recv()/\n * send() call is wrapped across more than one line). Timestamps are seconds\n * since the first event, so two traces of the same session compare cleanly\n * regardless of wall clock.\n */\n\nexport type TraceDir = 'recv' | 'send';\n\nexport interface TraceOptions {\n enabled?: boolean;\n /** Injectable for tests; defaults to Date.now. */\n clock?: () => number;\n /** Called with each finished line \u2014 wire this to a file stream. */\n sink?: (line: string) => void;\n}\n\nconst BYTES_PER_LINE = 16;\n\n/** Newlines in user-supplied text could otherwise forge a fake trace line. */\nfunction sanitizeText(text: string): string {\n return text.replace(/[\\r\\n]+/g, ' ');\n}\n\nexport class Trace {\n private enabled: boolean;\n private readonly clock: () => number;\n private readonly sink: ((line: string) => void) | undefined;\n private readonly buffered: string[] = [];\n private start: number | undefined;\n\n constructor(opts: TraceOptions = {}) {\n this.enabled = opts.enabled ?? false;\n this.clock = opts.clock ?? (() => Date.now());\n this.sink = opts.sink;\n }\n\n setEnabled(on: boolean): void {\n this.enabled = on;\n }\n\n isEnabled(): boolean {\n return this.enabled;\n }\n\n recv(bytes: Uint8Array, annotation?: string): void {\n this.emitBytes('<', bytes, annotation);\n }\n\n send(bytes: Uint8Array, annotation?: string): void {\n this.emitBytes('>', bytes, annotation);\n }\n\n /** A message with no bytes \u2014 negotiation milestones, program checks, etc. */\n note(text: string): void {\n if (!this.enabled) return;\n this.emit(`${this.stamp()} = # ${sanitizeText(text)}`);\n }\n\n /** A snapshot of the lines recorded so far. Mutating it does not affect the trace. */\n lines(): readonly string[] {\n return [...this.buffered];\n }\n\n toText(): string {\n return this.buffered.join('\\n');\n }\n\n private emitBytes(marker: '<' | '>', bytes: Uint8Array, annotation?: string): void {\n if (!this.enabled) return;\n if (bytes.length === 0) return;\n const stamp = this.stamp();\n const safeAnnotation = annotation !== undefined ? sanitizeText(annotation) : undefined;\n for (let off = 0; off < bytes.length; off += BYTES_PER_LINE) {\n const chunk = bytes.subarray(off, off + BYTES_PER_LINE);\n const hex = Array.from(chunk, (b) => b.toString(16).padStart(2, '0')).join(' ');\n const isFirst = off === 0;\n const isLast = off + BYTES_PER_LINE >= bytes.length;\n const lineMarker = isFirst ? marker : '+';\n const suffix = isLast && safeAnnotation ? ` # ${safeAnnotation}` : '';\n this.emit(`${stamp} ${lineMarker} ${hex}${suffix}`);\n }\n }\n\n private emit(line: string): void {\n // A sink means the caller owns persistence (e.g. a file stream); retaining\n // every line here too would mean a long session holds its entire trace in\n // memory regardless. Without a sink, buffering is the only way lines() and\n // toText() can ever return anything, so we keep it in that case.\n if (this.sink) {\n this.sink(line);\n } else {\n this.buffered.push(line);\n }\n }\n\n private stamp(): string {\n const now = this.clock();\n this.start ??= now;\n // Clamp at 0: a clock that steps backward (e.g. an NTP correction on a\n // non-monotonic clock) must not produce a negative elapsed time, which\n // parseTrace's line regex would fail to match and silently drop.\n const elapsed = Math.max(0, (now - this.start) / 1000);\n return elapsed.toFixed(3);\n }\n}\n\nexport interface TraceEvent {\n dir: TraceDir;\n bytes: Uint8Array;\n}\n\nconst HEX_BYTE = /^[0-9a-fA-F]{1,2}$/;\n\n/** Parses one line's whitespace-separated hex tokens, validating each. */\nfunction parseHexPayload(payload: string, lineNo: number, rawLine: string): Uint8Array {\n if (payload === '') return new Uint8Array(0);\n const tokens = payload.split(/\\s+/);\n const bytes = new Uint8Array(tokens.length);\n for (let i = 0; i < tokens.length; i++) {\n const tok = tokens[i]!;\n if (!HEX_BYTE.test(tok)) {\n throw new Error(\n `parseTrace: invalid hex byte \"${tok}\" on line ${lineNo}: ${rawLine}`,\n );\n }\n bytes[i] = Number.parseInt(tok, 16);\n }\n return bytes;\n}\n\nfunction concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n}\n\n/**\n * Parse a trace back into byte runs, for Replay(). Notes and comments are\n * dropped \u2014 they are commentary, not protocol. A '+' continuation line is\n * merged into the byte run of the event immediately before it, so a run that\n * Trace wrapped across several 16-byte lines round-trips as the single event\n * it originally was. This matters: Tasks 15-17 replay fixtures to test the\n * framer against real chunk boundaries, and a wrapped run merged back into one\n * event is what makes that possible.\n */\nexport function parseTrace(text: string): TraceEvent[] {\n const events: TraceEvent[] = [];\n const lines = text.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i]!;\n const line = raw.trim();\n const m = /^([0-9.]+)\\s+([<>=+])\\s*(.*)$/.exec(line);\n if (!m) continue;\n const lineNo = i + 1;\n const marker = m[2]!;\n if (marker === '=') continue;\n const payload = m[3]!.replace(/\\s*#.*$/, '').trim();\n const bytes = parseHexPayload(payload, lineNo, raw);\n if (bytes.length === 0) continue;\n\n if (marker === '+') {\n const prev = events[events.length - 1];\n if (!prev) {\n throw new Error(\n `parseTrace: continuation line ${lineNo} with no preceding event: ${raw}`,\n );\n }\n prev.bytes = concatBytes(prev.bytes, bytes);\n } else {\n events.push({ dir: marker === '<' ? 'recv' : 'send', bytes });\n }\n }\n return events;\n}\n", "import { TelnetCmd as T, TelnetOpt as O, TelnetSubopt as S, EnvironQual, TERMINAL_TYPE } from './constants.js';\nimport { parseEnvironSend, buildEnvironIs } from './newenviron.js';\nimport type { Trace } from './trace.js';\n\n/**\n * Telnet option negotiation and 3270 record framing.\n *\n * The framer is a byte-at-a-time state machine because a 3270 record has no\n * length field: it ends at IAC EOR, which can land anywhere relative to a TCP\n * segment boundary. Never assume a chunk is a record.\n */\n\nexport interface TelnetLayerOptions {\n write: (bytes: Uint8Array) => void;\n onRecord: (record: Uint8Array) => void;\n trace?: Trace;\n terminalType?: string;\n /** Offer TN3270E when the host asks. Default true; `-tn3270e off` clears it. */\n tn3270eEnabled?: boolean;\n /** Receives a TN3270E subnegotiation body with the option byte stripped. */\n onTn3270eSubneg?: (body: Uint8Array) => void;\n /**\n * Called whenever TN3270E goes OFF, so the owner can discard its negotiation.\n *\n * The mirror of `onTn3270eSubneg`, and an inbound event in the same sense: the host\n * withdrawing option 40 mid-session is something only this layer sees. The layer's\n * own flag is not the whole of the session's TN3270E state -- `Session.e` decides\n * whether records carry a 5-byte header -- so a teardown here that nobody was told\n * about leaves the framing on. See `disableTn3270e`.\n */\n onTn3270eDisabled?: () => void;\n /**\n * NEW-ENVIRON (option 39) variables, or absent to refuse the option entirely.\n *\n * CONDITIONAL LIKE OPTION 40, and for the same reason `TelnetLayerOptions.tn3270eEnabled`\n * exists as a knob rather than `DESIRED` growing a fourth entry: `DESIRED` is a constant\n * set of options we always want, and this one we want only when there is something to\n * say. Absent means \"refuse\" rather than \"want with nothing configured\" -- there is no\n * separate `environEnabled` boolean, because a `-devname`-less, `-user`-less client has\n * no reply to give a SEND, and agreeing to the option only to have nothing for the host\n * to learn is not a real acceptance.\n *\n * `uservars` IS A FUNCTION, NOT A MAP, because the interesting variable -- DEVNAME --\n * must be FRESH per request (see `DeviceName.next()` in devname.ts): a hosts asks\n * repeatedly and expects a new candidate name each time it refuses the last one. A\n * plain `ReadonlyMap<string,string>` frozen at construction could not do that; a\n * callback invoked once per reply can.\n */\n environ?: {\n readonly vars: ReadonlyMap<string, string>;\n readonly uservars: () => ReadonlyMap<string, string>;\n };\n}\n\nenum St { Data, Iac, Will, Wont, Do, Dont, Sb, SbIac }\n\n/** Options we actively want. */\nconst DESIRED = new Set<number>([O.BINARY, O.TERMINAL_TYPE, O.EOR, O.SUPPRESS_GO_AHEAD]);\n\n/**\n * Ceilings on the two per-byte accumulators.\n *\n * A record has no length field, so neither can be pre-sized \u2014 but both are\n * `number[]`, which boxes each byte at roughly 32 bytes of heap. Without a\n * ceiling, a host that never sends IAC EOR (or a stream desynchronized so that\n * EOR is consumed as an option byte) grows the heap ~32x the wire rate until the\n * process dies. A full 3278-2 rewrite is a few KB, so 64 KB is generous.\n */\nexport const MAX_RECORD_BYTES = 65536;\n/** x3270 uses a 1024-byte sbbuf (telnet.c:1806); it does not bounds-check, we do. */\nexport const MAX_SUBNEG_BYTES = 1024;\n\n/**\n * Copy `body` with every 0xFF doubled, as telnet requires of data bytes.\n *\n * RFC 854, TELNET COMMAND STRUCTURE (p.13-14): \"With the current set-up, only\n * the IAC need be doubled to be sent as data, and the other 255 codes may be\n * passed transparently.\"\n *\n * Body only: the caller appends its own framing (IAC EOR for a 3270 record, IAC\n * SE for a subnegotiation) *after* this returns, because that trailing IAC is a\n * command introducer and must stay single. x3270 draws the same line, though\n * from the other side \u2014 it builds the framed buffer first and then tells\n * net_hexnvt_out_framed() to skip the ends (telnet.c:3003-3004). Keeping the\n * framing outside is equivalent and needs no special cases.\n *\n * Shared by the record and subnegotiation paths deliberately: they carry\n * different payloads (3270 data vs telnet option data) and end with different\n * framing, but the escaping rule is one rule, and having it in one place is why\n * only one of the two could drift out of step in the first place.\n */\nfunction doubleIac(body: Iterable<number>): number[] {\n const out: number[] = [];\n for (const b of body) {\n out.push(b);\n if (b === T.IAC) out.push(T.IAC);\n }\n return out;\n}\n\nexport class TelnetLayer {\n private readonly write: (bytes: Uint8Array) => void;\n private readonly onRecord: (record: Uint8Array) => void;\n private readonly trace: Trace | undefined;\n private readonly terminalType: string;\n private readonly tn3270eEnabled: boolean;\n private readonly onTn3270eSubneg: ((body: Uint8Array) => void) | undefined;\n private readonly onTn3270eDisabled: (() => void) | undefined;\n private readonly environ: TelnetLayerOptions['environ'];\n /**\n * Set once DEVICE-TYPE and FUNCTIONS have both completed.\n *\n * ONE writer for `true` (`setTn3270eNegotiated`) and ONE for `false`\n * (`disableTn3270e`). Both of the ways TN3270E ends go through the latter, because\n * for a while only one of them did: see that method.\n */\n private tn3270eNegotiated = false;\n\n private state = St.Data;\n private record: number[] = [];\n private sb: number[] = [];\n /** Set when the current record blew the ceiling; suppresses its delivery. */\n private overlongRecord = false;\n\n /** Options we have told the host WE will do. */\n private readonly myOpts = new Set<number>();\n /** Options we have told the host IT may do. */\n private readonly hisOpts = new Set<number>();\n\n constructor(opts: TelnetLayerOptions) {\n this.write = opts.write;\n this.onRecord = opts.onRecord;\n this.trace = opts.trace;\n this.terminalType = opts.terminalType ?? TERMINAL_TYPE;\n this.tn3270eEnabled = opts.tn3270eEnabled ?? true;\n this.onTn3270eSubneg = opts.onTn3270eSubneg;\n this.onTn3270eDisabled = opts.onTn3270eDisabled;\n this.environ = opts.environ;\n }\n\n /**\n * True once BINARY and EOR are agreed in both directions \u2014 the point at\n * which the byte stream is 3270 records rather than NVT text.\n */\n is3270Mode(): boolean {\n // TWO ROUTES, AND THE SECOND IS NOT OPTIONAL. Classic TN3270 arrives here by\n // agreeing BINARY and EOR in both directions. TN3270E arrives by completing its\n // own negotiation, because RFC 2355 \u00A74 makes binary and EOR IMPLIED rather than\n // negotiated: \"a party to the negotiation that agrees to support TN3270E is\n // automatically required to support bi-directional binary and EOR\n // transmissions.\"\n //\n // Measured 2026-08-27: a server sending only `IAC DO TN3270E` -- no BINARY, no\n // EOR -- still gets 3270 records out of real s3270. With only the classic test\n // here, such a session negotiates perfectly and then discards every inbound byte\n // in storeRecordByte() and every record in flushRecord(), presenting as a blank\n // screen with a trace full of \"EOR received outside 3270 mode\".\n //\n // Negotiation COMPLETE, not merely the option agreed: during DEVICE-TYPE and\n // FUNCTIONS there is no datastream yet. That is the distinction s3270 draws\n // between its connected-unbound and connected-tn3270e states.\n //\n // THIS SHORT-CIRCUIT IS WHAT MAKES THE TEARDOWN LOAD-BEARING. It bypasses the\n // classic test below, so a session that keeps this flag after option 40 has gone\n // away keeps framing TN3270E against a host that stopped being TN3270E. Every path\n // that turns the option off therefore goes through `disableTn3270e`, which is the\n // only writer of `false`; read that method before adding a second one.\n if (this.tn3270eNegotiated) return true;\n return (\n this.myOpts.has(O.BINARY) && this.hisOpts.has(O.BINARY) &&\n this.myOpts.has(O.EOR) && this.hisOpts.has(O.EOR)\n );\n }\n\n /**\n * Called by the session when TN3270E negotiation completes, or is abandoned.\n *\n * `false` IS NOT A BARE FLAG WRITE, and that asymmetry is the point. Turning TN3270E\n * ON is genuinely just this flag -- the option was already agreed by DO/WILL, and\n * completing DEVICE-TYPE and FUNCTIONS adds nothing on the wire. Turning it OFF is\n * three things (the flag, `myOpts`, and telling the host), and a caller that cleared\n * only the flag would leave the exact half-teardown this class of bug is made of: the\n * host still believes we do option 40. So `false` routes through `refuseTn3270e()`,\n * which means there is NO way through this class to clear the flag alone.\n *\n * Nothing in the tree passes `false` today; the signature stays symmetric because\n * the name reads as a setter and a caller who writes one has to get it right whether\n * or not we anticipated them.\n */\n setTn3270eNegotiated(v: boolean): void {\n if (!v) {\n this.refuseTn3270e();\n return;\n }\n this.tn3270eNegotiated = true;\n }\n\n receive(chunk: Uint8Array): void {\n this.trace?.recv(chunk);\n for (const c of chunk) this.step(c);\n }\n\n /** Frame and transmit one inbound 3270 record. */\n sendRecord(payload: Uint8Array): void {\n const out = doubleIac(payload);\n out.push(T.IAC, T.EOR);\n const bytes = Uint8Array.from(out);\n this.trace?.send(bytes);\n this.write(bytes);\n }\n\n /**\n * SYSREQ is Telnet AO (RFC 2355 \u00A711; x3270 telnet.c:3636), not an AID.\n *\n * No TN3270E header and no IAC EOR: RFC 2355 \u00A78 recommends telnet commands travel\n * BETWEEN data messages rather than inside one, precisely because the receiver is\n * free to process an embedded command before or after the surrounding record.\n */\n sendSysreq(): void {\n const bytes = Uint8Array.of(T.IAC, T.AO);\n this.trace?.send(bytes, 'SYSREQ (IAC AO)');\n this.write(bytes);\n }\n\n /** The 3270 Attn key is Telnet BREAK (RFC 1576 \u00A78), not an AID. */\n sendAttn(): void {\n const bytes = Uint8Array.of(T.IAC, T.BREAK);\n this.trace?.send(bytes, 'Attn (IAC BREAK)');\n this.write(bytes);\n }\n\n private step(c: number): void {\n switch (this.state) {\n case St.Data:\n if (c === T.IAC) {\n this.state = St.Iac;\n } else {\n this.storeRecordByte(c);\n }\n return;\n\n case St.Iac:\n switch (c) {\n case T.IAC:\n // An escaped IAC is data, so it goes through exactly the same two\n // gates as a plain data byte in St.Data: it is dropped outside 3270\n // mode, and it counts against MAX_RECORD_BYTES. Pushing it\n // unconditionally leaked a 0xFF out of the pre-3270 banner into the\n // head of the first real record, and let a stream of IAC IAC grow\n // the accumulator past the ceiling. See storeRecordByte for the\n // x3270 lines.\n this.state = St.Data;\n this.storeRecordByte(0xff);\n return;\n case T.EOR:\n this.state = St.Data;\n this.flushRecord();\n return;\n case T.WILL: this.state = St.Will; return;\n case T.WONT: this.state = St.Wont; return;\n case T.DO: this.state = St.Do; return;\n case T.DONT: this.state = St.Dont; return;\n case T.SB: this.sb = []; this.state = St.Sb; return;\n case T.NOP:\n // A liveness probe. Explicitly no reply (RFC 1576 \u00A75).\n this.state = St.Data;\n return;\n default:\n this.state = St.Data;\n return;\n }\n\n case St.Do:\n this.onDo(c);\n this.state = St.Data;\n return;\n\n case St.Dont: {\n // OPTION 40 IS NOT JUST A BIT IN `myOpts`, so this arm cannot merely delete it:\n // a COMPLETED TN3270E negotiation also short-circuits is3270Mode() (see there),\n // and a host that withdraws the option after negotiating it would otherwise\n // leave us framing TN3270E -- a 5-byte data header prepended for a host with no\n // parser for it, and five bytes eaten off the front of inbound records that\n // never carried one. Measured 2026-09-17: a real z/VM 4.4 sends `IAC DONT\n // TN3270E` after our WILL, so this arm on option 40 is a live path and not a\n // theoretical one; there it arrived before the negotiation completed, which is\n // the only reason nothing broke.\n //\n // Routed through the SAME `disableTn3270e()` that refuseTn3270e() runs, rather\n // than clearing the flag here as well: two copies of one rule is how the two\n // paths diverged in the first place, and `Session.e` had the identical hole\n // twice (session.ts, `forgetTn3270e`).\n //\n // THE TEARDOWN IS UNCONDITIONAL AND ONLY THE REPLY IS GUARDED. `myOpts` and the\n // flag are two records of the same fact; guarding the teardown on the first\n // would leave the second stuck on if they ever disagreed, which is precisely\n // the failure being fixed -- and `refuseTn3270e()` has always cleared it\n // unconditionally. The reply stays guarded because it is an ACKNOWLEDGEMENT,\n // and RFC 854 requires that a request to enter a mode we are already in go\n // unacknowledged, \"essential to prevent endless loops in the negotiation\".\n // x3270's TNS_DONT guards the whole block the same way with `if (myopts[c])`\n // (Common/telnet.c:2039-2048, verified against the current source).\n const wasAgreed = c === O.TN3270E ? this.disableTn3270e() : this.myOpts.delete(c);\n if (wasAgreed) this.reply(T.WONT, c);\n this.state = St.Data;\n return;\n }\n\n case St.Will:\n this.onWill(c);\n this.state = St.Data;\n return;\n\n case St.Wont:\n // TWO BRANCHES, AND THE ORDER IS LOAD-BEARING. The first is the ordinary rule:\n // the host withdraws something IT was doing, so it leaves `hisOpts` and we\n // acknowledge with DONT.\n //\n // THE SECOND IS OPTION 40 ARRIVING BY THE WRONG VERB, and x3270 carries it as a\n // named special case whose comment reads, verbatim, \"Ugly hack for hosts that\n // send WONT TN3270E instead of DONT TN3270E\" (Common/telnet.c:1879-1889). The\n // correct byte for withdrawing an option WE do is `DONT`, handled in the\n // `St.Dont` arm above; some real hosts send `WONT` instead. Until 2026-09-19 we\n // did nothing at all with it \u2014 `hisOpts.delete(40)` never matches, because\n // option 40 is in `myOpts` \u2014 so the teardown never ran and\n // `tn3270eNegotiated` stayed true, which SHORT-CIRCUITS `is3270Mode()`. Same\n // consequence as the `DONT` hole fixed two days earlier: a 5-byte data header\n // prepended for a host with no parser for it, and five bytes eaten off inbound\n // records that never carried one. Found by the playback oracle's\n // `wont-tn3270e.trc`, where we receive `ff fc 28`, answer nothing, and the host\n // then drops the connection.\n //\n // `else if`, not a separate `if`: an option the host genuinely had agreed keeps\n // the ordinary DONT acknowledgement, which is what x3270's own ordering gives.\n // Answering WONT to a normal withdrawal would be a protocol error on every\n // other option.\n //\n // THE REPLY IS `WONT`, NOT `DONT`, because the option is OURS: we are saying we\n // stop doing it, not answering about what the host does.\n //\n // Routed through the SAME `disableTn3270e()` as the `St.Dont` arm and\n // `refuseTn3270e()`. That method is where this rule lives precisely because two\n // copies of one teardown is how these paths diverged twice already \u2014 once in\n // `Session.e`, once in this class.\n if (this.hisOpts.delete(c)) {\n this.reply(T.DONT, c);\n } else if (c === O.TN3270E && this.disableTn3270e()) {\n this.reply(T.WONT, c);\n }\n this.state = St.Data;\n return;\n\n case St.Sb:\n if (c === T.IAC) {\n this.state = St.SbIac;\n } else {\n this.storeSubnegByte(c);\n }\n return;\n\n case St.SbIac:\n if (c === T.SE) {\n this.handleSubnegotiation();\n this.state = St.Data;\n } else {\n // Not SE, so this is subnegotiation data. IAC IAC therefore stores one\n // 0xFF \u2014 the inbound un-doubling that mirrors what we now do on the\n // way out. x3270's TNS_SB_IAC has the same shape: it stores the byte\n // unconditionally with `*sbptr++ = c` and, for c != SE, falls out of\n // the `if` leaving that single copy (telnet.c:1994-1996). Note x3270\n // therefore keeps the trailing SE in sbbuf and we do not, which is why\n // its TN3270E parser has to scan for SE to find the length\n // (telnet.c:2188-2193) while our `sb` is already exactly the body.\n //\n // This must go through the same ceiling as St.Sb. It did not, so a\n // body of IAC IAC pairs bypassed MAX_SUBNEG_BYTES entirely: measured\n // at 200001 accumulated entries against a 1024-byte cap, which is the\n // heap exhaustion the cap exists to prevent, reached through the\n // escaped-byte door.\n this.state = St.Sb;\n this.storeSubnegByte(c);\n }\n return;\n }\n }\n\n /**\n * Append one already-unescaped byte to the subnegotiation accumulator.\n *\n * An unterminated subnegotiation would otherwise eat the session: St.Sb is left\n * only on IAC SE, so without a ceiling the rest of the stream is silently\n * consumed and the client presents as a hang. x3270 allocates a fixed\n * 1024-byte sbbuf (telnet.c:1806) and does not bounds-check it; we do.\n */\n private storeSubnegByte(b: number): void {\n if (this.sb.length >= MAX_SUBNEG_BYTES) {\n this.trace?.note(`subnegotiation exceeded ${MAX_SUBNEG_BYTES} bytes, abandoned`);\n this.sb = [];\n this.state = St.Data;\n return;\n }\n this.sb.push(b);\n }\n\n /**\n * Append one already-unescaped data byte to the record accumulator.\n *\n * Outside 3270 mode the byte is NVT text \u2014 a logon banner or a session manager\n * prompt. It must NOT enter the record accumulator, or it gets prepended to\n * the first real 3270 record and the parser reads a banner character as the\n * command byte. Stage 1 renders no NVT text, so dropping it is correct.\n *\n * x3270 applies this same `if (IN_NVT && !IN_E) ... else store3270in(c)` gate\n * separately in each of the two places that store a data byte: once in\n * TNS_DATA for a plain byte (telnet.c:1703, 1735-1736) and again in TNS_IAC\n * for an escaped one (telnet.c:1745, 1772-1773). Both of our callers funnel\n * through here instead, so the two cannot disagree.\n */\n private storeRecordByte(b: number): void {\n if (!this.is3270Mode()) return;\n if (this.record.length >= MAX_RECORD_BYTES) {\n this.trace?.note(`record exceeded ${MAX_RECORD_BYTES} bytes, discarded`);\n this.record = [];\n this.overlongRecord = true;\n return;\n }\n this.record.push(b);\n }\n\n /** Host asks us to enable an option. */\n private onDo(opt: number): void {\n // Before the DESIRED test, because option 40 is CONDITIONAL and DESIRED is a\n // constant set. The RFC 854 \"only on a real change\" guard applies here too.\n if (opt === O.TN3270E) {\n if (!this.tn3270eEnabled) {\n this.reply(T.WONT, opt);\n return;\n }\n if (!this.myOpts.has(opt)) {\n this.myOpts.add(opt);\n this.reply(T.WILL, opt);\n }\n return;\n }\n if (opt === O.NEW_ENVIRON) {\n // CONDITIONAL, like option 40 and for the same reason: `DESIRED` is a constant\n // set, and we want 39 only when there is something to say. Refusing when nothing\n // is configured keeps this feature dark by default, which is what holds its blast\n // radius on the existing negotiation tests at zero.\n if (this.environ === undefined) {\n this.reply(T.WONT, opt);\n return;\n }\n if (!this.myOpts.has(opt)) {\n this.myOpts.add(opt);\n this.reply(T.WILL, opt);\n }\n return;\n }\n if (DESIRED.has(opt)) {\n if (!this.myOpts.has(opt)) {\n this.myOpts.add(opt);\n this.reply(T.WILL, opt);\n }\n return;\n }\n if (opt === O.ECHO) {\n // Some hosts renegotiate ECHO repeatedly during a pre-3270 NVT login.\n // Answer as asked, but only on a real change: RFC 854 requires that a\n // request to enter a mode we are already in go unacknowledged, \"essential\n // to prevent endless loops in the negotiation\". x3270 guards every\n // accepted option the same way (telnet.c:2000, `if (!myopts[c])`).\n // Stage 1 implements no NVT-mode local echo.\n if (!this.myOpts.has(opt)) {\n this.myOpts.add(opt);\n this.reply(T.WILL, opt);\n }\n return;\n }\n // Everything else, including TN3270E, TIMING-MARK and 3270-REGIME.\n this.reply(T.WONT, opt);\n }\n\n /** Host offers to enable an option. */\n private onWill(opt: number): void {\n if (opt === O.BINARY || opt === O.EOR) {\n if (!this.hisOpts.has(opt)) {\n this.hisOpts.add(opt);\n this.reply(T.DO, opt);\n }\n return;\n }\n this.reply(T.DONT, opt);\n }\n\n private handleSubnegotiation(): void {\n if (this.sb[0] === O.TN3270E) {\n // Hand up the body only. The option byte is framing, and the trailing SE is\n // already absent because our accumulator drops it (see St.SbIac) -- which is\n // why the consumer can scan to end-of-buffer where x3270 has to scan for SE.\n // IAC IAC has already been un-doubled by storeSubnegByte.\n const body = Uint8Array.from(this.sb.slice(1));\n this.sb = [];\n this.onTn3270eSubneg?.(body);\n return;\n }\n if (this.sb[0] === O.TERMINAL_TYPE && this.sb[1] === S.SEND) {\n // The ttype is a subnegotiation *parameter*, so a 0xFF inside it must be\n // doubled like any other data byte. RFC 855 says so explicitly, in its\n // last paragraph: \"Finally, if parameters in an option \"subnegotiation\"\n // include a byte with a value of 255, it is necessary to double this byte\n // in accordance the general TELNET rules.\" (RFC 1091, which defines\n // TERMINAL-TYPE, is silent \u2014 it just calls the ttype \"an NVT ASCII\n // string\" \u2014 so RFC 855 is the governing text.) x3270 does the same, by\n // running its assembled reply through net_hexnvt_out_framed(..., true)\n // at telnet.c:2025.\n //\n // Mask before testing for IAC, not after. charCodeAt yields a UTF-16 code\n // unit, which Uint8Array.from would silently truncate mod 256 \u2014 U+01FF is\n // 511, is not equal to T.IAC, and truncates to 0xFF, so doubling the\n // untruncated value would still put a bare IAC on the wire.\n const name = Array.from(this.terminalType, (ch) => ch.charCodeAt(0) & 0xff);\n const body = doubleIac(name);\n // The leading IAC SB and trailing IAC SE bracket the body and are commands,\n // not data: they stay single.\n const out = Uint8Array.from([\n T.IAC, T.SB, O.TERMINAL_TYPE, S.IS, ...body, T.IAC, T.SE,\n ]);\n this.trace?.send(out, `TERMINAL-TYPE IS ${this.terminalType}`);\n this.write(out);\n this.sb = [];\n return;\n }\n if (this.sb[0] === O.NEW_ENVIRON && this.sb[1] === EnvironQual.SEND) {\n // `this.sb` is consumed on EVERY path out of this arm, malformed or not --\n // deliberately before the `requests === null` / `environ === undefined` check\n // below, because the accumulator has to be cleared either way or the next\n // subnegotiation inherits this one's tail. Compare the TN3270E and\n // TERMINAL-TYPE arms above, which clear it before or right at their own\n // dispatch for the same reason.\n const requests = parseEnvironSend(Uint8Array.from(this.sb.slice(2)));\n this.sb = [];\n if (requests === null || this.environ === undefined) {\n // TWO DIFFERENT REASONS TO DROP, ONE OUTCOME. `requests === null` is a\n // malformed body (parseEnvironSend's contract: null rather than throwing,\n // same reason as decodeHeader/parseBind -- a client cannot correct a host).\n // `environ === undefined` is a host that sent SEND despite our refusing the\n // option in onDo (WONT 39), or one that never even asked and sent it anyway;\n // RFC compliance is not guaranteed and this layer does not require\n // `myOpts.has(NEW_ENVIRON)` before answering, matching x3270's own dispatch\n // (Common/telnet.c:2048-2049), which gates the whole SEND handler on\n // `appres.new_environ` -- the configuration flag -- and never tests\n // `myopts[TELOPT_NEW_ENVIRON]` at all. Since `environ === undefined` is\n // exactly our equivalent of `!appres.new_environ`, this one check already\n // covers \"unconfigured\" without a separate `myOpts` gate that x3270 itself\n // does not have.\n this.trace?.note('malformed or unconfigured NEW-ENVIRON SEND, dropped');\n return;\n }\n const body = buildEnvironIs(requests, this.environ.vars, this.environ.uservars());\n // `body` starts with the IS qualifier (0x00) and is a subnegotiation\n // PARAMETER end to end -- RFC 855's last paragraph applies to the whole\n // thing, not just the part after the qualifier. Doubling the qualifier byte\n // too is harmless as well as correct: 0x00 can never equal IAC (0xff), so\n // `doubleIac` is a no-op on it either way. Contrast the TERMINAL-TYPE arm\n // above, which doubles only the ttype string -- there the IS qualifier\n // (S.IS, also 0x00) is written OUTSIDE `doubleIac`'s argument, into the `out`\n // array literal directly, so that arm's scope choice is invisible on the\n // wire for the same reason: 0x00 doubles to itself.\n const out = Uint8Array.from([\n T.IAC, T.SB, O.NEW_ENVIRON, ...doubleIac(body), T.IAC, T.SE,\n ]);\n this.trace?.send(out);\n this.write(out);\n return;\n }\n // Anything else is dropped; we advertised nothing that needs it. This also\n // catches a NEW-ENVIRON subnegotiation with a qualifier other than SEND --\n // EnvironQual.INFO (a host telling US something, unsolicited) or IS (which\n // makes no sense from a host to a client that never sent SEND) -- and traces\n // it by option number rather than dropping it silently, the same as any other\n // subnegotiation we did not advertise.\n const optLabel = this.sb.length > 0 ? String(this.sb[0]) : '(empty)';\n this.trace?.note(`ignored subnegotiation for option ${optLabel}`);\n this.sb = [];\n }\n\n private flushRecord(): void {\n if (!this.is3270Mode()) {\n // EOR before negotiation completed. x3270 logs and discards the\n // accumulator (telnet.c:1848-1859, `ibptr = ibuf`); so do we.\n if (this.record.length > 0) {\n this.trace?.note(`EOR received outside 3270 mode, ${this.record.length} bytes discarded`);\n this.record = [];\n }\n return;\n }\n if (this.overlongRecord) {\n // The record already exceeded MAX_RECORD_BYTES and was dropped; deliver\n // nothing rather than a truncated tail, and resync for the next one.\n this.overlongRecord = false;\n this.record = [];\n return;\n }\n if (this.record.length === 0) return; // nothing to deliver\n const rec = Uint8Array.from(this.record);\n this.record = [];\n this.onRecord(rec);\n }\n\n /**\n * Frame and send a TN3270E subnegotiation body.\n *\n * The body is doubled and the brackets are not, exactly as the TERMINAL-TYPE path\n * does it: a 0xff in a subnegotiation parameter is data and must be doubled\n * (RFC 855, final paragraph), while the IAC of IAC SB and IAC SE is a command\n * introducer and stays single.\n */\n sendTn3270eSubneg(body: Uint8Array): void {\n const out = Uint8Array.from([\n T.IAC, T.SB, O.TN3270E, ...doubleIac(body), T.IAC, T.SE,\n ]);\n this.trace?.send(out, 'TN3270E subnegotiation');\n this.write(out);\n }\n\n /**\n * Abandon TN3270E and fall back to traditional tn3270.\n *\n * Modeled on x3270's backoff_tn3270e(): tell the host no, then forget we ever had\n * the option, so both the classic BINARY/EOR route and a later renegotiation are\n * still reachable on this same layer. Latching it off would make a reconnect\n * silently decline. This is what makes on-by-default safe.\n *\n * The WONT is UNCONDITIONAL, unlike the one the St.Dont arm sends: this is us\n * INITIATING the change rather than acknowledging one, so RFC 854's\n * do-not-acknowledge-a-mode-you-are-in rule does not apply. x3270 writes it before\n * it clears `myopts` and never tests the bit (Common/telnet.c:2224-2240).\n */\n refuseTn3270e(): void {\n this.disableTn3270e();\n this.reply(T.WONT, O.TN3270E);\n }\n\n /**\n * Turn TN3270E off. THE ONE PLACE THAT DOES, which is the whole point of it existing.\n *\n * TN3270E ends two ways -- WE abandon it (`refuseTn3270e`, on a DEVICE-TYPE REJECT\n * with the LU list exhausted, or a FUNCTIONS set we will not accept) and the HOST\n * withdraws it (`IAC DONT 40`, the St.Dont arm). For a while only the first cleared\n * `tn3270eNegotiated`, so the second left that flag true with the option off, and the\n * flag short-circuits is3270Mode(). Both callers now run this, so the two cannot\n * drift: adding a third way off means calling this, not copying it.\n *\n * Returns whether the option HAD been agreed. That is the caller's business, not\n * ours, because the two callers owe the host different replies -- see each.\n *\n * x3270 gets there from the other end and it is worth knowing why we cannot copy it.\n * Its TNS_DONT clears `myopts[c]` and calls check_in3270() (Common/telnet.c:2039-2048),\n * and check_in3270 tests `myopts[TELOPT_TN3270E]` BEFORE `tn3270e_negotiated`\n * (:3104-3130) -- so there the mode is DERIVED, clearing the option bit is enough, and\n * `tn3270e_negotiated` may stay set harmlessly. Ours is a stored predicate, so ours\n * has to be cleared, here. Deliberately NOT also guarded inside is3270Mode() the way\n * check_in3270 guards it: that would make a missed teardown unobservable, and a\n * defect that no test can see is worse than one that reddens.\n */\n private disableTn3270e(): boolean {\n const wasAgreed = this.myOpts.delete(O.TN3270E);\n this.tn3270eNegotiated = false;\n // The owner's copy of the negotiation dies with ours. Fired from HERE and not from\n // the two callers for exactly the reason the teardown itself is here.\n this.onTn3270eDisabled?.();\n return wasAgreed;\n }\n\n private reply(cmd: number, opt: number): void {\n const bytes = Uint8Array.of(T.IAC, cmd, opt);\n this.trace?.send(bytes);\n this.write(bytes);\n }\n}\n", "import { FA, MODEL_2 } from './constants.js';\nimport { cp037, type CodePage } from './codepage.js';\n\n/**\n * The 3270 character buffer.\n *\n * Held as flat typed arrays, the way the hardware does it: one array of EBCDIC\n * bytes and a parallel array marking which positions are field attributes and\n * what they contain. Fields are DERIVED by scanning for attribute positions,\n * never stored as objects \u2014 a host that overwrites a field attribute mid-stream\n * (MVS and CICS both do) must change the field structure, and that falls out\n * for free when fields are computed rather than cached.\n */\n\n/**\n * Cell content is a tagged variant. Stage 1 had exactly one case; Programmable\n * Symbol Sets (a committed stage 4 deliverable) will add\n * `{ kind: 'ps', store, index }`, and consumers must dispatch on `kind` rather\n * than assume a code page lookup.\n *\n * The three extended attributes are OPTIONAL and their absence is meaningful:\n * it means \"this cell specifies nothing, fall through to the base field\n * attribute\". That is not the same as any concrete color, so do not default\n * them to 0x00 \u2014 `render.ts` needs the distinction. This mirrors x3270's\n * `struct ea` (include/globals.h:364-374), which likewise carries the character\n * and its attributes together.\n */\nexport type Cell = {\n kind: 'char';\n ebcdic: number;\n /** Foreground color identification, 0xF0-0xFF. Absent means unspecified. */\n fg?: number;\n /** Background color identification, 0xF0-0xFF. Absent means unspecified. */\n bg?: number;\n /** Highlighting value (`XAH.*`). Absent means unspecified. */\n gr?: number;\n};\n\n/** The subset of a cell's attributes a caller may set. See `Screen.setExtended`. */\nexport interface ExtendedAttributes {\n fg?: number;\n bg?: number;\n gr?: number;\n}\n\nexport interface Field {\n /** Address of the field attribute byte itself. */\n attrAddr: number;\n /**\n * Address of the first data cell (attrAddr + 1, wrapped). For a\n * zero-length field \u2014 one attribute immediately followed by another \u2014 this\n * IS the next attribute's address, not a data cell. Callers that park the\n * cursor here must check `length > 0` first; use `firstUnprotectedStart()`\n * or `typableFields()`, which already do.\n */\n start: number;\n /** Data cells in the field, excluding the attribute byte. */\n length: number;\n /**\n * The raw, unnormalized attribute byte exactly as the host sent it. Real\n * hosts set bits beyond the ones this module names (the base architecture\n * requires the top two bits on, so a plain \"unprotected\" byte arrives as\n * 0xC0, not 0x00). Consume this only via `&` with `FA.*` masks \u2014 never\n * `===` \u2014 or normalization differences will make an identical-looking\n * field compare unequal.\n */\n attr: number;\n protected: boolean;\n numeric: boolean;\n /** Protected + numeric means the cursor skips the field entirely. */\n autoSkip: boolean;\n intensified: boolean;\n hidden: boolean;\n modified: boolean;\n}\n\nexport interface ScreenSnapshot {\n rows: number;\n cols: number;\n cursor: number;\n cells: readonly Readonly<Cell>[];\n fields: readonly Readonly<Field>[];\n formatted: boolean;\n}\n\nexport interface ScreenOptions {\n /** The DEFAULT (Erase/Write) size. The screen starts here. */\n rows?: number;\n cols?: number;\n /**\n * The ALTERNATE (Erase/Write Alternate) size. Defaults to the default size,\n * which is what a model 2 has and what every caller predating alternate-size\n * support means.\n */\n alternateRows?: number;\n alternateCols?: number;\n codePage?: CodePage;\n}\n\n/** Rejects a geometry the buffer cannot be built from. */\nfunction checkGeometry(rows: number, cols: number): void {\n if (!Number.isInteger(rows) || rows <= 0) {\n throw new RangeError(`rows must be a positive integer, got ${rows}`);\n }\n if (!Number.isInteger(cols) || cols <= 0) {\n throw new RangeError(`cols must be a positive integer, got ${cols}`);\n }\n}\n\n/** Marker in the attribute array meaning \"this position is not an attribute\". */\nconst NOT_ATTR = -1;\n\nexport class Screen {\n /**\n * The CURRENT geometry, which Erase/Write and Erase/Write Alternate switch\n * between. Mutable for that reason -- `cursor` above is public and mutable for\n * the same one -- but only `resize` may write them.\n */\n rows = 0;\n cols = 0;\n size = 0;\n cursor = 0;\n\n /**\n * The Erase/Write size. The screen starts here, as x3270's does (ctlr.c:341).\n *\n * NO LONGER `readonly`, and only `setSizes` may write it. A BIND is the second\n * channel by which a host dictates geometry (Query Reply being the other), and it\n * rewrites BOTH sizes -- so this cannot be fixed at construction any more.\n */\n defaultSize: { readonly rows: number; readonly cols: number };\n /** The Erase/Write Alternate size. Equal to `defaultSize` on a model 2. */\n alternateSize: { readonly rows: number; readonly cols: number };\n\n private chars = new Uint8Array(0);\n /** attrs[i] >= 0 means position i holds that field attribute value. */\n private attrs = new Int16Array(0);\n /**\n * Extended attributes, one array each, parallel to `chars`.\n *\n * 0 means \"unspecified\". Every architected color is 0xF0-0xFF, so for `fgs`\n * and `bgs` the sentinel is unambiguous.\n *\n * For `grs` it genuinely collapses two states, and that is SAFE rather than\n * merely convenient: `XAH.DEFAULT` IS 0x00, so a host can set highlighting to\n * a value this array cannot distinguish from \"nothing set\". The manual defines\n * that value as meaning \"the default action of the device\"\n * (pages.txt:10329-10331), so it renders identically to no highlighting at\n * all \u2014 the same reasoning that lets `XAC_DEFAULT` (0x00 as a color VALUE)\n * fall through to the base field attribute rather than being stored.\n *\n * DO NOT reach for `XA.RESET` to justify this. An earlier version of this\n * comment did, and it was wrong in exactly the way `constants.ts` warns about:\n * `XA.RESET` is 0x00 as an attribute TYPE, meaning \"reset every type\", which\n * is a different concept from `XAH.DEFAULT` being 0x00 as a highlighting\n * VALUE. Both being zero is a coincidence of encoding, not a shared meaning.\n */\n private fgs = new Uint8Array(0);\n private bgs = new Uint8Array(0);\n private grs = new Uint8Array(0);\n private readonly codePage: CodePage;\n\n constructor(opts: ScreenOptions = {}) {\n const rows = opts.rows ?? MODEL_2.rows;\n const cols = opts.cols ?? MODEL_2.cols;\n checkGeometry(rows, cols);\n const altRows = opts.alternateRows ?? rows;\n const altCols = opts.alternateCols ?? cols;\n checkGeometry(altRows, altCols);\n this.defaultSize = { rows, cols };\n this.alternateSize = { rows: altRows, cols: altCols };\n this.codePage = opts.codePage ?? cp037;\n this.allocate(rows, cols);\n }\n\n // ---- size switching ----\n\n /**\n * Build the buffer at `rows` x `cols`, discarding whatever was there.\n *\n * Discarding is CORRECT rather than a shortcut: the only callers are\n * Erase/Write and Erase/Write Alternate, both of which erase the screen as\n * their defining act, so there is no surviving content a resize could be\n * accused of losing.\n */\n private allocate(rows: number, cols: number): void {\n this.rows = rows;\n this.cols = cols;\n this.size = rows * cols;\n this.chars = new Uint8Array(this.size);\n this.attrs = new Int16Array(this.size).fill(NOT_ATTR);\n this.fgs = new Uint8Array(this.size);\n this.bgs = new Uint8Array(this.size);\n this.grs = new Uint8Array(this.size);\n this.cursor = 0;\n }\n\n /**\n * Switch geometry, reporting whether anything actually changed.\n *\n * The return value is what lets a caller repaint only when it must: on a model\n * 2 the default and alternate sizes are equal, so every Erase/Write Alternate\n * in a whole session returns false and costs nothing.\n */\n resize(rows: number, cols: number): boolean {\n checkGeometry(rows, cols);\n if (rows === this.rows && cols === this.cols) return false;\n this.allocate(rows, cols);\n return true;\n }\n\n /** Erase/Write: back to the default size. */\n useDefaultSize(): boolean {\n return this.resize(this.defaultSize.rows, this.defaultSize.cols);\n }\n\n /** Erase/Write Alternate: to the alternate size. */\n useAlternateSize(): boolean {\n return this.resize(this.alternateSize.rows, this.alternateSize.cols);\n }\n\n /**\n * Replace what Erase/Write and Erase/Write Alternate MEAN.\n *\n * The only callers are BIND (which sets the host's geometry) and UNBIND (which\n * reverts to the model's). Deliberately narrow: `resize` switches the CURRENT\n * geometry between two fixed sizes, whereas this changes what those two sizes are,\n * and no existing method could express that.\n *\n * It does NOT change the current geometry or erase anything. x3270 calls\n * `ctlr_erase(false)` separately after `process_bind`, and keeping the two apart\n * means a caller that only wants to reinterpret EW/EWA does not pay for a repaint.\n *\n * Deliberately does NOT call `acceptBindDims` (bind.ts): that is the decision, this\n * is the mutation, and keeping them apart is what lets the decision be tested\n * without a Screen. A caller that skips the check can install a geometry\n * `acceptBindDims` would have refused -- that is intentional, not an oversight: the\n * check is a POLICY (x3270's `bind_limit` toggle can be off, in which case x3270\n * applies the BIND's geometry unchecked, ctlr.c's callers of `process_bind` do not\n * gate on it either), while this method is the mechanism every policy needs.\n */\n setSizes(\n def: { rows: number; cols: number },\n alt: { rows: number; cols: number },\n ): void {\n checkGeometry(def.rows, def.cols);\n checkGeometry(alt.rows, alt.cols);\n this.defaultSize = { rows: def.rows, cols: def.cols };\n this.alternateSize = { rows: alt.rows, cols: alt.cols };\n }\n\n /** Throws if `addr` is not an integer in [0, size). */\n private check(addr: number): void {\n if (!Number.isInteger(addr) || addr < 0 || addr >= this.size) {\n throw new RangeError(`address ${addr} out of range for a ${this.size}-cell buffer`);\n }\n }\n\n // ---- geometry ----\n\n /** Display row/column, 1-based, as the OIA and s3270 report them. */\n toRowCol(addr: number): { row: number; col: number } {\n this.check(addr);\n return {\n row: Math.floor(addr / this.cols) + 1,\n col: (addr % this.cols) + 1,\n };\n }\n\n /** Row and column are both 1-based. Throws if the result would be out of range. */\n fromRowCol(row: number, col: number): number {\n if (!Number.isInteger(row) || row < 1 || row > this.rows) {\n throw new RangeError(`row ${row} out of range for a ${this.rows}-row screen`);\n }\n if (!Number.isInteger(col) || col < 1 || col > this.cols) {\n throw new RangeError(`col ${col} out of range for a ${this.cols}-col screen`);\n }\n return (row - 1) * this.cols + (col - 1);\n }\n\n /** Next address, wrapping at the end of the buffer. */\n inc(addr: number): number {\n return (addr + 1) % this.size;\n }\n\n /** Previous address, wrapping at the start of the buffer. */\n dec(addr: number): number {\n return (addr - 1 + this.size) % this.size;\n }\n\n // ---- cells ----\n\n cellAt(addr: number): Cell {\n this.check(addr);\n const cell: Cell = { kind: 'char', ebcdic: this.chars[addr]! };\n // Conditional assignment, not `fg: x || undefined`: the property must be\n // ABSENT when unspecified, because `'fg' in cell` is how a consumer asks.\n if (this.fgs[addr]! !== 0) cell.fg = this.fgs[addr]!;\n if (this.bgs[addr]! !== 0) cell.bg = this.bgs[addr]!;\n if (this.grs[addr]! !== 0) cell.gr = this.grs[addr]!;\n return cell;\n }\n\n /**\n * Merge extended attributes into one cell.\n *\n * MERGE, NOT REPLACE, because the manual's composite rule requires it: \"The\n * set of type-value pairs applied during character processing is a composite,\n * by attribute type, of the last value specified in previously encountered SA\n * orders\" (p. 4-6, pages.txt:2995-2996). An SA setting color must leave a\n * previously set highlighting alone. Pass `clearExtended` to reset.\n */\n setExtended(addr: number, ext: ExtendedAttributes): void {\n this.check(addr);\n if (ext.fg !== undefined) this.fgs[addr] = ext.fg & 0xff;\n if (ext.bg !== undefined) this.bgs[addr] = ext.bg & 0xff;\n if (ext.gr !== undefined) this.grs[addr] = ext.gr & 0xff;\n }\n\n /** Return one cell's extended attributes to \"unspecified\". */\n clearExtended(addr: number): void {\n this.check(addr);\n this.fgs[addr] = 0;\n this.bgs[addr] = 0;\n this.grs[addr] = 0;\n }\n\n /**\n * Write a character. If the position held a field attribute, that attribute\n * is destroyed \u2014 the host is allowed to do this, and the field structure\n * changes as a result.\n *\n * Deliberately does NOT touch fgs/bgs/grs. The executor decides what\n * extended attributes a written character carries (it holds the running SA\n * state) and calls setExtended right after this. Clearing here would\n * discard them a moment later.\n */\n setChar(addr: number, ebcdic: number): void {\n this.check(addr);\n this.chars[addr] = ebcdic & 0xff;\n this.attrs[addr] = NOT_ATTR;\n }\n\n isFieldAttribute(addr: number): boolean {\n this.check(addr);\n return this.attrs[addr]! >= 0;\n }\n\n attributeAt(addr: number): number | null {\n this.check(addr);\n const a = this.attrs[addr]!;\n return a >= 0 ? a : null;\n }\n\n setFieldAttribute(addr: number, attr: number): void {\n this.check(addr);\n this.attrs[addr] = attr & 0xff;\n // An attribute position displays as a blank and holds no character.\n this.chars[addr] = 0x00;\n // \"If the display receives an SF order, it sets the associated extended\n // field attribute to its default value\" (p. 4-4, pages.txt:2869-2870). SFE\n // overrides this by calling setExtended AFTER this returns; a plain SF must\n // leave the position clean, or a field following a colored one inherits\n // color the host never gave it.\n this.clearExtended(addr);\n }\n\n isFormatted(): boolean {\n for (let i = 0; i < this.size; i++) if (this.attrs[i]! >= 0) return true;\n return false;\n }\n\n // ---- fields (all derived) ----\n\n /** The field governing `addr`, found by scanning backwards for an attribute. */\n fieldAt(addr: number): Field | null {\n this.check(addr);\n let a = addr;\n for (let n = 0; n < this.size; n++) {\n if (this.attrs[a]! >= 0) return this.makeField(a);\n a = this.dec(a);\n }\n return null; // unformatted\n }\n\n fields(): Field[] {\n const out: Field[] = [];\n for (let i = 0; i < this.size; i++) {\n if (this.attrs[i]! >= 0) out.push(this.makeField(i));\n }\n return out;\n }\n\n private makeField(attrAddr: number): Field {\n const attr = this.attrs[attrAddr]!;\n const start = this.inc(attrAddr);\n let length = 0;\n let a = start;\n while (a !== attrAddr && this.attrs[a]! < 0) {\n length++;\n a = this.inc(a);\n }\n const protectedField = (attr & FA.PROTECT) !== 0;\n const numeric = (attr & FA.NUMERIC) !== 0;\n const intensity = attr & FA.INTENSITY;\n return {\n attrAddr,\n start,\n length,\n attr,\n protected: protectedField,\n numeric,\n autoSkip: protectedField && numeric,\n intensified: intensity === FA.INT_HIGH_SEL,\n hidden: intensity === FA.INT_ZERO_NSEL,\n modified: (attr & FA.MODIFY) !== 0,\n };\n }\n\n /**\n * Set the MDT bit on the field attribute at `attrAddr`. `attrAddr` must be\n * the attribute's own address (`field.attrAddr`), not any address inside\n * the field \u2014 unlike x3270's `mdt_set`, this does not resolve an arbitrary\n * in-field address for you. Passing a non-attribute address is a silent\n * no-op; callers that have a `Field` should always pass its `attrAddr`.\n */\n setMDT(attrAddr: number): void {\n this.check(attrAddr);\n if (this.attrs[attrAddr]! >= 0) {\n this.attrs[attrAddr] = this.attrs[attrAddr]! | FA.MODIFY;\n }\n }\n\n /**\n * Reset MDT in EVERY field, protected or not. This is WCC reset-MDT: manual\n * Table 3-2 bit 7 says \"all MDT bits in the device's existing character\n * buffer are reset,\" and x3270's handler (`ctlr.c:1439-1451`, which tests\n * `ea_buf[baddr].fa` only) has no protection check. This matters because Read\n * Modified filters on MDT alone (`ctlr.c:822`, inside `ctlr_read_modified()`)\n * \u2014 a protected field left carrying MDT would otherwise leak\n * its data to the host on the next read. Erase Input is the\n * unprotected-only operation; see `clearUnprotectedMDT`.\n */\n clearAllMDT(): void {\n for (let i = 0; i < this.size; i++) {\n const a = this.attrs[i]!;\n if (a >= 0) this.attrs[i] = a & ~FA.MODIFY;\n }\n }\n\n /** Reset MDT in unprotected fields only (Erase Input, manual 4-14). */\n clearUnprotectedMDT(): void {\n for (let i = 0; i < this.size; i++) {\n const a = this.attrs[i]!;\n if (a >= 0 && (a & FA.PROTECT) === 0) {\n this.attrs[i] = a & ~FA.MODIFY;\n }\n }\n }\n\n // ---- clearing ----\n\n /** Erase/Write and the Clear key: everything goes, including attributes. */\n clear(): void {\n this.chars.fill(0x00);\n this.attrs.fill(NOT_ATTR);\n // EW/EWA \"resets any extended field attributes and character attributes\n // associated with the nulled characters to their default values\"\n // (pages.txt:2990-2992). This is also the path the Clear AID takes\n // (`Session.sendAID`'s `AID.CLEAR` branch), which is the manual's fourth SA reset trigger --\n // \"The Clear key is pressed\" (pages.txt:2979). So implementing it here\n // satisfies both rules at once.\n this.fgs.fill(0);\n this.bgs.fill(0);\n this.grs.fill(0);\n this.cursor = 0;\n }\n\n /**\n * Erase All Unprotected: null the data in unprotected fields and reset their\n * MDT. Field attributes themselves survive. On an unformatted screen there\n * are no field attributes to preserve \u2014 the whole buffer is, by definition,\n * unprotected \u2014 so this clears everything, matching x3270's\n * `else { ctlr_clear(true); }` at `ctlr.c:1443-1445`.\n */\n eraseAllUnprotected(): void {\n if (!this.isFormatted()) {\n this.clear();\n return;\n }\n for (const f of this.fields()) {\n if (f.protected) continue;\n let a = f.start;\n for (let n = 0; n < f.length; n++) {\n // Must write chars[] directly, not via setChar: setChar also clears\n // attrs[], which would destroy the very field attributes EAU is\n // required to preserve.\n this.chars[a] = 0x00;\n // Extended attributes go with the character data they decorate. EAU is\n // NOT one of the manual's four SA-state reset triggers, so this line is\n // an inference rather than a quotation \u2014 but leaving them would show\n // stale color on a cell whose data has just been nulled. Written\n // directly rather than via clearExtended for the same reason as chars[]\n // above: one bounds check per cell is wasted work in a hot loop.\n this.fgs[a] = 0;\n this.bgs[a] = 0;\n this.grs[a] = 0;\n a = this.inc(a);\n }\n this.attrs[f.attrAddr] = f.attr & ~FA.MODIFY;\n }\n }\n\n /**\n * The first cell a user could type into: the start of the first\n * unprotected field, skipping zero-length fields (a zero-length field's\n * `start` IS the next attribute \u2014 landing the cursor there and typing would\n * collapse two fields). Mirrors x3270's `next_unprotected`\n * (`ctlr.c:623-638`). Returns null if there is no such field.\n */\n firstUnprotectedStart(): number | null {\n for (const f of this.fields()) {\n if (!f.protected && f.length > 0) return f.start;\n }\n return null;\n }\n\n /**\n * Unprotected, non-auto-skip fields with room for data \u2014 the set Tab and\n * Back Tab cycle through. Zero-length fields are excluded for the same\n * reason `firstUnprotectedStart` excludes them.\n */\n typableFields(): Field[] {\n return this.fields().filter((f) => !f.protected && !f.autoSkip && f.length > 0);\n }\n\n /**\n * Visit every cell from `start` up to (excluding) `stop`, wrapping as\n * needed, passing each cell's governing field (or null if unformatted) and\n * whether the cell itself is that field's attribute byte. Carries the\n * attribute forward instead of calling `fieldAt` per cell, so a full sweep\n * is O(size) rather than O(size^2) \u2014 the naive per-cell `fieldAt` scan costs\n * ~3.7M operations over a full 1920-cell buffer. Mirrors x3270's\n * `current_fa` tracking (`ctlr.c:1809-1816`).\n */\n forEachCellWithField(\n start: number,\n stop: number,\n cb: (addr: number, field: Field | null, isAttr: boolean) => void,\n ): void {\n this.check(start);\n this.check(stop);\n let field = this.fieldAt(start);\n let a = start;\n do {\n const attr = this.attrs[a]!;\n const isAttr = attr >= 0;\n if (isAttr) field = this.makeField(a);\n cb(a, field, isAttr);\n a = this.inc(a);\n } while (a !== stop);\n }\n\n // ---- output ----\n\n /** Raw buffer contents, for tests and diagnostics. */\n readBuffer(): Uint8Array {\n return Uint8Array.from(this.chars);\n }\n\n /** One display row as text, 1-based. Nulls and attributes render as spaces. */\n rowText(row: number): string {\n if (!Number.isInteger(row) || row < 1 || row > this.rows) {\n throw new RangeError(`row ${row} out of range for a ${this.rows}-row screen`);\n }\n let out = '';\n const base = (row - 1) * this.cols;\n for (let c = 0; c < this.cols; c++) {\n const addr = base + c;\n if (this.attrs[addr]! >= 0) {\n out += ' ';\n continue;\n }\n const b = this.chars[addr]!;\n out += b === 0x00 ? ' ' : this.codePage.toUnicode(b);\n }\n return out;\n }\n\n toText(): string {\n const lines: string[] = [];\n for (let r = 1; r <= this.rows; r++) lines.push(this.rowText(r));\n return lines.join('\\n');\n }\n\n /**\n * An immutable view for the UI or for assertions. The `Readonly<Cell>` /\n * `Readonly<Field>` element types make `snap.cells[0].ebcdic = ...` a\n * compile error, and each array and element is also `Object.freeze`d so the\n * same mutation fails at runtime too (e.g. from plain JS callers).\n */\n snapshot(): ScreenSnapshot {\n const cells: Readonly<Cell>[] = new Array(this.size);\n // Via cellAt rather than building the object inline, deliberately: cellAt\n // owns the \"absent means unspecified\" conditional-assignment rule, and\n // duplicating it here is how the two would drift. The cost is one redundant\n // bounds check per cell \u2014 1920 of them, on a call made once per host record,\n // which is immaterial. The tight loop in eraseAllUnprotected makes the\n // opposite choice for the opposite reason; both are intentional.\n for (let i = 0; i < this.size; i++) {\n cells[i] = Object.freeze(this.cellAt(i));\n }\n const fields = this.fields().map((f) => Object.freeze(f));\n const formatted = fields.length > 0;\n return {\n rows: this.rows,\n cols: this.cols,\n cursor: this.cursor,\n cells: Object.freeze(cells),\n fields: Object.freeze(fields),\n formatted,\n };\n }\n}\n", "/**\n * BIND and UNBIND: the SNA session-management RUs a TN3270E host forwards as data\n * types BIND-IMAGE (0x03) and UNBIND (0x04).\n *\n * PURE. No session state, no I/O, no timers -- the caller owns all of that, exactly\n * as `tn3270e.ts` owns the negotiation state machine without owning a socket. That is\n * what lets every offset and every boundary below be tested against a byte array.\n *\n * BIND IS THE SECOND CHANNEL BY WHICH A HOST DICTATES GEOMETRY, the other being Query\n * Reply. That is the whole reason it matters here and not merely as trace decoration.\n *\n * Design doc: docs/superpowers/specs/2026-09-17-bind-image-and-bind-unbind-design.md\n * Plan: docs/superpowers/plans/2026-09-17-bind-image-and-bind-unbind.md\n */\n\nimport { MODEL_2, Tn3270eUnbindReason } from './constants.js';\nimport { cp037 } from './codepage.js';\n\n/** A BIND request unit begins with this. Anything else is not a BIND. */\nexport const BIND_RU = 0x31;\n\n/** Byte offsets within the BIND RU, from x3270's include/3270ds.h:433-441. */\nexport const BIND_OFF = {\n MAXRU_SEC: 10,\n MAXRU_PRI: 11,\n RD: 20,\n CD: 21,\n RA: 22,\n CA: 23,\n SSIZE: 24,\n PLU_NAME_LEN: 27,\n PLU_NAME: 28,\n} as const;\n\n/**\n * A LENGTH CAP, not an offset -- x3270's include/3270ds.h:442 names it\n * `BIND_PLU_NAME_MAX`, deliberately dropping the `_OFF_` infix every true offset\n * above carries, which is why it is not a member of `BIND_OFF`. A host may declare a\n * longer name than this and x3270 truncates rather than refusing.\n */\nexport const BIND_PLU_NAME_MAX = 8;\n\n/**\n * Decode a maximum-RU byte (x3270's `maxru`, Common/telnet.c:2440-2446).\n *\n * MANTISSA-EXPONENT, not an integer: the high bit doubles as a validity flag and as\n * the top bit of a 4-bit mantissa spanning bits 4-7 (so `(c >> 4) & 0x0f` is always\n * >= 8 on any byte that reaches the multiply), and bits 0-3 are the exponent, giving\n * `mantissa * 2^exponent`. A byte with the high bit clear means \"not specified\" and\n * decodes to 0 -- which is why this returns a number rather than throwing on it.\n *\n * The `& 0x0f` on the shifted mantissa is transcription fidelity with x3270, not a\n * behavioral guard: `c >> 4` on a byte (c <= 0xff) is already <= 15, so the mask\n * cannot change the result for any of the 256 possible inputs. Kept so a reader\n * comparing the two sources finds them identical character for character.\n */\nexport function maxRu(c: number): number {\n if (!(c & 0x80)) return 0;\n return ((c >> 4) & 0x0f) * (1 << (c & 0x0f));\n}\n\n/**\n * Geometry a BIND asked for.\n *\n * `alternate: 'caller'` is size code 0x03, which means \"model-2 default, and the\n * CLIENT's own alternate\". A pure function cannot know the client's model, and\n * resolving it here by importing a default would silently turn every 0x03 BIND into a\n * model 2 -- so the caller substitutes and the type makes forgetting impossible.\n */\nexport interface BindDims {\n readonly defaultRows: number;\n readonly defaultCols: number;\n readonly alternate: { readonly rows: number; readonly cols: number } | 'caller';\n}\n\nexport interface BindImage {\n /** EBCDIC-decoded primary LU name: WHICH APPLICATION you were just connected to. */\n readonly pluName: string;\n readonly maxRuSecondary: number;\n readonly maxRuPrimary: number;\n /** Byte 24 verbatim, kept so a trace can report a code we do not implement. */\n readonly sizeCode: number;\n /** Absent when the size code is one we have no table entry for. */\n readonly dims?: BindDims;\n}\n\n/**\n * Parse a BIND RU (x3270's `process_bind`, Common/telnet.c:2449).\n *\n * Returns null when this is not a BIND at all. EVERY OTHER FIELD IS OPTIONAL and\n * length-guarded, because a short BIND is malformed rather than truncated and there is\n * nothing to salvage -- the same contract as `decodeHeader`, and for the same reason:\n * a client cannot correct a host, and throwing here would surface to the operator as a\n * program check the host never caused.\n *\n * NOTE THE COMPARISON: x3270 guards with `buflen > OFFSET`, not `>=`, so a buffer whose\n * LAST byte is at OFFSET is treated as not reaching it. Preserved exactly, because the\n * boundary is what a fixture from a real host will land on.\n */\nexport function parseBind(body: Uint8Array): BindImage | null {\n if (body.length < 1 || body[0] !== BIND_RU) return null;\n\n const maxRuSecondary = body.length > BIND_OFF.MAXRU_SEC\n ? maxRu(body[BIND_OFF.MAXRU_SEC]!) : 0;\n const maxRuPrimary = body.length > BIND_OFF.MAXRU_PRI\n ? maxRu(body[BIND_OFF.MAXRU_PRI]!) : 0;\n\n const sizeCode = body.length > BIND_OFF.SSIZE ? body[BIND_OFF.SSIZE]! : 0;\n const dims = body.length > BIND_OFF.SSIZE ? decodeDims(body, sizeCode) : undefined;\n\n return {\n pluName: decodePluName(body),\n maxRuSecondary,\n maxRuPrimary,\n sizeCode,\n ...(dims === undefined ? {} : { dims }),\n };\n}\n\n/** The size-code table, Common/telnet.c:2485-2521. */\nfunction decodeDims(body: Uint8Array, sizeCode: number): BindDims | undefined {\n const at = (off: number): number => body[off] ?? 0;\n switch (sizeCode) {\n case 0x00:\n case 0x02:\n return {\n defaultRows: MODEL_2.rows, defaultCols: MODEL_2.cols,\n alternate: { rows: MODEL_2.rows, cols: MODEL_2.cols },\n };\n case 0x03:\n return {\n defaultRows: MODEL_2.rows, defaultCols: MODEL_2.cols,\n alternate: 'caller',\n };\n case 0x7e:\n // ONE pair, used for BOTH sizes. Bytes 22-23 are deliberately not read.\n return {\n defaultRows: at(BIND_OFF.RD), defaultCols: at(BIND_OFF.CD),\n alternate: { rows: at(BIND_OFF.RD), cols: at(BIND_OFF.CD) },\n };\n case 0x7f:\n return {\n defaultRows: at(BIND_OFF.RD), defaultCols: at(BIND_OFF.CD),\n alternate: { rows: at(BIND_OFF.RA), cols: at(BIND_OFF.CA) },\n };\n default:\n // x3270 clears bind_state here: a code we do not know is not a geometry we may\n // guess at. The BIND is still valid; it simply carries no dimensions.\n return undefined;\n }\n}\n\n/**\n * The primary LU name: WHICH APPLICATION the host has just connected us to.\n *\n * Four guards, each matching x3270 (Common/telnet.c:2562-2582) and each with a\n * distinct failure it prevents:\n * - the buffer must REACH the length byte (`buflen > BIND_OFF_PLU_NAME_LEN`, :2562);\n * - a declared length over 8 is CLAMPED, as x3270 clamps it, not refused (:2564-2565);\n * - a declared length of 0 means no name, not an empty one followed by stray bytes\n * (the `namelen > 0` half of :2567's `&&`);\n * - a declared length that does not leave enough buffer for the name yields nothing\n * at all (the other half of :2567's `&&`) -- see the note below for the exact bound\n * used here, which is NOT x3270's literal one.\n *\n * DELIBERATE DEPARTURE FROM x3270's LITERAL BOUND: x3270's C guard is\n * `buflen > BIND_OFF_PLU_NAME + namelen` -- STRICT greater-than. Copying `namelen`\n * bytes starting at offset 28 only touches indices up to `28 + namelen - 1`, so\n * `buflen >= 28 + namelen` (one byte less than x3270 demands) is everything the copy\n * itself needs; x3270's `>` refuses a buffer that ends exactly on the name's last\n * byte even though every byte the copy would read is present and valid. That reads as\n * an off-by-one bug in x3270 itself, and here it is NOT reproduced: this file's own\n * \"decodes EBCDIC\" test (bind.test.ts, `withName` with its default `declaredLen`)\n * builds a buffer sized to exactly `PLU_NAME + bytes.length` -- i.e. it sits exactly on\n * this boundary -- and is required to succeed. Matching x3270's `>` verbatim would make\n * that mandatory test fail, so the bound used below is `>=`\n * (`body.length < BIND_OFF.PLU_NAME + namelen` to reject), the logically-correct one,\n * not x3270's. See bind.test.ts's dedicated boundary test for the case this documents.\n */\nfunction decodePluName(body: Uint8Array): string {\n if (body.length <= BIND_OFF.PLU_NAME_LEN) return '';\n let namelen = body[BIND_OFF.PLU_NAME_LEN]!;\n if (namelen > BIND_PLU_NAME_MAX) namelen = BIND_PLU_NAME_MAX;\n if (namelen === 0) return '';\n if (body.length < BIND_OFF.PLU_NAME + namelen) return '';\n const slice = body.subarray(BIND_OFF.PLU_NAME, BIND_OFF.PLU_NAME + namelen);\n return cp037.decode(slice);\n}\n\n/** An UNBIND's reason, if it carried one. */\nexport interface UnbindInfo {\n /** Undefined when the body was empty: \"no reason given\", not reason zero. */\n readonly reason: number | undefined;\n /** The host is handing us to another application and a BIND is coming. */\n readonly forthcoming: boolean;\n}\n\n/**\n * Parse an UNBIND body (x3270's UNBIND handling, Common/telnet.c:2745-2765; the\n * reason byte itself is read at :2749-2750, gated the same way this function gates\n * it -- only when there is a byte to read).\n *\n * UNBIND is teardown WITH THE TCP CONNECTION STILL UP: bound state clears, the BIND's\n * sizing reverts, the screen erases, and we wait for another BIND. It is not a\n * disconnection and must not be treated as one.\n */\nexport function parseUnbind(body: Uint8Array): UnbindInfo {\n const reason = body.length > 0 ? body[0]! : undefined;\n return {\n reason,\n forthcoming: reason === Tn3270eUnbindReason.BIND_FORTHCOMING,\n };\n}\n\n/** Whether a BIND's geometry may be applied, and if not, why not. */\nexport interface BindDimsVerdict {\n readonly ok: boolean;\n /** Present when refused: a phrase for the trace and the OIA. */\n readonly why?: string;\n}\n\n/**\n * How long to wait for a BIND after granting BIND-IMAGE, before giving up and\n * executing what the host already sent.\n *\n * x3270 HAS NO SUCH TIMEOUT: with BIND-IMAGE granted and no BIND, it drops 3270 data\n * indefinitely (Common/telnet.c:2681) and the session appears wedged. We decline to\n * inherit that.\n *\n * FIVE SECONDS, AND THE REASON IS NOT MARGIN FOR ITS OWN SAKE. Every host in x3270's\n * trace collection sends its BIND in the same turn as FUNCTIONS, so one round-trip\n * would nearly always do -- but most of this client's users are on emulated hardware\n * and some are on REAL 370-class iron (P/370s and similar), which is slow, and those\n * are exactly the users with no other working client. A short timeout would punish\n * them and nobody else.\n *\n * TUNABLE ON PURPOSE, and documented in the README: the person who needs it longer is\n * running vintage hardware and is the least likely to be reading this source.\n */\nexport const NO_BIND_TIMEOUT_MS = 5000;\n\n/**\n * x3270's `bind_limit` range check (Common/telnet.c:2526-2557; verified against the\n * source, not just the plan citing it -- the plan's line range was off by three at\n * the top and one at the bottom, but the content matches).\n *\n * FOUR separate checks, not two: default over the model, default under model 2,\n * alternate over the model, alternate under model 2 (telnet.c:2527, :2533, :2539,\n * :2545). A valid default does not license an invalid alternate. Any failure keeps\n * OUR geometry entirely -- x3270's `else` branch that assigns `defROWS`/`altROWS`\n * etc. (telnet.c:2552-2555) is skipped entirely on any of the four `if`/`else if`\n * branches above it, so on failure x3270 does not clamp, it just leaves the\n * previous `defROWS`/`altROWS` alone; we do the same by returning a verdict rather\n * than a geometry.\n *\n * THE CONSEQUENCE IS COUNTERINTUITIVE AND IS NOT A BUG: the upper bound is the\n * configured model (`maxROWS`/`maxCOLS`) and the lower bound is model 2\n * (`MODEL_2_ROWS`/`MODEL_2_COLS`, 3270ds.h:446-447), so ON A MODEL 2 THEY COINCIDE\n * and BIND geometry is pinned to exactly 24x80. With the limit on, BIND can only\n * ever narrow a large model toward 24x80; it can never grow one past `-model`.\n * Growing past the model is oversize's job, and letting BIND do it here would\n * silently make this feature into that one.\n *\n * `alternate: 'caller'` never reaches this function with real BIND traffic: the\n * caller substitutes its own model first, and its own model is by definition in\n * range. Accepting it here anyway is a defensive default for the case where a\n * caller forgets, since the substitute would have been the model itself.\n */\nexport function acceptBindDims(\n dims: BindDims,\n model: { readonly rows: number; readonly cols: number },\n): BindDimsVerdict {\n const alt = dims.alternate;\n if (alt === 'caller') {\n return { ok: true };\n }\n const d = `${dims.defaultRows}x${dims.defaultCols}`;\n const a = `${alt.rows}x${alt.cols}`;\n const max = `${model.rows}x${model.cols}`;\n const min = `${MODEL_2.rows}x${MODEL_2.cols}`;\n if (dims.defaultRows > model.rows || dims.defaultCols > model.cols) {\n return { ok: false, why: `BIND default ${d} exceeds model ${max}` };\n }\n if (dims.defaultRows < MODEL_2.rows || dims.defaultCols < MODEL_2.cols) {\n return { ok: false, why: `BIND default ${d} is below ${min}` };\n }\n if (alt.rows > model.rows || alt.cols > model.cols) {\n return { ok: false, why: `BIND alternate ${a} exceeds model ${max}` };\n }\n if (alt.rows < MODEL_2.rows || alt.cols < MODEL_2.cols) {\n return { ok: false, why: `BIND alternate ${a} is below ${min}` };\n }\n return { ok: true };\n}\n", "import { Sfid, PID_QUERY, ReadPartitionType, ReqTyp, REQTYP_MASK } from '../constants.js';\n\n/**\n * Structured field framing for the OUTBOUND direction \u2014 outbound is the\n * manual's word for host-to-terminal, not ours-to-host. GA23-0059 p. 5-5:\n * \"The outbound structured fields are described under \"Outbound Structured\n * Fields\" / on page 5-11\" (pages.txt:4414-4415), and that section's list\n * includes \"01nn Read Partition\" (pages.txt:4419), the field parsed here. The\n * INBOUND structured fields are the ones we send; see the note on\n * Sfid.QUERY_REPLY in constants.ts, which uses \"inbound\" in that same sense.\n *\n * A Write Structured Field record carries one or more structured fields, each\n * `L L SFID <params...>` where the 16-bit L INCLUDES the two length bytes:\n * GA23-0059 p. 5-5 (pages.txt:4399-4400) \"Each structured field contains a\n * 2-byte length field. This field defines the length of / the structured field\n * (including the length bytes).\"\n *\n * The bytes here have already been un-doubled by the telnet layer\n * (telnet.ts:122), so a 0xFF in a parameter is a single 0xFF.\n */\n\nexport class SfParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SfParseError';\n }\n}\n\n/**\n * The extra parameters a Query List (TYPE=0x03) carries and a Query does not.\n *\n * GA23-0059 p. 5-52 (pages.txt:6371-6373) draws the line: for TYPE \"X'02' The\n * structured field ends after byte 4\", while for \"X'03' Byte 5 is a flag byte.\n * Bytes 6-n contain the QCODEs of the / Query Replies being requested.\" So this\n * is present for exactly one TYPE, which is why it is a separate optional member\n * rather than two more always-present numbers on readPartition.\n */\nexport interface QueryListParams {\n /**\n * REQTYP, already MASKED to bits 0-1 \u2014 compare against ReqTyp, not the raw\n * byte. See the note on REQTYP_MASK in constants.ts for why we mask where\n * x3270 does not.\n */\n readonly reqtyp: number;\n /**\n * QCODEs from byte 6 on, in the order the host sent them. MAY BE EMPTY, and\n * an empty list is meaningful rather than degenerate: under REQTYP=QCODE List\n * it selects the Null Query Reply (p. 5-52, pages.txt:6377-6379, \"If the value\n * is B'00' but no list is present (count field is valid), a Null Query Reply\n * is returned\"; x3270 sf.c:258-262 `if (buflen < 7) ... do_query_reply(\n * QR_NULL)`).\n *\n * Duplicates are NOT removed here. They are legal \u2014 \"It is not invalid for a\n * particular QCODE to appear / more than once in the list\" (pages.txt:8540-\n * 8541, p. 6-20) \u2014 and de-duplication belongs where the reply is built, since\n * the rule is about the REPLIES (\"the 3270 device or / workstation does not\n * return duplicate Query Replies\", pages.txt:8542-8544), not about the\n * request. Keeping the raw order also keeps the trace faithful.\n */\n readonly qcodes: readonly number[];\n}\n\nexport type StructuredField =\n /**\n * Read Partition. We answer TYPE=QUERY and TYPE=QUERY_LIST, both with\n * PID=0xFF; see the stage 2a spec and the Query List work that followed it.\n */\n | { kind: 'readPartition'; pid: number; type: number; queryList?: QueryListParams }\n /**\n * `SF_TRANSFER_DATA` (SFID 0xd0): a DFT file-transfer frame.\n *\n * `payload` is the PARAMETERS \u2014 the length bytes and the SFID are already\n * gone \u2014 so a `Data Insert`'s offsets in x3270's `ft_dft.c` are 3 more than the\n * offset here. **The `Open` is the exception and its offsets are unchanged**,\n * because `dft_open_request` receives a pointer already 3 bytes in;\n * `ft/dftFrames.ts` owns both rules and explains why.\n */\n | { kind: 'transferData'; payload: Uint8Array }\n /** Any SFID we do not implement: counted and traced, never fatal. */\n | { kind: 'unknownSf'; sfid: number; data: Uint8Array };\n\n/**\n * Smallest legal structured field: two length bytes and a one-byte SFID.\n *\n * GA23-0059 p. 5-5 (pages.txt:4409-4412): \"Except for the use of a Length\n * parameter value of zero, a structured field with a / one-byte type parameter\n * will be rejected if the Length field value is less than / three.\" Every SFID\n * we recognize is one byte \u2014 Read Partition's is X'01' at byte 2\n * (pages.txt:6345) \u2014 so three is our minimum. The manual's four-byte minimum\n * applies to the two-byte type fields we do not implement, e.g. Begin/End of\n * File, whose table row reads \"2-3 SFID X' OF85' Identifies this structured\n * field as\" (pages.txt:4683; the OCR renders the leading zero as the letter O\n * and inserts a space, so that field is really X'0F85').\n */\nconst MIN_SF_LENGTH = 3;\n\nexport function parseStructuredFields(payload: Uint8Array): StructuredField[] {\n const fields: StructuredField[] = [];\n let i = 0;\n\n while (i < payload.length) {\n if (i + 2 > payload.length) {\n throw new SfParseError(\n `structured field at offset ${i} truncated: ${payload.length - i} byte(s) left, need at least 2 for the length`,\n );\n }\n const declared = (payload[i]! << 8) | payload[i + 1]!;\n\n // A zero length is legal and means \"this field runs to the end of the\n // transmission\". GA23-0059 p. 5-5 (pages.txt:4402-4408): \"A length of zero\n // on an outbound or / inbound 3270DS indicates one of the following: ...\n // The length of the structured field should be determined using the end of\n // the / transmission (tor SNA, transmission =chain).\" [The \"tor\" is OCR\n // damage for \"for\".] x3270 does exactly this substitution, sf.c:138-140:\n // if (fieldlen == 0) {\n // fieldlen = buflen;\n // }\n // Resolving it to the remaining length here also makes the advance below\n // non-zero, so the loop still terminates.\n //\n // The substitution MUST stay ahead of the minimum check below, and the case\n // that proves it is a LEGAL zero-length field, not an illegal one: a Query\n // sent as `00 00 01 ff 02` at the end of a transmission resolves to 5 and\n // parses, but a minimum check applied to the declared 0 would reject it and\n // we would hang on the very request this stage exists to answer. (A bare\n // `00 00` is rejected under either ordering, so it does NOT demonstrate the\n // constraint \u2014 do not use it to convince yourself a reordering is safe.)\n const length = declared === 0 ? payload.length - i : declared;\n\n // An undersized length must be rejected BEFORE it is used to advance, or\n // the loop makes no progress.\n if (length < MIN_SF_LENGTH) {\n // Report the declared bytes when they were zero: `length` is synthesised\n // in that case, and an operator grepping a trace for it would find\n // nothing on the wire.\n throw new SfParseError(\n declared === 0\n ? `zero-length structured field at offset ${i} resolved to ${length} byte(s), below the minimum ${MIN_SF_LENGTH}`\n : `structured field at offset ${i} has length ${length}, below the minimum ${MIN_SF_LENGTH}`,\n );\n }\n if (i + length > payload.length) {\n throw new SfParseError(\n `structured field at offset ${i} has length ${length}, which runs past the end of a ${payload.length}-byte payload`,\n );\n }\n\n const sfid = payload[i + 2]!;\n // Parameters only: excludes the length bytes and the SFID.\n const params = payload.subarray(i + MIN_SF_LENGTH, i + length);\n\n if (sfid === Sfid.READ_PARTITION) {\n // PID at byte 3 and TYPE at byte 4 are both mandatory, so the field is at\n // least 5 bytes long \u2014 GA23-0059 p. 5-51 (pages.txt:6344-6351) gives the\n // format as L(0-1) SFID(2) PIO(3) TYPE(4), and x3270 rejects a shorter\n // one at sf.c:221 `if (buflen < 5)`. [\"PIO\" is OCR damage for \"PID\".]\n if (params.length < 2) {\n throw new SfParseError(\n `Read Partition at offset ${i} needs PID and TYPE, got ${params.length} byte(s)`,\n );\n }\n const pid = params[0]!;\n const type = params[1]!;\n\n // A Query List carries REQTYP at byte 5 and an optional QCODE list from\n // byte 6. Both live in `params`, which starts at byte 3, so REQTYP is\n // params[2] and the list is params[3...]. Off-by-three here would read the\n // PID as a request type.\n //\n // MISSING REQTYP IS AN ERROR, not a defaulted zero. x3270 rejects it\n // explicitly (sf.c:252-255):\n //\n // if (buflen < 6) {\n // trace_ds(\"error: missing request type\\n\");\n // return PDS_BAD_CMD;\n // }\n //\n // and its buflen counts from byte 0, so `< 6` means \"no byte 5\" \u2014 the same\n // condition as params.length < 3 here. Defaulting to B'00' would be worse\n // than rejecting: QCODE List with an absent list means Null Query Reply,\n // so we would answer a malformed request with a positive-looking \"we\n // support nothing\" that the host would believe.\n //\n // Note this check does NOT apply to a plain Query. p. 5-52 says TYPE=0x02\n // \"ends after byte 4\" (pages.txt:6371), so demanding byte 5 of a Query\n // would reject the very request MVS/TSO sends and that path is live-\n // verified. Hence the guard sits inside the QUERY_LIST branch.\n if (type === ReadPartitionType.QUERY_LIST) {\n if (params.length < 3) {\n throw new SfParseError(\n `Read Partition (Query List) at offset ${i} needs REQTYP at byte 5, `\n + `got ${params.length + 3} byte(s) of field`,\n );\n }\n fields.push({\n kind: 'readPartition',\n pid,\n type,\n queryList: {\n // Masked to bits 0-1 at the PARSE boundary so no consumer has to\n // remember to. See REQTYP_MASK in constants.ts.\n reqtyp: params[2]! & REQTYP_MASK,\n // Array.from, not subarray: a Uint8Array view would alias the\n // caller's record buffer, and these values outlive the parse in the\n // ExecuteResult the session acts on.\n qcodes: Array.from(params.subarray(3)),\n },\n });\n } else {\n // PID is RECORDED, not assumed: a non-0xFF value is a read against a\n // real partition, which we do not support, and the trace must show the\n // difference rather than silently treating it as a query.\n //\n // No `queryList` key at all rather than an explicit undefined, which\n // exactOptionalPropertyTypes rejects and which would also make the\n // existing toEqual assertions on this shape fail.\n fields.push({ kind: 'readPartition', pid, type });\n }\n } else if (sfid === Sfid.TRANSFER_DATA) {\n // No validation beyond this: a DFT frame's own shape is dft.ts's business,\n // and a malformed one must fail the TRANSFER, not the record. Copied rather\n // than subarray'd because the payload OUTLIVES this record -- dft.ts retains\n // received chunks and Session holds a transfer across records, so a view into\n // the inbound buffer would be rewritten by the next read.\n fields.push({ kind: 'transferData', payload: Uint8Array.from(params) });\n } else {\n fields.push({ kind: 'unknownSf', sfid, data: Uint8Array.from(params) });\n }\n\n i += length;\n }\n\n return fields;\n}\n\n/**\n * True for a PLAIN Query (TYPE=0x02) against the query PID \u2014 and nothing else.\n *\n * DELIBERATELY STILL NARROW now that Query List is implemented. This predicate\n * does not mean \"a request we answer\"; it means one specific request type, and\n * the caller distinguishes the two because their replies differ. Widening it to\n * match TYPE=0x03 would make a Query List take the plain-Query branch and send\n * the full capability set regardless of REQTYP \u2014 the exact bug the subsetting\n * code exists to avoid, and one that would pass every plain-Query test.\n * See queryListRequest below for the other half.\n *\n * A non-query PID still fails here: it is a read against a real partition, which\n * we do not support.\n */\nexport function isQueryRequest(sf: StructuredField): boolean {\n return sf.kind === 'readPartition'\n && sf.pid === PID_QUERY\n && sf.type === ReadPartitionType.QUERY;\n}\n\n/**\n * The Query List parameters if this is one we should answer, else undefined.\n *\n * Returns the PARAMS rather than a boolean so the caller cannot ask \"is it a\n * Query List?\" and then reach for `.queryList` separately, which under\n * noUncheckedIndexedAccess would need a non-null assertion at every use.\n *\n * The PID check is the same rejection x3270 makes, and it is a REJECTION rather\n * than an ignore \u2014 sf.c:248-251:\n *\n * if (partition != 0xff) {\n * trace_ds(\"error: illegal partition\\n\");\n * return PDS_BAD_CMD;\n * }\n *\n * and the manual lists it among the conditions under which \"Read Partition is\n * rejected\": \"The operation type is Query or Query List and the PIO is not\n * X'FF'\" (pages.txt:6404; \"PIO\" is OCR damage for \"PID\"). Note that covers Query\n * AND Query List with one clause, so both PID checks come from this one line.\n *\n * Undefined for a bad PID rather than throwing, matching isQueryRequest's shape:\n * whether an unanswerable Read Partition is a counted no-op or a program check\n * is the executor's policy decision, not this predicate's. See execute.ts.\n */\nexport function queryListRequest(sf: StructuredField): QueryListParams | undefined {\n if (sf.kind !== 'readPartition') return undefined;\n if (sf.type !== ReadPartitionType.QUERY_LIST) return undefined;\n if (sf.pid !== PID_QUERY) return undefined;\n // Present for every QUERY_LIST the parser emits \u2014 it throws when REQTYP is\n // missing \u2014 so this is a type narrowing, not a real branch.\n if (sf.queryList === undefined) return undefined;\n // B'11' is \"Reserved\" (pages.txt:6361) and there is no defined behavior for\n // it, so it is not a request we can answer. Screened HERE, at the same place\n // as the bad-PID case, so the caller's \"unanswerable\" branch handles both\n // alike and no invalid REQTYP can reach the reply builder.\n //\n // This ordering is load-bearing for session robustness, not just tidiness:\n // selectCapabilities throws a RangeError on a reserved REQTYP, and\n // session.ts handleRecord deliberately RETHROWS anything that is not a\n // ParseError/AddressError/ExecuteError as \"our own bug\" \u2014 which tears the\n // connection down. A host sending B'11' must not be able to do that. Rejecting\n // it before it becomes an sfReply keeps that throw as the unreachable\n // assertion it is meant to be.\n if (!isKnownReqtyp(sf.queryList.reqtyp)) return undefined;\n return sf.queryList;\n}\n\n/** Is this one of the three defined REQTYP values? Expects a masked value. */\nfunction isKnownReqtyp(reqtyp: number): boolean {\n return reqtyp === ReqTyp.QCODE_LIST\n || reqtyp === ReqTyp.EQUIVALENT\n || reqtyp === ReqTyp.ALL;\n}\n", "import { Cmd, SnaCmd, Order } from '../constants.js';\nimport { decodeAddress, AddressError } from '../address.js';\nimport { parseStructuredFields, SfParseError, type StructuredField } from './sf.js';\n\n/**\n * Turn one 3270 record into a command plus a token list. Pure: no Screen, no\n * mutation, no I/O. Anything malformed throws ParseError, which the session\n * turns into a program check.\n */\n\nexport class ParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ParseError';\n }\n}\n\nexport type CommandName =\n | 'Write'\n | 'EraseWrite'\n | 'EraseWriteAlternate'\n | 'EraseAllUnprotected'\n | 'ReadBuffer'\n | 'ReadModified'\n | 'ReadModifiedAll'\n | 'WriteStructuredField'\n | 'NoOp';\n\n/**\n * One SFE/MF attribute type-value pair. GA23-0059 p. 4-4 draws the SFE format\n * as a four-column figure that the OCR flattens to four lines, quoted verbatim\n * (the OCR splits \"Pairs\" into \"Pai rs\"):\n *\n * Number of\n * Attribute\n * Type-Value Attribute Attribute\n * X'29' Pai rs Type Value\n *\n * So: order byte, count of pairs, then that many (type, value) byte pairs.\n *\n * Pairs are kept in wire order, and duplicates are NOT collapsed. Consumers\n * MUST take the LAST occurrence of a repeated attribute type, per p. 4-5\n * (pages.txt:2899-2901): \"All attribute types and values are checked for\n * validity. If the same attribute / type-value pair appears more than once, the\n * last specification for a repeated / attribute type takes effect.\" Use\n * findLast, not find \u2014 the stage 2a plan said find and was wrong.\n *\n * The basic field attribute arrives as a pair of type 0xC0 \u2014 NOT the X'C8' the\n * figure below the format OCRs as. See XA_3270 in constants.ts, which pins the\n * value and records that OCR damage with its page citations.\n */\nexport interface AttributePair {\n type: number;\n value: number;\n}\n\nexport type Token =\n | { kind: 'data'; bytes: Uint8Array }\n | { kind: 'sba'; address: number }\n | { kind: 'sf'; attr: number }\n | { kind: 'ic' }\n | { kind: 'pt' }\n | { kind: 'ra'; stop: number; fill: number; ge: boolean }\n | { kind: 'eua'; stop: number }\n | { kind: 'ge'; ebcdic: number }\n /**\n * SA/MF: recognized so they can be skipped by length, not executed. `order` is\n * narrowed to the two the parser can actually emit, so adding a third deferred\n * order forces every consumer's if/else on it to be revisited rather than\n * letting the new one parse and go silently uncounted.\n */\n | { kind: 'deferred'; order: typeof Order.SA | typeof Order.MF; data: Uint8Array }\n /** SFE with its attribute pairs decoded. Defines a field; see execute.ts. */\n | { kind: 'sfe'; pairs: AttributePair[] }\n /** One structured field from a WSF record, parsed by stream/sf.ts. */\n | { kind: 'structuredField'; field: StructuredField };\n\nexport interface ParsedRecord {\n command: CommandName;\n /** Present only for the write commands. */\n wcc?: number;\n tokens: Token[];\n}\n\n/** Commands that take a WCC byte. */\nconst WRITE_COMMANDS = new Set<CommandName>([\n 'Write', 'EraseWrite', 'EraseWriteAlternate',\n]);\n\nfunction commandOf(byte: number): CommandName | null {\n switch (byte) {\n case SnaCmd.W: case Cmd.W: return 'Write';\n case SnaCmd.EW: case Cmd.EW: return 'EraseWrite';\n case SnaCmd.EWA: case Cmd.EWA: return 'EraseWriteAlternate';\n case SnaCmd.EAU: case Cmd.EAU: return 'EraseAllUnprotected';\n case SnaCmd.RB: case Cmd.RB: return 'ReadBuffer';\n case SnaCmd.RM: case Cmd.RM: return 'ReadModified';\n case SnaCmd.RMA: case Cmd.RMA: return 'ReadModifiedAll';\n // Both WSF encodings, like every other command. Cmd.WSF is 0x11, the same\n // value as Order.SBA \u2014 safe here because this function is only ever called\n // on the command byte, never on an order byte.\n case SnaCmd.WSF: case Cmd.WSF: return 'WriteStructuredField';\n case Cmd.NOP: return 'NoOp';\n default: return null;\n }\n}\n\nexport function parseRecord(record: Uint8Array): ParsedRecord {\n if (record.length === 0) throw new ParseError('empty record');\n\n const cmdByte = record[0]!;\n const command = commandOf(cmdByte);\n if (command === null) {\n throw new ParseError(`unknown command 0x${cmdByte.toString(16).padStart(2, '0')}`);\n }\n\n let i = 1;\n let wcc: number | undefined;\n if (WRITE_COMMANDS.has(command)) {\n if (i >= record.length) throw new ParseError(`${command} with no WCC byte`);\n wcc = record[i++]!;\n }\n\n // Non-SNA WSF is 0x11, the same value as the SBA order; position tells them\n // apart, which is why this check is on the command byte only.\n if (command === 'WriteStructuredField') {\n const data = record.subarray(i);\n try {\n const tokens: Token[] = parseStructuredFields(data)\n .map((field) => ({ kind: 'structuredField', field }));\n return { command, tokens };\n } catch (e) {\n // Surface SF framing errors as ParseError so session.ts maps them to\n // X PROG the same way as every other malformed record. This is not\n // cosmetic: `Session.handleRecord`'s catch program-checks ParseError/AddressError and\n // RETHROWS anything else as \"our own bug\", which drops the connection. An\n // SfParseError escaping here would disconnect on a malformed host record.\n //\n // Rejecting the record as a whole discards well-formed fields ahead of\n // the bad one. x3270 instead dispatches each field as it walks, so an\n // earlier Read Partition has already been answered before it bails on a\n // later length error (sf.c:146-151 return after the per-field dispatch at\n // sf.c:153-183) \u2014 and it calls that out as a wart, sf.c:185-191: \"if we\n // have already / generated some output, then we have already positively /\n // acknowledged the request, so if we fail here, we have no / way to\n // return the error indication.\" Accepted here because a partial WSF is a\n // host bug and X PROG is the honest answer; revisit if a real host is\n // found that splits fields that way.\n if (e instanceof SfParseError) throw new ParseError(e.message);\n throw e;\n }\n }\n\n const tokens: Token[] = [];\n let run: number[] = [];\n\n const flushRun = (): void => {\n if (run.length) {\n tokens.push({ kind: 'data', bytes: Uint8Array.from(run) });\n run = [];\n }\n };\n\n const need = (n: number, what: string): void => {\n if (i + n > record.length) throw new ParseError(`${what} truncated at end of record`);\n };\n\n const address = (what: string): number => {\n need(2, what);\n const b1 = record[i++]!;\n const b2 = record[i++]!;\n try {\n return decodeAddress(b1, b2);\n } catch (e) {\n if (e instanceof AddressError) throw new ParseError(`${what}: ${e.message}`);\n throw e;\n }\n };\n\n while (i < record.length) {\n const b = record[i]!;\n switch (b) {\n case Order.SBA: {\n flushRun();\n i++;\n tokens.push({ kind: 'sba', address: address('SBA') });\n break;\n }\n case Order.SF: {\n flushRun();\n i++;\n need(1, 'SF');\n tokens.push({ kind: 'sf', attr: record[i++]! });\n break;\n }\n case Order.IC: {\n flushRun();\n i++;\n tokens.push({ kind: 'ic' });\n break;\n }\n case Order.PT: {\n flushRun();\n i++;\n tokens.push({ kind: 'pt' });\n break;\n }\n case Order.RA: {\n flushRun();\n i++;\n const stop = address('RA');\n // RA may carry a Graphic Escape before its fill character, in which case\n // the fill is the byte AFTER the GE. x3270 checks for this explicitly\n // (ctlr.c:1739-1746). Taking the byte after the address unconditionally\n // would store 0x08 as the fill and leak the real character out as a\n // stray data byte.\n need(1, 'RA fill character');\n let ge = false;\n if (record[i] === Order.GE) {\n ge = true;\n i++;\n need(1, 'RA GE fill character');\n }\n tokens.push({ kind: 'ra', stop, fill: record[i++]!, ge });\n break;\n }\n case Order.EUA: {\n flushRun();\n i++;\n tokens.push({ kind: 'eua', stop: address('EUA') });\n break;\n }\n case Order.GE: {\n flushRun();\n i++;\n need(1, 'GE');\n tokens.push({ kind: 'ge', ebcdic: record[i++]! });\n break;\n }\n case Order.SA: {\n // Two operand bytes: attribute type and value.\n flushRun();\n i++;\n need(2, 'SA');\n tokens.push({ kind: 'deferred', order: Order.SA, data: Uint8Array.from(record.subarray(i, i + 2)) });\n i += 2;\n break;\n }\n case Order.SFE: {\n // One count byte, then that many type/value pairs (p. 4-4). A count of\n // zero is legal and still defines a field; p. 4-5, quoted with the\n // manual's own line break after \"defaults\":\n // \"If SFE is sent with no type-value pairs (zero value for number of pairs), defaults\n // are set.\"\n // x3270's loop over the count does the same, running zero times without\n // special-casing (ctlr.c:1833).\n flushRun();\n i++;\n need(1, 'SFE count');\n const count = record[i]!;\n const operandLen = 1 + count * 2;\n need(operandLen, 'SFE');\n const pairs: AttributePair[] = [];\n for (let p = 0; p < count; p++) {\n const at = i + 1 + p * 2;\n pairs.push({ type: record[at]!, value: record[at + 1]! });\n }\n tokens.push({ kind: 'sfe', pairs });\n i += operandLen;\n break;\n }\n case Order.MF: {\n // Same wire shape as SFE \u2014 p. 4-7 draws MF with the identical four-column\n // figure, \"Number of / Attribute / Type/Value Attribute Attribute /\n // X'2C' Pai rs Type Value\" (OCR splits \"Pairs\" into \"Pai rs\"; note MF's\n // heading reads \"Type/Value\" where SFE's reads \"Type-Value\"). But MF\n // MODIFIES an existing field rather than defining one \u2014 p. 4-7, quoted\n // with the manual's own line break after \"attributes\":\n // \"The MF order begins a sequence that updates field and extended field attributes\n // at the current buffer address.\"\n // Stage 2a does not implement it. Kept opaque and counted so the live\n // run can show whether any host actually sends it.\n flushRun();\n i++;\n need(1, 'MF count');\n const count = record[i]!;\n const operandLen = 1 + count * 2;\n need(operandLen, 'MF');\n tokens.push({\n kind: 'deferred',\n order: Order.MF,\n data: Uint8Array.from(record.subarray(i, i + operandLen)),\n });\n i += operandLen;\n break;\n }\n default:\n run.push(b);\n i++;\n break;\n }\n }\n flushRun();\n\n return wcc === undefined ? { command, tokens } : { command, wcc, tokens };\n}\n\n/**\n * Nested union: the return type makes a missing SF kind a compile error.\n *\n * Extracted rather than switched inline because a nested switch inside\n * describeRecord's own switch gets no exhaustiveness checking, and a\n * StructuredField variant added by a later stage would silently vanish from the\n * trace. x3270's equivalent dispatch has a case per outbound SFID (sf.c:153-183).\n *\n * \"unknownSF\" not \"SF\": describeRecord's `case 'sf'` below already emits SF( for\n * the Start Field ORDER, so one grep over a trace would return both. x3270 words\n * this one \"unsupported ID 0x%02x\" (sf.c:180).\n */\nfunction describeStructuredField(f: StructuredField): string {\n switch (f.kind) {\n case 'readPartition': {\n // PID is padded like every other hex here: 0x00 is a reachable, meaningful\n // value (a read of partition 0, NOT a query), and \"pid=0x0\" would read as\n // a truncation bug in the emulator rather than a byte off the wire.\n const base = `pid=0x${f.pid.toString(16).padStart(2, '0')}`\n + `,type=0x${f.type.toString(16).padStart(2, '0')}`;\n // REQTYP and the QCODE list are traced when present, because on a Query\n // List they are what decides the reply \u2014 a trace showing only the TYPE\n // could not distinguish \"asked for everything\" from \"asked for one unit we\n // do not have\", and those produce very different records. x3270 traces the\n // same information, e.g. `trace_ds(\"List(\")` then each see_qcode\n // (sf.c:256-267).\n //\n // The reqtyp shown is the MASKED value stream/sf.ts recorded, not the raw\n // byte 5, so a host setting reserved bits 2-7 will show its reqtyp here\n // without them. That is the value we acted on, which is what a trace\n // should show.\n if (f.queryList === undefined) return `ReadPartition(${base})`;\n const qcodes = f.queryList.qcodes\n .map((q) => `0x${q.toString(16).padStart(2, '0')}`).join(',');\n return `ReadPartition(${base}`\n + `,reqtyp=0x${f.queryList.reqtyp.toString(16).padStart(2, '0')}`\n // Empty is spelled out rather than left as `qcodes=`: an absent list is\n // the Null Query Reply trigger (p. 5-52), a meaningful state, not a\n // formatting accident.\n + `,qcodes=[${qcodes}])`;\n }\n case 'transferData': {\n // THE REQUEST TYPE IS TRACED, not just the length, and that is the whole\n // value of this line: the -ddm probe could only report\n // `unknownSF(0xd0,38B)` and the type had to be decoded by hand afterwards to\n // learn the frames were TR_OPEN_REQ. Naming it here is what lets a live run\n // be judged BY TRACE, which is the rule for this feature -- after DFT works,\n // a CUT transfer and a DFT transfer both end in a transferred file and only\n // the trace distinguishes them.\n //\n // The type is read here rather than imported from ft/dftFrames.ts: parse.ts\n // is the outbound stream decoder and must not depend on the transfer engine.\n // A payload too short to hold one is traced as such rather than throwing --\n // describeRecord promises never to throw.\n if (f.payload.length < 2) return `FileTransferData(short,${f.payload.length}B)`;\n const type = ((f.payload[0]! << 8) | f.payload[1]!).toString(16).padStart(4, '0');\n return `FileTransferData(0x${type},${f.payload.length}B)`;\n }\n case 'unknownSf':\n return `unknownSF(0x${f.sfid.toString(16).padStart(2, '0')},${f.data.length}B)`;\n }\n}\n\n/** One-line annotation of a record, for the trace. Never throws. */\nexport function describeRecord(record: Uint8Array): string {\n let parsed: ParsedRecord;\n try {\n parsed = parseRecord(record);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return `unparseable record (${msg})`;\n }\n const parts: string[] = [parsed.command];\n if (parsed.wcc !== undefined) {\n parts.push(`WCC=0x${parsed.wcc.toString(16).padStart(2, '0')}`);\n }\n for (const t of parsed.tokens) {\n switch (t.kind) {\n case 'data': parts.push(`data[${t.bytes.length}]`); break;\n case 'sba': parts.push(`SBA(${t.address})`); break;\n case 'sf': parts.push(`SF(0x${t.attr.toString(16).padStart(2, '0')})`); break;\n case 'ic': parts.push('IC'); break;\n case 'pt': parts.push('PT'); break;\n case 'ra': parts.push(`RA(->${t.stop},${t.ge ? 'GE ' : ''}0x${t.fill.toString(16).padStart(2, '0')})`); break;\n case 'eua': parts.push(`EUA(->${t.stop})`); break;\n case 'ge': parts.push(`GE(0x${t.ebcdic.toString(16).padStart(2, '0')})`); break;\n case 'deferred': parts.push(`deferred(0x${t.order.toString(16)},${t.data.length}B)`); break;\n // Both halves padded like every other hex here: a pair value of 0x00 is a\n // reachable, meaningful byte (type 0xC0 value 0x00 is an unprotected\n // alphanumeric field), and \"0x0\" would read as a truncation bug in the\n // emulator rather than what the host actually sent.\n case 'sfe':\n parts.push(`SFE(${t.pairs.map((p) =>\n `0x${p.type.toString(16).padStart(2, '0')}`\n + `=0x${p.value.toString(16).padStart(2, '0')}`).join(',')})`);\n break;\n case 'structuredField': parts.push(describeStructuredField(t.field)); break;\n }\n }\n return parts.join(' ');\n}\n", "import { WCC, FA, Order, XA, XA_3270, MODEL_3, MODEL_4, MODEL_5 } from '../constants.js';\nimport type { Screen } from '../screen.js';\nimport type { ParsedRecord, Token, CommandName } from './parse.js';\nimport { isQueryRequest, queryListRequest } from './sf.js';\nimport type { QueryRequest } from '../queryreply.js';\n\n/**\n * The running character-attribute state an SA order maintains.\n *\n * A MAP BY TYPE, not a single value, because the manual requires a composite:\n * \"The set of type-value pairs applied during character processing is a\n * composite, by attribute type, of the last value specified in previously\n * encountered SA orders\" (p. 4-7, pages.txt:2995-2996). One value would make an\n * SA color silently clear a preceding SA highlighting.\n *\n * Lives for one write command and is discarded: \"Another write type command is\n * sent\" returns the set to defaults (p. 4-6, pages.txt:2978), which x3270 does by\n * zeroing default_fg/bg/gr at the top of write processing (ctlr.c:1414-1416).\n * Declared inside `execute` for exactly that reason \u2014 module scope would leak it\n * between records, which is reset trigger 2 silently not happening.\n *\n * The Clear key \u2014 the manual's third trigger (pages.txt:2979) \u2014 resets it\n * through Screen.clear() instead, because Clear originates locally rather than in\n * a datastream, so there is no SaState alive to reset when it happens. The fourth\n * trigger is a power switch.\n *\n * A MEMBER'S ABSENCE IS LOAD-BEARING, and is not the same as a zero: absent means\n * \"this type is at its default, write nothing\", where 0x00 under FOREGROUND is\n * `XAC_DEFAULT`, a value the host legitimately set. `delete` rather than `= 0` in\n * the reset paths below for that reason.\n */\ninterface SaState {\n fg?: number;\n bg?: number;\n gr?: number;\n}\n\n/**\n * Every attribute type back to its default.\n *\n * `delete`, not `= 0`: absence means \"unspecified\", where 0x00 is a value the host\n * can legitimately set. See the note on SaState.\n *\n * ONE CALLER, the SA X'00' handler. An earlier version of this comment also\n * claimed \"the plain-SF reset\", which was true when written and is now wrong: the\n * SF case deliberately leaves the running state alone (see the long note in the\n * `'sf'` arm), because clearing character state on a field event conflates the two\n * scopes. Left as a named function rather than inlined because the manual treats\n * \"all attribute types to their defaults\" as one operation.\n */\nfunction resetSa(sa: SaState): void {\n delete sa.fg;\n delete sa.bg;\n delete sa.gr;\n}\n\n/**\n * What each SA attribute type does to the running state.\n *\n * A TABLE RATHER THAN A SWITCH because two callers need the same answer and they\n * MUST NOT DRIFT: the `'deferred'` case decides what to apply, and the token loop\n * decides what to count in `setAttributeIgnored`. With the membership test written\n * out separately, adding a fifth type to one and forgetting the other would make\n * that counter report applied work as dropped \u2014 the exact silent lie its contract\n * exists to rule out. Here `saTypeImplemented` is *derived* from this table, so\n * adding an entry updates both callers at once and forgetting is not possible.\n *\n * Same motive as the `_never` guard in the token loop: make the failure a\n * structural impossibility rather than something a reviewer has to notice.\n *\n * KEYED BY `XA`'s OWN VALUES, not by `number`: under a `Record<number, ...>` a\n * typo'd `0x99` entry would typecheck silently and quietly become \"implemented\".\n * `Partial` because not every `XA` member has a handler \u2014 `XA.CHARSET` deliberately\n * does not.\n *\n * The cost of that tightening is that a wire byte, being a plain `number`, can no\n * longer index this directly; `saHandler` below owns the one widening cast, so the\n * table stays closed to typos while lookups stay honest.\n */\ntype SaHandler = (sa: SaState, value: number) => void;\nconst SA_HANDLERS: Readonly<Partial<Record<(typeof XA)[keyof typeof XA], SaHandler>>> = {\n // \"All character attributes reset all character attribute types that are\n // specifiable in the SA order to their default value. Attribute types affected\n // are color, highlighting, and character set\" (p. 4-18, pages.txt:3449-3456) \u2014\n // so ALL types, not one. Twelve of these are in the committed TK5 fixture, so\n // this path is live. Character set is among the types reset and we store none,\n // so there is nothing to drop for it. x3270 zeroes all five of its defaults in\n // its XA_ALL arm (ctlr.c:1915-1921).\n //\n // The value is ignored deliberately: \"The only valid value setting is X'00'; all\n // others are reserved\" (pages.txt:3452). x3270 does not look at it either.\n [XA.RESET]: (sa) => { resetSa(sa); },\n // These three store the VALUE the host sent, INCLUDING 0x00 \u2014 which under them\n // is XAC_DEFAULT/XAH.DEFAULT, \"device default\", a legitimate setting and NOT a\n // reset. See the TYPE-vs-VALUE warning in constants.ts.\n [XA.FOREGROUND]: (sa, value) => { sa.fg = value; },\n [XA.BACKGROUND]: (sa, value) => { sa.bg = value; },\n [XA.HIGHLIGHTING]: (sa, value) => { sa.gr = value; },\n};\n\n/**\n * The handler for a wire attribute-type byte, or undefined if we drop that type.\n *\n * THE ONE PLACE the table is indexed, so the one place the unavoidable widening\n * lives. `SA_HANDLERS` is keyed by `XA`'s literal union to stop a typo'd entry\n * typechecking (see its note), but a byte off the wire is a plain `number`, so\n * some cast is required to look one up. Confining it here keeps it auditable and\n * keeps both callers below honest \u2014 neither can reach the table directly.\n */\nfunction saHandler(type: number): SaHandler | undefined {\n return (SA_HANDLERS as Readonly<Record<number, SaHandler | undefined>>)[type];\n}\n\n/**\n * Does the SA order apply this attribute type, or drop it?\n *\n * Derived from `SA_HANDLERS` so it cannot disagree with what is actually applied.\n * `XA.CHARSET` (Programmable Symbol Sets, out of scope) and anything unrecognized\n * are false, and stay counted by `setAttributeIgnored`.\n *\n * Presence of a handler, not `Object.hasOwn`, now that the lookup is centralised:\n * one accessor, so \"is it implemented\" and \"what does it do\" cannot answer from\n * different places. An explicitly-undefined entry would count as unimplemented,\n * which is also what applying it would do \u2014 the two still agree.\n */\nfunction saTypeImplemented(type: number): boolean {\n return saHandler(type) !== undefined;\n}\n\n/**\n * Apply a parsed record to a screen.\n *\n * Throws ExecuteError for conditions the hardware treats as a program check\n * (an address beyond the buffer, for instance). The session catches it, shows\n * X PROG in the OIA, and keeps the connection up.\n */\n\nexport class ExecuteError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ExecuteError';\n }\n}\n\nexport interface ExecuteResult {\n /** The host asked us to send something back. */\n readRequest?: Extract<CommandName, 'ReadBuffer' | 'ReadModified' | 'ReadModifiedAll'>;\n /**\n * The host asked what this terminal can do, and we should answer.\n *\n * Carries WHICH request it was, not just that one happened: a plain Query and\n * the three Query List versions produce different sets of units, and the\n * session cannot tell them apart from a bare flag. This replaced the string\n * literal 'queryReply' when Query List landed.\n *\n * The DECISION of which units to send is not made here \u2014 it belongs with the\n * capability list in queryreply.ts, which owns the Table 6-1 rules. This field\n * carries the request; selectCapabilities interprets it.\n */\n sfReply?: QueryRequest;\n /**\n * DFT file-transfer payloads from this record, in arrival order. Surfaced as a\n * REQUEST for the session to answer, exactly as `sfReply` is \u2014 `execute` stays\n * pure and does no I/O.\n *\n * An ARRAY because one `WriteStructuredField` may carry several fields, and\n * taking only the first would lose data silently: a truncated file with nothing\n * in the log.\n */\n transferData?: Uint8Array[];\n /** WCC bit 6: unlock the keyboard. */\n keyboardRestore: boolean;\n /**\n * This command releases the enter-inhibit condition raised by a Query.\n *\n * Distinct from keyboardRestore, and deliberately so. keyboardRestore is the\n * host asking, via WCC bit 6, to unlock EVERYTHING; this is the far narrower\n * \"a write happened, so the screen the Query froze is no longer frozen\", and\n * it holds whether or not the WCC sets that bit. The session applies each\n * separately.\n *\n * True for exactly four commands, which are exactly the four x3270 clears\n * KL_ENTER_INHIBIT on:\n *\n * - Erase/Write and Erase/Write Alternate, via ctlr_erase's opening\n * `kybd_inhibit(false);` (Common/ctlr.c:546-550); process_ds routes both\n * there, `case CMD_EWA: ... ctlr_erase(true);` and `case CMD_EW: ...\n * ctlr_erase(false);` (ctlr.c:615-625).\n * - Erase All Unprotected, via ctlr_erase_all_unprotected (ctlr.c:1303-1309).\n * - Write, via ctlr_write (ctlr.c:1406). Note this one covers all three\n * write commands, since process_ds calls ctlr_write after ctlr_erase for\n * EW and EWA too \u2014 hence EW/EWA clear it twice in x3270, harmlessly.\n *\n * And false for everything else, which is what makes the inhibit outlast a\n * read: process_ds sends CMD_RB/RM/RMA to ctlr_read_buffer/ctlr_read_modified,\n * CMD_WSF to write_structured_field, and CMD_NOP to a trace line and nothing\n * else (ctlr.c:632-657) \u2014 none of the three clearing functions among them.\n */\n releasesEnterInhibit: boolean;\n /** WCC bit 5: sound the alarm. */\n alarm: boolean;\n /** WCC bit 4 asked for a local copy and we have no printer. */\n printerUnavailable: boolean;\n /**\n * The screen changed geometry, so a renderer must re-place everything rather\n * than diff against a buffer of the wrong shape. False for a whole session on\n * a model 2, where the default and alternate sizes are equal.\n */\n resized: boolean;\n /** How many structured fields we skipped, for the trace. */\n structuredFieldsIgnored: number;\n /**\n * SA orders parsed and dropped \u2014 the ones whose attribute TYPE we do not\n * implement, which is `XA.CHARSET` and anything unrecognized.\n *\n * NOT a count of SA orders seen. Color, background, highlighting and the\n * X'00' reset are applied now, and counting those would break what this field\n * is for: a zero must keep meaning \"we never saw one we had to throw away\"\n * rather than \"we stopped looking\". The session reports it as dropped work\n * (`Session.handleRecord`'s `ignored orders:` trace line), so an applied order\n * appearing here would be a false\n * report of a gap.\n *\n * Per-record, and nothing sums these across a session: the live run measures\n * SA and MF by grepping the trace for `deferred(0x28` / `deferred(0x2c`\n * (parse.ts describeRecord emits them), which is what aggregates.\n */\n setAttributeIgnored: number;\n /**\n * MF orders parsed and dropped.\n *\n * A NONZERO VALUE HERE IS A FOLD-INTO-2B SIGNAL: MF modifies an existing\n * field's attributes, so ignoring one can leave a field's protection stale\n * and the operator unable to type where they should. See the stage 2a spec.\n * Measured via the trace grep, as for setAttributeIgnored above.\n */\n modifyFieldIgnored: number;\n /** Set when a recoverable protocol fault occurred. */\n programCheck?: string;\n}\n\nexport function execute(screen: Screen, record: ParsedRecord): ExecuteResult {\n const result: ExecuteResult = {\n keyboardRestore: false,\n releasesEnterInhibit: false,\n alarm: false,\n printerUnavailable: false,\n resized: false,\n structuredFieldsIgnored: 0,\n setAttributeIgnored: 0,\n modifyFieldIgnored: 0,\n };\n\n switch (record.command) {\n case 'NoOp':\n return result;\n\n case 'ReadBuffer':\n case 'ReadModified':\n case 'ReadModifiedAll':\n result.readRequest = record.command;\n return result;\n\n case 'EraseAllUnprotected':\n // Screen.eraseAllUnprotected handles the unformatted case by clearing the\n // whole buffer (x3270 ctlr.c:1443-1445 `else ctlr_clear(true)`), since an\n // unformatted buffer is entirely unprotected.\n screen.eraseAllUnprotected();\n // EAU also unlocks the keyboard and homes the cursor. Use the screen's\n // own helper, which skips zero-length fields the way x3270's\n // next_unprotected does (ctlr.c:623-638) \u2014 a zero-length field's `start`\n // is another field attribute, and parking the cursor there corrupts the\n // buffer on the next keystroke.\n screen.cursor = screen.firstUnprotectedStart() ?? 0;\n result.keyboardRestore = true;\n // Redundant against keyboardRestore just above, which already unlocks\n // everything \u2014 set anyway so the flag means \"this command releases the\n // inhibit\" for all four commands uniformly, rather than \"\u2026except where\n // some other flag happens to cover it\". x3270 is equally redundant here:\n // ctlr_erase_all_unprotected calls kybd_inhibit(false) at its top\n // (ctlr.c:1309) and do_reset(false) at its bottom (ctlr.c:1350), and the\n // latter clears the same bit again (kybd.c:2062).\n result.releasesEnterInhibit = true;\n return result;\n\n case 'WriteStructuredField':\n for (const t of record.tokens) {\n if (t.kind !== 'structuredField') continue;\n // Two predicates, not one, and both check the PID. isQueryRequest is a\n // plain Query (TYPE=0x02); queryListRequest is a Query List (0x03) and\n // hands back its REQTYP and QCODE list. They are kept separate because\n // the replies differ \u2014 see the note on isQueryRequest in stream/sf.ts.\n //\n // The subsetting rules span p. 6-19 AND p. 6-20, not p. 6-19 alone.\n // p. 6-19 introduces the selector \u2014 \"an additional parameter, REQTYP\n // (Request Type), bits 0-1 of byte 5 and, / optionally, a list of\n // QCODES starting at byte 6\" (pages.txt:8508-8509) \u2014 and the rules for\n // each of its three values are the table on p. 6-20. Those rules are\n // implemented in queryreply.ts selectCapabilities, which is where the\n // capability list they filter lives; this case only classifies.\n //\n // WHY THIS MATTERS FOR VM: VM/370's MECAFF IND$FILE asks with a Query\n // List, not a Query, and waits for a reply. While this branch counted\n // 0x03 as ignored, file transfer on VM/CMS hung forever. MVS/TSO sends a\n // plain Query and was unaffected.\n const list = queryListRequest(t.field);\n if (isQueryRequest(t.field)) {\n result.sfReply = { kind: 'query' };\n } else if (list !== undefined) {\n result.sfReply = { kind: 'queryList', reqtyp: list.reqtyp, qcodes: list.qcodes };\n } else if (t.field.kind === 'transferData') {\n // Before the fallback, so a DFT frame is NOT counted in\n // structuredFieldsIgnored: it is answered, not ignored, and a counter\n // that said otherwise would misreport a working transfer in the trace.\n (result.transferData ??= []).push(t.field.payload);\n } else {\n // Everything else: a read against a real partition (non-0xFF PID on a\n // query, which x3270 rejects at sf.c:230-251), a TYPE we do not\n // implement, or an SFID we do not implement. Counted and traced rather\n // than answered \u2014 an unanswered request is honest; a guessed answer is\n // not.\n //\n // Counted rather than program-checked, which is where we knowingly\n // diverge from x3270: it returns PDS_BAD_CMD for a bad PID, and under\n // TN3270E that becomes a negative response (telnet.c:3432-3436). We\n // negotiate no TN3270E yet, so there is nowhere to send one, and\n // dropping the session over a field we can simply ignore would be\n // worse behavior than the trace line. Revisit with stage 2b.\n result.structuredFieldsIgnored++;\n }\n }\n return result;\n\n // EW and EWA are THE screen-size switch, and nothing to do with TN3270E --\n // x3270 does it at `ctlr.c:558-561`, `newROWS = altROWS` against\n // `newROWS = defROWS`, with no reference to the telnet option. On a model 2\n // the two sizes are equal, so both still clear the same buffer and `resized`\n // stays false for the whole session.\n case 'EraseWrite':\n result.resized = screen.useDefaultSize();\n screen.clear();\n result.releasesEnterInhibit = true;\n break;\n\n case 'EraseWriteAlternate':\n result.resized = screen.useAlternateSize();\n screen.clear();\n result.releasesEnterInhibit = true;\n break;\n\n case 'Write':\n result.releasesEnterInhibit = true;\n break;\n }\n\n const wcc = record.wcc ?? 0;\n // WCC reset-MDT is UNCONDITIONAL: manual Table 3-2 bit 7 says \"all MDT bits in\n // the device's existing character buffer are reset\", and x3270's handler\n // (ctlr.c:1439-1451) calls mdt_clear on every field attribute with no\n // protection check. This matters because ctlr_read_modified filters on\n // FA_IS_MODIFIED alone (ctlr.c:822) \u2014 a protected field carrying MODIFY would\n // otherwise leak its data to the host. Erase Input is the unprotected-only\n // one; see Screen.clearUnprotectedMDT.\n if (wcc & WCC.RESET_MDT) screen.clearAllMDT();\n if (wcc & WCC.KEYBOARD_RESTORE) result.keyboardRestore = true;\n if (wcc & WCC.SOUND_ALARM) result.alarm = true;\n if (wcc & WCC.START_PRINTER) result.printerUnavailable = true;\n\n let addr = 0;\n /** True when the previous token wrote something \u2014 PT nulls only then. */\n let wroteSinceOrder = false;\n // Fresh per write command, and deliberately not per session; see SaState.\n // This declaration IS reset trigger 2.\n const sa: SaState = {};\n\n for (const token of record.tokens) {\n // Tally the orders we drop, here rather than in applyToken because\n // applyToken has no access to the result.\n //\n // The commands that return before this loop cannot deliver a deferred token\n // that matters: WSF parses to structuredField tokens only, and NoOp, EAU and\n // Read Buffer/Read Modified/Read Modified All carry no data field at all\n // (NOP is \"sent with no WCC or data\", pages.txt:1730; EAU's format at\n // pages.txt:1951-1958 has no data field), so an SA or MF riding along in one\n // is a malformed record the real hardware would never see. Deferred tokens\n // on those six paths therefore go uncounted, and that is the whole of the\n // gap \u2014 verified by walking every case in the switch above.\n // An exhaustive switch, not an if/else chain: 8017c49 narrowed\n // deferred.order to SA|MF, so the never guard turns \"a third deferred order\n // was added and nothing counts it\" into a compile error. Without it such an\n // order would be silently uncounted, and a zero here would then mean \"we\n // never saw one\" when it actually meant \"we never looked\" \u2014 the precise\n // failure these counters exist to rule out.\n if (token.kind === 'deferred') {\n switch (token.order) {\n case Order.SA:\n // ONLY the types still dropped, now that four of them are applied\n // below. Counting an implemented type would make a nonzero here mean\n // \"some SA arrived\" rather than \"an SA was thrown away\", and the\n // session's `ignored orders:` trace line reports it as the latter.\n // The predicate is shared with the case that does the applying, so the\n // two cannot drift; see saTypeImplemented.\n if (!saTypeImplemented(token.data[0]!)) result.setAttributeIgnored++;\n break;\n case Order.MF: result.modifyFieldIgnored++; break;\n default: { const _never: never = token.order; void _never; }\n }\n }\n addr = applyToken(screen, token, addr, () => { wroteSinceOrder = true; }, wroteSinceOrder, sa);\n if (token.kind !== 'data' && token.kind !== 'ge' && token.kind !== 'ra') {\n wroteSinceOrder = false;\n }\n }\n\n return result;\n}\n\n/**\n * Stamp the running SA state onto a cell just written.\n *\n * Called after `setChar`, never before: `setChar` deliberately leaves extended\n * attributes alone (see its comment) precisely so this can run second.\n *\n * CLEARS FIRST, and that is not redundant. \"Character attributes are associated\n * with a character and not with the character's position in the buffer. Thus,\n * whenever a character is overwritten by a new character (or cleared or erased),\n * the old character attribute is overwritten by the character attribute of the\n * new character\" (p. 4-16, pages.txt:3388-3391). `Screen.setExtended` MERGES, to\n * serve the composite rule, so merging the state alone would leave a previous\n * record's color on a cell this record has just overwritten. x3270 has no such\n * hazard because it stamps all three unconditionally (ctlr.c:2141-2143) through\n * `ctlr_add_fg`, whose `ea_buf[baddr].fg = color` assigns rather than merges\n * (ctlr.c:2852-2867) \u2014 so the clear here is what makes this an assignment,\n * matching x3270 exactly.\n *\n * Note the consequence: this must NOT short-circuit when the state is empty. An\n * empty state means \"default\", and stamping a default over a stale attribute is\n * the whole point.\n */\nfunction applySa(screen: Screen, addr: number, sa: SaState): void {\n screen.clearExtended(addr);\n screen.setExtended(addr, sa);\n}\n\nfunction applyToken(\n screen: Screen,\n token: Token,\n addr: number,\n markWrote: () => void,\n wroteSinceOrder: boolean,\n sa: SaState,\n): number {\n switch (token.kind) {\n case 'sba':\n requireOnScreen(screen, token.address, 'SBA');\n return token.address;\n\n case 'sf':\n // \"If the display receives an SF order, it sets the associated extended\n // field attribute to its default value\" (p. 4-4, pages.txt:2869-2870). That\n // is entirely a FIELD-level rule, and setFieldAttribute already satisfies it\n // by clearing this cell's extended attributes (screen.ts, Task 3).\n //\n // AND THE RUNNING SA STATE IS DELIBERATELY LEFT ALONE. An earlier version\n // called resetSa here, reading \"sets the extended field attribute to its\n // default\" as also clearing character state. It does not, and the two must\n // not be conflated \u2014 the same field-scope/character-scope error as the SFE\n // seeding removed below, in the opposite direction: there a field attribute\n // was given a character lifetime, here a field event was destroying\n // character state.\n //\n // The manual lists exactly four things that reset the SA set, and SF is not\n // among them: \"A new SA order changes it. / Another write type command is\n // sent. / The Clear key is pressed. / Power at the display is switched off\"\n // (p. 4-6, pages.txt:2977-2980), followed by \"These four actions all return\n // the established set ... to their default value\" (:2981-2982) \u2014 \"these\n // four\", so the list is closed. SA state applies to \"subsequently\n // interpreted characters in the data stream\" (:2969-2971), and an SF does not\n // stop them being subsequent.\n //\n // x3270 agrees exactly: its ORDER_SF zeroes the FA CELL's color\n // (START_FIELD at ctlr.c:1394-1398, plus :1486-1487) and never touches\n // default_fg/default_bg/default_gr, whose only assignments are write-command\n // reset (:1414-1416) and the SA order itself (:1905, :1917).\n screen.setFieldAttribute(addr, token.attr);\n return screen.inc(addr);\n\n case 'ic':\n screen.cursor = addr;\n return addr;\n\n case 'data': {\n let a = addr;\n for (const b of token.bytes) {\n screen.setChar(a, b);\n applySa(screen, a, sa);\n a = screen.inc(a);\n }\n markWrote();\n return a;\n }\n\n case 'ge':\n // Stage 1 has no loadable character sets, so a graphic-escaped character\n // is stored as an ordinary byte. When Programmable Symbol Sets land, this\n // is where the cell becomes {kind:'ps',...} instead.\n screen.setChar(addr, token.ebcdic);\n // A GE character carries the running attributes like any other character;\n // x3270 stamps default_fg/bg/gr on it identically (ctlr.c:1739-1741).\n applySa(screen, addr, sa);\n markWrote();\n return screen.inc(addr);\n\n case 'ra': {\n requireOnScreen(screen, token.stop, 'RA');\n // do-while: stop === addr fills the whole buffer, matching x3270.\n // token.ge is deliberately carried and not acted on here: stage 1 has no\n // loadable character sets, so a graphic-escaped fill is stored as the\n // ordinary byte it is. Programmable Symbol Sets (a committed stage 4\n // deliverable) is where the cell would become {kind:'ps',...} instead,\n // and the flag exists so that change stays local to this case.\n let a = addr;\n do {\n screen.setChar(a, token.fill);\n // RA-filled positions are \"subsequently interpreted characters\" and take\n // the running attributes; x3270 stamps them per fill iteration inside its\n // own do-while (ctlr.c:1669-1671).\n applySa(screen, a, sa);\n a = screen.inc(a);\n } while (a !== token.stop);\n markWrote();\n return a;\n }\n\n case 'eua': {\n requireOnScreen(screen, token.stop, 'EUA');\n // Carry the governing attribute forward instead of calling fieldAt per\n // cell: fieldAt is O(size), so a per-cell call over a full buffer is\n // ~3.7M operations. x3270 does the same by tracking current_fa and\n // updating it when it encounters an attribute (ctlr.c:1809-1816).\n let a = addr;\n let protectedHere = screen.fieldAt(a)?.protected ?? false;\n do {\n const attr = screen.attributeAt(a);\n if (attr !== null) {\n // A field attribute: never erased, and it changes what follows.\n protectedHere = (attr & FA.PROTECT) !== 0;\n } else if (!protectedHere) {\n screen.setChar(a, 0x00);\n // NOT applySa: EUA nulls rather than writes, so it must not stamp the\n // running state \u2014 it clears instead. \"Field attributes and extended\n // field attributes are not affected by EUA. Character attributes for\n // every character changed to nulls are reset to their defaults\" (p.\n // 4-11, pages.txt:3165-3166). Needed as its own call because setChar\n // deliberately leaves extended attributes alone; without it a nulled\n // cell keeps the color of the character that used to be there.\n screen.clearExtended(a);\n }\n a = screen.inc(a);\n } while (a !== token.stop);\n return a;\n }\n\n case 'pt': {\n // Advance to the first data cell of the next unprotected field. If the\n // previous token wrote data, null what we skip over.\n let a = addr;\n for (let n = 0; n < screen.size; n++) {\n if (screen.isFieldAttribute(a)) {\n const attr = screen.attributeAt(a)!;\n const isUnprotected = (attr & FA.PROTECT) === 0;\n if (isUnprotected) return screen.inc(a);\n } else if (wroteSinceOrder) {\n screen.setChar(a, 0x00);\n // Same rule as EUA above, and stated for PT explicitly: \"The PT order\n // resets the character attribute to its default value for each\n // character set to nulls\" (p. 4-9, pages.txt:3090-3091). x3270 zeroes\n // fg, bg and gr beside the null (ctlr.c:1555-1560). Guarded by\n // wroteSinceOrder along with the null itself, because a PT that\n // \"immediately follows a command, order, or order sequence\" leaves the\n // buffer unmodified (pages.txt:3088-3089) \u2014 and an attribute is buffer\n // content.\n screen.clearExtended(a);\n }\n a = screen.inc(a);\n }\n return 0; // unformatted: PT homes\n }\n\n case 'sfe': {\n // SFE DEFINES A FIELD. The 0xC0 pair carries the basic field attribute; the\n // color and highlighting pairs become the field's EXTENDED FIELD ATTRIBUTE,\n // stored on the attribute cell, and also seed the running SA state \u2014 see the\n // two uses at the bottom of this case. 0x43 character set is still dropped \u2014\n // Programmable Symbol Sets are out of scope \u2014 as is anything else.\n //\n // A missing 0xC0 pair does NOT mean \"no field\": p. 4-5 says unspecified\n // attribute types take their defaults, so the field exists with attribute\n // 0x00 (unprotected, unintensified, MDT clear). Skipping it would lose\n // the field, which is the failure SFE is implemented to prevent. x3270\n // does exactly this, ctlr.c:1883-1885: `if (!any_fa) { START_FIELD(0); }`\n // \u2014 note 0, not FA.PRINTABLE, even though a real host's plain unprotected\n // field arrives as 0xC0.\n //\n // findLast, not find: p. 4-5 (pages.txt:2899-2901, OCR intact) says \"All\n // attribute types and values are checked for validity. If the same\n // attribute / type-value pair appears more than once, the last\n // specification for a repeated / attribute type takes effect.\" x3270 gets\n // this for free by calling START_FIELD on every 0xC0 it walks past\n // (ctlr.c:1838-1842), so its last one is the one left in the buffer.\n const basic = token.pairs.findLast((p) => p.type === XA_3270);\n screen.setFieldAttribute(addr, basic?.value ?? 0x00);\n\n // Collect the extended pairs. In pair order, assigning as it goes, so a\n // repeat resolves to the last one: \"All attribute types and values are\n // checked for validity. If the same attribute type-value pair appears more\n // than once, the last specification for a repeated attribute type takes\n // effect\" (p. 4-5, pages.txt:2899-2901). Same rule as findLast above, by a\n // different means \u2014 findLast is needed there because setFieldAttribute is\n // called once.\n //\n // XA.RESET is deliberately NOT handled: \"The attribute type X'00' can\n // appear only in the SA order\" (p. 4-18, pages.txt:3456), so in an SFE it is\n // not a reset but an invalid type, and invalid types \"are rejected\"\n // (pages.txt:2897-2898) \u2014 i.e. ignored, leaving the other pairs to stand.\n // x3270 agrees: its SFE arm for XA_ALL traces the pair and advances past it\n // without touching efa_fg/bg/gr (ctlr.c:1869-1871), where its SA arm for the\n // same type zeroes all five defaults (ctlr.c:1915-1921). An earlier draft\n // treated it as a reset here, which would have let a trailing X'00' silently\n // discard a color the host did set.\n const efa: SaState = {};\n for (const p of token.pairs) {\n if (p.type === XA.FOREGROUND) efa.fg = p.value;\n else if (p.type === XA.BACKGROUND) efa.bg = p.value;\n else if (p.type === XA.HIGHLIGHTING) efa.gr = p.value;\n }\n\n // Store them on the FIELD ATTRIBUTE CELL, which is where the EXTENDED FIELD\n // ATTRIBUTE lives, and which is the ONLY place SFE writes them. This is the\n // level a character attribute falls back to: \"If there are field attributes\n // in the character buffer and if a character attribute specifies default for\n // any character property (color, highlighting, or character set), the\n // character is displayed using the value of that property established for the\n // field in the extended field attribute. Otherwise, the character attribute\n // overrides the field attribute\" (p. 4-16, pages.txt:3383-3387).\n //\n // THE FIELD'S COLOR LIVES HERE AND NOWHERE ELSE, so this is what makes it\n // survive at all. A later record that overwrites one character mid-field,\n // with no SFE and no SA, correctly clears that character's own attribute\n // (pages.txt:3388-3391) and must have something to fall back on \u2014 otherwise\n // one colorless cell sits between colored neighbors, inside a field the\n // host still defines as colored.\n //\n // The FA cell is exactly where x3270 puts them, immediately after its pair\n // loop: `ctlr_add_fg(buffer_addr, efa_fg)` and siblings at ctlr.c:1886-1889,\n // with buffer_addr still on the attribute position (it increments at :1891).\n // Its renderer reads them straight back off that cell \u2014 `if (xea[i].fg)\n // fa_fg = xea[i].fg & 0x0f; else fa_fg = color_from_fa(fa)`\n // (fprint_screen.c:581-585), then falls back to it per character at :754-758.\n //\n // Safe to write unconditionally: setFieldAttribute above has just cleared\n // this cell (pages.txt:2869-2870), so an empty `efa` leaves it clean rather\n // than inheriting the previous field's color. CONSUMING this fallback is\n // Task 5's job in render.ts; storing it is this task's.\n screen.setExtended(addr, efa);\n\n // AND DELIBERATELY NOTHING ELSE \u2014 in particular, SFE does NOT seed the\n // running SA state, and must not.\n //\n // THE TWO ATTRIBUTES HAVE DIFFERENT SCOPES, and the running state carries\n // only one of them. SA is character-scoped and follows the buffer address:\n // it applies to \"subsequently interpreted characters in the data stream\"\n // (p. 4-6, pages.txt:2969-2971), wherever those land. An extended FIELD\n // attribute is field-scoped: it belongs to the field, and reaches a character\n // only through the fallback above. Seeding a field-scoped value into the\n // character-scoped running variable gives it the WRONG LIFETIME.\n //\n // An earlier version did seed it, and the bug was real rather than\n // theoretical. Within a single record: a plain SF at 10 with no extended\n // attributes, then SBA 0, SFE(fg=yellow), a character at 1, then SBA to 11\n // and a character at 11. Cell 11 came out yellow \u2014 but it belongs to the\n // plain field at 10, which the host gave no color at all. An SBA (or PT) can\n // move the write address into a DIFFERENT, ALREADY-EXISTING field without\n // passing an SF or SFE, so nothing clears the seeding and a field's color\n // follows the address across the boundary. sa.test.ts pins this.\n //\n // The redundancy could not be waved through as harmless on the grounds that\n // a character attribute is authoritative when set (pages.txt:3386-3387):\n // that licenses redundancy only where the two levels AGREE. Here they\n // disagree, and the wrong one wins precisely because a character attribute\n // genuinely set to yellow beats a field carrying none.\n //\n // x3270 does exactly what this now does: `default_fg`/`default_bg`/\n // `default_gr` appear NOWHERE in its SFE case (ctlr.c:1816-1894) \u2014 its\n // assignments to them are confined to write-command reset (:1414-1416) and\n // the SA order (:1905, :1917) \u2014 and it relies wholly on the FA-cell fallback.\n return screen.inc(addr);\n }\n\n case 'deferred': {\n // MF is still unimplemented and still counted; see modifyFieldIgnored.\n if (token.order === Order.MF) return addr;\n\n // SA. token.data is exactly [type, value] (parse.ts:240-246). An absent\n // handler is a type we do not implement, which the token loop has already\n // counted via saTypeImplemented \u2014 same accessor, so the two cannot disagree.\n saHandler(token.data[0]!)?.(sa, token.data[1]!);\n return addr;\n }\n\n case 'structuredField':\n return addr;\n }\n}\n\n/**\n * The architected model whose alternate size would hold `addr`, if any.\n *\n * Exists because \"SBA address 3279 beyond buffer end 1919\" is a true statement\n * that tells an operator nothing they can act on. The overwhelmingly likely cause\n * of a host addressing past our buffer is that its device is defined as a bigger\n * model than we claimed -- VM/370 takes a display's geometry from DMKRIO, not from\n * our TERMINAL-TYPE, so it will send 43 rows to a client that said 3278-2 and the\n * session dies with a program check on the first SBA past row 24. Naming the model\n * that fits turns a dead session into a one-flag fix.\n */\nfunction modelThatWouldFit(addr: number): string | undefined {\n const candidates = [\n ['3278-3', MODEL_3], ['3278-4', MODEL_4], ['3278-5', MODEL_5],\n ] as const;\n for (const [name, geom] of candidates) {\n if (addr < geom.rows * geom.cols) return `${name} (${geom.rows}x${geom.cols})`;\n }\n return undefined;\n}\n\nfunction requireOnScreen(screen: Screen, addr: number, what: string): void {\n if (addr < 0 || addr >= screen.size) {\n let msg = `${what} address ${addr} beyond buffer end ${screen.size - 1}`;\n // Only for an address PAST the end, and only when a real model would hold it:\n // a negative address is a malformed stream, and an address past even a model 5\n // is not a geometry mismatch either.\n if (addr >= screen.size) {\n const fits = modelThatWouldFit(addr);\n if (fits !== undefined) {\n msg += `. The host is addressing a screen larger than this session's `\n + `${screen.rows}x${screen.cols}; its device is probably defined as a `\n + `${fits}, which -model must match`;\n }\n }\n throw new ExecuteError(msg);\n }\n}\n", "import { AID, Qcode, ReqTyp, Sfid, XAH } from './constants.js';\nimport { Color } from './palette.js';\n\n/**\n * Query Reply, the answer to a host's Read Partition (Query).\n *\n * Built from a CAPABILITY LIST rather than a hardcoded byte blob, so\n * advertising something later is one list entry and the Summary unit cannot\n * disagree with what is actually sent.\n *\n * Every unit here is one we honor, which is the rule that decides what goes in.\n * Stage 2a shipped the minimal honest set \u2014 Summary, Usable Area, Implicit\n * Partition \u2014 and deliberately withheld Color and Highlighting because they\n * invite the SA orders it did not implement. Now that SA and the four-level\n * color resolution are in, the two are honest and are advertised.\n *\n * ADVERTISING THEM IS NOT WHAT MAKES HOST COLOR ARRIVE. MVS 3.8j TK5 sends SA\n * color whether or not we ask \u2014 the committed trace fixture has 113 SA orders\n * against a client that advertised neither unit \u2014 so these two are for\n * correctness with better-behaved hosts, not a prerequisite. x3270 sends ten\n * units; we still send five. See the stage 2a design doc.\n */\n\nexport interface ScreenGeometry {\n /** The DEFAULT (Erase/Write) size. */\n readonly rows: number;\n readonly cols: number;\n /**\n * The ALTERNATE (Erase/Write Alternate) size. Absent means \"same as default\",\n * which is a model 2 and what every caller predating alternate-size support\n * means.\n *\n * WHICH UNITS USE WHICH, verified against x3270's sf.c rather than guessed,\n * because getting it inconsistent would advertise two different maxima in one\n * reply: Usable Area is the MAXIMUM (`sf.c:718-719` writes maxCOLS/maxROWS),\n * BUFFSZ is the MAXIMUM product (`sf.c:731`, which x3270 itself labels\n * \"questionable\"), and only Implicit Partition carries both -- default then\n * alternate (`sf.c:919-922`).\n */\n readonly alternate?: { readonly rows: number; readonly cols: number };\n}\n\n/** The alternate size, defaulting to the default size. */\nfunction alt(g: ScreenGeometry): { readonly rows: number; readonly cols: number } {\n return g.alternate ?? { rows: g.rows, cols: g.cols };\n}\n\nexport interface Capability {\n readonly qcode: number;\n /**\n * Unit body AFTER `L L SFID QCODE` \u2014 the builder writes that prefix.\n *\n * `all` is EVERY capability we support, NOT the subset being sent in this\n * particular reply. Only Summary reads it, and that distinction is the whole\n * point: see the note on `summary` below.\n */\n readonly params: (geometry: ScreenGeometry, all: readonly Capability[]) => number[];\n /**\n * Does a plain Read Partition (Query) return this unit?\n *\n * This is Table 6-1's \"Query\" column (p. 6-21, pages.txt:8584+), and it is\n * genuinely per-reply rather than universal: Begin/End of File is \"No X'9F'\n * No Yes\" (pages.txt:8587) and Graphic Symbol Sets is \"No X'B6' No Yes\"\n * (pages.txt:8604) \u2014 returned for All, never for a Query or an Equivalent.\n *\n * REQUIRED, not optional-defaulting-to-true, deliberately. All three units we\n * ship today are \"Yes\", so a default would be correct for every existing\n * entry and silently wrong for the first \"No\" someone adds \u2014 and the failure\n * would be invisible: an Equivalent reply carrying a unit a Query would never\n * return, which is exactly the regression this field exists to prevent. A\n * required field makes the author look the row up in Table 6-1 and turns\n * forgetting into a compile error.\n */\n readonly returnedForQuery: boolean;\n}\n\n/**\n * What the host asked for, distilled from the parsed Read Partition.\n *\n * Lives here rather than in stream/sf.ts because the RULES for turning it into\n * units live here, next to the capability list they filter. stream/sf.ts owns\n * the bytes; this owns the meaning.\n */\nexport type QueryRequest =\n /** Read Partition TYPE=0x02. */\n | { readonly kind: 'query' }\n /** Read Partition TYPE=0x03. reqtyp is already masked to bits 0-1. */\n | { readonly kind: 'queryList'; readonly reqtyp: number; readonly qcodes: readonly number[] };\n\n/**\n * Big-endian 16-bit, range-checked.\n *\n * Throwing rather than masking, which is what encodeAddress and encodeAttribute\n * do at comparable boundaries. Masking was silently wrong in three ways that all\n * produced a plausible-looking record a host would act on: a negative cols\n * emitted X'FFFF', which the telnet layer then IAC-doubles into wire garbage; a\n * fractional geometry truncated W/H while BUFFSZ kept the fraction, so the reply\n * contradicted itself; and a params list over 65531 bytes wrapped L.\n *\n * This does NOT reject zero, and must not: X'0000' is a legal value for several\n * fields in this reply (the reserved base bytes, and BUFFSZ on a partitioned\n * device per p. 6-104). A zero screen DIMENSION is separately illegal, and\n * buildQueryReply enforces that where it belongs.\n */\nconst u16 = (n: number): number[] => {\n if (!Number.isInteger(n) || n < 0 || n > 0xffff) {\n throw new RangeError(`not a 16-bit value: ${n}`);\n }\n return [(n >> 8) & 0xff, n & 0xff];\n};\n\n/**\n * Self-defining parameter: `L SDPID <params>`, where L counts itself.\n *\n * Derived, not hand-written, for the same reason buildQueryReply derives the\n * outer length. Hardcoding X'0B' means a later SDP edit has to remember to\n * update a magic number, and nothing but the x3270 fixture would notice.\n */\nconst sdp = (id: number, params: number[]): number[] => [2 + params.length, id, ...params];\n\n/**\n * Summary (QCODE 0x80), GA23-0059 p. 6-96 for the format, p. 6-20 for the rule.\n *\n * Format is just the generic Query Reply header plus a list: \"4-N LIST List of\n * supported QCODES\" (pages.txt:11421, p. 6-96). That page's QCODE row is OCR\n * rubble \u2014 \"3 QCODE x\u00B7ao\u00B7 Identifies this Query Reply as Summary\"\n * (pages.txt:11419) \u2014 which is another reason 0x80 comes from the prose.\n *\n * Every device must support it \u2014 \"All 3270 data stream devices or workstations\n * must support the Summary Query Reply, QCODE = X '80'\" (pages.txt:8568-8569,\n * p. 6-20; note the OCR spaces inside X '80') \u2014 and it lists the QCODEs of all\n * supported replies INCLUDING its own: \"(The QCODE for the Summary Query Reply\n * itself is also included in the list.)\" (pages.txt:8572-8573). That falls out\n * of mapping the capability list, which is the point of deriving it.\n *\n * Do not take Summary's QCODE from Table 6-1: that row OCRs as \"Summary Yes\n * X'BO' Yes Yes\" (pages.txt:8635), and 0xB0 is a real QCODE (Segment) so the\n * damage is not self-evident. See the note on Qcode in constants.ts.\n *\n * SUMMARY LISTS EVERYTHING WE SUPPORT, NOT WHAT THIS PARTICULAR REPLY CARRIES.\n * That is the opposite of what the Query List brief for this work assumed, and\n * the sources are unambiguous on it \u2014 twice over:\n *\n * 1. The manual defines the content as total capability, not reply contents:\n * \"The Summary Query Reply provides / a list of the QCODEs of all the Query\n * Replies supported by the 3270 data stream / device or workstation.\"\n * (pages.txt:8570-8572, p. 6-20; emphasis on \"all ... supported\").\n * 2. The next sentence gives the REASON, and it is a functional one rather\n * than a matter of taste: \"The Summary Query Reply provides the host with\n * the only / indication of support of functions where the associated Query\n * Reply is returned in / reply to a Query List = QCODE List or All.\"\n * (pages.txt:8574-8576.) Summary is how a host discovers what it may ASK\n * for next. Narrowing the list to the current reply would make a QCODE-List\n * reply advertise a device that supports only what was just requested, and\n * a host walking Summary to plan a second request would never learn about\n * the rest.\n *\n * x3270 agrees and does not vary by request: do_qr_summary loops the whole\n * reply table (sf.c:699-708), `for (i = 0; i < NSR; i++) ... *obptr++ =\n * replies[i].code`, with no reference to the QCODE list that selected it. So a\n * QCODE-List request for just 0x81 gets an x3270 Summary listing all eleven.\n *\n * This is why Capability.params receives `all` \u2014 the full support list \u2014 and\n * not the subset being sent. Do NOT \"fix\" that to the subset: it would look\n * more self-consistent and would be wrong.\n *\n * SUMMARY IS ALSO ALWAYS SENT, whatever the request. p. 6-96 states it as an\n * unconditional rule: \"The Summary Query Reply must always be sent inbound in\n * reply to a Read Partition / structured field specifying Query, or Query List\n * (QCODE List=X'80', / Equivalent, or All).\" (pages.txt:11409-11411.) Note that\n * covers QCODE List, so a list naming only 0x81 still gets Summary. buildReply\n * enforces this; see the note there, which is also where x3270's narrower\n * behavior is recorded.\n */\nconst summary: Capability = {\n qcode: Qcode.SUMMARY,\n returnedForQuery: true, // Table 6-1: \"Summary Yes ... Yes Yes\" (pages.txt:8635)\n params: (_geometry, all) => all.map((c) => c.qcode),\n};\n\n/**\n * Usable Area (QCODE 0x81), GA23-0059 p. 6-101.\n *\n * L L SFID QCODE FLAGS FLAGS W W H H UNITS Xr(4) Yr(4) AW AH BUFFSZ(2).\n * \"Bytes 0 through 20 must always be included in the Usable Area Query Reply\n * Base. Bytes 21 through 26 (parameters BUFFSZ and XMIN, YMIN, XMAX, YMAX) must\n * be present if any self-defining parameters are included.\"\n * (pages.txt:11645-11647, p. 6-102.)\n *\n * We send NO self-defining parameters and set no VCP flag, so by that rule\n * BUFFSZ is optional for us \u2014 p. 6-104 says \"If there are no self-defining\n * parameters, no variable character cell parameters, and the BUFFSZ parameter is\n * not applicable, then both the BUFFSZ and the XMIN, YMIN, XMAX, YMAX can be\n * omitted\" (pages.txt:11783-11785). We include it anyway: BUFFSZ IS applicable\n * to us, because p. 6-104 scopes it to exactly our case \u2014 \"It applies only to a\n * device that does not support partitions\" (pages.txt:11779-11780) \u2014 and we\n * support no partitions. Including it is also what x3270 does and what this host\n * accepted. Total 23 bytes = x3270's L=0x17.\n *\n * The fixed values are x3270's (Common/sf.c:711-732, do_qr_usable_area), which\n * this host accepted.\n * They are dimensional constants of the device, not capability claims, so\n * copying them advertises nothing we do not honor. Each is checked against the\n * manual's byte table below.\n */\nconst usableArea: Capability = {\n qcode: Qcode.USABLE_AREA,\n // Table 6-1: \"Usable Area Yes X'81' Yes Yes\" (pages.txt:8638) \u2014 that row is\n // OCR-clean, unlike Summary's.\n returnedForQuery: true,\n params: (geometry) => [\n // FLAGS byte 4. Bits 4-7 are ADDR; X'1' = \"12/14-bit addressing allowed\"\n // (pages.txt:11601). PP, HC and the reserved bits are all zero: we are a\n // display, not a page printer and not a hard copy device. This is a real\n // claim and we honor it \u2014 address.ts decodeAddress handles both the 14-bit\n // binary and 12-bit coded forms and rejects the reserved 10 flag.\n 0x01,\n // FLAGS byte 5. VCP=0 (variable cells not supported \u2014 which is why no\n // XMIN/YMIN/XMAX/YMAX follow BUFFSZ), CHAR=0 (matrix character),\n // CELLUNITS=0. NOTE the manual's CELLUNITS wording, pages.txt:11612-11614:\n // B'0' means bytes 6-9 are in CELLS, B'1' means pels. So W and H below are\n // cell counts, which is what makes 80x24 correct there.\n 0x00,\n // The MAXIMUM, not the default: x3270 writes maxCOLS/maxROWS here\n // (sf.c:718-719). Reporting the default would tell a host the alternate size\n // it is about to be offered does not fit in the usable area.\n ...u16(alt(geometry).cols), // 6-7 W: width of usable area, in cells\n ...u16(alt(geometry).rows), // 8-9 H: height of usable area, in cells\n // 10 UNITS. The manual's values are \"X'OO' Inches\" / \"X'01' Millimeters\"\n // (pages.txt:11619-11620; the O in X'OO' is OCR of a zero). So 0x01 does\n // mean millimeters, matching x3270's own comment \"units (mm)\" (sf.c:720).\n //\n // KNOWN INCONSISTENCY, inherited deliberately from x3270 and NOT a typo\n // here. This byte says millimeters, but the Xr/Yr fractions below are\n // inch-scaled, so the pair does not describe a physical device. Worked out:\n // Xr = 10/741 is 1/74.1, i.e. 74.1 pel centers per unit; at 9 pels per cell\n // and 80 cells that is 9.7 units across. Read as INCHES the screen is\n // 9.7 x 5.2 in, an 11-inch diagonal, which is a real 3279-2. Read as\n // MILLIMETERS it is 9.7 x 5.2 mm, which is absurd. The manual's own worked\n // example settles the scale: \"UNITS X'OO' / Xr X' 00020091 ' (2/145 inch)\"\n // for a device with \"72.5 pels/inch horizontally\" (pages.txt:11762-11765),\n // and 2/145 is exactly 1/72.5 \u2014 the same inches-per-pel form as 10/741.\n //\n // Left as-is on purpose: these four values are byte-identical to what this\n // host ACCEPTED from x3270, they are inert until someone implements\n // graphics (x3270 flags them \"If we ever implement graphics, these will need\n // to change\", sf.c:724-727), and changing UNITS to 0x00 would trade a\n // documented inconsistency for an undocumented deviation from the reference.\n // Anyone doing graphics work must revisit UNITS and Xr/Yr together.\n 0x01,\n // 11-14 Xr: \"Distance between points in X direction as a fraction, measured\n // in UNITS, with 2-byte numerator and 2-byte denominator\" (pages.txt:11626).\n // 0x000a / 0x02e5 = 10/741. x3270's Xr_3279_2 = 0x000a02e5 (sf.c:56).\n // NOTE it is the PAIR 10 and 741, not one 32-bit quantity \u2014 reading it as\n // a single number is the mistake to avoid.\n 0x00, 0x0a, 0x02, 0xe5,\n // 15-18 Yr: same numerator/denominator form in the Y direction.\n // 0x0002 / 0x006f = 2/111, i.e. 55.5 pel centers per unit. x3270's\n // Yr_3279_2 = 0x0002006f (sf.c:57).\n 0x00, 0x02, 0x00, 0x6f,\n // 19 AW: \"Number of X units in default cell\" (pages.txt:11632). \"X units\"\n // are the pel pitches defined by Xr, so this is 9 pels of cell width \u2014 NOT\n // a count of millimeters or inches. x3270's SW_3279_2 = 0x09 (sf.c:54).\n 0x09,\n // 20 AH: \"Number of Y units in default cell\" (pages.txt:11633), so 12 pels\n // of cell height. 9x12 is the standard 3279 cell. SH_3279_2 = 0x0c (sf.c:55).\n 0x0c,\n // 21-22 BUFFSZ: \"Character buffer size (bytes)\" (pages.txt:11634). Cells\n // and bytes coincide for a non-DBCS display of this size. x3270 writes\n // maxCOLS*maxROWS here and flags it \"buffer, questionable\" (sf.c:731).\n ...u16(alt(geometry).rows * alt(geometry).cols),\n ],\n};\n\n/**\n * Implicit Partition (QCODE 0xA6), GA23-0059 p. 6-71.\n *\n * NESTED, and the nesting is easy to get wrong. The BASE is\n * `L L SFID QCODE FLAGS FLAGS` with bytes 4-5 \"FLAGS X'OOOO' Reserved\"\n * (pages.txt:10528, p. 6-71 \u2014 those O characters are OCR of zeros), and the\n * Sizes-for-Display self-defining parameter sits inside it (p. 6-72):\n * `L=X'OB' SDPID=X'01' FLAGS=X'OO' WD WD HD HD WA WA HA HA`\n * (pages.txt:10558-10566). 6 + 11 = 17, which is x3270's L=0x11. Omitting the\n * two reserved bytes shifts every byte after them.\n *\n * SDPID 0x01 is display-specific: the printer form of this parameter is\n * \"SDPID X'03'\" at p. 6-73 (pages.txt:10596), so 0x01 is not a generic \"sizes\"\n * tag and picking it is a claim that we are a display.\n *\n * Alternate equals default deliberately: p. 6-72 requires that a device with no\n * alternate screen size report the default as its alternate \u2014 \"If the device\n * does not have an alternate screen size, the value for the alternate screen\n * size must be that of the default screen size.\" (pages.txt:10548-10549).\n * Stage 2a does not implement mid-session resize, so claiming a second size\n * would be a lie the host would act on.\n *\n * NOTE we differ from x3270 in DERIVATION, not in bytes: x3270 hardcodes the\n * default as literal 80 and 24 (sf.c:919-920) and uses maxCOLS/maxROWS for the\n * alternate. Its two alternate comments are also swapped \u2014 sf.c:921 writes\n * maxCOLS labeled \"alternate height\", sf.c:922 writes maxROWS labeled\n * \"alternate width\" \u2014 so do not read those labels as authority for the field\n * order; the manual's WA-then-HA table is. We drive all four from the geometry,\n * which equals x3270's output at 24x80 and stays self-consistent off it.\n */\nconst implicitPartition: Capability = {\n qcode: Qcode.IMPLICIT_PARTITION,\n // Table 6-1: \"Implicit Partition Yes X'A6' Yes Yes\" (pages.txt:8608).\n returnedForQuery: true,\n params: (geometry) => [\n 0x00, 0x00, // base bytes 4-5 FLAGS: reserved, X'0000'\n // SDPID X'01', then FLAGS and the four sizes. L comes out as X'0B' \u2014 the\n // manual's value \u2014 because sdp() counts itself: 2 + 9.\n ...sdp(0x01, [\n 0x00, // SDP FLAGS: reserved\n ...u16(geometry.cols), ...u16(geometry.rows), // WD HD \u2014 default\n ...u16(alt(geometry).cols), ...u16(alt(geometry).rows), // WA HA \u2014 alternate\n ]),\n ],\n};\n\n/**\n * Color (QCODE 0x86), GA23-0059 p. 6-36 (pages.txt:9198+, NOT p. 6-38).\n *\n * Body after the header: `FLAGS NP` then NP (CAV, CI) pairs\n * (pages.txt:9214-9225).\n *\n * - FLAGS is zero. Its only defined bit is PRTBLK, \"Printer only - black ribbon\n * is loaded\" (pages.txt:9217-9221); we are a display and everything else in\n * the byte is reserved. x3270 calls it \"no options\" (sf.c:743).\n * - NP = 16, x3270's color_max (sf.c:738).\n * - THE FIRST PAIR IS MANDATORY AND IS NOT AN IDENTITY: \"All devices that send\n * Query Reply (Color) are required to have the values CAV1 = X' 00', Cl 1 =\n * value associated with the device default color, as the first entry\"\n * (pages.txt:9268-9270). CAV1 = X'00' is XAC_DEFAULT, the \"device default\"\n * color VALUE \u2014 the same 0x00 render.ts's usableColor rejects so that\n * resolution falls through to the base map. CI1 must name a real color, and\n * may be anything \"except X' 00'\" (pages.txt:9266-9267).\n * - CI1 IS GREEN, not white and not black. x3270 writes `0xf0 +\n * HOST_COLOR_GREEN` = 0xF4 (sf.c:746, 3270ds.h:317), and p. 6-36's color\n * table gives \"Green X'F4'\" (pages.txt:9251). Taken from Color.GREEN rather\n * than written as a literal because it must agree with render.ts: level 4\n * resolves an ordinary unprotected normal-intensity field to GREEN, so the\n * advertised default is the color we actually paint.\n * - The remaining fifteen are IDENTITY pairs, 0xF1..0xFF. The manual allows\n * either identity or the device default \u2014 \"The device must either display the\n * color whose color identifier is the same as the color attribute value or\n * display the device default color\" (pages.txt:9236-9238) \u2014 and identity is\n * the honest answer here, because PALETTE_3279 has an entry for every one of\n * the fifteen.\n *\n * WE ALWAYS ADVERTISE THE IDENTITY, where x3270 emits 0x00 as the CI outside\n * mode3279 (sf.c:747-754). Our own capture was taken in that monochrome state,\n * so the fixture reads `00 f4 f1 00 f2 00 ...` \u2014 a real divergence, not an\n * oversight, and the reason is structural: x3270's mode3279 is a MODEL choice\n * that also selects its terminal type (\"327\" + '9' vs '8', model.c:135-137,\n * telnet.c:2104), so for x3270 the negotiation and the rendering travel\n * together. Ours do not. TERMINAL_TYPE is IBM-3278-2-E regardless of mode3279,\n * which is a per-render presentation flag with no path into this module.\n * Gating on it would make the same session advertise different color support\n * depending on a rendering option the host cannot see.\n *\n * It also stays TRUE under mode3279: false, which is why the divergence is safe\n * rather than merely convenient. CI does not mean \"I will light this pixel\"; it\n * identifies \"the colors that are displayed or printed ... for each of the\n * accepted color attribute values\" (pages.txt:9233-9235), and Screen stores the\n * CAV per cell whatever a later renderer chooses to do with it.\n *\n * NO Default Background Color self-defining parameter. x3270 can append a 4-byte\n * `04 02 00 f0` when its screen has a background color and appres.qr_bg_color is\n * set (sf.c:756-765); the captured x3270 did not, and neither do we \u2014 we have no\n * settable default background to report.\n */\nconst color: Capability = {\n qcode: Qcode.COLOR,\n // Table 6-1: \"Color Yes X'86' Yes Yes\" (pages.txt:8589).\n returnedForQuery: true,\n params: () => {\n // NP is DERIVED from the list below, not written as 0x10, for the same\n // reason sdp() counts itself and buildQueryReply derives L: a hand-written\n // count is a magic number that a later edit to the pairs would leave stale,\n // and a host that trusts NP would then mis-parse the rest of the record.\n const pairs = [[0x00, Color.GREEN]];\n for (let cav = 0xf1; cav <= 0xff; cav++) pairs.push([cav, cav]);\n return [0x00, pairs.length, ...pairs.flat()];\n },\n};\n\n/**\n * Highlighting (QCODE 0x87), GA23-0059 p. 6-65 (pages.txt:10304+, NOT p. 6-53).\n *\n * Body after the header: `NP` then NP (Vi, Ai) pairs \u2014 \"NP Number of\n * attribute-value/action pairs\", \"Vi Data stream attribute value accepted\",\n * \"Ai Data stream action\" (pages.txt:10343-10345).\n *\n * Five pairs, matching x3270's do_qr_highlighting (sf.c:769-784), and the first\n * is DEFAULT -> NORMAL rather than DEFAULT -> DEFAULT. Both halves of that are\n * required reading:\n *\n * - The X'00' entry is compulsory: \"If a device accepts the highlight\n * attribute, then it must accept attribute value X' 00' (default\n * specification).\" (pages.txt:10312-10313.)\n * - Answering NORMAL names the action outright. An action of X'00' would mean\n * \"the device action for the corresponding attribute value is the same as the\n * action for the attribute value X '00' (the default action of the device)\"\n * (pages.txt:10329-10331) \u2014 true but circular. x3270 answers XAH_NORMAL\n * (sf.c:774-775) and so do we.\n *\n * The other four are identities, and each is a claim render.ts honors: it sets\n * exactly one of blink/reverse/underscore/intensify by equality against these\n * values. Exclusivity is the architecture's, not ours: \"This structured field\n * indicates that the device supports highlighting on an exclusive basis. That\n * is, one and only one of the highlight values can be applied to a field or\n * character location.\" (pages.txt:10326-10328.)\n *\n * INTENSIFY (X'F8') is the value to double-check rather than the one to doubt:\n * Chapter 4's highlighting table omits it entirely (pages.txt:3487-3498) and only\n * this unit's section lists it (pages.txt:10314-10325). See the trap note on XAH\n * in constants.ts.\n */\nconst highlighting: Capability = {\n qcode: Qcode.HIGHLIGHTING,\n // Table 6-1: \"Highlighting Yes X'87' Yes Yes\" (pages.txt:8605).\n returnedForQuery: true,\n params: () => {\n // Derived NP again \u2014 see the note in `color`.\n const pairs: number[][] = [\n [XAH.DEFAULT, XAH.NORMAL],\n [XAH.BLINK, XAH.BLINK],\n [XAH.REVERSE, XAH.REVERSE],\n [XAH.UNDERSCORE, XAH.UNDERSCORE],\n [XAH.INTENSIFY, XAH.INTENSIFY],\n ];\n return [pairs.length, ...pairs.flat()];\n },\n};\n\n/**\n * DFT's buffer size: the default, and the bounds a caller-supplied value is\n * clamped to.\n *\n * `DFT_BUF 16384` (globals.h:417, inside an `#if !defined` so it is overridable\n * at build time), `DFT_MIN_BUF 256` and `DFT_MAX_BUF 32767` (globals.h:419-420).\n * x3270 applies the bounds in `set_dft_buffersize` (ft_dft.c:728-748), which is\n * also where the default is chosen, and `do_qr_ddm` calls it with 0 when no\n * transfer is running (sf.c:897).\n */\nexport const DFT_BUF_DEFAULT = 16384;\nexport const DFT_BUF_MIN = 256;\nexport const DFT_BUF_MAX = 32767;\n\n/** Clamp a DFT buffer size, mirroring set_dft_buffersize (ft_dft.c:740-747). */\nexport function boundDftBufferSize(size: number): number {\n if (size > DFT_BUF_MAX) return DFT_BUF_MAX;\n if (size < DFT_BUF_MIN) return DFT_BUF_MIN;\n return size;\n}\n\n/**\n * Distributed Data Management (QCODE 0x95) \u2014 the DFT advertisement.\n *\n * NOT in DEFAULT_CAPABILITIES: it is added only when `-ddm on` is passed, which\n * is the whole point of the flag. A host chooses DFT over CUT on seeing this\n * unit, so advertising it unconditionally would silently move every live host\n * off the CUT path that this project's transfer evidence was gathered on. See\n * docs/superpowers/specs/2026-09-24-dft-file-transfer-design.md.\n *\n * Body after `L L SFID QCODE`, from do_qr_ddm (sf.c:899-906) and the manual's\n * byte table (pages.txt:9812-9822):\n *\n * 4-5 FLAGS reserved, X'0000'\n * 6-7 LIMIN max DDM bytes/transmission inbound\n * 8-9 LIMOUT max DDM bytes/transmission outbound\n * 10 NSS number of subsets supported = 1\n * 11 DDMSS subset identifier = X'01', \"DDM Copy Subset 1\"\n *\n * The manual names bytes 6-7 UMIN and 8-9 LIMOUT in its table and then calls\n * them UMIN/UMOUT in the prose (pages.txt:9818-9819 against :9827-9838) \u2014 OCR\n * of LIMIN/LIMOUT, which is what x3270 calls them. X'0000' in either would mean\n * \"no implementation limit\" (pages.txt:9830, :9838); we send a real bound\n * because we will have one.\n *\n * NSS and DDMSS are a single SET16 of 0x0101 in x3270 (sf.c:906) with the\n * comment `/* NSS=01, DDMSS=01 *\\/`. Written as two bytes here because they are\n * two fields, and X'01' is the only defined DDMSS value (pages.txt:9844-9848).\n */\nexport function ddmCapability(bufferSize = DFT_BUF_DEFAULT): Capability {\n const size = boundDftBufferSize(bufferSize);\n return {\n qcode: Qcode.DDM,\n // Table 6-1: \"Distributed Data Management Yes X'95' Yes Yes\"\n // (pages.txt:8595) \u2014 Query column \"Yes\".\n returnedForQuery: true,\n params: () => [\n 0x00, 0x00, // FLAGS: reserved\n (size >> 8) & 0xff, size & 0xff, // LIMIN\n (size >> 8) & 0xff, size & 0xff, // LIMOUT\n 0x01, // NSS\n 0x01, // DDMSS: DDM Copy Subset 1\n ],\n };\n}\n\n/**\n * `base` with the DDM unit inserted in ascending-QCODE position.\n *\n * Inserted before Implicit Partition (0xA6) rather than appended, because wire\n * order is list order and ascending QCODE is what makes our captures comparable\n * with x3270's (see DEFAULT_CAPABILITIES). The manual imposes no ordering\n * requirement (pages.txt:8534-8539).\n */\nexport function withDdm(\n base: readonly Capability[],\n bufferSize?: number,\n): readonly Capability[] {\n const ddm = ddmCapability(bufferSize);\n const at = base.findIndex((c) => c.qcode > ddm.qcode);\n if (at < 0) return [...base, ddm];\n return [...base.slice(0, at), ddm, ...base.slice(at)];\n}\n\n/**\n * What we advertise. Adding a unit is one entry here.\n *\n * ORDER IS WIRE ORDER: buildQueryReply emits units in list order, so inserting\n * Color and Highlighting before Implicit Partition changed the inbound byte\n * stream. That is deliberate and it matches the order x3270 sends (its reply\n * table runs 0x80, 0x81, ..., 0x86, 0x87, ..., 0xa6, sf.c:82-92), which is also\n * ascending QCODE. The manual imposes no requirement \u2014 \"There is no requirement\n * as to the order of the QCODES ... or the order that the requested Query\n * Replies are / returned\" (pages.txt:8534-8539) \u2014 so this is for comparability\n * with x3270 captures, not conformance.\n */\nexport const DEFAULT_CAPABILITIES: readonly Capability[] = [\n summary, usableArea, color, highlighting, implicitPartition,\n];\n\n/**\n * The complete inbound record: AID 0x88 then each unit, length-prefixed.\n *\n * One AID for the whole set, not one per unit: \"If the structured field is one\n * of a set of Query Reply structured fields, only the first is preceded by an\n * AID of X'88'.\" (pages.txt:8648-8649, p. 6-22.)\n *\n * The AID is 0x88, which is AID.SF. It is NOT AID.QREPLY (0x61), whose name in\n * that same enum reads as though it belonged here. 0x61 is the AID on a Read\n * Modified or Read Buffer that a Read Partition triggers \u2014 x3270 passes\n * AID_QREPLY to ctlr_read_modified and ctlr_read_buffer (sf.c:314, :324, :332),\n * never to a Query Reply. Different case entirely.\n *\n * Returns pure 3270 data. IAC doubling is the telnet layer's job (telnet.ts:82,\n * in sendRecord), which matters here because 0xFF appears in real reply content\n * \u2014 see the Color unit in the x3270 fixture.\n */\nexport function buildQueryReply(\n capabilities: readonly Capability[],\n geometry: ScreenGeometry,\n /**\n * Everything the device supports, when that differs from what is being SENT.\n *\n * Only Summary reads it, and it exists because Summary's content is defined as\n * total capability rather than reply contents \u2014 \"a list of the QCODEs of all\n * the Query Replies supported by the 3270 data stream / device or workstation\"\n * (pages.txt:8570-8572, p. 6-20). Without this parameter a QCODE-List reply's\n * Summary would advertise only the units that request happened to select, and\n * a host walking Summary to plan its next request would never learn the rest\n * \u2014 the exact failure p. 6-20 warns about when it calls Summary \"the only /\n * indication of support\" for QCODE-List-returned functions (pages.txt:8574).\n *\n * DEFAULTS TO `capabilities`, which is right for the plain-Query path where\n * the two are the same list, and keeps this a compatible addition.\n */\n supported: readonly Capability[] = capabilities,\n): Uint8Array {\n // Nonzero is a GEOMETRY rule, not an encoding rule, so u16 is the wrong place\n // for it: X'0000' is a legitimate 16-bit field elsewhere in this very reply\n // (p. 6-104 has BUFFSZ \"set to zero\" for a partitioned device, and X'0000'\n // is the required value of the two reserved base bytes). What is illegal is a\n // zero SCREEN dimension \u2014 p. 6-72, \"Default and alternate values must be\n // nonzero\" \u2014 and a zero would otherwise sail through u16 unnoticed.\n // The alternate is checked too, and by the same rule: p. 6-72 says \"Default and\n // alternate values must be nonzero\", so a zero alternate is exactly as illegal\n // as a zero default and would otherwise sail through u16 unnoticed.\n for (const [name, value] of [\n ['rows', geometry.rows], ['cols', geometry.cols],\n ['alternate.rows', alt(geometry).rows], ['alternate.cols', alt(geometry).cols],\n ] as const) {\n if (!Number.isInteger(value) || value <= 0) {\n throw new RangeError(`geometry.${name} must be a positive integer, got ${value}`);\n }\n }\n const out: number[] = [AID.SF];\n for (const cap of capabilities) {\n // `supported`, NOT `capabilities`: see the parameter's own note. Passing the\n // subset here is the bug that makes a QCODE-List Summary under-report, and\n // it is invisible on the plain-Query path where the two lists are equal.\n const params = cap.params(geometry, supported);\n // Length covers the two length bytes, SFID, QCODE and the parameters.\n const length = 4 + params.length;\n out.push(...u16(length), Sfid.QUERY_REPLY, cap.qcode, ...params);\n }\n return Uint8Array.from(out);\n}\n\n/**\n * The Null Query Reply: \"we support none of the QCODEs you listed.\"\n *\n * Five bytes on the wire, four of them the structured field. The unit's own\n * table, p. 6-77 (pages.txt:10768-10771), is fully legible for once:\n *\n * 0-1 L X'0004' Length of this structured field\n * 2 SFID X'81' Identifies this structured field as a Query Reply\n * 3 QCODE X'FF' Identifies this Query Reply as Null\n *\n * so the field is `00 04 81 ff` and buildQueryReply's AID.SF precedes it, giving\n * `88 00 04 81 ff`. Note L=4 with NO parameter bytes, which is why the Null\n * capability's params returns the empty array and why buildQueryReply's\n * `4 + params.length` produces the manual's value without a special case.\n *\n * BUILT THROUGH THE SAME PATH AS EVERY OTHER REPLY, not hand-assembled. x3270\n * does the same \u2014 do_qr_null's entire body is a trace line (sf.c:688-692), with\n * the framing supplied by do_query_reply's common `obptr += 2; *obptr++ =\n * SFID_QREPLY; *obptr++ = code` and its length fixup (sf.c:658-684). One framing\n * path means the AID, the SFID and the length arithmetic cannot drift between\n * this reply and the others.\n *\n * The 0xFF is CONTENT and gets IAC-doubled by the telnet layer on its way out,\n * exactly as the 0xFF in x3270's Color unit does. That is sendRecord's job\n * (telnet.ts:82), not ours; a caller comparing these bytes to a wire capture\n * must un-double first.\n */\nconst nullReply: Capability = {\n qcode: Qcode.NULL,\n // Table 6-1 gives Null \"No X'FF' No No\" (pages.txt:8612) \u2014 it is returned in\n // response to no request type as a matter of SUPPORT. It reaches the wire only\n // by the explicit \"nothing matched\" path in buildReply, never by enumeration,\n // which is why it is absent from DEFAULT_CAPABILITIES and why this is false.\n returnedForQuery: false,\n params: () => [],\n};\n\nexport function buildNullQueryReply(geometry: ScreenGeometry): Uint8Array {\n // No `supported` argument: the Null reply carries no Summary, so nothing reads\n // it. The default (equal to the sent list) is inert here.\n return buildQueryReply([nullReply], geometry);\n}\n\n/**\n * Choose the units to send for a Query or a Query List, then build the reply.\n *\n * THE ONE ENTRY POINT the session should use. The three REQTYP versions are\n * p. 6-20's table (pages.txt:8526-8560) and x3270's dispatch is sf.c:244-305.\n *\n * WHY SUMMARY IS FORCED IN, and where we knowingly differ from x3270. p. 6-96 is\n * unconditional: \"The Summary Query Reply must always be sent inbound in reply\n * to a Read Partition / structured field specifying Query, or Query List (QCODE\n * List=X'80', / Equivalent, or All).\" (pages.txt:11409-11411.) x3270 does NOT\n * honor that for a QCODE List \u2014 its filter is a plain membership test,\n * sf.c:268-272:\n *\n * for (i = 0; i < NSR; i++) {\n * if (memchr(&buf[6], (char)replies[i].code, buflen-6) && ...) {\n * do_query_reply(replies[i].code);\n *\n * so a list naming only 0x81 gets 0x81 alone and no Summary. We follow the\n * MANUAL here rather than x3270, for the reason p. 6-20 gives: Summary is \"the\n * only / indication of support of functions where the associated Query Reply is\n * returned in / reply to a Query List = QCODE List or All\"\n * (pages.txt:8574-8576) \u2014 omit it and a host has no way to discover what else to\n * ask for. It is also strictly safer: an extra Summary is a unit the host asked\n * about implicitly and can ignore, whereas a missing one removes information.\n *\n * This does mean a QCODE-List reply can carry a unit that was not listed. That\n * is intended and is not a duplicate-reply violation; the no-duplicates rule\n * (pages.txt:8542-8544) is about sending the SAME QCODE twice, which the\n * de-duplication below prevents.\n *\n * A note on what \"matching\" means for the QCODE list: p. 5-52 says \"All QCODE\n * values in the list are valid. Those QCODEs not supported are / ignored.\"\n * (pages.txt:6395-6396) \u2014 an unsupported QCODE is skipped, never an error. So\n * there is no validation of the host's list, only intersection.\n */\nexport function selectCapabilities(\n request: QueryRequest,\n capabilities: readonly Capability[],\n): readonly Capability[] {\n // Query and Equivalent are the same selection, and expressing it once is the\n // point. p. 6-20 defines Equivalent as \"Requests the 3270 device or\n // workstation to return / the same Query Replies that would be returned in\n // reply to a Query\" (pages.txt:8545-8547), so the two share this filter by\n // DERIVATION rather than by coincidence.\n //\n // FILTERED ON returnedForQuery, not \"return everything\". Today every entry is\n // true, so this is an identity map and Equivalent equals All \u2014 a fact about\n // our current three units, NOT a property of the protocol. Writing the filter\n // now is what stops a later \"No\" capability (Begin/End of File, say) from\n // silently leaking into an Equivalent reply. Deleting it because it looks like\n // a no-op would reintroduce exactly that bug.\n const queryEquivalent = (): readonly Capability[] =>\n capabilities.filter((c) => c.returnedForQuery);\n\n if (request.kind === 'query') return queryEquivalent();\n\n switch (request.reqtyp) {\n case ReqTyp.ALL:\n // \"Requests the 3270 data stream device or / workstation to return all the\n // Query Replies / supported. The Query List = All can contain a / QCODE\n // list. However, the QCODE list is ignored\" (pages.txt:8553-8557). Hence\n // `capabilities` unfiltered and request.qcodes untouched \u2014 the list is not\n // consulted, not even to intersect. p. 5-52 puts it as \"the All flag\n // overrides the list\" (pages.txt:6388-6389).\n //\n // Unfiltered means All is the one path that may include a unit a Query\n // would not, which is Table 6-1's whole third column.\n return capabilities;\n\n case ReqTyp.EQUIVALENT: {\n // \"Optionally, a list of QCODES / can also be included.\" (pages.txt:8546-\n // 8547.) Since every unit we have is Query-returnable, that list can add\n // nothing today \u2014 the union below is already the whole capability list.\n // It is written as a union anyway so the semantics stay right when a\n // non-Query-returnable capability arrives: p. 5-52 says Equivalent sends\n // the Query set \"in addition to / those QCODEs (if any) that are specified\n // in the QCODE list\" (pages.txt:6381-6382).\n //\n // Order is preserved from `capabilities`, not from the host's list, so\n // Summary stays first as it does on every other path. The manual permits\n // any order \u2014 \"There is no requirement as to the order of the QCODES ...\n // or the order that the requested Query Replies are / returned\"\n // (pages.txt:8534-8539) \u2014 but consistency keeps the tests and traces\n // comparable across request types.\n const listed = new Set(request.qcodes);\n return capabilities.filter((c) => c.returnedForQuery || listed.has(c.qcode));\n }\n\n case ReqTyp.QCODE_LIST: {\n // \"Contains a list of one or more Query Reply / QCODES. The 3270 data\n // stream device or / workstation returns all the requested Query / Replies\n // (QCODES listed) that are supported. If / none of the requested Query\n // Replies are / supported, a Null Query Reply is returned.\"\n // (pages.txt:8529-8534.)\n //\n // AN EMPTY LIST IS THE NULL CASE, and it arrives here rather than being\n // rejected. p. 5-52 is explicit: \"If the value / is B'00' but no list is\n // present (count field is valid), a Null Query Reply is / returned.\"\n // (pages.txt:6377-6379.) x3270 short-circuits it before even looking,\n // sf.c:258-262:\n //\n // if (buflen < 7) {\n // trace_ds(\")\\n\");\n // do_query_reply(QR_NULL);\n //\n // where buflen < 7 means \"no byte 6\", i.e. an absent list. We need no\n // special case for it: an empty `listed` matches nothing, `chosen` comes\n // back empty, and the empty check below yields the Null reply. Same\n // output, one path.\n //\n // The Set also delivers the no-duplicates rule for free: a list naming\n // 0x81 three times still yields one unit, because we iterate\n // `capabilities` and test membership rather than iterating the host's list.\n // That is x3270's structure too (its memchr runs per reply, not per listed\n // QCODE) and it is the reason duplicates need no explicit handling.\n const listed = new Set(request.qcodes);\n const chosen = capabilities.filter((c) => listed.has(c.qcode));\n // Nothing matched: the Null Query Reply, and NOT a lone Summary. The\n // always-send-Summary rule from p. 6-96 does not rescue this case, because\n // the Null unit's OWN section, p. 6-77, has a worked example that forbids\n // it \u2014 for a device supporting A, B, C asked for X, Y, Z, \"The device\n // sends the Null Query Reply because the device does not support any / of\n // the requested features.\" (pages.txt:10761-10762). A reply of Summary\n // alone would say \"supported nothing you asked for\" too, but by a form no\n // host is required to read that way. Emit nothing else alongside it\n // either: the unit's whole purpose is to be a bare four-byte negative.\n if (chosen.length === 0) return [nullReply];\n // RETURN EXACTLY WHAT WAS ASKED FOR. No forced Summary.\n //\n // An earlier version of this function prepended Summary unconditionally,\n // reading p. 6-96's \"The Summary Query Reply must always be sent inbound in\n // reply to a Read / Partition structured field specifying Query, or Query\n // List (QCODE List=X'80', / Equivalent, or All)\" (pages.txt:11409-11411) as\n // \"always, whatever the list says\". **That reading was wrong.**\n //\n // `X'80'` there is SUMMARY'S OWN QCODE, not a REQTYP value, so the sentence\n // means \"when the QCODE list names 0x80\". The manual uses the identical\n // boilerplate for every unit with that unit's code substituted \u2014 the Null\n // reply says `QCODE List=X'FF'` (pages.txt:10745-10746) and Begin/End of\n // File says `QCODE List=X'9F'` (pages.txt:8801-8802). Reading 0x80 as a\n // request type would force those to mean something incoherent.\n //\n // The tell was internal: the `chosen.length === 0` branch above already\n // reads `X'FF'` correctly as a QCODE, and this branch then read `X'80'` the\n // other way, thirty lines apart in the same function.\n //\n // x3270 gets this right and we now match it: its QCODE-List arm emits only\n // codes found in the host's list, `if (memchr(&buf[6], replies[i].code,\n // buflen-6))`, then the Null reply if none matched (Common/sf.c:268-277).\n // Forcing Summary in would send a unit the host did not ask for.\n return chosen;\n }\n\n default:\n // B'11' is \"Reserved\" (pages.txt:6361). x3270 rejects an unknown request\n // type with PDS_BAD_CMD (sf.c:301-303), which is a program check, so the\n // caller raises one rather than inventing a reply. Returning an empty\n // array instead would put a bare AID on the wire, which is not a legal\n // record.\n throw new RangeError(\n `unsupported Query List REQTYP 0x${request.reqtyp.toString(16).padStart(2, '0')}`,\n );\n }\n}\n\n/**\n * Build the reply to a Query or Query List in one step.\n *\n * The session calls this and nothing else. Keeping selection and building\n * behind one function means the Null case cannot be reached with a stray AID and\n * no units, and that Summary's \"must always be sent\" rule has exactly one\n * enforcement point.\n */\nexport function buildReply(\n request: QueryRequest,\n capabilities: readonly Capability[],\n geometry: ScreenGeometry,\n): Uint8Array {\n // The third argument is what keeps Summary honest: the first is the subset\n // being SENT, the third is everything SUPPORTED. They differ for exactly the\n // QCODE-List case, and conflating them was a real defect this function's tests\n // caught. See buildQueryReply's `supported` parameter.\n return buildQueryReply(\n selectCapabilities(request, capabilities), geometry, capabilities);\n}\n", "import { TERMINAL_TYPE, MODEL_2, MODEL_3, MODEL_4, MODEL_5 } from './constants.js';\n\n/**\n * Terminal type resolution for the telnet TERMINAL-TYPE subnegotiation.\n *\n * The `-E` suffix means EXTENDED DATA STREAM, a 3270 capability claim inside\n * the terminal-type string. It is NOT the TN3270E telnet option (40), which is\n * stage 2b; conflating the two produced a wrong diagnosis once already.\n *\n * MVS 3.8j TSO rejects a bare IBM-3278-2 with IKT00405I and accepts\n * IBM-3278-2-E, but claiming -E invites a Read Partition (Query) that must be\n * answered \u2014 hence Query Reply landing in the same stage.\n */\n\nexport class TerminalTypeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TerminalTypeError';\n }\n}\n\n/** A model: what to call it on the wire, and what its alternate size is. */\nexport interface Model {\n readonly ttype: string;\n /**\n * The ALTERNATE (Erase/Write Alternate) size. The default size is 24x80 for\n * every model -- see the note on MODEL_3 in constants.ts -- so this is the only\n * geometry a model number actually decides.\n */\n readonly alternate: { readonly rows: number; readonly cols: number };\n}\n\n/**\n * Model number to wire name and alternate size.\n *\n * Sizes per x3270 `include/3270ds.h:446-453`. Models 3, 4 and 5 were held back\n * until the screen could switch size on Erase/Write Alternate, because claiming\n * one is a promise to the host that it may do exactly that.\n *\n * NOT here: `IBM-DYNAMIC`, which is not a model but \"ask me via Query Reply\", and\n * which x3270 sends only for oversize (`telnet.c:2100`, `create_3270_termtype`).\n * Oversize and IBM-DYNAMIC now live in `geometry.ts`, which builds on this table\n * rather than duplicating it (see `resolveGeometry`, which calls `resolveTerminalType`\n * and `resolveAlternateSize` below); this table stays the models only.\n *\n * Keys are uppercase because lookups are case-folded; see resolveTerminalType.\n */\nexport const KNOWN_MODELS: Readonly<Record<string, Model>> = {\n '3278-2': { ttype: 'IBM-3278-2', alternate: MODEL_2 },\n '3278-2-E': { ttype: 'IBM-3278-2-E', alternate: MODEL_2 },\n '3278-3': { ttype: 'IBM-3278-3', alternate: MODEL_3 },\n '3278-3-E': { ttype: 'IBM-3278-3-E', alternate: MODEL_3 },\n '3278-4': { ttype: 'IBM-3278-4', alternate: MODEL_4 },\n '3278-4-E': { ttype: 'IBM-3278-4-E', alternate: MODEL_4 },\n '3278-5': { ttype: 'IBM-3278-5', alternate: MODEL_5 },\n '3278-5-E': { ttype: 'IBM-3278-5-E', alternate: MODEL_5 },\n};\n\n/**\n * The model a request names, or undefined for \"no model asked for\".\n *\n * Shared by `resolveTerminalType` and `resolveAlternateSize` so the two cannot\n * disagree about what a string means -- the whole point of them being separate\n * functions over one table.\n */\nfunction lookUpModel(opts: TerminalTypeOptions): Model | undefined {\n if (opts.model === undefined) return undefined;\n // Accept `3278-2-E`, `IBM-3278-2-E`, and any casing of either. x3270 matches\n // the prefix with strncasecmp(\"IBM-\", res, 4) and the suffix with\n // strchr(\"Ee\", res[7]) in Common/model.c canonical_model_x(), so a model\n // string s3270 accepts is accepted here too. Case folding cannot invent a\n // model: an unknown number still misses the table and throws.\n const folded = opts.model.toUpperCase();\n const bare = folded.startsWith('IBM-') ? folded.slice(4) : folded;\n // Own-property check, not a bare index: KNOWN_MODELS is a plain object, so\n // `KNOWN_MODELS['CONSTRUCTOR']`-style lookups would otherwise walk up to\n // Object.prototype and return a function that the `undefined` guard below\n // would wave through as if it were a model.\n const found = Object.hasOwn(KNOWN_MODELS, bare) ? KNOWN_MODELS[bare] : undefined;\n if (found === undefined) {\n throw new TerminalTypeError(\n `unknown model ${JSON.stringify(opts.model)}; known models are `\n + `${Object.keys(KNOWN_MODELS).join(', ')}. `\n + 'Use --terminal-type to send an arbitrary string.',\n );\n }\n return found;\n}\n\n/**\n * The alternate screen size to build the session with.\n *\n * Undefined means \"no model was named\", which leaves the session at a model 2\n * where the alternate size equals the default. A raw `terminalType` string also\n * yields undefined ON PURPOSE: we cannot know what geometry an arbitrary string\n * implies, and guessing one from a substring would be worse than staying 24x80 --\n * the host is told what we claim by the string itself, and if the operator sends\n * `IBM-3278-4` that way they get the size they asked the host for but not a\n * buffer that can hold it. Use `-model` to get both.\n */\nexport function resolveAlternateSize(\n opts: TerminalTypeOptions,\n): { readonly rows: number; readonly cols: number } | undefined {\n if (opts.terminalType !== undefined) return undefined;\n return lookUpModel(opts)?.alternate;\n}\n\nexport interface TerminalTypeOptions {\n /** A model number, with or without the IBM- prefix. */\n model?: string;\n /** A complete ttype string, used verbatim. Wins over `model`. */\n terminalType?: string;\n}\n\nexport function resolveTerminalType(opts: TerminalTypeOptions): string {\n if (opts.terminalType !== undefined) {\n if (opts.terminalType.length === 0) {\n throw new TerminalTypeError('terminal type must not be empty');\n }\n // Verbatim, and deliberately not case-folded: this is the escape hatch for\n // sending a host exactly the bytes the user asked for.\n return opts.terminalType;\n }\n\n // The default comes from TERMINAL_TYPE, not from KNOWN_MODELS['3278-2-E']. Both spell\n // IBM-3278-2-E today, but TERMINAL_TYPE is the single source of truth that telnet.ts\n // already defaults to (`opts.terminalType ?? TERMINAL_TYPE`), so the two default paths\n // cannot drift. KNOWN_MODELS is a convenience list of what a user may ask for by number;\n // editing it must never change what a session with no options negotiates.\n //\n // WHY -E IS THE DEFAULT, changed 2026-09-14. A bare IBM-3278-2 is REJECTED by MVS 3.8j\n // TSO with IKT00405I and no logon -- documented at the top of this file since stage 2a and\n // reproduced live through the GUI. Both live systems accept -E, so the flagless default is\n // the one that works on both. `-model 3278-2` still asks for the bare type.\n //\n // AN EARLIER COMMENT HERE CLAIMED THIS WAS LOAD-BEARING FOR THE VM/370 CONFORMANCE\n // COMPARISON. It is not, and the difference was measured rather than argued: flipping this\n // constant fails exactly four expectation tests, while conformance.test.ts and\n // golden.test.ts pass untouched. The offline comparison filters negotiation out of what it\n // diffs (conformance.test.ts, `isNegotiation`). The live script (conformance-vm.txt) has\n // TWO invocations that must be checked separately: the s3270 reference line was already\n // pinned to `-model 3278-2`, but our client's line originally passed no `-model` at all --\n // that gap was the one place the audit for this change first missed and then had to fix, by\n // adding `-model 3278-2` to our line too. Both invocations now name model 2 explicitly, so\n // neither depends on this constant.\n if (opts.model === undefined) return TERMINAL_TYPE;\n return lookUpModel(opts)!.ttype;\n}\n", "import { MODEL_2 } from './constants.js';\nimport { resolveTerminalType, resolveAlternateSize, type TerminalTypeOptions } from './termtype.js';\n\n/**\n * Oversize: an alternate screen larger than the model's, and the geometry a session negotiates.\n *\n * An emulator extension, not 3270 architecture -- see the spec,\n * docs/superpowers/specs/2026-11-04-oversize-and-model-switching-design.md. The rules are\n * x3270's: `ctlr.c:check_rows_cols` (`:321-335`) for the limits, `telnet.c:2100`\n * (`create_3270_termtype`) for IBM-DYNAMIC, `model.c:204-205`/`:333-335` for the extended-data-\n * stream requirement below.\n *\n * PURE. The measure function is a parameter, not part of the setting -- see the note on\n * `resolveGeometry`'s signature.\n */\n\n/** x3270's MAX_ROWS_COLS (include/3270ds.h:456): the cells a 14-bit address reaches. */\nexport const MAX_OVERSIZE_CELLS = 0x3fff;\n\nexport type Oversize =\n | { readonly kind: 'none' }\n | { readonly kind: 'fixed'; readonly rows: number; readonly cols: number }\n | { readonly kind: 'auto' };\n\nexport interface Size { readonly rows: number; readonly cols: number }\n\n/**\n * What the operator asked for. Plain data, deliberately: Task 2's `Session.setGeometry` and the\n * web gateway's `setGeometry` message both REPLACE this object wholesale, and a function\n * attached to it would be silently dropped by that replacement. The measure function is instead\n * a second, separate argument to `resolveGeometry`, supplied fresh by the caller every time.\n */\nexport interface GeometrySetting extends TerminalTypeOptions {\n readonly oversize: Oversize;\n}\n\nexport interface ResolvedGeometry {\n readonly terminalType: string;\n readonly default: Size;\n readonly alternate: Size;\n /** True when the terminal type is IBM-DYNAMIC, i.e. an oversize is in effect. */\n readonly dynamic: boolean;\n /** For the operator: an `auto` that fell back to the model. */\n readonly notice?: string;\n /** For the trace only: an `auto` cut to the 14-bit limit. */\n readonly clamp?: string;\n}\n\nexport class OversizeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OversizeError';\n }\n}\n\nexport function describeSize(s: Size): string {\n return `${s.rows} rows by ${s.cols} columns`;\n}\n\nconst FORM = '<cols>x<rows>, auto or none';\n\n/** Echoes a bad argument in an error message without letting it push the remedy off a line. */\nfunction truncate(text: string): string {\n return text.length > 20 ? `${text.slice(0, 20)}...` : text;\n}\n\n/**\n * Parses the `-oversize` flag's argument. x3270's own order, `model.c:343`: `<cols>x<rows>`,\n * so `160x62` is 62 rows by 160 columns -- see the spec's note on why this project's new\n * messages still say the size in words rather than repeat that ordering.\n */\nexport function parseOversize(text: string): Oversize {\n const t = text.trim().toLowerCase();\n if (t === 'none') return { kind: 'none' };\n if (t === 'auto') return { kind: 'auto' };\n const m = /^([0-9]+)x([0-9]+)$/.exec(t);\n if (m === null) throw new OversizeError(`-oversize takes ${FORM}, not ${JSON.stringify(truncate(text))}`);\n const cols = Number(m[1]);\n const rows = Number(m[2]);\n // `Number.isSafeInteger` rather than just checking for NaN: the regexp accepts an arbitrarily\n // long run of digits, and `Number(...)` on one past 2^53 either loses precision silently or,\n // past about 309 digits, overflows to Infinity. Both must be refused here, with the ordinary\n // parse message -- not let an Infinity or an imprecise value reach the cell-count arithmetic\n // below as e.g. `Infinityx24`.\n if (!Number.isSafeInteger(cols) || !Number.isSafeInteger(rows)) {\n throw new OversizeError(`-oversize takes ${FORM}, not ${JSON.stringify(truncate(text))}`);\n }\n // Zero is not a size. x3270 refuses it the same way it refuses a non-numeric argument --\n // `ctlr.c:258-263`, \"Invalid oversize ... columns: zero\" / \"... rows: zero\", both before the\n // model-floor and cell-limit checks -- so this is the malformed-argument case, not a\n // too-small oversize.\n if (cols === 0 || rows === 0) throw new OversizeError(`-oversize takes ${FORM}, not ${JSON.stringify(truncate(text))}`);\n return { kind: 'fixed', rows, cols };\n}\n\n/** A positive integer, or an `OversizeError` naming which field and what it actually was. */\nfunction positiveInt(value: number, field: 'rows' | 'cols'): number {\n if (!Number.isInteger(value) || value <= 0) {\n throw new OversizeError(`oversize needs a positive whole number of ${field}, not ${value}`);\n }\n return value;\n}\n\n/**\n * Resolves a model plus an oversize setting into a terminal type and the two sizes a session\n * negotiates with.\n *\n * `measure` is NOT a field of `setting` -- see the comment on `GeometrySetting`. It is called at\n * most once, and only for `oversize: { kind: 'auto' }`; `none` and `fixed` never touch it.\n */\nexport function resolveGeometry(\n setting: GeometrySetting,\n measure?: () => Size | undefined,\n): ResolvedGeometry {\n // `setting` satisfies TerminalTypeOptions structurally (it extends it), and passing a\n // variable -- as opposed to an object LITERAL -- does not trigger TypeScript's excess-\n // property check, so the extra `oversize` field is not a problem here.\n const modelTtype = resolveTerminalType(setting);\n // resolveAlternateSize returns undefined for \"no model named\" (session stays a model 2, 24x80)\n // and for a raw --terminal-type (deliberately: we cannot know what geometry an arbitrary\n // string implies). Model 2's own alternate IS 24x80, so the fallback below is exactly right\n // for both cases, not a stand-in for one of them.\n const modelAlt = resolveAlternateSize(setting) ?? MODEL_2;\n const def: Size = { rows: MODEL_2.rows, cols: MODEL_2.cols };\n const plain: ResolvedGeometry = { terminalType: modelTtype, default: def, alternate: modelAlt, dynamic: false };\n // For messages: the model as the host will see it, i.e. the resolved terminal type with its\n // IBM- prefix stripped -- not `setting.model` as the operator typed it, so `-model\n // ibm-3278-4-e` is reported as `model 3278-4-E` and not in whatever case or spelling it was\n // given in.\n const modelName = modelTtype.replace(/^IBM-/, '');\n\n const o = setting.oversize;\n if (o.kind === 'none') return plain;\n if (setting.terminalType !== undefined) {\n throw new OversizeError('-oversize and --terminal-type cannot both be given: an oversize '\n + 'is advertised as IBM-DYNAMIC');\n }\n // x3270 drops oversize to nothing when extended_data_stream is off, silently\n // (`oversize_init`, `model.c:204-205` and `:333-335`, \"Without extended, no oversize\"). That\n // would connect a session with a geometry the operator never asked for, so refuse instead of\n // following x3270 here. The product default with no `-model` is `IBM-3278-2-E`, which already\n // claims it, so this only bites an explicit non-`-E` `-model`.\n if (!modelTtype.endsWith('-E')) {\n throw new OversizeError(`oversize needs an extended model: use -model ${modelName}-E -- `\n + `model ${modelName} does not claim the extended data stream that Query Reply needs`);\n }\n\n if (o.kind === 'fixed') {\n const rows = positiveInt(o.rows, 'rows');\n const cols = positiveInt(o.cols, 'cols');\n const asked = `${cols}x${rows}`;\n const cells = rows * cols;\n if (cells > MAX_OVERSIZE_CELLS) {\n throw new OversizeError(`oversize ${asked} refused: use at most ${MAX_OVERSIZE_CELLS} cells `\n + `-- that is ${cells}`);\n }\n if (rows < modelAlt.rows || cols < modelAlt.cols) {\n throw new OversizeError(`oversize ${asked} refused: use at least ${modelAlt.cols}x${modelAlt.rows} `\n + `-- model ${modelName} is ${describeSize(modelAlt)}`);\n }\n return { terminalType: 'IBM-DYNAMIC', default: def, alternate: { rows, cols }, dynamic: true };\n }\n\n // o.kind === 'auto'\n if (measure === undefined) {\n throw new OversizeError('oversize auto needs a window to measure; give -oversize <cols>x<rows> instead');\n }\n const measured = measure();\n // A window the front end cannot vouch for -- unmeasured, or measured to something that is\n // not actually a usable size (NaN, a fraction, zero, negative) -- gets the same fallback as\n // \"no window size known\": there is no fit to check, honest or otherwise.\n const fits: Size | undefined = (measured !== undefined\n && Number.isInteger(measured.rows) && measured.rows > 0\n && Number.isInteger(measured.cols) && measured.cols > 0)\n ? { rows: measured.rows, cols: measured.cols } // copied, not the caller's object\n : undefined;\n if (fits === undefined) {\n return { ...plain, notice: 'oversize auto: no window size known; using the model' };\n }\n if (fits.rows < modelAlt.rows || fits.cols < modelAlt.cols) {\n return {\n ...plain,\n notice: `oversize auto: window fits ${describeSize(fits)}, less than model `\n + `${modelName}'s ${describeSize(modelAlt)}; using the model`,\n };\n }\n if (fits.rows * fits.cols > MAX_OVERSIZE_CELLS) {\n const rows = Math.floor(MAX_OVERSIZE_CELLS / fits.cols);\n // A very wide window can clamp BELOW the model's rows (past 682 columns for a model 2;\n // sooner for a taller model's floor). That is the same outcome as a window too small: the\n // model is the floor, so fall back to it rather than hand back fewer rows than the model\n // guarantees.\n if (rows < modelAlt.rows) {\n return {\n ...plain,\n notice: `oversize auto: window fits ${describeSize(fits)}, too wide to keep model `\n + `${modelName}'s ${modelAlt.rows} rows inside ${MAX_OVERSIZE_CELLS} cells; using the model`,\n };\n }\n return {\n terminalType: 'IBM-DYNAMIC', default: def, alternate: { rows, cols: fits.cols }, dynamic: true,\n clamp: `oversize auto: window fits ${describeSize(fits)}; cut to ${rows} rows, the 14-bit address limit`,\n };\n }\n return { terminalType: 'IBM-DYNAMIC', default: def, alternate: fits, dynamic: true };\n}\n", "import {\n AID, Order, isShortReadAID, ADDRESS_CODE_TABLE,\n EBCDIC_SOH, EBCDIC_PERCENT, EBCDIC_SLASH, EBCDIC_STX,\n} from './constants.js';\nimport { encodeAddress } from './address.js';\nimport type { Screen } from './screen.js';\n\n/**\n * Build the inbound (terminal to host) byte stream.\n *\n * IAC doubling is NOT done here \u2014 that is the telnet layer's job, so these\n * functions produce pure 3270 data and stay easy to test.\n */\n\n/**\n * Read Modified / Read Modified All.\n *\n * GA23-0059-07: a short read transfers \"only an AID byte\". x3270 confirms:\n * ctlr_read_modified writes the AID and jumps to rm_done for PA1-3 and Clear\n * when `all` is false. Read Modified All suppresses the short read, and\n * Selector Pen sends the cursor but no field data.\n *\n * `AID.SYSREQ` is the fourth case and the strange one: it produces a TEST REQUEST READ,\n * which carries the SOH `%` `/` STX heading INSTEAD OF the AID and cursor address. See the\n * branch below. This function mirrors x3270's `ctlr_read_modified` switch case for case,\n * which is why the branch lives here rather than in a function of its own: in x3270 the\n * heading and the ordinary AID are two arms of one `switch` over the same `aid_byte`\n * (`Common/ctlr.c:768-808`), and both fall into the same field scan afterwards.\n */\nexport function buildReadModified(screen: Screen, aid: number, all: boolean): Uint8Array {\n const out: number[] = [];\n\n // TEST REQUEST READ \u2014 the Sys Req / Test Req key on a classic session.\n //\n // FOUR HEADING BYTES AND NO AID. Not a variation on the ordinary read: the AID byte and\n // the two-byte cursor address are both REPLACED, not prefixed. GA23-0059-07: the heading\n // is followed by data that is \"the same as described previously for read-modified\n // operations, excluding the 3-byte read heading (AID and cursor address)\"\n // (pages.txt:13786-13789). x3270's `case AID_SYSREQ` writes exactly these four and\n // `break`s (ctlr.c:770-777) \u2014 the `break` leaves the SWITCH, not the function, so the\n // field scan below still runs and the data DOES follow. Only the 3-byte heading is gone.\n //\n // Confirming that the manual and x3270 agree here mattered, because it is easy to read\n // that `break` as \"send four bytes and stop\". It is not.\n //\n // No ETX terminates it. The manual's BSC form has one (pages.txt:13780-13785); the\n // non-SNA form is \"the same as for the BSC environment, except there is no ETX\"\n // (pages.txt:14180-14184). We are neither: a telnet record ends at IAC EOR, which\n // sendRecord appends.\n if (aid === AID.SYSREQ) {\n out.push(EBCDIC_SOH, EBCDIC_PERCENT, EBCDIC_SLASH, EBCDIC_STX);\n out.push(...modifiedData(screen, all));\n return Uint8Array.from(out);\n }\n\n out.push(aid);\n\n if (!all && isShortReadAID(aid)) {\n return Uint8Array.from(out); // AID alone\n }\n\n out.push(...encodeAddress(screen.cursor, screen.size));\n\n // Selector Pen reports position only.\n const sendData = all || aid !== AID.SELECT;\n if (!sendData) return Uint8Array.from(out);\n\n out.push(...modifiedData(screen, all));\n\n return Uint8Array.from(out);\n}\n\n/**\n * The data portion of a read: everything after the heading, whichever heading it was.\n *\n * Shared by the ordinary AID path and the test request read because x3270 shares it too \u2014\n * both arms of the `aid_byte` switch fall through to the same field scan\n * (`Common/ctlr.c:810-960`). Extracted verbatim from `buildReadModified`; the comments are\n * the original ones and the behavior is unchanged for every existing caller.\n */\nfunction modifiedData(screen: Screen, all: boolean): number[] {\n const out: number[] = [];\n\n // UNFORMATTED SCREEN: there are no fields to iterate, so walk the whole buffer\n // and send every non-null character, with no SBA orders at all. x3270 does\n // exactly this in ctlr_read_modified's `else` branch (ctlr.c:997-1057): it\n // loops from address 0 emitting `ea_buf[baddr].ec` wherever that is nonzero.\n //\n // This is not a corner case. VM/370's logon screen is unformatted \u2014 verified\n // against a live VM/CE 1.2 host, which sends its logo with zero field\n // attributes \u2014 so without this branch everything the operator types before the\n // first formatted panel is silently dropped and LOGON never reaches CP.\n if (!screen.isFormatted()) {\n for (let a = 0; a < screen.size; a++) {\n const ebcdic = screen.cellAt(a).ebcdic;\n if (ebcdic !== 0x00) out.push(ebcdic);\n }\n return out;\n }\n\n for (const field of screen.fields()) {\n if (!all && !field.modified) continue;\n\n const data: number[] = [];\n let a = field.start;\n for (let n = 0; n < field.length; n++) {\n data.push(screen.cellAt(a).ebcdic);\n a = screen.inc(a);\n }\n // Trailing nulls are not transmitted; embedded ones are.\n while (data.length > 0 && data[data.length - 1] === 0x00) data.pop();\n\n // A modified field ALWAYS gets its SBA, even with no content left after\n // trimming. x3270 writes the SBA the moment it finds a modified field, before\n // examining any content (ctlr.c:822-829): `if (FA_IS_MODIFIED(...)) { ...\n // *obptr++ = ORDER_SBA; ENCODE_BADDR(obptr, baddr); ...`.\n //\n // Verified by byte-comparison against real x3270 driving the same VM/370 host\n // through an identical script: pressing Enter on a modified empty field, it\n // sent `7d 5b 60 11 5b 60` (AID, cursor, SBA(1760)) where we sent\n // `7d 5b 60` and omitted the SBA. The SBA tells the host WHICH field the\n // operator cleared, which is information the host cannot otherwise recover.\n out.push(Order.SBA, ...encodeAddress(field.start, screen.size), ...data);\n }\n\n return out;\n}\n\n/**\n * Encode a field attribute for transmission inbound.\n *\n * The attribute goes out through the same 64-entry code table as a 12-bit\n * address, after masking off the two \"printable\" high bits. x3270 does exactly\n * this in both places it sends an attribute inbound:\n * ctlr.c:1112-1114 `fa = ea_buf[baddr].fa & ~FA_PRINTABLE; ... code_table[fa]`\n * ctlr.c:1248 `code_table[ea_buf[baddr].fa & ~FA_PRINTABLE]`\n *\n * So a protected field is 0x60 on the wire, not 0x20. FA_PRINTABLE is 0xC0 and\n * the defined bits (protect, numeric, intensity, MDT) all fall inside 0x3D, so\n * masking with 0x3F is equivalent to x3270's `& ~FA_PRINTABLE` for every\n * attribute a host can send, and keeps the index inside the 64-entry table.\n */\nexport function encodeAttribute(attr: number): number {\n const encoded = ADDRESS_CODE_TABLE[attr & 0x3f];\n if (encoded === undefined) throw new Error(`unencodable attribute 0x${attr.toString(16)}`);\n return encoded;\n}\n\n/**\n * Read Buffer: the entire buffer, with each field attribute rendered as an SF\n * order followed by the attribute value, and every other position as its\n * character byte.\n */\nexport function buildReadBuffer(screen: Screen, aid: number): Uint8Array {\n const out: number[] = [aid, ...encodeAddress(screen.cursor, screen.size)];\n for (let a = 0; a < screen.size; a++) {\n const attr = screen.attributeAt(a);\n if (attr !== null) {\n out.push(Order.SF, encodeAttribute(attr));\n } else {\n out.push(screen.cellAt(a).ebcdic);\n }\n }\n return Uint8Array.from(out);\n}\n", "/**\n * Operator Information Area state.\n *\n * Deliberately STATE, not rendered text: stage 1 has no GUI but still needs\n * somewhere to record a program check, and stage 2's renderer formats this same\n * object into the status line. toText() exists for the CLI and for tests, and\n * follows x3270's wording so the two are comparable.\n */\n\nexport enum KeyboardState {\n Unlocked = 'unlocked',\n /**\n * Connected but the host has not written yet, so there is nothing to type\n * into. x3270 calls this KL_AWAITING_FIRST and sets it both on connect and on\n * entering 3270 mode, with the comment \"Wait for any output or a\n * WCC(restore) from the host\" (kybd.c:584, :613). Without it, a script that\n * connects and immediately types races the host's first screen \u2014 verified\n * against a live VM/370: String() was refused because the keyboard was busy,\n * while Wait(Unlock) had already returned because nothing was pending.\n */\n AwaitingFirstWrite = 'awaitingfirst',\n /**\n * We answered a Read Partition (Query) and the host has not written since.\n *\n * GA23-0059 p. 5-53 lists this as step 1 of Read Partition processing\n * (pages.txt:6413): \"1. The enter-inhibit condition is raised.\" The host has\n * asked a question about the device and considers the screen frozen until it\n * writes again, so the operator must not type into it.\n *\n * x3270 calls this KL_ENTER_INHIBIT, \"Awaiting unlock after QueryReply\"\n * (include/kybd.h:45), raised by kybd_inhibit(true) (Common/kybd.c:528) from\n * query_reply_end() (Common/sf.c:926-930).\n *\n * NOTE THE DIVERGENCE FROM THE MANUAL'S LETTER, which we take deliberately.\n * p. 3-22 (pages.txt:2562-2564) says: \"Any enter action, including a trigger\n * action, sets the enter-inhibit condition for the device (logical\n * terminal). The enter-inhibit condition allows keystroking but does / not\n * allow an enter action.\" \u2014 i.e. a device-wide condition that blocks only AID\n * generation, and it adds \"There is no / indicator associated with the\n * enter-inhibit condition\" (pages.txt:2569-2570). We instead refuse\n * keystroking outright and show an indicator, as x3270 does.\n *\n * That is right for a device with no explicit partitions, by the manual's own\n * reasoning two lines later (pages.txt:2565-2567): \"The enter-inhibit\n * condition has significance only for the partitions other than INPID /\n * because the input-inhibit conditions associated with INPID (for example,\n * TWAIT, / PWAIT or System Lock) override the enter-inhibit condition.\" The\n * weak reading exists so an operator can \"jump from INPID to one of the\n * other partitions and enter data in that partition\" (pages.txt:2568-2569).\n * We support only the implicit partition 0, so the partition being read IS\n * INPID, and the manual's own rule is that the stronger inhibit governs\n * there. x3270 implements no partitions either, which is presumably why it\n * made the same call.\n */\n EnterInhibit = 'enterinhibit',\n ProtectedField = 'protected',\n Numeric = 'numeric',\n Overflow = 'overflow',\n ProgramCheck = 'progcheck',\n SystemWait = 'systemwait',\n MinusFunction = 'minusfunction',\n}\n\nexport class Oia {\n connected = false;\n /**\n * There is a host to reconnect TO, i.e. `Session.reconnect()` would be accepted.\n *\n * Set by `Session.connect` and deliberately NOT cleared when the connection ends, because the\n * whole point is what the status line says while disconnected. It exists so `toText` can offer\n * the key HONESTLY: a session that has never connected \u2014 the state the GUI's replay seam and\n * the gateway's `--replay` mode sit in permanently \u2014 has nothing to reconnect to, and telling\n * its operator to press Enter would be an instruction that silently does nothing.\n */\n reconnectable = false;\n tn3270Mode = false;\n waitingForHost = false;\n insertMode = false;\n keyboard: KeyboardState = KeyboardState.Unlocked;\n /** Set alongside KeyboardState.ProgramCheck. */\n programCheckCode: number | undefined;\n /** Host asked for the alarm on the last write. */\n alarm = false;\n\n inhibit(state: KeyboardState): void {\n this.keyboard = state;\n }\n\n /**\n * Raise the enter-inhibit condition, without demoting a stronger inhibit.\n *\n * Step 1 of Read Partition processing, GA23-0059 p. 5-53 (pages.txt:6413):\n * \"1. The enter-inhibit condition is raised.\"\n *\n * WHY IT YIELDS TO AN EXISTING INHIBIT. x3270 keeps every lock reason as its\n * own bit in one word, so kybd_inhibit(true) \u2014 `kybdlock_set(KL_ENTER_INHIBIT,\n * \"kybd_inhibit\")` (Common/kybd.c:528) \u2014 cannot possibly erase another\n * reason. We hold a single state, so the equivalent has to be written down,\n * and \"weakest loses\" is the faithful translation: the operator stays\n * refused either way (isInhibited covers both), while the state that survives\n * is the one whose release rule is broader.\n *\n * Concretely this matters twice, and in both cases keeping the incumbent is\n * the right answer:\n *\n * - AwaitingFirstWrite, the case TSO actually produces, since it queries\n * before writing anything. Enter-inhibit is released by the next Erase/\n * Write, EWA, EAU or Write; AwaitingFirstWrite is released by those AND by\n * a WCC keyboard-restore, so it is released by a strictly larger set of\n * records and must not be dropped in favor of the narrower rule. It is\n * also the truer description \u2014 there is no screen yet at all, rather than\n * a screen that is merely frozen. x3270 orders its status line the same\n * way, testing KL_AWAITING_FIRST before KL_ENTER_INHIBIT in all four\n * renderers (c3270/screen.c:2383-2386).\n *\n * - ProgramCheck, which the operator must clear with Reset and which\n * reports a real protocol fault. Letting a routine query overwrite it\n * would hide that fault behind an ordinary wait.\n */\n enterInhibit(): void {\n if (this.isInhibited()) return;\n this.keyboard = KeyboardState.EnterInhibit;\n }\n\n /**\n * Release the enter-inhibit condition and nothing else.\n *\n * x3270 clears the one bit \u2014 `kybdlock_clr(KL_ENTER_INHIBIT, \"kybd_inhibit\")`\n * (Common/kybd.c:533) \u2014 so this must not be reset(), which would also clear a\n * program check the host never acknowledged. Narrow by construction: it does\n * nothing unless enter-inhibit is the state we are actually in, which is also\n * what makes it safe to call on every write.\n */\n releaseEnterInhibit(): void {\n if (this.keyboard !== KeyboardState.EnterInhibit) return;\n this.keyboard = KeyboardState.Unlocked;\n }\n\n /**\n * True for the inhibits the OPERATOR caused, as opposed to the host.\n *\n * x3270 groups exactly these behind one mask, `KL_OERR_MASK` (0x000f), whose\n * declaration is commented \"Operator errors:\" and whose four members are\n * KL_OERR_PROTECTED, KL_OERR_NUMERIC, KL_OERR_OVERFLOW and KL_OERR_DBCS\n * (include/kybd.h:35-39). We have no DBCS, so three.\n *\n * The distinction is load-bearing for input: an operator error is cleared by\n * the operator's own next sensible keystroke, while a host-imposed lock is\n * cleared only by the host. See Keyboard.type.\n */\n isOperatorError(): boolean {\n return this.keyboard === KeyboardState.ProtectedField\n || this.keyboard === KeyboardState.Numeric\n || this.keyboard === KeyboardState.Overflow;\n }\n\n programCheck(code: number): void {\n this.keyboard = KeyboardState.ProgramCheck;\n this.programCheckCode = code;\n }\n\n /** The Reset key clears an input inhibit. */\n reset(): void {\n this.keyboard = KeyboardState.Unlocked;\n this.programCheckCode = undefined;\n }\n\n /** True when the operator may not type. */\n isInhibited(): boolean {\n return this.keyboard !== KeyboardState.Unlocked;\n }\n\n /** x3270-style single-line rendering. */\n toText(): string {\n const parts: string[] = [];\n if (!this.connected) {\n // ONE PART, NOT TWO, so the ` -- ` phrase cannot be separated from the state it explains by\n // the two-space join below, and so every existing `toContain('X Disconnected')` still holds.\n //\n // WHY THE STATUS LINE ADVERTISES A KEY AT ALL, which nothing else in this OIA does: the key\n // is a DIVERGENCE from x3270, whose Enter never reconnects (`Enter_action`, Common/kybd.c,\n // has no such branch \u2014 its `Reconnect()` action is the only route, and there is no keyboard\n // binding for it). An operator coming from x3270 has no reason to guess, and VM prints\n // \"Press Enter or Clear to continue\" after a LOGOFF, which is precisely the moment this\n // helps. `X Disconnected` alone does not tell anyone the key exists.\n //\n // THE WIDTH BUDGET, MEASURED rather than assumed: 42 columns here, and the worst case with\n // every other part this method can add while disconnected \u2014 `X PROG754` (9) and the insert\n // caret (1), `waitingForHost` being cleared by `handleClose` \u2014 is 42 + 2 + 9 + 2 + 1 = 56,\n // inside the 80 of any real 3270 model, and `session.test.ts` asserts that bound. Both renderers TRUNCATE rather than wrap\n // (`canvas/src/drawlist.ts`'s `oiaCells`, `tui/src/render.ts`'s status line), so anything\n // longer would silently lose its tail on a narrow geometry.\n //\n // CLEAR RECONNECTS TOO and is deliberately not named: the OIA is a status line with a\n // column budget, not a help screen, and naming the key an operator already has their hand on\n // is enough to say the route exists. The README and `BINDING_INTENT` carry both.\n parts.push(this.reconnectable\n ? 'X Disconnected -- press Enter to reconnect'\n : 'X Disconnected');\n } else if (this.tn3270Mode) {\n parts.push('4 A');\n } else {\n parts.push('4');\n }\n\n switch (this.keyboard) {\n case KeyboardState.ProgramCheck:\n parts.push(`X PROG${this.programCheckCode ?? 0}`);\n break;\n case KeyboardState.ProtectedField:\n parts.push('X Protected');\n break;\n case KeyboardState.Numeric:\n parts.push('X Numeric');\n break;\n case KeyboardState.Overflow:\n parts.push('X Overflow');\n break;\n case KeyboardState.MinusFunction:\n parts.push('X -f');\n break;\n case KeyboardState.SystemWait:\n parts.push('X SYSTEM');\n break;\n case KeyboardState.AwaitingFirstWrite:\n parts.push('X Wait');\n break;\n // \"X Wait\", the same text as AwaitingFirstWrite, because that is exactly\n // what x3270 shows. Three of its four status renderers spell it out\n // literally \u2014 `} else if (kybdlock & (KL_ENTER_INHIBIT | KL_BID)) {\n // other_msg = \"X Wait\";` at c3270/screen.c:2385-2386, identically at\n // wc3270/screen.c:3122-3123 and Common/vstatus.c:279-280 (voia_msg). The\n // fourth routes through a message table, `do_msg(TWAIT)`\n // (x3270/status.c:643-645), whose enumerator is declared `TWAIT, /* X\n // Wait */` (x3270/status.c:212). b3270 agrees over the wire, mapping the\n // bit to OiaLockTwait \u2014 `#define OiaLockTwait \"twait\"`\n // (include/b3270proto.h:194) \u2014 with the note \"OiaLockInhibit is now\n // deprecated, we use OiaLockTwait instead\" (Common/b3270/status.c:199).\n //\n // So there IS a sensible equivalent and we match it rather than invent\n // one. The two remain distinct KeyboardStates despite rendering alike,\n // because they are released by different records; the operator cannot\n // act on the difference, but the protocol does.\n case KeyboardState.EnterInhibit:\n parts.push('X Wait');\n break;\n case KeyboardState.Unlocked:\n break;\n }\n\n if (this.waitingForHost) parts.push('X Wait');\n if (this.insertMode) parts.push('^');\n return parts.join(' ');\n }\n}\n", "import { cp037, type CodePage } from './codepage.js';\nimport { EBCDIC_DUP, EBCDIC_FIELD_MARK } from './constants.js';\nimport { Oia, KeyboardState } from './oia.js';\nimport type { Screen, Field } from './screen.js';\n\n/**\n * 3270 keyboard actions over a screen buffer.\n *\n * Operates on ACTIONS (Enter, Tab, EraseEOF), never on physical keys \u2014 mapping\n * keys to actions is the GUI's job in stage 2, and the CLI's job here. Every\n * method that can be refused returns false and records why in the OIA rather\n * than throwing, because an input inhibit is a normal operating condition.\n */\nexport class Keyboard {\n insertMode = false;\n\n constructor(\n private readonly screen: Screen,\n readonly oia: Oia,\n private readonly codePage: CodePage = cp037,\n ) {}\n\n // ---- typing ----\n\n /** Type one character. Returns false if the keyboard is inhibited. */\n type(char: string): boolean {\n const s = this.screen;\n\n // A HOST-IMPOSED LOCK REFUSES THE KEYSTROKE OUTRIGHT, and leaves the state\n // alone for the host to clear.\n //\n // Before this guard nothing enforced any lock on typing: only the CLI's\n // Wait(Settle) and Wait(InputField) consulted isInhibited (both predicates in\n // `cli/src/runner.ts`, spelled `if (this.session.oia.isInhibited()) return false;`), so a\n // caller reaching Keyboard.type directly could write into a screen the host\n // had frozen \u2014 and after answering a Query that is precisely the screen it\n // would be writing into. An unenforced state is not a fix.\n //\n // x3270 refuses on ANY lock bit and it is the first thing key_Character\n // does: `if (kybdlock) { ... enq_fta(key_Character_wrapper, ...); return\n // true; }` (Common/kybd.c:1201-1210), the DBCS twin key_WCharacter doing\n // the same at kybd.c:1489-1497. It parks the keystroke on the typeahead\n // queue rather than discarding it \u2014 a queue we do not have, so we refuse\n // and return false, which is this class's documented contract for a\n // refusal.\n //\n // OPERATOR ERRORS ARE EXCLUDED, and must be, or the exclusion breaks two\n // shipped behaviors: \"refuses a letter in a numeric field\" types '5'\n // successfully straight after the refused 'A', and the auto-skip test types\n // on after a full field. x3270 draws the same line, clearing KL_OERR_MASK\n // and continuing where a host lock would have deferred \u2014 see\n // OERR_CLEAR_OR_ENQ (kybd.c:147-158), whose two arms are exactly\n // `kybdlock_clr(KL_OERR_MASK, action)` versus `enq_ta(action, ...)`.\n // Semantically: an operator error is the operator's own to correct with the\n // next keystroke, whereas AwaitingFirstWrite, EnterInhibit, SystemWait and\n // ProgramCheck are the host's to release.\n if (this.oia.isInhibited() && !this.oia.isOperatorError()) return false;\n\n const field = s.fieldAt(s.cursor);\n\n if (field !== null) {\n if (field.protected) {\n this.oia.inhibit(KeyboardState.ProtectedField);\n return false;\n }\n if (field.numeric && !/[0-9.\\-+,]/.test(char)) {\n this.oia.inhibit(KeyboardState.Numeric);\n return false;\n }\n }\n\n const ebcdic = this.codePage.fromUnicode(char);\n\n if (this.insertMode && field !== null) {\n if (!this.shiftRight(field, s.cursor)) {\n this.oia.inhibit(KeyboardState.Overflow);\n return false;\n }\n }\n\n s.setChar(s.cursor, ebcdic);\n if (field !== null) s.setMDT(field.attrAddr);\n this.advanceAfterType(field);\n return true;\n }\n\n /** Type a string, stopping at the first refusal. */\n typeString(text: string): boolean {\n for (const ch of text) {\n if (!this.type(ch)) return false;\n }\n return true;\n }\n\n /**\n * The Dup key: write 0x1c, set MDT, then TAB.\n *\n * THE TAB IS THE PART THAT LOOKS WRONG AND IS RIGHT. `kybd.c:1435` suppresses\n * key_Character's auto-skip for a keyboard-generated Dup \u2014 `if (auto_skip &&\n * (pasting || (ebc != EBC_dup)))`, commented \"for all pasted data (even DUP),\n * and for all keyboard-generated data except DUP\" \u2014 so it is easy to conclude\n * that Dup advances one position and stops. It does not: `Dup_action` moves the\n * cursor itself once key_Character returns (`kybd.c:2788-2792`),\n *\n * if (key_Character(EBC_dup, false, false, oerr_fail, &consumed)) {\n * if (consumed) { cursor_move(next_unprotected(cursor_addr)); }\n *\n * so the NET effect of the key is always a move to the next unprotected field,\n * from anywhere in the field. The manual states it directly (p. 7-12,\n * pages.txt:12637-12638): \"Operation of this key causes a X'1C' code to be\n * entered into the presentation space, a Tab key operation to be performed, and\n * the MDT bit to be set to 1.\" It is what the key MEANS \u2014 \"duplicate the rest\n * of this field from the previous record\" leaves nothing more to type here.\n *\n * What the suppression buys is that the tab happens ONCE. `advanceAfterType`\n * already tabs at the end of a field, so advancing first and tabbing after\n * would skip a whole field when Dup is pressed in the last cell. That is\n * exactly what x3270 avoids by starting `next_unprotected` from the field\n * attribute instead of from the next field's first data cell.\n *\n * `tab()` is the faithful equivalent of `next_unprotected` (ctlr.c:518-539)\n * here: both look for the next unprotected field of non-zero width, both wrap,\n * and both fall back to address 0 when there is none. Its extra `!autoSkip`\n * filter costs nothing, an auto-skip field being protected by definition.\n */\n dup(): boolean {\n return this.writeControl(EBCDIC_DUP, {\n cursorAfter: 'tab',\n // A NUMERIC FIELD TAKES DUP, and it is the manual's list that says so rather\n // than x3270's byte test. The permitted set names DUP explicitly (p. 4-13,\n // pages.txt:3261-3262): \"Numeric fields are limited to numeric characters,\n // the minus and decimal sign characters, and the duplicate (DUP) control.\"\n // Duplicating the previous record's date in a numeric data-entry field is\n // the key's whole purpose, so refusing it there would break the one thing it\n // is for.\n //\n // x3270's numeric test permits only EBC_0..EBC_9, plus, minus, period and\n // comma (kybd.c:1232-1238) and so refuses DUP as well \u2014 but it is gated on\n // `appres.numeric_lock`, which defaults to ResFalse (x3270/resources.c:337),\n // so stock x3270 refuses NEITHER Dup nor Field Mark here. That byte set is\n // the shape of the numeric-lock feature, not a ruling on DUP; the manual is,\n // so the manual wins.\n allowedInNumericField: true,\n });\n }\n\n /**\n * The Field Mark key: write 0x1e and advance like any other typed character.\n *\n * `kybd.c:2825` is a bare `return key_Character(EBC_fm, ...)` with nothing after\n * it, so FM gets the ordinary typed-character advance \u2014 right, because a field\n * mark marks a boundary INSIDE a field, so there may well be more to type after\n * it. The manual (p. 7-13, pages.txt:12653-12655): the field mark character\n * \"informs the application program of the end of a field in an unformatted\n * buffer or subfield in a formatted buffer\" \u2014 quoted with its OCR repaired, the\n * scan reading \"formatted butter\", so do not grep for the clean phrase.\n *\n * THE MANUAL CORROBORATES THE ASYMMETRY BY OMISSION, which is worth more than\n * the x3270 code alone: its Field Mark paragraph describes the byte and the MDT\n * bit and names NO cursor operation, where Dup's paragraph one page earlier\n * spells out \"a Tab key operation to be performed\". Two keys documented side by\n * side, one given a tab and the other not.\n */\n fieldMark(): boolean {\n return this.writeControl(EBCDIC_FIELD_MARK, {\n cursorAfter: 'advance',\n // Field Mark is NOT in the manual's permitted set for a numeric field; see\n // the note on `dup()` for the set and for why we enforce it at all.\n allowedInNumericField: false,\n });\n }\n\n /**\n * Write one EBCDIC control byte as if typed, then move the cursor.\n *\n * DELIBERATELY NOT `type(ch)`: these bytes have no sensible Unicode source\n * character, so routing them through `codePage.fromUnicode` would mean\n * inventing one. It also leaves `type()`'s numeric test \u2014 a Unicode regex \u2014\n * untouched, and states the equivalent EBCDIC rule here instead.\n *\n * Everything up to the write is `type()`'s sequence, for the reasons documented\n * there: a host-imposed lock refuses outright while an operator error does not,\n * a protected field is an operator error, and insert mode shifts the field\n * right first.\n *\n * BOTH PER-KEY POLICIES ARE THE CALLER'S, and neither is optional. An earlier\n * version took the cursor rule as a parameter but decided the numeric rule in\n * here by comparing the byte against `EBCDIC_DUP` \u2014 so a function promising to\n * write \"one EBCDIC control byte\" applied a Dup-only exemption to whatever byte\n * it was handed. Table 4-3 has two more members this codebase already names,\n * NUL and SUB, and a later `nul()` routed through here would have been refused\n * in a numeric field \u2014 nulls being exactly what an empty numeric cell holds \u2014\n * with nobody having edited this method to cause it. Required fields make that\n * unwritable: a new key cannot compile without stating both answers.\n */\n private writeControl(\n ebcdic: number,\n policy: {\n /** `'tab'` is the manual's own word for what Dup does; `'advance'` is what a typed character does. */\n cursorAfter: 'tab' | 'advance';\n /** Whether a numeric field accepts this byte. Per-KEY, so it cannot be inferred here. */\n allowedInNumericField: boolean;\n },\n ): boolean {\n const s = this.screen;\n if (this.oia.isInhibited() && !this.oia.isOperatorError()) return false;\n\n const field = s.fieldAt(s.cursor);\n if (field !== null) {\n if (field.protected) {\n this.oia.inhibit(KeyboardState.ProtectedField);\n return false;\n }\n // The caller's flag, not a byte test: which control characters a numeric\n // field accepts is a fact about each KEY and is documented on each one. No\n // character-class test is possible or wanted for a control code.\n if (field.numeric && !policy.allowedInNumericField) {\n this.oia.inhibit(KeyboardState.Numeric);\n return false;\n }\n }\n\n if (this.insertMode && field !== null) {\n if (!this.shiftRight(field, s.cursor)) {\n this.oia.inhibit(KeyboardState.Overflow);\n return false;\n }\n }\n\n s.setChar(s.cursor, ebcdic);\n if (field !== null) s.setMDT(field.attrAddr);\n if (policy.cursorAfter === 'tab') this.tab();\n else this.advanceAfterType(field);\n return true;\n }\n\n /**\n * Move on after typing. At the end of a field, skip to the next typable one \u2014\n * this is what makes \"type into a panel\" work.\n *\n * NOT byte-for-byte x3270's auto-skip, and the difference is here rather than\n * hidden: x3270's loop (kybd.c:1436-1442) walks off attribute bytes one at a\n * time and stops at the first position that is not one, calling\n * next_unprotected only for a field flagged auto-skip \u2014 so it can leave the\n * cursor in a PROTECTED non-auto-skip field, whereas `tab()` always finds a\n * typable one. Dup and Field Mark inherit that difference rather than\n * introduce it; what matters for them is that Field Mark advances exactly as a\n * typed character does, which it does by construction.\n */\n private advanceAfterType(field: Field | null): void {\n const s = this.screen;\n const next = s.inc(s.cursor);\n if (field === null) {\n s.cursor = next;\n return;\n }\n const endOfField = next === field.attrAddr\n || (s.isFieldAttribute(next) && next !== field.attrAddr);\n if (endOfField) {\n this.tab();\n } else {\n s.cursor = next;\n }\n }\n\n /** Push field contents right from `from`; false if the field would overflow. */\n private shiftRight(field: Field, from: number): boolean {\n const s = this.screen;\n const last = this.lastCellOf(field);\n if (s.cellAt(last).ebcdic !== 0x00) return false; // no room\n let a = last;\n while (a !== from) {\n const prev = s.dec(a);\n s.setChar(a, s.cellAt(prev).ebcdic);\n a = prev;\n }\n return true;\n }\n\n private lastCellOf(field: Field): number {\n let a = field.start;\n for (let n = 1; n < field.length; n++) a = this.screen.inc(a);\n return a;\n }\n\n // ---- movement ----\n\n left(): void { this.screen.cursor = this.screen.dec(this.screen.cursor); }\n right(): void { this.screen.cursor = this.screen.inc(this.screen.cursor); }\n\n up(): void {\n const s = this.screen;\n s.cursor = (s.cursor - s.cols + s.size) % s.size;\n }\n\n down(): void {\n const s = this.screen;\n s.cursor = (s.cursor + s.cols) % s.size;\n }\n\n /**\n * First typable cell, or 0 if there is none.\n *\n * Delegates to the screen so the zero-length-field guard lives in one place:\n * a zero-length field's `start` IS the next field attribute, and parking the\n * cursor there corrupts the buffer on the next keystroke. x3270's\n * next_unprotected (ctlr.c:623-638) skips them for the same reason.\n */\n home(): void {\n this.screen.cursor = this.screen.firstUnprotectedStart() ?? 0;\n }\n\n /** Next typable field. Wraps. */\n tab(): void {\n const s = this.screen;\n const fields = s.typableFields();\n if (fields.length === 0) { s.cursor = 0; return; }\n const current = s.fieldAt(s.cursor);\n const after = fields.find((f) => f.attrAddr > (current?.attrAddr ?? -1));\n s.cursor = (after ?? fields[0]!).start;\n }\n\n /**\n * Start of the field we are in; if already there, the previous typable field.\n */\n backTab(): void {\n const s = this.screen;\n const fields = s.typableFields();\n if (fields.length === 0) { s.cursor = 0; return; }\n const current = s.fieldAt(s.cursor);\n // The fast path \u2014 \"already inside a typable field, so go to its start\" \u2014\n // must require length > 0 as well as unprotected. A zero-length field's\n // `start` IS the next field's attribute byte, so without that check\n // backTab parks the cursor on an attribute and the next keystroke destroys\n // that field's boundary. x3270's BackTab_action guards the same way, with\n // `!ea_buf[nbaddr].fa` in its search loop (kybd.c:1976-1979).\n if (current !== null && !current.protected && current.length > 0\n && s.cursor !== current.start) {\n s.cursor = current.start;\n return;\n }\n const before = [...fields].reverse()\n .find((f) => f.attrAddr < (current?.attrAddr ?? s.size));\n s.cursor = (before ?? fields[fields.length - 1]!).start;\n }\n\n /** First unprotected cell at or after the start of the next line. */\n newline(): void {\n const s = this.screen;\n const nextLine = (Math.floor(s.cursor / s.cols) + 1) % s.rows * s.cols;\n let a = nextLine;\n for (let n = 0; n < s.size; n++) {\n if (!s.isFieldAttribute(a)) {\n const f = s.fieldAt(a);\n if (f === null || !f.protected) { s.cursor = a; return; }\n }\n a = s.inc(a);\n }\n s.cursor = nextLine;\n }\n\n moveCursor(addr: number): void {\n this.screen.cursor = ((addr % this.screen.size) + this.screen.size) % this.screen.size;\n }\n\n // ---- erasing ----\n\n /** Null from the cursor to the end of the field. */\n eraseEOF(): void {\n const s = this.screen;\n const f = s.fieldAt(s.cursor);\n if (f === null) return;\n if (f.protected) {\n this.oia.inhibit(KeyboardState.ProtectedField);\n return;\n }\n let a = s.cursor;\n while (!s.isFieldAttribute(a)) {\n s.setChar(a, 0x00);\n a = s.inc(a);\n if (a === s.cursor) break; // wrapped the whole buffer\n }\n s.setMDT(f.attrAddr);\n }\n\n /**\n * Clear every unprotected field, reset their MDT, home the cursor.\n *\n * eraseAllUnprotected already resets MDT on the unprotected fields it clears\n * (manual 3-8), which is the Erase Input rule \u2014 unlike WCC reset-MDT, which is\n * unconditional. Do not reach for clearAllMDT here.\n */\n eraseInput(): void {\n this.screen.eraseAllUnprotected();\n this.home();\n }\n\n backspace(): void {\n const s = this.screen;\n const f = s.fieldAt(s.cursor);\n if (f !== null && f.protected) {\n this.oia.inhibit(KeyboardState.ProtectedField);\n return;\n }\n const prev = s.dec(s.cursor);\n if (s.isFieldAttribute(prev)) return; // at the start of the field\n s.cursor = prev;\n s.setChar(prev, 0x00);\n if (f !== null) s.setMDT(f.attrAddr);\n }\n\n /** Delete under the cursor, shifting the remainder of the field left. */\n deleteChar(): void {\n const s = this.screen;\n const f = s.fieldAt(s.cursor);\n if (f === null) return;\n if (f.protected) {\n this.oia.inhibit(KeyboardState.ProtectedField);\n return;\n }\n let a = s.cursor;\n while (true) {\n const next = s.inc(a);\n if (s.isFieldAttribute(next) || next === f.attrAddr) {\n s.setChar(a, 0x00);\n break;\n }\n s.setChar(a, s.cellAt(next).ebcdic);\n a = next;\n }\n s.setMDT(f.attrAddr);\n }\n\n reset(): void {\n this.oia.reset();\n }\n\n setInsertMode(on: boolean): void {\n this.insertMode = on;\n this.oia.insertMode = on;\n }\n}\n", "/**\n * TN3270E (RFC 2355): the option-40 subnegotiation state machine and the 5-byte\n * data header codec.\n *\n * PURE BY DESIGN \u2014 bytes in, decisions and bytes out, no socket and no session. The\n * negotiation is the part most likely to be wrong, and a pure function can be driven\n * directly with the transcript recorded from real s3270 in\n * docs/superpowers/specs/2026-08-27-stage2b-tn3270e-design.md. That is the same\n * reason queryreply.ts is pure.\n *\n * Design doc: docs/superpowers/specs/2026-08-27-stage2b-tn3270e-design.md\n * Plan: docs/superpowers/plans/2026-08-27-stage2b-tn3270e.md\n */\nimport { Tn3270eDataType, Tn3270eFunc, Tn3270eOp, Tn3270eReason } from './constants.js';\n\n/**\n * RFC 2355 \u00A78.1: DATA-TYPE, REQUEST-FLAG, RESPONSE-FLAG, then a 2-byte SEQ-NUMBER.\n * x3270 calls the same number EH_SIZE (include/tn3270e.h).\n */\nexport const TN3270E_HEADER_BYTES = 5;\n\nexport interface Tn3270eHeader {\n dataType: number;\n requestFlag: number;\n responseFlag: number;\n seq: number;\n}\n\n/**\n * Build the true header bytes.\n *\n * DELIBERATELY DOES NOT ESCAPE 0xFF. RFC 2355 \u00A78.1.4 requires that a 0xff inside\n * SEQ-NUMBER be doubled \u2014 \"this is standard IAC escaping\" \u2014 and it will be, by\n * doubleIac() in telnet.ts, which every outbound record already passes through.\n * Escaping here as well would double it twice, and would also mangle any record\n * whose 3270 payload happens to contain a 0xff. Prepend this to the payload and\n * hand the single buffer to sendRecord(), and the requirement is met by\n * construction rather than by a second escaping implementation that could drift out\n * of step with the first. The end-to-end behavior is pinned at the session level.\n *\n * The `& 0xff` on each field is INTENT, NOT PROTECTION: `Uint8Array.of` already\n * truncates mod 256, established by deleting a mask and watching the test still\n * pass. They are kept because they say what the field is, and because they become\n * load-bearing the moment this is rewritten to build a `number[]` or write through a\n * DataView \u2014 neither of which truncates for you.\n */\nexport function encodeHeader(h: Tn3270eHeader): Uint8Array {\n return Uint8Array.of(\n h.dataType & 0xff,\n h.requestFlag & 0xff,\n h.responseFlag & 0xff,\n (h.seq >> 8) & 0xff,\n h.seq & 0xff,\n );\n}\n\n/**\n * Read a header off the front of an inbound record, or null if the record cannot\n * hold one.\n *\n * A record of exactly five bytes is valid and carries no data: RFC 2355 \u00A78 permits\n * `<TN3270E Header><IAC EOR>`, which is how PRINT-EOJ arrives and how a bare\n * RESPONSE could. So the test is `< TN3270E_HEADER_BYTES`, not `<=`.\n *\n * Returns null rather than throwing. Four bytes is not a truncated message we can\n * salvage, it is a malformed one, and the caller should trace and drop it: a client\n * cannot correct a host, and an exception here would surface to the operator as a\n * program check the host never caused.\n */\nexport function decodeHeader(record: Uint8Array): Tn3270eHeader | null {\n if (record.length < TN3270E_HEADER_BYTES) return null;\n return {\n dataType: record[0]!,\n requestFlag: record[1]!,\n responseFlag: record[2]!,\n seq: (record[3]! << 8) | record[4]!,\n };\n}\n\n/**\n * True only for the data type that carries a 3270 datastream we can execute.\n *\n * This is the gate that keeps a bind image, an unbind reason code or NVT text out of\n * the 3270 executor, where any of them would produce a spurious program check\n * attributable to nothing the host did wrong. SCS-DATA is excluded too: it is SNA\n * Character Stream, which belongs to the printer session rather than here.\n */\nexport function carriesDatastream(dataType: number): boolean {\n return dataType === Tn3270eDataType.DATA_3270;\n}\n\n/**\n * The functions we ask for.\n *\n * BIND-IMAGE IS REQUESTED, and the hazard that once justified omitting it has been\n * measured away. Granted BIND-IMAGE and sent no BIND, real s3270 never enters 3270\n * mode (telnet.c:2339, and the gate at telnet.c:2681 that drops 3270_DATA until\n * `tn3270e_bound`). THAT REMAINS TRUE. What changed is knowing how often it happens:\n * 29 of 29 hosts in x3270's own trace collection that grant BIND-IMAGE send a BIND\n * immediately after FUNCTIONS, counted BY BYTES across all 71 traces -- grepping for\n * decoded `< BIND` annotation lines gives a FALSE answer, because older traces carry\n * no annotations. The advertise-then-stay-silent case exists only in our own\n * e-server.py, which we configured to do it.\n *\n * So we adopt the gate deliberately AND refuse to inherit the hang: a pre-BIND\n * 3270-DATA record is retained and executed if NO_BIND_TIMEOUT_MS expires (bind.ts).\n * Asking for it is what gives BIND, UNBIND and the whole geometry channel a\n * verification path at all -- 46 recorded traces stop dead at FUNCTIONS without it.\n *\n * The two printer functions, SCS-CTL-CODES and DATA-STREAM-CTL, are printer-session\n * functions by RFC 2355 \u00A77.2.2 and belong to the printer stage.\n *\n * CONTENTION-RESOLUTION is not in RFC 2355 at all; x3270 requests it and so do we,\n * but nothing here depends on a host granting it.\n *\n * ORDER MATTERS TO THE TESTS, NOT TO THE PROTOCOL: this is s3270's own order\n * (00 02 04 05), so a byte-for-byte comparison against a recorded s3270 FUNCTIONS\n * REQUEST matches without sorting either side.\n */\nexport const REQUESTED_FUNCTIONS: readonly number[] = [\n Tn3270eFunc.BIND_IMAGE,\n Tn3270eFunc.RESPONSES,\n Tn3270eFunc.SYSREQ,\n Tn3270eFunc.CONTENTION_RESOLUTION,\n];\n\n/**\n * The functions THIS session asks for. Defaults to REQUESTED_FUNCTIONS; with\n * `bindImage` false (`SessionOptions.bindImage`, Task 11's `-bind-image off`),\n * BIND-IMAGE is left out of the request.\n *\n * A FUNCTION, DELIBERATELY NOT A MUTATION OF THE CONSTANT: `REQUESTED_FUNCTIONS`\n * stays the fixed default-on list so the conformance tests above (a byte-for-byte\n * comparison against a recorded s3270 FUNCTIONS REQUEST) keep comparing against\n * exactly what they always have, and so every existing caller that reads the\n * constant directly keeps working unchanged.\n *\n * THE RESULT OF THIS CALL IS WHAT `addsNothing` MUST BE JUDGED AGAINST, not\n * `REQUESTED_FUNCTIONS` itself. If `addsNothing` used the constant instead, a host\n * that grants BIND-IMAGE on a session that asked it OFF would look like it \"added\n * nothing\" -- because BIND-IMAGE is still in the constant -- and would be silently\n * adopted into `agreed`. `bindImageGranted()` (session.ts) reads `agreed` alone, so\n * the BIND gate would then arm itself on a session whose operator specifically\n * asked to skip the whole BIND/UNBIND channel: exactly the outcome `-bind-image off`\n * exists to prevent, reached anyway because the refusal check consulted the wrong\n * list.\n */\nexport function requestedFunctions(bindImage: boolean): readonly number[] {\n return bindImage\n ? REQUESTED_FUNCTIONS\n : REQUESTED_FUNCTIONS.filter((f) => f !== Tn3270eFunc.BIND_IMAGE);\n}\n\nexport type Tn3270ePhase =\n | 'idle'\n | 'awaitingDeviceType'\n | 'awaitingFunctions'\n | 'negotiated'\n | 'backedOff';\n\nexport interface Tn3270eState {\n readonly phase: Tn3270ePhase;\n /** Functions agreed. Empty with phase 'negotiated' is \"basic TN3270E\" (\u00A79). */\n readonly agreed: readonly number[];\n readonly terminalType: string;\n /** LU names still to try, in order. Advanced on REJECT. */\n readonly lus: readonly string[];\n readonly luIndex: number;\n readonly deviceType?: string;\n /** The LU the SERVER reported, which need not be the one we asked for. */\n readonly lu?: string;\n /**\n * THE FUNCTIONS THIS SESSION ASKS FOR, fixed at `initialState()` time by\n * `requestedFunctions(bindImage)`. Per-session in exactly the way `terminalType`\n * and `lus` already are: `-bind-image off` (Task 11) must change what one\n * session requests without touching any other session's negotiation, including\n * one already in flight on a different connection.\n *\n * Carried on the STATE, not threaded as a parameter to `negotiate()`, because\n * `negotiate()` is pure and already receives the state on every call -- adding a\n * parameter to a pure function that is called from three places inside itself\n * (the FUNCTIONS REQUEST reply, the counter-offer, and `addsNothing`) would mean\n * getting it right at three call sites instead of one field read from `st`.\n */\n readonly requested: readonly number[];\n}\n\nexport type Tn3270eEffect =\n | { kind: 'complete'; agreed: readonly number[] }\n | { kind: 'backoff'; why: string };\n\nexport interface NegotiateResult {\n readonly next: Tn3270eState;\n /** Subnegotiation body to send, WITHOUT the option byte. The caller frames it. */\n readonly reply?: Uint8Array;\n readonly effect?: Tn3270eEffect;\n}\n\nexport function initialState(\n o: { terminalType: string; lus: readonly string[]; bindImage?: boolean },\n): Tn3270eState {\n return {\n phase: 'idle',\n agreed: [],\n terminalType: o.terminalType,\n lus: o.lus,\n luIndex: 0,\n // Defaults to on, matching `SessionOptions.bindImage`'s own default -- see that\n // field's comment in session.ts.\n requested: requestedFunctions(o.bindImage ?? true),\n };\n}\n\nconst toAscii = (s: string): number[] => Array.from(s, (c) => c.charCodeAt(0) & 0xff);\nconst fromAscii = (b: Uint8Array): string => String.fromCharCode(...b);\n\n\n/** Every function code we know. An inbound code outside this is \"unrecognized\". */\nconst KNOWN_FUNCS: readonly number[] = Object.values(Tn3270eFunc);\n\n/**\n * Decode a function list, discarding codes we do not know.\n *\n * RFC 2355 \u00A77.2.2 requires exactly this: \"If in the process of functions negotiation\n * an unrecognized function code is recieved, the recipient should simply remove that\n * function code from the list and continue normal functions negotiation.\" So an\n * unknown code is NOT an error and must not abort the session.\n *\n * DISCARDING MUST HAPPEN BEFORE the addsNothing() comparison. Judged the other way\n * round, an unknown code looks like an illegal addition and triggers backoff, turning\n * a conforming server into a refused one. x3270 gets this for free by decoding into a\n * bitmap that cannot hold an unknown bit (tn3270e_fdecode).\n */\nfunction decodeFuncs(list: Uint8Array): number[] {\n return [...list].filter((f) => KNOWN_FUNCS.includes(f));\n}\n\n/**\n * True when `offered` contains nothing outside THIS SESSION's requested list\n * (`st.requested`), NOT the module constant `REQUESTED_FUNCTIONS`.\n *\n * See `Tn3270eState.requested`'s comment for what goes wrong if this read the\n * constant instead: with `-bind-image off`, BIND-IMAGE would look like something\n * we always ask for and a host granting it unasked would be silently accepted.\n */\nfunction addsNothing(st: Tn3270eState, offered: readonly number[]): boolean {\n return offered.every((f) => st.requested.includes(f));\n}\n\n/** Build DEVICE-TYPE REQUEST <ttype> [CONNECT <lu>] for the state's current LU. */\nfunction deviceTypeRequest(st: Tn3270eState): Uint8Array {\n const lu = st.lus[st.luIndex];\n return Uint8Array.from([\n Tn3270eOp.DEVICE_TYPE, Tn3270eOp.REQUEST, ...toAscii(st.terminalType),\n ...(lu === undefined ? [] : [Tn3270eOp.CONNECT, ...toAscii(lu)]),\n ]);\n}\n\n/**\n * Advance the negotiation by one received subnegotiation body.\n *\n * `body` excludes the option byte AND the trailing IAC SE, so `body[0]` is the first\n * operation. (x3270 keeps the SE in its sbbuf, which is why its parser scans for it\n * \u2014 telnet.c:2219 \u2014 while ours can scan to the end of the buffer instead.)\n *\n * Pure: returns a new state and never mutates the one it was handed, so the session\n * can decide what to do with the result before adopting it.\n *\n * An unrecognized or misordered body yields no reply and no state change. Silence is\n * the correct response to a message we cannot parse, and it is exactly what real\n * s3270 does when handed a misordered SEND DEVICE-TYPE.\n */\nexport function negotiate(st: Tn3270eState, body: Uint8Array): NegotiateResult {\n // SEND DEVICE-TYPE. THE VERB COMES FIRST HERE (0x08 0x02) and the noun first in\n // our reply (0x02 0x07). That asymmetry is real -- x3270 pins it at telnet.c:2199,\n // where the test is `sbbuf[2] == TN3270E_OP_DEVICE_TYPE` -- and reversing it makes\n // s3270 log \"DEVICE-TYPE ??8\" and then stall, with no reject and no error.\n if (body[0] === Tn3270eOp.SEND && body[1] === Tn3270eOp.DEVICE_TYPE) {\n return {\n next: { ...st, phase: 'awaitingDeviceType' },\n reply: deviceTypeRequest(st),\n };\n }\n\n if (body[0] === Tn3270eOp.DEVICE_TYPE && body[1] === Tn3270eOp.IS) {\n // DEVICE-TYPE IS <type> [CONNECT <name>]; the CONNECT clause is optional and\n // \u00A77.1.4 does not require it. x3270 scans to SE or CONNECT (telnet.c:2219-2221);\n // our body has no SE, so end-of-buffer plays that role.\n const rest = body.subarray(2);\n const sep = rest.indexOf(Tn3270eOp.CONNECT);\n const type = fromAscii(sep === -1 ? rest : rest.subarray(0, sep));\n const lu = sep === -1 ? '' : fromAscii(rest.subarray(sep + 1));\n return {\n // An empty name is treated as no LU, not as an LU called nothing: only one of\n // those should reach the status line.\n next: {\n ...st, phase: 'awaitingFunctions', deviceType: type,\n ...(lu === '' ? {} : { lu }),\n },\n reply: Uint8Array.from([\n Tn3270eOp.FUNCTIONS, Tn3270eOp.REQUEST, ...st.requested,\n ]),\n };\n }\n\n if (body[0] === Tn3270eOp.FUNCTIONS && body[1] === Tn3270eOp.IS) {\n const offered = decodeFuncs(body.subarray(2));\n if (!addsNothing(st, offered)) {\n // x3270 calls this \"Host illegally added function(s)\" (telnet.c:2327) and\n // abandons TN3270E outright rather than trying to reconcile. So do we: a server\n // that grants what we did not request is not one to keep bargaining with.\n // BIND-IMAGE does not exercise this branch on a session that requested it (the\n // default) -- what lands here then is a printer function or anything else\n // outside the four we ask for. On a session built with `-bind-image off`\n // (Task 11), BIND-IMAGE is EXCLUDED from `st.requested` and so DOES land here\n // if the host grants it anyway: that is the whole protection `-bind-image off`\n // relies on, since `addsNothing` above is judged against `st.requested`, not\n // the fixed module constant.\n return {\n next: { ...st, phase: 'backedOff' },\n effect: { kind: 'backoff', why: 'host illegally added function(s)' },\n };\n }\n // SILENCE IS THE REPLY. Real s3270 sends nothing here, and an echoed FUNCTIONS IS\n // would still appear to work against a tolerant server -- which is why the\n // absence of a reply is asserted in the tests.\n //\n // An empty list is legal and completes: RFC 2355 \u00A79 calls it \"basic TN3270E\".\n return {\n next: { ...st, phase: 'negotiated', agreed: offered },\n effect: { kind: 'complete', agreed: offered },\n };\n }\n\n if (body[0] === Tn3270eOp.FUNCTIONS && body[1] === Tn3270eOp.REQUEST) {\n const offered = decodeFuncs(body.subarray(2));\n if (addsNothing(st, offered)) {\n // They want what we want, or less: adopt it, confirm with IS, and finish\n // (telnet.c:2293-2301).\n return {\n next: { ...st, phase: 'negotiated', agreed: offered },\n reply: Uint8Array.from([Tn3270eOp.FUNCTIONS, Tn3270eOp.IS, ...offered]),\n effect: { kind: 'complete', agreed: offered },\n };\n }\n // They want something we cannot do: counter with the common subset and STAY in\n // negotiation (telnet.c:2306-2311). No 'complete' effect here -- emitting one\n // would put the session in 3270 mode before the host has agreed to anything.\n const common = offered.filter((f) => st.requested.includes(f));\n return {\n next: { ...st, phase: 'awaitingFunctions' },\n reply: Uint8Array.from([Tn3270eOp.FUNCTIONS, Tn3270eOp.REQUEST, ...common]),\n };\n }\n\n if (body[0] === Tn3270eOp.DEVICE_TYPE && body[1] === Tn3270eOp.REJECT) {\n // THE REASON IS CHECKED BEFORE TRYING ANOTHER LU, matching telnet.c:2263-2267.\n // UNSUPPORTED-REQ is about the request TYPE rather than the resource, so no other\n // LU would fare better and retrying would only add noise to the wire.\n //\n // body[3] is undefined when the REASON clause is absent, which \u00A77.1.5 shows as\n // present but a truncated body could omit. undefined compares equal to nothing,\n // so it falls through to the rejection paths below rather than throwing or being\n // mistaken for success.\n const reason = body[2] === Tn3270eOp.REASON ? body[3] : undefined;\n if (reason === Tn3270eReason.UNSUPPORTED_REQ) {\n return {\n next: { ...st, phase: 'backedOff' },\n effect: { kind: 'backoff', why: 'host rejected request type' },\n };\n }\n const nextIndex = st.luIndex + 1;\n if (nextIndex < st.lus.length) {\n // deviceTypeRequest() is given the UPDATED state so it reads the new luIndex.\n // Passing `st` would resend the name that was just rejected -- an endless\n // exchange against a host that keeps saying no, which is much harder to\n // diagnose than a clean failure.\n const next: Tn3270eState = {\n ...st, luIndex: nextIndex, phase: 'awaitingDeviceType',\n };\n return { next, reply: deviceTypeRequest(next) };\n }\n // Out of LUs, or there never were any. x3270 distinguishes the two messages\n // (telnet.c:2275-2277), and the distinction is what tells an operator whether to\n // fix an LU name or the model.\n return {\n next: { ...st, phase: 'backedOff' },\n effect: {\n kind: 'backoff',\n why: st.lus.length > 0 ? 'host rejected resource(s)' : 'device type rejected',\n },\n };\n }\n\n return { next: st };\n}\n", "import {\n AID, MODEL_2, TERMINAL_TYPE, Tn3270eDataType, Tn3270eFunc, Tn3270eResponseFlag,\n Tn3270eSense, VALID_AIDS,\n} from './constants.js';\nimport { Screen } from './screen.js';\nimport { Keyboard } from './keyboard.js';\nimport { Oia, KeyboardState } from './oia.js';\nimport { Trace, parseTrace } from './trace.js';\nimport { TelnetLayer, type TelnetLayerOptions } from './telnet.js';\nimport { DeviceName } from './devname.js';\nimport {\n initialState, negotiate, encodeHeader, decodeHeader, carriesDatastream,\n TN3270E_HEADER_BYTES, type Tn3270eState, type Tn3270eHeader,\n} from './tn3270e.js';\nimport { parseRecord, ParseError, describeRecord } from './stream/parse.js';\nimport { execute, ExecuteError } from './stream/execute.js';\nimport { buildReadModified, buildReadBuffer } from './inbound.js';\nimport { buildReply, DEFAULT_CAPABILITIES, withDdm, type QueryRequest } from './queryreply.js';\nimport { DftTransfer } from './ft/dft.js';\nimport { AddressError } from './address.js';\nimport { cp037, type CodePage } from './codepage.js';\nimport { parseBind, parseUnbind, acceptBindDims, NO_BIND_TIMEOUT_MS } from './bind.js';\nimport { resolveGeometry, type GeometrySetting, type ResolvedGeometry, type Size } from './geometry.js';\n\n/**\n * A single TN3270 session: socket, telnet layer, screen, keyboard.\n *\n * No module-level state anywhere in this file \u2014 construct as many as you like.\n * That is what makes multi-session tabs a UI change rather than a core rewrite.\n */\n\n/** The transport, injected so tests and Replay() need no real socket. */\nexport interface Connection {\n write(bytes: Uint8Array): void;\n close(): void;\n onData: ((bytes: Uint8Array) => void) | undefined;\n onClose: (() => void) | undefined;\n onError: ((err: Error) => void) | undefined;\n}\n\nexport interface SessionOptions {\n connect: (host: string, port: number) => Connection | Promise<Connection>;\n /** The DEFAULT (Erase/Write) screen size. Always 24x80 on a real model. */\n rows?: number;\n cols?: number;\n /**\n * The ALTERNATE (Erase/Write Alternate) size, from the model number. Defaults\n * to the default size, which is a model 2.\n */\n alternateRows?: number;\n alternateCols?: number;\n codePage?: CodePage;\n /** Telnet TERMINAL-TYPE to advertise. Defaults to IBM-3278-2-E. */\n terminalType?: string;\n /** Offer TN3270E. Defaults to true; `-tn3270e off` and the N: prefix clear it. */\n tn3270e?: boolean;\n /** LU names to request via CONNECT, tried in order as REJECTs come back. */\n lus?: readonly string[];\n /**\n * Range-check a BIND's geometry against the model before applying it. Defaults to\n * true, matching x3270's `bind_limit` resource (Common/glue.c:458) and its polarity:\n * the flag that turns it OFF, not on. `-bind-limit off` (Task 11) will drive this\n * directly; this task defaults it and wires the check.\n */\n bindLimit?: boolean;\n /**\n * Request the BIND-IMAGE function. Defaults to true; `-bind-image off` clears it.\n *\n * Off means the gate never closes, because the gate is conditional on the host\n * having AGREED the function.\n */\n bindImage?: boolean;\n /**\n * Advertise Query Reply (DDM), QCODE 0x95, which is what lets a host choose DFT\n * file transfer instead of CUT. **Defaults to TRUE as of 2026-09-29**; `-ddm off`\n * clears it.\n *\n * ## THE FLIP, AND WHY IT WAITED\n *\n * This is the one capability here that is not simply \"do we honor it\": the client\n * does not select a transfer protocol, it declares a capability and the HOST picks.\n * So the flag changes which protocol live hosts speak. It shipped DEFAULT OFF on\n * 2026-09-24 precisely because DFT did not work yet \u2014 turning it on made a host\n * offer a protocol we could not parse, and every transfer witness this project had\n * was gathered over CUT.\n *\n * **The user's plan then was: ship it off, measure which hosts offer DFT, then flip.\n * DFT now works and is live-verified** (11 DFT frames against MVS 3.8j TK5 at 43x80,\n * 249 bytes byte-identical both directions, `docs/live-testing.md`), so the flip is\n * their call of 2026-09-29.\n *\n * **WHAT TURNING IT ON MEANS, stated plainly because it is a real trade:** a host\n * that speaks DFT will now choose DFT, so the CUT path gets less live exercise than\n * it did. CUT is still fully implemented, still the only option at 24x80 against a\n * CUT-only host, and `-ddm off` still restores exactly the old behavior \u2014 which is\n * how every pre-2026-09-29 CUT measurement can be reproduced.\n *\n * See docs/superpowers/specs/2026-09-24-dft-file-transfer-design.md.\n */\n ddm?: boolean;\n /**\n * Inbound/outbound DDM byte limit advertised in the DDM Query Reply, clamped to\n * 256..32767. Only read when `ddm` is on. Defaults to 16384.\n */\n dftBufferSize?: number;\n /**\n * Device-name template for NEW-ENVIRON's DEVNAME uservar. Absent means we refuse\n * telnet option 39 entirely.\n *\n * Trailing `=` characters become a counter: `foo===` yields foo001, foo002, ... See\n * devname.ts for why a host wants a fresh name per request.\n */\n devname?: string;\n /**\n * Value for NEW-ENVIRON's USER var. Defaults to $USER, then $USERNAME, then UNKNOWN,\n * matching x3270 (telnet_new_environ.c:221-228).\n *\n * THIS PUTS THE LOCAL ACCOUNT NAME ON THE WIRE to any host that asks for it. x3270\n * does the same unconditionally. Documented in the README rather than left for someone\n * to discover in a trace.\n */\n user?: string;\n /**\n * The model and oversize, as one setting resolved at EVERY connect -- GEOMETRY MODE.\n *\n * Absent means legacy mode: `terminalType`, `rows`/`cols` and `alternateRows`/`alternateCols`\n * fix the session's geometry at construction, exactly as before this option existed. Given,\n * it replaces all of them, and the constructor refuses it alongside any of them: two sources\n * for one terminal type would leave a reader of the call site unable to tell which one the\n * host saw. See `Session.setGeometry`, and the spec,\n * docs/superpowers/specs/2026-11-04-oversize-and-model-switching-design.md.\n */\n geometry?: GeometrySetting;\n /**\n * Measure the window for `oversize: { kind: 'auto' }`, called once per connect and never at\n * construction (see the spec's \"Why auto is measured at connect, and only then\").\n *\n * A SESSION option rather than a field of `geometry`, and kept for the session's lifetime:\n * `setGeometry` REPLACES the setting wholesale, and a function riding inside it would be\n * dropped by the first replacement that did not happen to repeat it -- the reason\n * `GeometrySetting` is plain data (geometry.ts).\n */\n measure?: () => Size | undefined;\n}\n\n/**\n * Settings that belong to ONE connection rather than to the session.\n *\n * In s3270 these are properties of a HOST \u2014 they are written in the host argument\n * (`N:`, `LUname@`), and `Connect()` can name a different host every time. Passing\n * them to the constructor alone would make them properties of the process, so a CLI\n * script that connects to a plain host and then to a TN3270E one could only be right\n * about one of them. Each field falls back to the `SessionOptions` value.\n *\n * Lives on the connection, and so is discarded with it: see `handleClose`.\n */\nexport interface ConnectOptions {\n /** Offer TN3270E on this connection. The `N:` host prefix passes `false`. */\n tn3270e?: boolean;\n /** LU names to request on this connection, tried in order as REJECTs come back. */\n lus?: readonly string[];\n}\n\n/**\n * `transferEnd` fires when a DFT transfer finishes, however it finished \u2014 success,\n * the host's own error message, a cancellation, or a malformed frame. A front end\n * uses it to close a progress display without polling.\n *\n * `transferProgress` fires once per DFT data frame accepted, and it exists because a DFT\n * transfer is otherwise COMPLETELY SILENT between its start and its end. That silence was a\n * live bug, not a cosmetic gap: `transferRun.ts` re-arms its 30-second per-frame deadline only\n * from the CUT path, so nothing re-armed it during a DFT transfer and **any DFT transfer taking\n * longer than 30 seconds was killed by a timer meant to detect a stalled CUT host.** Measured\n * 2026-10-02 against MVS/TSO: a 249-byte file finished well inside the window and a 200 KB one\n * died with `stalled: no CUT frame from the host within 30s, 0 bytes` \u2014 a message that was\n * doubly misleading, since the host was neither stalled nor speaking CUT.\n *\n * It also gives DFT the progress display it never had, which is the same fact seen from the\n * operator's side.\n *\n * `geometry` fires on every connect in geometry mode (`SessionOptions.geometry`), once the\n * setting has been resolved and the screen resized, and BEFORE the socket is opened -- so a\n * front end can print `lastGeometryNotice` (an `auto` that fell back to the model) before\n * anything the host sends. Never in legacy mode, where nothing is resolved.\n *\n * `connectSettled` fires once when the last connection attempt in flight ENDS, succeeded or\n * failed, with `isConnecting()` already false -- the counterpart of 'geometry' for a front end\n * that disables controls while an attempt is in flight. A failed attempt emits nothing else.\n *\n * Not exported from `index.ts` and referenced only by `on`/`off`/`listenerCount`/\n * `emit`, so widening it has no blast radius (checked 2026-09-25, re-checked 2026-10-02).\n */\nexport type SessionEvent =\n | 'screen' | 'connect' | 'disconnect' | 'alarm' | 'transferEnd' | 'transferProgress'\n | 'geometry' | 'connectSettled';\n\n/** Program check codes. x3270 shows a number after \"X PROG\". */\nconst PROG_INVALID_COMMAND = 754;\nconst PROG_INVALID_ADDRESS = 755;\n\nexport class Session {\n readonly screen: Screen;\n readonly keyboard: Keyboard;\n readonly oia = new Oia();\n readonly trace = new Trace();\n\n private readonly opts: SessionOptions;\n private conn: Connection | undefined;\n private telnet: TelnetLayer | undefined;\n private error: string | undefined;\n private readonly listeners = new Map<SessionEvent, Set<() => void>>();\n /**\n * The DFT transfer in flight, if any. `undefined` means a `SF_TRANSFER_DATA`\n * frame is unexpected \u2014 which x3270 also treats as a no-op rather than an\n * error, tracing \"(no transfer in progress)\" (`ft_dft.c:97-100`).\n */\n private dft: DftTransfer | undefined;\n /**\n * The in-flight transfer's DFT buffer size, for the DDM Query Reply to advertise.\n *\n * ## WHY THIS IS NOT JUST `opts.dftBufferSize`\n *\n * THE SIZE WE ADVERTISE AND THE SIZE WE CHUNK BY MUST BE ONE NUMBER, which\n * `DftOptions.bufferSize` has always said and nothing enforced. Before this field\n * existed the two could differ and did: with `dftBufferSize: 512` the Query Reply\n * advertised 512 while the engine every driver builds chunked by the 16384 default\n * \u2014 a 32x overrun of the frame size we had promised the host. Task 11 measured the\n * host READING that advertisement and quoting it back in its `Open` (recordSize =\n * advertised \u2212 17), so the number is not decorative.\n *\n * x3270 has the same single source of truth and reads it the same way: `do_qr_ddm`\n * takes `ftc->dft_buffersize` from the in-flight transfer and falls back to the\n * resource default only when none is running (`sf.c:890-897`).\n *\n * ## A DELIBERATE DIVERGENCE: WE CLEAR IT, x3270 DOES NOT\n *\n * x3270 never clears `ftc` \u2014 the only assignment in `ft.c` is `:807` \u2014 so after any\n * transfer its Query Reply keeps quoting that transfer's size forever. Cleared here\n * at all four sites that clear `dft`, so a host querying between transfers gets the\n * session default back. Fifth instance in this file of \"one path sets state and\n * another must clear it\"; see `handleClose`.\n */\n private dftBuffer: number | undefined;\n /**\n * Host records applied since connect. Monotonic; never reset.\n *\n * Exposed so a caller can wait for the stream to QUIESCE rather than for a\n * particular screen predicate. tnz uses exactly this technique \u2014 it polls the\n * session's byte count and proceeds once it stops changing, with a WAITSLEEP\n * interval (ati.py:1965-1976) \u2014 and it is the robust answer when a host sends\n * one logical screen as several records, which VM/370 does.\n */\n private records = 0;\n /** TN3270E negotiation state. Undefined until the host offers option 40. */\n private e: Tn3270eState | undefined;\n /** Outbound SEQ-NUMBER. Only advances when RESPONSES was agreed (\u00A78.1.4). */\n private eSeq = 0;\n /**\n * True once a BIND has been received on a session that granted BIND-IMAGE.\n *\n * Mirrors x3270's `tn3270e_bound` (Common/telnet.c:182), which the gate below reads\n * at telnet.c:2681-2682 and BIND/UNBIND set and clear at :2742 and :2752. Meaningless\n * -- and never consulted -- on a session that never granted the function: see\n * `bindImageGranted()`.\n *\n * Cleared in `forgetTn3270e()`, alongside `pendingBindRecord`: see that method's\n * comment for why a negotiation ending is the one place both belong, and for the\n * enumeration of every path that reaches it.\n */\n private bound = false;\n /**\n * The most recent 3270-DATA record withheld by the BIND gate (`handleRecord`).\n *\n * ONLY THE MOST RECENT, DELIBERATELY NOT A QUEUE: a host that painted twice before\n * binding has overwritten its own first screen, so keeping the first would show the\n * operator a screen the host itself has abandoned, and an unbounded queue is a\n * memory hole a remote party controls -- it decides how many records precede its\n * own BIND. `wants` is the decoded header, kept alongside the header-stripped body\n * because they are exactly `executeRecord`'s two parameters: the timeout path\n * (Task 9) calls `executeRecord(pendingBindRecord.body, pendingBindRecord.wants)`,\n * so a host that asked for ALWAYS-RESPONSE on the withheld record still gets one.\n *\n * Cleared in `forgetTn3270e()`; see `bound`'s comment.\n */\n private pendingBindRecord: { wants: Tn3270eHeader; body: Uint8Array } | undefined;\n /**\n * The no-BIND deadline: gives up waiting for a BIND after NO_BIND_TIMEOUT_MS\n * (bind.ts) and executes `pendingBindRecord` anyway. Armed by `armNoBindTimer()`\n * when the gate first withholds a record; see that method for why it is not\n * re-armed per record and why it is not `.unref()`'d.\n */\n private noBindTimer: ReturnType<typeof setTimeout> | undefined;\n /**\n * Whether a BIND's geometry is range-checked against the model before being applied.\n * x3270's `bind_limit` (Common/glue.c:458, default true) -- see `SessionOptions.bindLimit`.\n */\n private readonly bindLimit: boolean;\n /**\n * THE MODEL'S ALTERNATE GEOMETRY, CAPTURED AT CONSTRUCTION -- NOT READ FROM\n * `this.screen.alternateSize`.\n *\n * `screen.alternateSize` is mutable now that a BIND can rewrite it (`Screen.setSizes`),\n * so by the time an UNBIND arrives it may hold whatever the MOST RECENT BIND asked for,\n * not the model the session was built with. x3270 keeps the same distinction: `altROWS`/\n * `altCOLS` are the LIVE, mutable values `process_bind` overwrites (telnet.c:2554-2555),\n * while UNBIND's revert (telnet.c:2760-2761) assigns them back from `maxROWS`/`maxCOLS` --\n * the model's fixed maximum, set once at `-model` parse time and never touched by BIND.\n * This field is that same fixed point on our side. Deriving it from the screen instead\n * would revert an UNBIND to the PREVIOUS BIND's geometry rather than the model's -- wrong,\n * and it looks completely correct while doing so. See the mutation test in\n * tn3270e-session.test.ts pinning exactly this.\n *\n * IN GEOMETRY MODE (`SessionOptions.geometry`) IT IS THE RESOLVED ALTERNATE FOR THE CURRENT\n * CONNECTION, written by `applyGeometry()` early in every `connect()` -- hence no longer\n * `readonly`. Still the fixed point the paragraph above describes, just fixed per connection\n * rather than per session: no BIND writes it, only a connect. This mirrors x3270 making an\n * oversize the model's maximum (`ctlr.c:334`, `ov_cols = maxCOLS = ovc`), so BIND's limit\n * (`acceptBindDims(dims, model)` in `handleBind`), BIND's screen-size byte 0x03 (`'caller'`)\n * and UNBIND's revert all read the oversize from here with no change of their own.\n */\n private modelAlternate: { readonly rows: number; readonly cols: number };\n /**\n * The model and oversize to resolve at the next connect, or `undefined` for LEGACY MODE, in\n * which the constructor's `terminalType`/alternate options are fixed for the session's\n * lifetime and nothing below is resolved. See `SessionOptions.geometry`.\n *\n * A property of the session's configuration, like `target`, so `handleClose` leaves it alone.\n */\n private setting: GeometrySetting | undefined;\n /**\n * The terminal type this CONNECTION advertises, resolved from `setting` at connect; read\n * instead of `opts.terminalType` by the TelnetLayer build and by TN3270E's DEVICE-TYPE\n * REQUEST, so the two can never disagree. `undefined` in legacy mode, where both fall back to\n * `opts.terminalType` exactly as before.\n */\n private ttype: string | undefined;\n /** See `lastGeometryNotice`. */\n private geometryNotice: string | undefined;\n /** See `resolvedGeometry`. */\n private resolved: ResolvedGeometry | undefined;\n /** This connection's overrides. Empty between connections. See ConnectOptions. */\n private per: ConnectOptions = {};\n /**\n * The last target `connect()` was ASKED for, kept so `reconnect()` can replay it.\n *\n * ## IT SURVIVES `handleClose()`, AND THAT IS NOT A VIOLATION OF ITS RULE\n *\n * `handleClose` discards everything that belongs to the connection \u2014 the telnet layer, the\n * TN3270E state, `per` \u2014 because a second connection must not inherit the first one's\n * negotiation. This field is the one thing here that is NOT a property of the connection: it\n * is a property of the session's CONFIGURATION, the answer to \"which host is this session\n * for\", and it is read only when there is no connection at all. Clearing it on close would\n * make `reconnect()` unreachable by construction, since the only time anyone wants to\n * reconnect is after a close.\n *\n * `per` is copied in rather than aliased, and stored HERE as well as in `this.per`, precisely\n * because the two have different lifetimes: `this.per` dies with the connection (see\n * `ConnectOptions`), while the remembered copy is part of the target. `N:` and `LU@` are\n * written in the HOST argument, so \"the host we were told to talk to\" includes them \u2014\n * replaying the host and port without them would silently reconnect with TN3270E back on to a\n * host the operator spelled `N:`.\n *\n * NOT the resolved socket, and NOT a TLS decision: see `reconnect()` for why that matters.\n */\n private target: { host: string; port: number; per: ConnectOptions } | undefined;\n /**\n * A `reconnect()` between its call and its socket. See that method: this is the half of\n * x3270's `PCONNECTED` that `isConnected()` does not cover, and it is what stops two quick\n * presses of the reconnect key opening two sockets.\n */\n private reconnecting = false;\n /**\n * How many `connect()` calls are between their start and their end -- the FIRST connect's\n * equivalent of `reconnecting`, which covers only `reconnect()`. A COUNT and not a flag, because\n * two overlapping `connect()` calls are legal and the first to finish must not clear the\n * second's.\n *\n * WHY `setGeometry` NEEDS IT: `connect()` resolves the setting BEFORE its first await, so a\n * change accepted while the socket or TLS handshake is still in flight would never reach the\n * host -- and a front end would show it as the current geometry all the same.\n */\n private connecting = 0;\n /**\n * THE PENDING AID: which key the operator last pressed, for the host to learn about on\n * its own schedule rather than ours.\n *\n * This is x3270's `aid` global (declared `include/kybd.h:51`, defined `Common/kybd.c:111`\n * as `AID_NO`). It exists because **a Read Modified is not always a reply.** The operator\n * path and the host path are separate: `key_AID` transmits immediately AND records the\n * byte (`kybd.c:922-923`), and a *later* host-initiated read reports the same byte again\n * (`ctlr.c:641` Read Modified, `:647` Read Modified All, `:635` Read Buffer \u2014 all three\n * pass this one variable). A host polling with Read Modified is how an application gets\n * responsiveness without waiting for the terminal to volunteer anything.\n *\n * 0x60 (`AID.NONE`, x3270's `AID_NO`) means \"no key generated this read\", which is a real\n * value on the wire and not a sentinel for \"unset\".\n *\n * WHY IT IS NOT MERELY COSMETIC: `buildReadModified`'s short-read branch is reachable\n * only through it. Press Clear, and a host that reads before writing must get the AID\n * byte ALONE \u2014 no cursor address, no field data. With this hardcoded to 0x60 that branch\n * was dead code on the host path, and we would have answered with a full screen scrape\n * under the wrong AID.\n *\n * Cleared in four places, mirroring x3270 one for one. Three are host actions and live in\n * `executeRecord`; the fourth is `handleClose`, which is this codebase's equivalent of the\n * connection-scoped part of `restore_keyboard` (`ctlr.c:593`) \u2014 and it is there for the\n * reason recorded twice already in this file: one teardown path clearing state while\n * another does not is a bug shape this project has paid for with both `e` and `dft`.\n */\n private pendingAid: number = AID.NONE;\n\n constructor(opts: SessionOptions) {\n this.opts = opts;\n this.bindLimit = opts.bindLimit ?? true;\n if (opts.geometry !== undefined) {\n // ONE SOURCE OF GEOMETRY, refused rather than merged: with both, a reader of the call\n // site could not tell which terminal type the host saw.\n const legacy = (['terminalType', 'rows', 'cols', 'alternateRows', 'alternateCols'] as const)\n .filter((k) => opts[k] !== undefined);\n if (legacy.length > 0) {\n throw new Error(`geometry replaces ${legacy.join(', ')}: give one or the other, not both`);\n }\n // Validated as `setGeometry` validates, so a setting that every connect would refuse --\n // a fixed oversize below the model, `-oversize` on a non-`-E` model -- is refused here,\n // to whoever built the session, and not at the first connect.\n this.validateGeometry(opts.geometry);\n this.setting = opts.geometry;\n // THE MODEL'S GEOMETRY, NOT THE OVERSIZE'S: `auto` must not be measured here (the GUI's\n // font atlas, which its measure reads, does not exist yet -- see the spec's \"Why auto is\n // measured at connect, and only then\"), so the oversize is dropped for this one call and\n // the real resolve waits for `connect()`. A fixed oversize therefore also starts at the\n // model's sizes, which is harmless: nothing is on the screen before a connect.\n const g = resolveGeometry({ ...opts.geometry, oversize: { kind: 'none' } });\n this.screen = new Screen({\n rows: g.default.rows,\n cols: g.default.cols,\n alternateRows: g.alternate.rows,\n alternateCols: g.alternate.cols,\n ...(opts.codePage ? { codePage: opts.codePage } : {}),\n });\n this.modelAlternate = g.alternate;\n this.ttype = g.terminalType;\n } else {\n this.screen = new Screen({\n rows: opts.rows ?? MODEL_2.rows,\n cols: opts.cols ?? MODEL_2.cols,\n ...(opts.alternateRows !== undefined ? { alternateRows: opts.alternateRows } : {}),\n ...(opts.alternateCols !== undefined ? { alternateCols: opts.alternateCols } : {}),\n ...(opts.codePage ? { codePage: opts.codePage } : {}),\n });\n // Captured from OPTIONS, not from `this.screen.alternateSize` -- see the field's own\n // comment for why reading it back off the screen would be the trap.\n this.modelAlternate = {\n rows: opts.alternateRows ?? opts.rows ?? MODEL_2.rows,\n cols: opts.alternateCols ?? opts.cols ?? MODEL_2.cols,\n };\n }\n this.keyboard = new Keyboard(this.screen, this.oia, opts.codePage ?? cp037);\n }\n\n /** The setting the next connect will resolve, or `undefined` in legacy mode. */\n get geometrySetting(): GeometrySetting | undefined { return this.setting; }\n\n /**\n * The last connect's operator notice -- an `auto` that fell back to the model -- or\n * `undefined` when that connect had none (or in legacy mode, or before any connect). Every\n * front end prints it on stderr, though not necessarily on the `geometry` event itself -- the\n * TUI holds a reconnect's until it has left the alternate screen; see the spec's \"Data flow on\n * a connect\" for why none can hold it on screen.\n */\n get lastGeometryNotice(): string | undefined { return this.geometryNotice; }\n\n /**\n * What the last connect RESOLVED -- terminal type and both sizes -- or `undefined` in legacy\n * mode or before any connect. Not `screen.alternateSize`, which a BIND rewrites mid-session:\n * this is what the host was told, so a front end can compare a fresh `resolveGeometry` against\n * it to say whether a reconnect would change anything.\n */\n get resolvedGeometry(): ResolvedGeometry | undefined { return this.resolved; }\n\n /**\n * Replace the model and oversize, to take effect at the NEXT connect.\n *\n * REFUSED WHILE CONNECTED, and while a `reconnect()` is between its call and its socket\n * (`reconnecting` is the half of x3270's `PCONNECTED` that `conn` does not cover -- see that\n * field). The terminal type has already gone out in telnet negotiation and no 3270 message\n * lets a terminal announce a new size, so a mid-session change could only be a lie to the\n * host. x3270 refuses the same way; it can also queue the change for the disconnect\n * (`XN_DEFER`), which this deliberately does not: a pending change the operator cannot see\n * is a surprise.\n *\n * VALIDATED NOW, by `validateGeometry`, so a bad setting is refused to the operator who\n * typed it rather than at some later connect.\n *\n * Does not touch the screen; `connect()` does that.\n */\n setGeometry(next: GeometrySetting): void {\n if (this.setting === undefined) {\n throw new Error('setGeometry needs a session built with the geometry option; '\n + 'this one fixed its terminal type at construction');\n }\n if (this.conn !== undefined) {\n throw new Error('disconnect first; model and oversize apply at the next connect');\n }\n // NOT `disconnect first` here: there is no connection to disconnect yet, and the one being\n // made has already resolved the setting it will advertise. See `connecting`.\n if (this.reconnecting || this.connecting > 0) {\n throw new Error('wait for the connection attempt to finish; '\n + 'model and oversize apply at the next connect');\n }\n this.validateGeometry(next);\n this.setting = next;\n }\n\n /**\n * Throw whatever `resolveGeometry` would throw for `setting` at a connect, WITHOUT measuring.\n *\n * `auto` is resolved against a STAND-IN measure that reports no window, never the real one:\n * measuring is the connect's job, and the window may change before then. The stand-in takes\n * geometry.ts's \"no window size known\" fallback, but only after every rule `auto` can break\n * has run -- the terminal-type conflict and the `-E` requirement both precede the\n * `measure()` call there (`resolveGeometry`, geometry.ts) -- so it refuses exactly what a\n * connect would. With no measure option at all, `undefined` is passed through and geometry.ts\n * gives its own \"auto needs a window to measure\" refusal.\n *\n * An earlier version validated `auto` with its oversize swapped to `none`, which returns\n * before either rule: `{ model: '3278-2', oversize: auto }` was accepted here and refused only\n * at the next connect.\n */\n private validateGeometry(setting: GeometrySetting): void {\n resolveGeometry(setting, this.opts.measure !== undefined ? () => undefined : undefined);\n }\n\n /**\n * Apply a geometry `connect()` has already resolved.\n *\n * TWO HALVES, AND `connect()` RUNS THE TEARDOWN BETWEEN THEM. The RESOLVE comes first, before\n * the teardown and before `target` is recorded, so a refusal opens no socket, disturbs no live\n * connection and leaves `reconnect()` exactly as it was -- a first connect refused there still\n * has \"No previous host\". THIS half comes after the teardown, because it erases the screen and\n * emits: run before it, a connect while connected would clear the OLD host's screen with its\n * connection still up, and announce 'screen' and 'geometry' while `isConnected()` was still\n * true, ahead of that connection's own 'disconnect'.\n *\n * THE SCREEN IS RESIZED AND CLEARED ONLY WHEN THE GEOMETRY CHANGED. Every front end runs in\n * geometry mode, so this runs on every reconnect, and a reconnect to the same model must\n * leave the operator's last screen exactly where legacy mode leaves it -- on it. When it did\n * change, the path is UNBIND's (`handleUnbind`): `setSizes`, `useDefaultSize`, then an\n * explicit `clear()` because `useDefaultSize` alone skips the erase when the screen is already\n * at the default size -- the usual case, though not a certain one, since a BIND can have set\n * another. IN PLACE, never a new `Screen`: `Keyboard` holds a\n * reference to this one. And, like UNBIND, a 'screen' event announces the erase, since a\n * front end repaints only on 'screen'.\n */\n private applyGeometry(g: ResolvedGeometry): void {\n const alt = this.modelAlternate;\n const changed = g.terminalType !== this.ttype\n || g.alternate.rows !== alt.rows || g.alternate.cols !== alt.cols;\n if (changed) {\n this.screen.setSizes(g.default, g.alternate);\n this.screen.useDefaultSize();\n this.screen.clear();\n this.modelAlternate = g.alternate;\n this.ttype = g.terminalType;\n }\n if (g.notice !== undefined) this.trace.note(g.notice);\n if (g.clamp !== undefined) this.trace.note(g.clamp);\n this.geometryNotice = g.notice;\n this.resolved = g;\n if (changed) this.emit('screen');\n this.emit('geometry');\n }\n\n /** The model's alternate geometry -- see `modelAlternate`'s field comment. */\n private modelSize(): { readonly rows: number; readonly cols: number } {\n return this.modelAlternate;\n }\n\n on(event: SessionEvent, fn: () => void): void {\n let set = this.listeners.get(event);\n if (!set) { set = new Set(); this.listeners.set(event, set); }\n set.add(fn);\n }\n\n /**\n * Stop calling `fn` for `event`. A no-op if it was never registered.\n *\n * ## WHY THIS EXISTS: A SESSION CAN OUTLIVE ITS LISTENER\n *\n * For three of the four front ends it does not \u2014 a TUI or a GUI registers once and lives as long\n * as the process. The web gateway is different BY DESIGN: its sessions outlive their sockets so an\n * operator can reload, or survive a wifi handoff, and reattach to the running 3270 session. Each\n * attach registers three listeners, and with no way to remove them a session reattached ten times\n * carried thirty. Every later screen change then ran `drawList` and `deflateSync` thirty times,\n * twenty-nine of them for dead connections that discard the result \u2014 unbounded in the number of\n * reconnections, and invisible because the output was correct throughout.\n *\n * A no-op rather than a throw for an unknown function, because the gateway calls this on every\n * socket close, including sockets that never reached `hello` and so registered nothing. Throwing\n * there would turn an ordinary disconnect into an error on a path with nobody to tell.\n */\n off(event: SessionEvent, fn: () => void): void {\n this.listeners.get(event)?.delete(fn);\n }\n\n /**\n * How many listeners `event` has. Exists so a leak is OBSERVABLE to a test: without it the only\n * symptom of the bug above is wasted CPU, which no assertion can see.\n */\n listenerCount(event: SessionEvent): number {\n return this.listeners.get(event)?.size ?? 0;\n }\n\n private emit(event: SessionEvent): void {\n for (const fn of this.listeners.get(event) ?? []) fn();\n }\n\n /**\n * Start a DFT transfer. The caller drives it by letting host records arrive \u2014\n * there is nothing to send first, because DFT is host-driven: the host opens,\n * asks and closes, and we answer.\n *\n * Replaces any transfer already in flight. Not refused, because the previous one\n * can only still be here if it never completed, and a caller that has decided to\n * start a new transfer has more current information than this method does.\n */\n startDftTransfer(transfer: DftTransfer): void {\n this.dft = transfer;\n // READ OFF THE ENGINE, already clamped by its own constructor (`dft.ts:172`, the same\n // `boundDftBufferSize` the advertisement uses). Taking it from here rather than from a\n // second parameter is what makes the advertised and chunked sizes structurally incapable\n // of disagreeing: there is ONE value and both sides read it.\n this.dftBuffer = transfer.bufferSize;\n }\n\n /** The DFT transfer in flight, for a front end to read progress or cancel. */\n get dftTransfer(): DftTransfer | undefined { return this.dft; }\n\n /**\n * Release a registered DFT transfer without telling the host anything.\n *\n * FOR THE LOSER OF A PROTOCOL RACE, which is the only caller. `Transfer()` registers a\n * DFT transfer *before* priming the host, because a fast host's first `0xd0` arrives\n * from inside `handleRecord` before any driver code runs again \u2014 so `handleTransferData`\n * must already have one. When the host answers with a CUT frame instead, that\n * registration has to go, or `answerRead` would keep replaying its retained frame at a\n * host now running a CUT transfer.\n *\n * SENDS NOTHING, and that is the whole difference from `DftTransfer.cancel()`: that\n * negotiates an end with a host that is mid-transfer, while this discards an engine the\n * host never addressed. Synthesising an abort here would put bytes on the wire that no\n * captured session contains \u2014 the same rule `transferRun.ts` gives for refusing to\n * invent one from a timeout.\n *\n * Idempotent, so a driver need not track whether it has already committed.\n */\n cancelDftTransfer(): void {\n this.dft = undefined;\n this.dftBuffer = undefined;\n }\n\n /**\n * Did we advertise DDM, i.e. offer the host the chance to choose DFT?\n *\n * FOR A DIAGNOSTIC MESSAGE, which is the only caller and the reason this is not just\n * `opts.ddm` read at the call site: `opts` is private, and a front end driving a transfer\n * has no other route to the flag. A host that speaks only DFT cannot CHOOSE DFT unless the\n * Query Reply carried QCODE 0x95 (`queryreply.ts`, appended at `:1410` only when this is\n * on), so \"the transfer timed out and DDM was never advertised\" is a different failure from\n * \"the transfer timed out\", and forgetting `-ddm on` will be the commonest one.\n *\n * REPORTS WHAT WE OFFERED, NOT WHAT THE HOST DID WITH IT. The host may ignore the unit\n * entirely -- VM/370's MECAFF does, and stays on CUT -- so this must not be read as \"DFT is\n * available\". Defaults to false, like the flag.\n */\n get ddmAdvertised(): boolean { return this.opts.ddm !== false; }\n\n isConnected(): boolean {\n return this.conn !== undefined;\n }\n\n is3270Mode(): boolean {\n return this.telnet?.is3270Mode() ?? false;\n }\n\n lastError(): string | undefined {\n return this.error;\n }\n\n /**\n * Build the `environ` option for a fresh `TelnetLayer`, or `undefined` to refuse\n * option 39 -- ONE place, called from both `connect()` and `replay()`, so the two\n * can never disagree about what a host learns. `forgetTn3270e()` is the precedent\n * for consolidating related state into one method rather than trusting two call\n * sites to stay in step; this is the same discipline applied to a different piece\n * of per-connection state.\n *\n * `undefined` UNLESS `devname` OR `user` WAS CONFIGURED, matching `TelnetLayerOptions.environ`'s\n * own contract: agreeing to the option with nothing to tell a host is not a real\n * acceptance. Note that `user` ALONE already crosses that line, because `user`\n * defaults from `$USER`/`$USERNAME`/`UNKNOWN` and is therefore ALMOST ALWAYS resolvable\n * even when nobody passed `-devname` -- see the field comments on `devname`/`user` and\n * the Task 7 report for why this is the deliberately-chosen \"blast radius\".\n *\n * A FRESH `DeviceName` PER CALL, not one held for the session's lifetime: verified\n * against x3270's own C source, `net_connect()` calls `environ_init()` on EVERY\n * connect (`Common/telnet.c:688`, called from `host_connect()` at :645), and\n * `host_reconnect()` re-enters `host_connect()` (`Common/host.c:786`) rather than\n * reusing state -- so x3270 itself resets its devname counter on every connect\n * INCLUDING every reconnect. A counter that survived across `reconnect()` here would\n * offer a host `foo004` on a fresh connection where x3270 would offer `foo001`,\n * which is a real, observable divergence on the wire, not a cosmetic one.\n */\n private buildEnviron(): TelnetLayerOptions['environ'] {\n const { devname, user } = this.opts;\n if (devname === undefined && user === undefined) return undefined;\n const vars = new Map<string, string>();\n // USER first, matching x3270's environ_init order (telnet_new_environ.c:220-245).\n const resolvedUser = user ?? process.env['USER'] ?? process.env['USERNAME'] ?? 'UNKNOWN';\n vars.set('USER', resolvedUser);\n\n const dn = devname !== undefined ? new DeviceName(devname) : undefined;\n return {\n vars,\n uservars: () => {\n const m = new Map<string, string>();\n // DEVNAME, then IBMELF, then IBMAPPLID -- x3270's own insertion order, which a\n // whole-group SEND dump reproduces on the wire (see newenviron.ts's doc comment\n // on Map iteration order).\n if (dn !== undefined) m.set('DEVNAME', dn.next());\n // IBMELF IS THE EXPRESS LOGON FEATURE, identified 2026-09-20 from IBM's Personal\n // Communications 15.0 docs (*Express Logon Feature*) -- x3270's own source\n // documents it nowhere, `telnet_new_environ.c:237-238`'s only comment being\n // \"/* Set IBMELF. */\", so this is IBM's meaning rather than a derived one.\n //\n // ELF replaces an interactive logon with a CLIENT CERTIFICATE: the host's TN3270\n // server validates the certificate, obtains a passticket and logs the user on\n // without a userid or password crossing the network. IBM is explicit that \"the\n // host session must be configured for SSL with client authentication in order to\n // play an ELF macro\" -- so client authentication is a PREREQUISITE, not an option.\n //\n // WE SEND `YES` AND CANNOT HONOR IT, WHICH IS DELIBERATE BUT WORTH KNOWING.\n // This client has no client-certificate support at all (see the README's *What is\n // not implemented*: -certfile/-keyfile/-clientcert are all absent), so TLS here\n // authenticates the host to us and never us to the host. A host that acted on this\n // `YES` would ask for a certificate we cannot produce. We send it anyway because\n // x3270 does, unconditionally, and a host keying off its mere presence should see\n // the same bytes from both clients -- but NO HOST HAS BEEN TRIED: the user has\n // access to none that implements ELF, so what a real one does with our `YES` is\n // unmeasured. If client certificates are ever implemented, THIS is the variable\n // that makes ELF reachable, and the pairing with IBMAPPLID below is how.\n m.set('IBMELF', 'YES');\n // IBMAPPLID is ELF's other half: the application ID IBM's docs describe as stored\n // in the ELF macro and replayed with the secured connection. x3270 reads it from\n // its OWN environment (`getenv(\"IBMAPPLID\")`, telnet_new_environ.c:241-244) and\n // falls back to the literal string \"None\" when unset. We have no equivalent\n // environment variable to read -- nothing in this codebase sets one -- so the\n // fallback is all we ever send, which is consistent with not supporting ELF.\n m.set('IBMAPPLID', 'None');\n // CODEPAGE: x3270 derives this from `cgcsgid & 0xffff`, formatted `%03d` when\n // under 100 else `%d` (telnet_new_environ.c:255-260). We have no cgcsgid field\n // anywhere in this codebase (`codepage.ts`'s `CodePage` carries only a `name`\n // like 'cp037'), so the value is derived from that name instead: stripping the\n // 'cp' prefix from our default and only code page, 'cp037', yields exactly the\n // \"037\" x3270 computes for ITS default codepage (`Common/codepage.c:250`,\n // `set_codepage_number` falling back to \"037\"). Not hardcoded as a bare string:\n // if `codePage` ever changes, this changes with it rather than silently lying\n // about which page is live.\n //\n // CHARSET is deliberately NOT sent: x3270 derives it from `(cgcsgid >> 16) &\n // 0xffff`, the GCSGID half of the SAME field we do not model, and there is no\n // honest value to compute here without inventing one.\n // KBDTYPE is deliberately NOT sent: x3270 only sends it when `kybdtype` is\n // configured, which nothing in this codebase sets.\n // Confirmed WORTH sending despite the original design spec's \"no trace asks for\n // this\" belief: four traces (dbcs-wrap.trc among them) send an empty-body SEND,\n // which parseEnvironSend expands to \"every variable\", and x3270 answers with all\n // six -- so a real host asking for everything gets this one from x3270 today.\n const codePageName = this.opts.codePage?.name ?? cp037.name;\n m.set('CODEPAGE', codePageName.replace(/^cp/i, ''));\n return m;\n },\n };\n }\n\n /**\n * True while a `connect()` -- a first connect or a reconnect's -- has started and not finished:\n * TCP or TLS setup, before `isConnected()`. True already when 'geometry' fires inside it, so a\n * front end can disable its geometry controls from that event.\n */\n isConnecting(): boolean {\n return this.connecting > 0 || this.reconnecting;\n }\n\n async connect(host: string, port: number, per: ConnectOptions = {}): Promise<void> {\n // COUNTED BEFORE ANYTHING ELSE, so 'geometry' (emitted by `applyGeometry` below) already sees\n // it, and cleared in `finally` on every exit -- a refused geometry, a refused socket, success.\n this.connecting += 1;\n try {\n await this.connectNow(host, port, per);\n } finally {\n this.connecting -= 1;\n // Inside a `reconnect()`, `reconnecting` is still true here: that method's own `finally`\n // emits instead, so one attempt settles once.\n if (!this.isConnecting()) this.emit('connectSettled');\n }\n }\n\n private async connectNow(host: string, port: number, per: ConnectOptions): Promise<void> {\n // RESOLVED FIRST, before anything below can touch the live connection or the remembered\n // target; APPLIED after the teardown. See `applyGeometry` for why it is split around it.\n // `reconnect()` comes through here too, so `auto` is re-measured on every reconnect -- the\n // only moment the host can learn a new size.\n const geometry = this.setting !== undefined\n ? resolveGeometry(this.setting, this.opts.measure)\n : undefined;\n\n // Tear down any live connection first. Without this the old Connection is\n // dropped without close(), and its onClose/onError closures still capture\n // `this` \u2014 so when the stale socket eventually closes it calls\n // handleClose() and tears down the NEW session.\n if (this.conn !== undefined) this.disconnect();\n if (geometry !== undefined) this.applyGeometry(geometry);\n\n // AFTER the teardown, which clears the previous connection's overrides \u2014 set it\n // before and disconnect() would wipe the ones just passed in.\n this.per = per;\n\n // REMEMBERED BEFORE THE AWAIT, so a target that never came up is still a target: a refused\n // socket or a TLS handshake that failed is exactly the case an operator wants to retry, and\n // recording it only on success would leave the reconnect key dead after the one failure\n // that makes it useful. Copied, not aliased: a caller that reuses and mutates its options\n // object must not silently change where this session reconnects to.\n //\n // The OIA carries the same fact so the STATUS LINE can offer the key, and it is set here for\n // the same reason: see `Oia.reconnectable`.\n this.target = { host, port, per: { ...per } };\n this.oia.reconnectable = true;\n\n const conn = await this.opts.connect(host, port);\n this.conn = conn;\n this.error = undefined;\n this.oia.connected = true;\n // The keyboard is locked until the host writes something: there is no screen\n // to type into yet. x3270 sets KL_AWAITING_FIRST here for the same reason\n // (kybd.c:580-585). This is what makes Wait(Unlock) meaningful immediately\n // after Connect \u2014 without it the wait returns at once and a script types\n // into a blank buffer.\n this.oia.waitingForHost = true;\n this.oia.inhibit(KeyboardState.AwaitingFirstWrite);\n\n // Built ONCE, not inline, so the conditional spread below and the value it spreads\n // cannot evaluate `buildEnviron()` twice and hand the TelnetLayer a DIFFERENT\n // `DeviceName` (and thus a different starting counter) than the one the presence\n // check just examined.\n const environ = this.buildEnviron();\n // The resolved terminal type in geometry mode; `undefined` in legacy mode, where\n // `opts.terminalType` is read exactly as before.\n const terminalType = this.ttype ?? this.opts.terminalType;\n this.telnet = new TelnetLayer({\n write: (b) => conn.write(b),\n onRecord: (r) => this.handleRecord(r),\n trace: this.trace,\n // Spread conditionally: with exactOptionalPropertyTypes an explicit\n // `terminalType: undefined` is a type error, and passing it would also\n // bypass the layer's own `?? TERMINAL_TYPE` default if that guard ever\n // became a truthiness check.\n ...(terminalType ? { terminalType } : {}),\n tn3270eEnabled: this.per.tn3270e ?? this.opts.tn3270e ?? true,\n onTn3270eSubneg: (body) => { this.handleTn3270eSubneg(body, this.telnet); },\n onTn3270eDisabled: () => { this.tn3270eDisabled(); },\n // `environ` is `undefined` unless `devname` or `user` was configured -- see\n // `buildEnviron()`'s own comment. Spread conditionally for the same\n // exactOptionalPropertyTypes reason as `terminalType` above: an explicit\n // `environ: undefined` is a type error here.\n ...(environ !== undefined ? { environ } : {}),\n });\n\n // Each callback checks identity against the `conn` it closes over, not just\n // `this.conn !== undefined`. Real transports fire data/close/error\n // asynchronously (not necessarily inside our call to close()), so a stale\n // connection's event can still arrive after connect() has already swapped\n // in a new one. Without the identity check, stale data would be fed into\n // the NEW telnet layer, and a stale close/error would tear down the live\n // connection via handleClose().\n conn.onData = (bytes) => {\n if (this.conn !== conn) return;\n this.telnet?.receive(bytes);\n this.oia.tn3270Mode = this.is3270Mode();\n };\n conn.onClose = () => { if (this.conn === conn) this.handleClose(); };\n conn.onError = (err) => {\n if (this.conn !== conn) return;\n this.error = err.message;\n this.trace.note(`transport error: ${err.message}`);\n this.handleClose();\n };\n\n this.emit('connect');\n }\n\n disconnect(): void {\n this.conn?.close();\n this.handleClose();\n }\n\n /**\n * Connect again to the host this session was last asked for.\n *\n * ## WHY IT REUSES `this.opts.connect` AND TAKES NO ARGUMENTS \u2014 THE TLS TRAP\n *\n * `SessionOptions.connect` is INJECTED, and the injected function is where the TLS decision\n * lives: `frontend`'s `defaultSession` closes over a `TlsOptions` (`tcpConnect(h, p, tls)`),\n * chosen once from the command line. Replaying the remembered host and port THROUGH THAT SAME\n * FUNCTION therefore reuses the original decision by construction, and there is nothing here\n * that could re-derive it.\n *\n * That is not a stylistic preference. A PLAINTEXT HOST DOES NOT REJECT A TLS HANDSHAKE, IT\n * HANGS: Hercules writes `IAC DO TERMINAL-TYPE` and waits, and OpenSSL reads that leading 0xff\n * as a record content type and blocks for a length that never arrives. TLS is ON by default in\n * this client and the Hercules hosts are reached with `-insecure`, so anything that re-parsed\n * the front end's arguments \u2014 or built a fresh socket factory \u2014 would turn a keypress into a\n * hung client against the two hosts this project actually tests against. No parameters is also\n * what keeps the WEB gateway safe: a browser pressing Enter cannot name a host or a scheme,\n * because there is no argument for it to name one in.\n *\n * ## THE TWO REFUSALS, BOTH x3270's\n *\n * `Reconnect_action` (`Common/host.c`, verified against the current source) is exactly these\n * two checks and nothing else:\n *\n * if (PCONNECTED) { popup_an_error(AnReconnect \"(): Already connected\"); return false; }\n * if (current_host == NULL) {\n * popup_an_error(AnReconnect \"(): No previous host to connect to\"); return false; }\n * host_reconnect();\n *\n * So both are refusals rather than no-ops, and the wording is x3270's own \u2014 including the\n * capitalised `Reconnect()` prefix, which is the s3270 ACTION name: `cli/src/runner.ts` passes\n * these messages straight through to a script, exactly as it copies `check_argc`'s wording for\n * `Connect()`, so a script written for s3270 reads the same text from us.\n *\n * `PCONNECTED` in x3270 is \"connected OR half-connected\", which is why the first check also\n * covers a connect attempt ALREADY IN FLIGHT and not just `this.conn` -- a RECONNECT's (via\n * `reconnecting`) AND A FIRST `connect()`'s (via `isConnecting()`, which also covers\n * `connecting`). Our `isConnected()` sees only the completed case, and this method is reachable\n * from a KEY: two quick presses would otherwise start two connects, both of which passed\n * `connect()`'s teardown check while `this.conn` was still undefined, so the first socket would\n * be replaced without being closed and left open on the mainframe forever \u2014 an LU leaked per\n * impatient keypress. A FIRST connect counts too, and not only a second `reconnect()`:\n * `connectNow` records `this.target` and sets `oia.reconnectable` BEFORE its own first await, so\n * a first connect still dialling (TLS setup against a plaintext host hangs rather than refusing)\n * already looks, from here, like a session with somewhere to go back to. Checking only\n * `this.conn !== undefined || this.reconnecting` missed exactly that case: `this.conn` is still\n * undefined and `reconnecting` is the reconnect flag, not the first-connect one, so the guard let\n * `reconnect()` through, which tore down `this.conn` (a no-op, since it was undefined) and opened\n * a SECOND socket while the first was still outstanding -- never closed, because nothing but the\n * now-overwritten `this.conn` could reach it. `isConnecting()` is the one check that already\n * covers both halves (`connecting > 0` for a first connect, `reconnecting` for a reconnect), so\n * the guard here uses it instead of naming `reconnecting` on its own.\n *\n * ## `async`, BECAUSE `connect` IS\n *\n * It resolves when the connection is up, so the CLI's `Reconnect()` can report failure the way\n * `Connect()` does. INTERACTIVE CALLERS MUST STILL CONTAIN THE REJECTION: `applyAction` is\n * synchronous and an unhandled rejection ends the process on modern Node \u2014 in the web gateway\n * that would take every other operator's session with it. See `applyAction`.\n *\n * The failure is recorded in `lastError()` and in the trace before it is rethrown, so a front\n * end that can only swallow it still leaves the reason somewhere findable.\n */\n async reconnect(): Promise<void> {\n if (this.conn !== undefined) {\n throw new Error('Reconnect(): Already connected');\n }\n // COVERS A RECONNECT ALREADY IN FLIGHT (`reconnecting`) AND A FIRST CONNECT STILL DIALLING\n // (`connecting`), both of `isConnecting()`'s halves -- see the doc comment above for why a\n // first connect counts too. A DIFFERENT MESSAGE from the one above: `this.conn !== undefined`\n // is a genuinely live connection, which is what x3270's wording describes; this is a connection\n // attempt still in the air, which is not \"already connected\" by any honest reading, and a\n // script or an operator reading the refusal deserves the true one.\n if (this.isConnecting()) {\n throw new Error('Reconnect(): a connection attempt is already in progress');\n }\n const target = this.target;\n if (target === undefined) throw new Error('Reconnect(): No previous host to connect to');\n this.reconnecting = true;\n try {\n // The remembered ConnectOptions are copied out again, so the session's memory of its target\n // cannot be reached through the object handed to `connect()`.\n await this.connect(target.host, target.port, { ...target.per });\n } catch (err) {\n const why = err instanceof Error ? err.message : String(err);\n this.error = why;\n this.trace.note(`reconnect to ${target.host}:${target.port} failed: ${why}`);\n throw err;\n } finally {\n this.reconnecting = false;\n if (!this.isConnecting()) this.emit('connectSettled');\n }\n }\n\n private handleClose(): void {\n if (this.conn === undefined) return;\n this.conn = undefined;\n this.telnet = undefined;\n this.oia.connected = false;\n this.oia.tn3270Mode = false;\n this.oia.waitingForHost = false;\n // The pending AID is a property of the CONNECTION, not of the Session: a keypress sent\n // to one host must never be reported to the next one. See `pendingAid`, and note this\n // is the same one-teardown-path-clears-it shape already fixed here for `e` and `dft`.\n this.pendingAid = AID.NONE;\n // A DFT TRANSFER BELONGS TO THE CONNECTION CARRYING IT. Without this, a transfer\n // registered on one connection stays registered on the next, and `answerRead`'s\n // retained-frame replay would answer a host that knows nothing about it.\n //\n // FOURTH TIME THIS SHAPE HAS BEEN FIXED IN THIS FILE \u2014 `Session.e` once cleared only\n // on the REJECT path, `IAC DONT TN3270E` once cleared the option but not\n // `tn3270eNegotiated`, and the pending AID above is the third. One teardown path clears\n // the state and another does not.\n this.dft = undefined;\n this.dftBuffer = undefined;\n // TN3270E state DIES WITH THE CONNECTION THAT NEGOTIATED IT. Until this line, `e`\n // was cleared only by the REJECT backoff below, so a second connection to a host\n // that never mentions option 40 inherited `phase: 'negotiated'` \u2014 and then\n // corrupted traffic in both directions: decodeHeader ate the first five bytes of\n // every inbound record, and sendRecord prepended a header the plain host parses as\n // 3270 data. The CLI reaches this with two `Connect()` actions in one script,\n // which is its ordinary mode of operation.\n //\n // Here rather than in connect(), because this is the ONE place a connection ends:\n // connect() tears down a live predecessor through disconnect(), so both routes\n // pass through here. `eSeq` too \u2014 it is only reset on a completed negotiation\n // (below), so a second session that never negotiates would keep counting from the\n // first one's total. Both live in `forgetTn3270e()`, because a connection ending is\n // not the only way a negotiation ends: see that method.\n this.forgetTn3270e();\n // The connection's own overrides go with it, for the same reason: `N:` applied to\n // one host must not silently disable TN3270E for the next one.\n this.per = {};\n // `this.target` AND `this.oia.reconnectable` ARE DELIBERATELY NOT CLEARED HERE, and this is\n // the one exception to the rule above. They record which host this SESSION is for, not\n // anything the closing connection negotiated, and `reconnect()` runs only after this method\n // has already run \u2014 clearing them would make the feature unreachable rather than clean. See\n // the field's own comment.\n this.emit('disconnect');\n }\n\n /**\n * Apply one host record.\n *\n * Protocol violations become a program check and the session stays up; that\n * is what real hardware does, and a client that dies on a malformed record is\n * useless against real hosts.\n */\n /** Count of host records applied. Monotonic. */\n recordCount(): number {\n return this.records;\n }\n\n private handleRecord(record: Uint8Array): void {\n let body = record;\n /** The header of a TN3270E record, kept so the catch below can answer it. */\n let wants: ReturnType<typeof decodeHeader> = null;\n if (this.inTn3270e()) {\n const h = decodeHeader(record);\n if (h === null) {\n // Shorter than a header: malformed rather than truncated, so there is nothing\n // to salvage. Trace and drop, rather than handing the parser five bytes of\n // nothing and raising a program check the host never caused.\n this.trace.note(\n `TN3270E record shorter than a header, ${record.length} bytes, dropped`);\n return;\n }\n // BIND and UNBIND are dispatched BEFORE the carriesDatastream/gate checks below,\n // because neither carries a 3270 datastream and the gate condition below tests\n // `!this.bound` -- reaching it before `handleBind` has had a chance to flip that\n // flag would make a session's OWN BIND look like more pre-BIND data.\n //\n // BOTH ARE GATED ON `bindImageGranted()`, NOT DISPATCHED UNCONDITIONALLY ON THE\n // DATA TYPE. This departs from the plan's sketch, which dispatched on data type\n // alone -- but x3270 does not: `case TN3270E_DT_BIND_IMAGE` and `case\n // TN3270E_DT_UNBIND` each open with `if (!b8_bit_is_set(&e_funcs,\n // TN3270E_FUNC_BIND_IMAGE)) return 0;` (Common/telnet.c:2709 and :2746) before\n // doing anything else. A record carrying data type BIND-IMAGE on a session that\n // never negotiated the FUNCTION is not a BIND we are owed; it falls through to\n // the same \"not implemented, dropped\" trace as any other type we do not handle,\n // exactly as it did before this task and exactly as x3270's silent `return 0`\n // does. Confirmed against session.test.ts's own regression: `negotiateE()`\n // grants RESPONSES and SYSREQ only, so its BIND-IMAGE-data-type test is (and\n // must stay) a \"not implemented\" trace, not a bind.\n if (this.bindImageGranted() && h.dataType === Tn3270eDataType.BIND_IMAGE) {\n this.handleBind(record.subarray(TN3270E_HEADER_BYTES));\n return;\n }\n if (this.bindImageGranted() && h.dataType === Tn3270eDataType.UNBIND) {\n this.handleUnbind(record.subarray(TN3270E_HEADER_BYTES));\n return;\n }\n if (!carriesDatastream(h.dataType)) {\n // RESPONSE, NVT-DATA, SSCP-LU-DATA, PRINT-EOJ. None of these carries a 3270\n // datastream, and feeding one to the executor would raise a spurious program\n // check. Traced rather than silently ignored: the trace is how we would find\n // out a real host sends these.\n this.trace.note(\n `TN3270E data type 0x${h.dataType.toString(16)} not implemented, dropped`);\n return;\n }\n if (this.bindImageGranted() && !this.bound) {\n // THE GATE (x3270's telnet.c:2681-2682), with the hang removed. x3270 returns\n // here and the record is gone; we keep the most recent one so the Task 9\n // timeout can run it through this same `executeRecord`. Only the most recent:\n // a host that painted twice before binding has overwritten its own first\n // screen, and a queue would be a memory hole a remote party controls. See\n // `pendingBindRecord`'s own comment.\n this.pendingBindRecord = { wants: h, body: record.subarray(TN3270E_HEADER_BYTES) };\n this.armNoBindTimer();\n this.trace.note('3270 data before BIND, retained');\n return;\n }\n body = record.subarray(TN3270E_HEADER_BYTES);\n wants = h;\n }\n this.executeRecord(body, wants);\n }\n\n /**\n * Parse and apply one 3270 datastream, once `handleRecord` has decided it is safe\n * to run: past the BIND gate, with any TN3270E header already stripped.\n *\n * PULLED OUT OF `handleRecord` SO THE RETAINED PATH AND THE LIVE PATH CANNOT DRIFT.\n * `handleBind` (Task 9's timeout does the same) calls this directly on a record\n * `pendingBindRecord` held back, with the exact `wants` that record arrived with --\n * so a host that asked for ALWAYS-RESPONSE on the withheld record still gets one,\n * and every side effect below (OIA, alarm, Query reply, program check) runs exactly\n * as it would have if the BIND had arrived first. Two copies of this logic, one for\n * \"arrived before BIND\" and one for \"arrived after\", is exactly the kind of drift\n * this project has already paid for once with `e` (see `forgetTn3270e`).\n */\n private executeRecord(body: Uint8Array, wants: Tn3270eHeader | null): void {\n this.records++;\n if (this.trace.isEnabled()) {\n this.trace.note(describeRecord(body));\n }\n try {\n const parsed = parseRecord(body);\n const result = execute(this.screen, parsed);\n\n // Release the enter-inhibit condition raised by an earlier Query.\n //\n // Placed before the two branches below for READABILITY, narrowest rule\n // first, and NOT because the order is load-bearing: it was checked by\n // moving this block after them, and all 45 session tests still passed.\n // The three rules turn out to commute, because the state EnterInhibit\n // can coexist with is only itself. Reaching the AwaitingFirstWrite\n // branch requires the state to BE AwaitingFirstWrite, and enterInhibit()\n // never overwrites that, so the two can never contend for the same\n // record. Do not read the sequence here as an invariant.\n //\n // releaseEnterInhibit, not reset(): it clears that one state and leaves\n // any other alone, mirroring x3270's single-bit `kybdlock_clr(\n // KL_ENTER_INHIBIT, \"kybd_inhibit\")` (Common/kybd.c:533). A reset() here\n // would let a routine host write clear a program check the host has not\n // acknowledged.\n //\n // Unconditional on the flag rather than guarded by \"are we inhibited\":\n // x3270 calls kybd_inhibit(false) on every Erase/EAU/Write regardless of\n // the current lock (ctlr.c:550, :1309, :1406), and releaseEnterInhibit is\n // itself a no-op unless EnterInhibit is the live state.\n if (result.releasesEnterInhibit) {\n this.oia.releaseEnterInhibit();\n }\n\n if (result.keyboardRestore) {\n // THE PENDING AID GOES WITH THE KEYBOARD LOCK, and the pairing is x3270's, not a\n // convenience: `if (wcc_keyboard_restore) { aid = AID_NO; do_reset(false); }`\n // (ctlr.c:2168-2170) and the same two lines again for EAU (ctlr.c:1349-1350). The\n // host restoring the keyboard is exactly the moment the previous keypress stops\n // being the pending answer, because the operator is now free to press another.\n //\n // Gated on `keyboardRestore`, which `execute` sets for a WCC restore bit AND\n // unconditionally for EAU (execute.ts:275) -- so one line here covers two of\n // x3270's four clear sites. A write that leaves the keyboard LOCKED must NOT clear\n // it: the host has not given the screen back, so our answer is still the same key.\n this.pendingAid = AID.NONE;\n this.oia.waitingForHost = false;\n this.oia.reset();\n } else if (this.oia.keyboard === KeyboardState.AwaitingFirstWrite\n && parsed.command !== 'WriteStructuredField') {\n // \"Wait for any output OR a WCC(restore)\" (x3270 kybd.c:583): the\n // initial post-connect lock is released by the host writing anything at\n // all, not only by an explicit keyboard-restore. VM/370's logo arrives\n // with WCC 0x42 (restore set) but a host that omits the bit must not\n // leave us locked out forever.\n //\n // ...and a Write Structured Field is NOT such a write: it puts nothing\n // in the buffer. TSO sends its Read Partition (Query) BEFORE any write,\n // so without this exclusion the operator gets an unlocked keyboard over\n // a blank screen.\n this.oia.waitingForHost = false;\n this.oia.reset();\n }\n if (result.alarm) {\n this.oia.alarm = true;\n this.emit('alarm');\n }\n if (result.readRequest !== undefined) {\n this.answerRead(result.readRequest);\n }\n if (result.sfReply !== undefined) {\n this.answerQuery(result.sfReply);\n }\n if (result.transferData !== undefined) {\n for (const payload of result.transferData) this.handleTransferData(payload);\n }\n // The only thing that OBSERVES the SA/MF counters. execute() bumps them\n // per record and nothing sums them, so without a line here a live run\n // could not tell \"we saw no SA/MF\" from \"nobody looked\" \u2014 the precise\n // failure those counters exist to rule out. Named in the message so a\n // trace grep finds them.\n //\n // isEnabled() first, matching describeRecord above: this runs on every\n // record, and trace.note's own guard would still have us build the string.\n if (this.trace.isEnabled()\n && (result.setAttributeIgnored > 0 || result.modifyFieldIgnored > 0)) {\n this.trace.note(\n `ignored orders: SA=${result.setAttributeIgnored} MF=${result.modifyFieldIgnored}`);\n }\n this.emit('screen');\n if (wants?.responseFlag === Tn3270eResponseFlag.ALWAYS_RESPONSE) {\n // Only ALWAYS-RESPONSE gets a positive answer. ERROR-RESPONSE means \"tell me\n // only if it went wrong\", so a success there is answered with silence.\n this.sendResponse(wants.seq, true, Tn3270eSense.DEVICE_END);\n }\n } catch (err) {\n // The response goes to the host and the program check to the operator: BOTH,\n // not either. A silent negative response would leave an operator looking at a\n // screen that never updated with no indication why.\n //\n // The sense code is mapped from the ERROR rather than from our program check\n // code, because RFC 2355's two reachable senses are finer-grained than our two\n // codes: 0x00 is \"an invalid 3270 command was received\" and 0x02 is \"an illegal\n // 3270 buffer address or order sequence\". A ParseError is the former, an\n // AddressError the latter -- even though both report program check 754.\n // DO NOT collapse the program check codes to match: they are pinned by goldens\n // and by the live VM/370 measurement.\n const answerable = wants !== null\n && wants.responseFlag !== Tn3270eResponseFlag.NO_RESPONSE;\n if (err instanceof ParseError || err instanceof AddressError) {\n this.programCheck(PROG_INVALID_COMMAND, err.message);\n if (answerable) {\n this.sendResponse(wants!.seq, false, err instanceof ParseError\n ? Tn3270eSense.COMMAND_REJECT\n : Tn3270eSense.OP_CHECK);\n }\n } else if (err instanceof ExecuteError) {\n this.programCheck(PROG_INVALID_ADDRESS, err.message);\n if (answerable) this.sendResponse(wants!.seq, false, Tn3270eSense.OP_CHECK);\n } else {\n // Our own bug: never swallowed.\n throw err;\n }\n }\n }\n\n /**\n * Advance TN3270E negotiation by one subnegotiation body.\n *\n * All the protocol logic is in tn3270e.ts; this only moves bytes and applies\n * effects, which is what lets the state machine be tested without a socket.\n *\n * The layer is passed in rather than read from `this.telnet`, because replay()\n * builds a local one and leaves the field undefined.\n */\n private handleTn3270eSubneg(body: Uint8Array, layer: TelnetLayer | undefined): void {\n this.e ??= initialState({\n // The resolved type in geometry mode, so DEVICE-TYPE REQUEST names what TERMINAL-TYPE\n // IS named -- IBM-DYNAMIC under an oversize, as x3270 sends: its `tn3270e_request`\n // (`telnet.c:2122`) calls the same `create_3270_termtype` that returns IBM-DYNAMIC\n // whenever an oversize is set (`:2100-2101`).\n terminalType: this.ttype ?? this.opts.terminalType ?? TERMINAL_TYPE,\n lus: this.per.lus ?? this.opts.lus ?? [],\n bindImage: this.opts.bindImage ?? true,\n });\n const r = negotiate(this.e, body);\n this.e = r.next;\n if (r.reply) layer?.sendTn3270eSubneg(r.reply);\n if (r.effect?.kind === 'complete') {\n this.eSeq = 0;\n layer?.setTn3270eNegotiated(true);\n this.trace.note(\n `TN3270E negotiated, device ${this.e.deviceType ?? '?'}`\n + `${this.e.lu === undefined ? '' : ` LU ${this.e.lu}`}`\n + `, functions: ${r.effect.agreed.join(',') || '(none: basic TN3270E)'}`);\n } else if (r.effect?.kind === 'backoff') {\n // Tell the host no and forget the option, so the classic BINARY/EOR route is\n // still reachable on this same connection. x3270's backoff_tn3270e().\n //\n // The reason is traced BEFORE the state goes, and the state goes before\n // refuseTn3270e(): that call reaches `tn3270eDisabled()` back through the layer,\n // which traces a line of its own only if there is still state to discard. So the\n // specific reason above is what a trace of this path shows, not a generic one.\n this.trace.note(`TN3270E abandoned: ${r.effect.why}`);\n this.forgetTn3270e();\n layer?.refuseTn3270e();\n }\n }\n\n /**\n * Forget a TN3270E negotiation: `e`, and the sequence counter that belongs to it.\n *\n * ONE place, THREE callers \u2014 the connection ending (`handleClose`), our own backoff\n * (`handleTn3270eSubneg`) and the HOST withdrawing option 40 mid-session\n * (`tn3270eDisabled`). The third was missing, and it is the same defect as the first\n * one layer along: `e` outliving the negotiation makes `inTn3270e()` true on a session\n * that is no longer TN3270E, so `handleRecord` eats five bytes off the front of every\n * inbound record and `sendInbound` prepends five the host has no parser for. That\n * exact corruption shipped once already, through `connect()` rather than through a\n * `DONT` \u2014 see the comment in `handleClose`.\n *\n * `eSeq` goes with it: it is zeroed only on a COMPLETED negotiation, so a\n * renegotiation on this same connection would otherwise keep counting from the\n * abandoned one's total. Nothing observes that today, because a session with no\n * negotiation writes no SEQ-NUMBER \u2014 which is the argument for putting it here rather\n * than at each caller, where \"nothing observes it\" would have to be re-derived.\n *\n * `bound`, `pendingBindRecord` and `noBindTimer` go with it for the SAME reason, not\n * a new one: the BIND gate is meaningless without a live TN3270E negotiation to have\n * granted the function, so every path that ends one must also end the gate, or a\n * second negotiation on this connection (or the next connection entirely) could\n * inherit `bound = true` from the first and skip the gate for data the new host has\n * not yet earned -- or, worse, inherit a `pendingBindRecord` addressed to a screen\n * geometry that no longer applies and execute it against the wrong buffer size. This\n * project has shipped exactly that shape of bug twice already for `e` itself (see\n * above and `tn3270eDisabled`), which is why the gate's state is retired in the same\n * place rather than trusted to a fourth call site remembering to do it by hand.\n */\n private forgetTn3270e(): void {\n this.e = undefined;\n this.eSeq = 0;\n this.bound = false;\n this.pendingBindRecord = undefined;\n this.clearNoBindTimer();\n }\n\n /**\n * Cancel the no-BIND deadline, if one is running. A no-op otherwise -- every caller\n * (here, `handleBind`, `handleUnbind`) reaches this on paths where a timer may or may\n * not be pending, and forcing each of them to check first would just move the guard\n * out of the one place that needs to get it right.\n *\n * PULLED OUT rather than left inline in each of the three call sites deliberately:\n * this project has twice shipped a bug where one teardown path cleared a piece of\n * state and another did not (see `forgetTn3270e`'s own history with `e`). One method\n * is what makes that drift impossible here.\n */\n private clearNoBindTimer(): void {\n if (this.noBindTimer !== undefined) {\n clearTimeout(this.noBindTimer);\n this.noBindTimer = undefined;\n }\n }\n\n /**\n * The telnet layer turned TN3270E off. Our negotiation goes with it.\n *\n * Wired to `TelnetLayerOptions.onTn3270eDisabled`, so it runs for BOTH ways the option\n * dies \u2014 the host's `IAC DONT 40` and our own `refuseTn3270e()` \u2014 rather than only the\n * one whose call site remembered. The host's is the one that had no route here at all:\n * a mid-session `DONT` does not close the connection, so `handleClose` never runs and\n * `e` survived a host that had stopped speaking TN3270E.\n *\n * The trace line is conditional because the backoff path clears `e` itself and has\n * already traced its own, more specific reason before calling `refuseTn3270e()`. A\n * line here regardless would report the same event twice with the second one vaguer.\n */\n private tn3270eDisabled(): void {\n if (this.e !== undefined) {\n this.trace.note('TN3270E turned off; negotiation state discarded');\n }\n this.forgetTn3270e();\n }\n\n /** True once TN3270E negotiation completed, i.e. records carry a header. */\n private inTn3270e(): boolean {\n return this.e?.phase === 'negotiated';\n }\n\n /**\n * True when the host agreed the BIND-IMAGE FUNCTION (0x00), NOT when a record of\n * BIND-IMAGE DATA TYPE (0x03) has arrived \u2014 those are two different meanings of the\n * same name, one negotiated once, the other carried on every BIND record. This\n * method answers only the first question, which is the one the gate in\n * `handleRecord` needs: whether a BIND is owed at all.\n */\n private bindImageGranted(): boolean {\n return this.e?.agreed.includes(Tn3270eFunc.BIND_IMAGE) ?? false;\n }\n\n /**\n * A BIND: the host naming an application and, often, dictating geometry.\n *\n * THE ORDER HERE MATTERS AND MATCHES x3270: parse, apply geometry, erase, THEN\n * release the gate. Verified against `process_bind`/`process_eor`\n * (Common/telnet.c:2449-2590, :2708-2744): `process_bind` runs to completion --\n * including `ctlr_erase(false)` at :2559 -- and only once it returns does the\n * `TN3270E_DT_BIND_IMAGE` case in `process_eor` set `tn3270e_bound = 1` (:2742).\n * Releasing the gate FIRST would let the retained record drain through\n * `executeRecord` at the OLD geometry, which is the bug the gate exists to\n * prevent -- a record painted at 24x80 and then silently viewed through a 43x80\n * buffer, or vice versa.\n */\n private handleBind(body: Uint8Array): void {\n const bind = parseBind(body);\n if (bind === null) {\n this.trace.note('BIND-IMAGE record that is not a BIND, dropped');\n return;\n }\n if (bind.dims !== undefined) {\n const model = this.modelSize();\n const alt = bind.dims.alternate === 'caller' ? model : bind.dims.alternate;\n const dims = { ...bind.dims, alternate: alt };\n const verdict = this.bindLimit\n ? acceptBindDims(dims, model)\n : { ok: true as const };\n if (verdict.ok) {\n this.screen.setSizes(\n { rows: dims.defaultRows, cols: dims.defaultCols }, alt);\n // `useDefaultSize()` ALONE IS NOT x3270's `ctlr_erase(false)`, and this is a real\n // divergence caught by reading ctlr.c rather than trusting the plan's sketch.\n // `ctlr_erase` calls `ctlr_clear(true)` -- the actual data-clearing memset --\n // UNCONDITIONALLY, at ctlr.c:552, BEFORE the `if (alt == screen_alt && ROWS ==\n // newROWS && COLS == newCOLS) return;` early-return at :565-567. That early\n // return only skips the RESIZE bookkeeping and the `screen_disp`/`ctlr_blanks`\n // repaint -- it does not un-clear a buffer already cleared three lines earlier.\n // `Screen.resize()` (which `useDefaultSize()` calls) conflates the two: it\n // returns false and skips its own allocate-and-blank whenever the new default\n // equals the CURRENT geometry, which would leave a BIND that keeps the default\n // size (changing only the alternate, the common case for a same-application\n // reconnect) painted over the operator's last screen instead of erased. `clear()`\n // is therefore called explicitly and unconditionally, matching `ctlr_clear`'s\n // placement ahead of the size check rather than behind it.\n this.screen.useDefaultSize();\n this.screen.clear();\n } else {\n this.trace.note(`${verdict.why}; keeping our geometry`);\n }\n }\n this.bound = true;\n this.clearNoBindTimer();\n const held = this.pendingBindRecord;\n this.pendingBindRecord = undefined;\n if (held !== undefined) {\n this.executeRecord(held.body, held.wants);\n } else {\n // Only emitted when NOTHING was drained: `executeRecord` (line ~625) already\n // emits 'screen' once it finishes painting the retained record, and that repaint\n // reflects the geometry change made above -- a second emit here would fire twice\n // for one logical frame. But a BIND that resized/erased with no pre-BIND record\n // waiting has made a screen change nothing else will announce, so this path must\n // emit or a resize-only BIND is invisible to both renderers.\n this.emit('screen');\n }\n }\n\n /**\n * An UNBIND: teardown with the TCP connection still up.\n *\n * Reverts the BIND's sizing, erases, and closes the gate again to await another\n * BIND. BIND_FORTHCOMING says one IS coming -- the host handing us between\n * applications -- and treating that as a disconnection would drop a session the\n * host meant to keep. x3270 does exactly this at Common/telnet.c:2752-2764:\n * `tn3270e_bound = 0`, `defROWS`/`defCOLS` back to `MODEL_2_ROWS`/`MODEL_2_COLS`,\n * `altROWS`/`altCOLS` back to `maxROWS`/`maxCOLS` (the MODEL's fixed maximum, not\n * whatever a BIND last set it to), then `ctlr_erase(false)`.\n *\n * Reverts to `modelSize()` -- fixed at construction in legacy mode, and at each connect in\n * geometry mode, never by a BIND -- and NOT to\n * `this.screen.alternateSize`: see that field's own comment for why deriving it\n * from the screen would revert to the wrong geometry after two BINDs in a row.\n */\n private handleUnbind(body: Uint8Array): void {\n const info = parseUnbind(body);\n this.trace.note(\n `UNBIND reason ${info.reason ?? 'absent'}`\n + `${info.forthcoming ? ' (BIND forthcoming)' : ''}`);\n const model = this.modelSize();\n this.screen.setSizes({ rows: MODEL_2.rows, cols: MODEL_2.cols }, model);\n // Explicit, unconditional `clear()` for the same reason as `handleBind`: `ctlr_erase`\n // clears before it checks whether the size changed (ctlr.c:552 precedes the\n // early-return at :565-567), so an UNBIND reverting to a default size that happens to\n // match the current one must still erase.\n this.screen.useDefaultSize();\n this.screen.clear();\n this.bound = false;\n this.pendingBindRecord = undefined;\n this.clearNoBindTimer();\n this.emit('screen');\n }\n\n /**\n * Start the no-BIND deadline, unless one is already running.\n *\n * NOT restarted per record: the deadline is \"how long since the host should have\n * bound\", and re-arming on every pre-BIND write would let a chatty host defer it\n * forever -- which is the hang, arrived at by a different route.\n *\n * NOT `.unref()`'d -- checked against every other timer in this repo\n * (`grep -rn 'setTimeout\\|\\.unref('`) and none of them call `.unref()` either: the\n * TUI's ESC timer (`app.ts:534,544`) and the web gateway's grace timer\n * (`web/src/sessions.ts:109`) both stay ref'd and instead rely on an explicit\n * `clearTimeout` reaching every teardown path, exactly the discipline\n * `forgetTn3270e` already applies here. That is precedent, not just a tie-breaker:\n * `app.ts:303`'s comment on the ESC timer names the same failure mode this task's\n * plan cites -- a ref'd timer holds the event loop open for its own duration after\n * `restore()` -- and that codebase's answer was `clearTimeout` on every exit path,\n * not `.unref()`.\n *\n * Ref'd is also the behaviorally correct choice on its own terms, not merely the\n * consistent one: this timer's whole job is to recover a frame the operator cannot\n * otherwise see once the host goes quiet. A CLI script whose only outstanding work\n * is this timeout is precisely the case where staying alive to paint that frame is\n * wanted, not a leak to suppress -- `.unref()` would let such a process exit right\n * out from under its own recovery. Nothing here holds the loop open longer than\n * `NO_BIND_TIMEOUT_MS`, and `forgetTn3270e()` clears it on every path that ends a\n * negotiation (a clean `disconnect()`, our own backoff, or the host withdrawing\n * option 40), so an interactive front end that tears down normally never waits out\n * the 5s regardless.\n */\n private armNoBindTimer(): void {\n if (this.noBindTimer !== undefined) return;\n this.noBindTimer = setTimeout(() => {\n this.noBindTimer = undefined;\n const held = this.pendingBindRecord;\n this.pendingBindRecord = undefined;\n // TRACED BEFORE EXECUTING, so the trace shows the cause ahead of its effect\n // even if executing throws.\n this.trace.note(\n `no BIND within ${NO_BIND_TIMEOUT_MS}ms; executing the retained record at our own geometry`);\n if (held !== undefined) this.executeRecord(held.body, held.wants);\n }, NO_BIND_TIMEOUT_MS);\n }\n\n /**\n * Send a TN3270E RESPONSE message (RFC 2355 \u00A710.4.1).\n *\n * `seq` is COPIED from the message being answered rather than generated, and this\n * deliberately does not go through sendInbound(): a response must not consume one\n * of our outbound sequence numbers, or our numbering drifts out of step with what\n * the host is acknowledging.\n *\n * A no-op when RESPONSES was not agreed. A server asking for a response on such a\n * session is out of spec, and answering would put a message on the wire it has no\n * parser for.\n */\n private sendResponse(seq: number, positive: boolean, sense: number): void {\n if (!this.e?.agreed.includes(Tn3270eFunc.RESPONSES)) return;\n const header = encodeHeader({\n dataType: Tn3270eDataType.RESPONSE,\n requestFlag: 0,\n responseFlag: positive\n ? Tn3270eResponseFlag.POSITIVE_RESPONSE\n : Tn3270eResponseFlag.NEGATIVE_RESPONSE,\n seq,\n });\n const msg = new Uint8Array(header.length + 1);\n msg.set(header, 0);\n msg[header.length] = sense;\n this.telnet?.sendRecord(msg);\n }\n\n /**\n * Answer one DFT frame.\n *\n * A frame arriving with no transfer in flight is IGNORED, matching x3270's\n * `ft_state == FT_NONE` early return (`ft_dft.c:97-100`). It must not throw: a\n * host can send one at any time, and `handleRecord` rethrows non-protocol\n * errors as our own bug, which drops the connection. That would make an\n * unexpected frame a remotely-triggerable disconnect \u2014 the same shape as the\n * gateway kill the keypad branch's task ordering created.\n */\n private handleTransferData(payload: Uint8Array): void {\n const transfer = this.dft;\n if (transfer === undefined) {\n this.trace.note('SF_TRANSFER_DATA with no transfer in progress, ignored');\n return;\n }\n let step;\n try {\n step = transfer.handle(payload);\n } catch (err) {\n // A malformed frame ends the TRANSFER, never the session -- the rule CUT\n // already follows. Without this catch a DftFrameError would escape into\n // handleRecord and drop the connection.\n this.trace.note(`DFT frame rejected: ${err instanceof Error ? err.message : String(err)}`);\n this.dft = undefined;\n this.dftBuffer = undefined;\n this.emit('transferEnd');\n return;\n }\n // Trace BEFORE the reply, so the log reads in the order things happened.\n // Without this the `unsupported` field would be dead code and an\n // unimplemented request type would be an unexplainable stall with an empty\n // log -- x3270 logs `Unsupported(0x%04x)` for exactly this reason.\n if (step.unsupported !== undefined) {\n this.trace.note(\n `DFT request type 0x${step.unsupported.toString(16).padStart(4, '0')} not implemented`);\n }\n if (step.reply !== undefined) this.sendInbound(step.reply);\n if (step.done !== undefined) {\n this.dft = undefined;\n this.dftBuffer = undefined;\n this.emit('transferEnd');\n return;\n }\n // ONE PER ACCEPTED FRAME, and ONLY when the transfer is still running -- a `done` frame has\n // already emitted `transferEnd` above and returned, so a front end never sees progress after\n // the ending. See `SessionEvent` for why this event exists at all: without it a DFT transfer\n // is silent, and a silent transfer was being killed at 30 seconds by CUT's stall detector.\n this.emit('transferProgress');\n }\n\n /**\n * Send one inbound record, adding the TN3270E header when the session has one.\n *\n * THE HEADER IS PREPENDED AND THE WHOLE THING HANDED TO sendRecord, so it flows\n * through the existing doubleIac(). RFC 2355 \u00A78.1.4 requires a 0xff inside\n * SEQ-NUMBER to be doubled, and doing it this way satisfies that by construction\n * rather than with a second escaping implementation that could drift out of step\n * with the first. Writing the header separately would put a bare 0xff on the wire\n * once the counter passes 255 and desynchronise the host's telnet parser\n * mid-record, which presents as a hang rather than an error.\n */\n private sendInbound(payload: Uint8Array): void {\n if (!this.inTn3270e()) {\n this.telnet?.sendRecord(payload);\n return;\n }\n const header = encodeHeader({\n dataType: Tn3270eDataType.DATA_3270,\n requestFlag: 0,\n responseFlag: Tn3270eResponseFlag.NO_RESPONSE,\n seq: this.eSeq,\n });\n const framed = new Uint8Array(header.length + payload.length);\n framed.set(header, 0);\n framed.set(payload, header.length);\n // Advance only when RESPONSES was agreed. \u00A78.1.4: otherwise the field \"should\n // always be set to 0x0000\". x3270 gates the increment the same way\n // (telnet.c:3350), and masks to 15 bits.\n if (this.e?.agreed.includes(Tn3270eFunc.RESPONSES)) {\n this.eSeq = (this.eSeq + 1) & 0x7fff;\n }\n this.telnet?.sendRecord(framed);\n }\n\n private programCheck(code: number, why: string): void {\n this.oia.programCheck(code);\n this.oia.waitingForHost = false;\n this.trace.note(`program check ${code}: ${why}`);\n this.emit('screen');\n }\n\n /**\n * A host-initiated read. It carries the PENDING AID \u2014 the key the operator last pressed,\n * or 0x60 if none \u2014 which is how a host that polls with Read Modified learns about a\n * keypress on its own schedule. See `pendingAid`.\n */\n private answerRead(kind: 'ReadBuffer' | 'ReadModified' | 'ReadModifiedAll'): void {\n // DFT SHORT-CIRCUIT. x3270 does this at BOTH read sites -- ctlr.c:760 in\n // ctlr_read_modified and ctlr.c:986 in ctlr_read_buffer -- returning\n // immediately when the AID is AID_SF. (The design spec cites :761 and :987;\n // the actual lines are one lower, verified.) Ours is one function, so one guard\n // covers both, and the `all` variant comes free.\n //\n // Guarded on there being a RETAINED FRAME, not merely on a transfer being in\n // flight: a DOWNLOAD retains nothing and must still answer an ordinary read.\n //\n // The retained BYTES are replayed rather than asking the engine for a frame.\n // That is what makes a replay idempotent -- re-deriving would consume more\n // source and hand the host the NEXT chunk, corrupting the file while both reads\n // appeared to succeed.\n //\n // OMITTING THIS STALLS UPLOADS ONLY. A download never reaches here, so the\n // whole receive path passes with this missing -- which is exactly why it is\n // its own task, and why deleting it reddens only the two replay tests.\n const retained = this.dft?.retainedFrame;\n if (retained !== undefined) {\n this.trace.note('Read Modified during a DFT upload: replaying the retained frame');\n this.sendInbound(retained);\n return;\n }\n // THE PENDING AID, not a hardcoded 0x60: x3270 passes its `aid` global to all three\n // reads (ctlr.c:635, :641, :647). When no key has been pressed this IS 0x60, so the\n // no-keypress case is unchanged \u2014 what changes is that a poll after a keypress now\n // names the key, and that a short-read AID reaches buildReadModified's short-read\n // branch. NOT consumed by the read: a polling host may read repeatedly and must get\n // the same answer until it writes. See `pendingAid`.\n const payload = kind === 'ReadBuffer'\n ? buildReadBuffer(this.screen, this.pendingAid)\n : buildReadModified(this.screen, this.pendingAid, kind === 'ReadModifiedAll');\n this.sendInbound(payload);\n }\n\n /**\n * Answer a Read Partition (Query or Query List) with our capabilities, then\n * lock the keyboard.\n *\n * Deliberately does NOT touch the screen or the cursor: a Query is a question\n * about the device, not a write to it. That holds for a Query List too \u2014\n * p. 5-53's step list (pages.txt:6413-6427) treats the two identically apart\n * from which replies go inbound, and nowhere among its seven steps is a buffer\n * change.\n *\n * It DOES touch the keyboard, which is step 1 of Read Partition processing,\n * GA23-0059 p. 5-53 (pages.txt:6413): \"1. The enter-inhibit condition is\n * raised.\" The host has frozen the screen pending its own next write, and\n * until that arrives the operator must not type into it. x3270 raises it in\n * query_reply_end (Common/sf.c:926-930), which is the whole function:\n *\n * net_output();\n * kybd_inhibit(true);\n *\n * REPLY FIRST, THEN INHIBIT, matching that ordering exactly. The manual's own\n * step list is the other way round \u2014 the inhibit is step 1 and step 5 says\n * that for a Query \"a / set of Query Replies is transmitted inbound\"\n * (pages.txt:6420-6421, the slash marking the OCR line break) \u2014 but\n * the two are indistinguishable from outside, because sendRecord neither\n * consults the keyboard state nor yields, and x3270's concrete ordering is\n * the better guide for anyone diffing the two clients. What would be a real\n * bug is the reverse of what we do: raising it first through a path that\n * checked the lock before transmitting would swallow our own reply.\n *\n * `this.telnet?.` and not a throw, matching answerRead: both are reached only\n * from handleRecord, which the telnet layer itself calls, so a missing telnet\n * means the transport went away mid-record and there is nowhere to send. The\n * throwing convention belongs to the operator-initiated senders (sendAID,\n * sendAttn), where a caller is present to be told.\n *\n * Note the inhibit is raised even on that transport-gone path. That is\n * correct: the host asked and the screen is frozen whether or not our answer\n * reached it, and a session whose socket has just vanished is not one to\n * unlock a keyboard over.\n */\n private answerQuery(request: QueryRequest): void {\n // The DEFAULT size and the ALTERNATE size, NOT the current one. A host that\n // asks while we happen to be in alternate mode must still be told what the\n // default is, and `screen.rows` is whichever mode we are in right now --\n // reading it here would make the reply depend on the moment it was asked.\n const geometry = {\n rows: this.screen.defaultSize.rows,\n cols: this.screen.defaultSize.cols,\n alternate: this.screen.alternateSize,\n };\n // buildReply, not buildQueryReply: it applies the REQTYP rules and the\n // always-send-Summary rule in one place.\n //\n // It CAN throw a RangeError, on a reserved REQTYP (B'11'), and handleRecord\n // does not catch that \u2014 it rethrows non-protocol errors as \"our own bug\",\n // which drops the connection. A host must not be able to trigger that, so\n // the reserved value is screened out in stream/sf.ts queryListRequest before\n // it ever becomes an sfReply. The throw is an unreachable assertion, and\n // there are tests at both levels pinning that.\n // DDM is appended rather than living in DEFAULT_CAPABILITIES because it is\n // opt-in: see SessionOptions.ddm. Appending keeps the list in ascending QCODE\n // order for every subset (0x95 < 0xa6 only by luck of where it lands, so\n // insert before Implicit Partition rather than at the end) -- wire order is\n // list order, and matching x3270's ascending order is what makes captures\n // comparable. buildReply then applies the REQTYP rules to whichever list it\n // is given, so no other code changes.\n // THE IN-FLIGHT TRANSFER'S SIZE WINS, falling back to the session default when no\n // transfer is running -- x3270's own precedence, `do_qr_ddm` reading\n // `ftc->dft_buffersize` or `set_dft_buffersize(0)` (`sf.c:890-897`). This is the read\n // half of what makes the advertised and chunked sizes one number; see `dftBuffer`.\n // `!== false`, NOT truthiness: the default is ON as of 2026-09-29 and absent must mean\n // on, while `-ddm off` must mean off. Written the same way in `ddmAdvertised`, and the\n // two MUST agree -- a front end tells the operator \"DDM was not advertised\" from that\n // getter, so a polarity mismatch would produce advice that contradicts the wire.\n const capabilities = this.opts.ddm !== false\n ? withDdm(DEFAULT_CAPABILITIES, this.dftBuffer ?? this.opts.dftBufferSize)\n : DEFAULT_CAPABILITIES;\n this.sendInbound(buildReply(request, capabilities, geometry));\n // enterInhibit, not inhibit(EnterInhibit): it yields to a stronger inhibit\n // already in force. Before the host's first write that is\n // AwaitingFirstWrite \u2014 the case TSO produces, since it queries before\n // writing \u2014 and demoting it there would narrow the release rule from \"any\n // write, or a WCC keyboard-restore\" to \"any write\". See Oia.enterInhibit.\n this.oia.enterInhibit();\n }\n\n /**\n * Operator pressed a key that generates an AID.\n *\n * THE BYTE IS CHECKED, and that check is the backstop for a defect that reached a live host: an\n * out-of-range `PF_AIDS[n - 1]!` is `undefined`, and `buildReadModified`'s `Uint8Array.from`\n * coerces that to **0**, so a bogus `0x00` AID went to a mainframe and locked the keyboard.\n * `pfAID`/`paAID` fix the callers; this makes the bad byte unsendable by any caller written later.\n *\n * Checked BEFORE the connected test, so the diagnosis does not depend on whether a socket happens\n * to be open: a caller passing a nonsense AID has a bug either way, and hearing 'not connected'\n * first would send them looking at the transport.\n */\n sendAID(aid: number): void {\n if (!VALID_AIDS.has(aid)) {\n throw new RangeError(`${aid} is not an AID byte; use AID, pfAID(n) or paAID(n)`);\n }\n if (this.telnet === undefined) throw new Error('not connected');\n\n // RECORDED BEFORE IT IS SENT, and it OUTLIVES this record. x3270 does both in two\n // adjacent lines -- `aid = aid_code; ctlr_read_modified(aid, false);` (kybd.c:922-923)\n // -- so the byte is not merely an argument to this transmission: it is the answer to\n // any host-initiated read that arrives before the host's next write. See `pendingAid`.\n this.pendingAid = aid;\n\n const payload = buildReadModified(this.screen, aid, false);\n this.sendInbound(payload);\n\n // The Clear key blanks the buffer locally as well as telling the host.\n if (aid === AID.CLEAR) {\n this.screen.clear();\n this.emit('screen');\n }\n\n // Any AID locks the keyboard until the host restores it. A short read is no\n // exception: the host still owns the next move, and buildReadModified\n // already decides what a short read puts on the wire.\n this.oia.waitingForHost = true;\n this.oia.inhibit(KeyboardState.SystemWait);\n }\n\n /**\n * The Sys Req key. TWO COMPLETELY DIFFERENT THINGS ON THE WIRE, chosen by whether the\n * session is TN3270E.\n *\n * x3270's `SysReq_action` splits on `IN_E`, and nothing else (`Common/kybd.c:2849-2870`):\n *\n * if (IN_E) net_abort();\n * else { ...; key_AID(AID_SYSREQ); }\n *\n * TN3270E: Telnet IAC AO, via `sendSysreq()`. `net_abort()` then checks the SYSREQ\n * function bit itself (`Common/telnet.c:3632-3648`), which is what the guard below\n * mirrors. An E session that declined the function sends NOTHING \u2014 and specifically not\n * the classic form as a fallback, because x3270 never reaches the `else` arm on an E\n * session. A host that negotiated TN3270E and refused SYSREQ has said what it wants.\n * Not a data message, so it spends no sequence number.\n *\n * CLASSIC: a TEST REQUEST READ \u2014 the four bytes SOH `%` `/` STX, then the modified field\n * data, and NO AID BYTE AT ALL. `key_AID(AID_SYSREQ)` looks like it transmits 0xf0 and\n * does not; `ctlr_read_modified` intercepts that AID and substitutes the heading\n * (`Common/ctlr.c:770-777`). `buildReadModified` carries the same branch and the\n * citations. THIS IS THE PATH BOTH OF THIS PROJECT'S LIVE HOSTS TAKE: VM/370 CE and MVS\n * 3.8j TK5 each answer `IAC WILL TN3270E` with DONT, measured three times.\n *\n * Routed through `sendAID` because that is this codebase's `key_AID`: the keyboard lock it\n * sets is not incidental, it is what x3270 does for every AID including this one\n * (`kybd.c:918-923`). `sendAID` validates 0xf0 against `VALID_AIDS`, where `AID.SYSREQ`\n * already sits, and hands the record to `sendInbound`, so the framing question is answered\n * the same way as for Enter.\n *\n * SILENT, NEVER THROWING, on every refusal. The key exists on the keyboard whatever the\n * host granted or the transport is doing, so pressing it is not the operator's mistake;\n * `applyAction` swallows refusals into the OIA and the CLI's `SysReq()` reports none.\n * That is why the not-connected check is here rather than left to `sendAID`, which throws.\n *\n * REFUSES ON AN INHIBITED KEYBOARD, WHERE x3270 WOULD QUEUE. It refuses outright on\n * `KL_OIA_MINUS` and calls `enq_ta` on any other lock (`kybd.c:2858-2864`). We have no\n * action queue and one is not worth inventing for a single key, so both become a refusal:\n * the press is declined rather than deferred, nothing goes on the wire out of turn, and\n * the OIA already shows the operator why.\n */\n sysreq(): void {\n if (this.telnet === undefined) {\n this.trace.note('SYSREQ ignored: not connected');\n return;\n }\n if (this.inTn3270e()) {\n if (!this.e?.agreed.includes(Tn3270eFunc.SYSREQ)) {\n this.trace.note('SYSREQ ignored: function not negotiated');\n return;\n }\n this.telnet.sendSysreq();\n return;\n }\n if (this.oia.isInhibited()) {\n this.trace.note(`SYSREQ refused: input inhibited (${this.oia.toText()})`);\n return;\n }\n this.sendAID(AID.SYSREQ);\n }\n\n /** Attn is Telnet BREAK (RFC 1576 \u00A78), not an AID. */\n sendAttn(): void {\n if (this.telnet === undefined) throw new Error('not connected');\n this.telnet.sendAttn();\n }\n\n /**\n * Drive the screen from a recorded trace, with no socket. Only host-to-\n * terminal bytes are replayed; what we sent last time is not re-sent.\n */\n replay(traceText: string): void {\n if (this.conn !== undefined) {\n // Refuse rather than transmit. A recorded trace contains the host's\n // negotiation AND its read commands; replaying it on a live session makes\n // handleRecord answer those reads through this.telnet, i.e. down the real\n // socket. Verified: a trace ending in a Read Buffer sent 60 40 40 ... to\n // the live host.\n throw new Error('replay() requires a disconnected session; disconnect first');\n }\n const events = parseTrace(traceText);\n // Same helper `connect()` uses, and for the same reason: a replayed trace that\n // refused option 39 a live connection would have accepted (or vice versa) would\n // silently exercise different negotiation code than a live session ever does --\n // exactly the hole Task 8's playback oracle depends on being closed here.\n const environ = this.buildEnviron();\n const telnet: TelnetLayer = new TelnetLayer({\n write: () => { /* discard: replay is one-directional */ },\n onRecord: (r) => this.handleRecord(r),\n // Wired so a replayed negotiation is OBSERVABLE the same way a live one is: without\n // this, `this.trace` never learns what replay()'s own TelnetLayer decided to send in\n // reply to a DO/SEND, and the only two consumers that could ever prove `environ` was\n // (or was not) wired into replay() -- a human reading a trace, and this file's own\n // \"replay() answers option 39 the same way connect() would\" test -- would both be\n // looking at an always-empty trace regardless of what replay() actually did. `write`\n // stays a discard sink -- this does not put anything back on a socket, since replay()\n // opens none -- but the trace is exactly the introspection connect() already gets.\n trace: this.trace,\n tn3270eEnabled: this.opts.tn3270e ?? true,\n // Wired even though writes are discarded: replaying a TN3270E trace still has\n // to advance the state machine, or handleRecord never learns to strip the\n // 5-byte header and every replayed record is parsed one command byte early.\n // `telnet` rather than `this.telnet`, which replay leaves undefined.\n onTn3270eSubneg: (body) => { this.handleTn3270eSubneg(body, telnet); },\n // Wired here too, and for the same reason the subnegotiation callback is: a\n // recorded trace can contain the host's `IAC DONT TN3270E`, and a replay that kept\n // `e` past it would strip a header from every later record that has none.\n onTn3270eDisabled: () => { this.tn3270eDisabled(); },\n ...(environ !== undefined ? { environ } : {}),\n });\n for (const ev of events) {\n if (ev.dir === 'recv') telnet.receive(ev.bytes);\n }\n this.emit('screen');\n }\n}\n", "/**\n * IND$FILE CUT-mode codec: the 6-bit alphabet, the four quadrant translation\n * tables, the XOR checksum, and the two stateful byte conversions.\n *\n * Pure and host-independent. No screen, no session, no I/O. Frames, the state\n * machine and the CLI action are separate units; see\n * docs/superpowers/specs/2026-08-18-indfile-cut-transfer-design.md.\n *\n * Ported from x3270 4.5, `Common/ft_cut.c`, which the design doc names as the\n * de facto protocol definition: CUT is underspecified, and x3270 is the client\n * these hosts have actually been driven with for decades. Every table and every\n * branch below cites a line in that file. Constants were extracted from the C\n * with a script and diffed against this file, not retyped -- see\n * packages/core/test/ft/cut.test.ts, which re-derives the invariants.\n *\n * ## THE NAMING TRAP\n *\n * x3270's \"upload\" and \"download\" are named from the HOST's point of view,\n * which is inverted from ordinary usage. Its `upload_convert` (`ft_cut.c:134`,\n * commented at `:129` \"Convert a buffer for uploading (host->local)\") is what a\n * user calls a DOWNLOAD, i.e. IND$FILE GET. Its `download_convert`\n * (`ft_cut.c:307`) and `store_download` (`ft_cut.c:270`) are what a user calls\n * an UPLOAD, i.e. IND$FILE PUT.\n *\n * This module therefore uses `hostToLocal` and `localToHost`, which cannot be\n * misread. The words \"upload\" and \"download\" appear nowhere in an identifier\n * here, only in citations that state the real direction.\n *\n * ## SCOPE: binary mode only, for now\n *\n * ASCII mode is DEFERRED. `upload_convert` also does CR/EOF suppression when\n * `ascii_flag && cr_flag` (`ft_cut.c:190-192`), a remap through the local\n * multi-byte encoding when `ascii_flag && remap_flag` (`ft_cut.c:193-262`), and\n * DBCS shift-in/shift-out state (`ft_cut.c:206-236`); `download_convert` has\n * the mirror image (`ft_cut.c:336-380`). None of that is implemented. The\n * design doc makes binary the default precisely because a wrong default\n * silently corrupts a MODULE, so binary is also the honest first delivery.\n */\n\n/** Number of quadrants. `#define NQ 4` (ft_cut.c:56). */\nexport const NQ = 4;\n\n/** Elements per quadrant. `#define NE 77` (ft_cut.c:57). */\nexport const NE = 77;\n\n/**\n * The \"OTHER 2\" quadrant, which is the one containing NULL.\n * `#define OTHER_2 2` (ft_cut.c:58), whose own comment reads\n * `\"OTHER 2\" quadrant (includes NULL)`.\n */\nexport const OTHER_2 = 2;\n\n/** Translation of NULL. `#define XLATE_NULL 0xc1` (ft_cut.c:59). */\nexport const XLATE_NULL = 0xc1;\n\n/**\n * The 6-bit alphabet, used for sequence numbers, lengths and the checksum.\n * `static char table6[]` (ft_cut.c:105-106), verbatim:\n *\n * \"abcdefghijklmnopqrstuvwxyz&-.,:+ABCDEFGHIJKLMNOPQRSTUVWXYZ012345\"\n *\n * 64 characters, all distinct, so index <-> character is a bijection.\n */\nexport const TABLE6 = 'abcdefghijklmnopqrstuvwxyz&-.,:+ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';\n\n/**\n * The 77-character quadrant index alphabet. `static char alphas[NE + 1]`\n * (ft_cut.c:61-62), verbatim -- note the LEADING SPACE, which is element 0 and\n * is load-bearing, not formatting:\n *\n * \" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?\"\n *\n * The C declares it `[NE + 1]` because of the NUL terminator; the alphabet\n * itself is NE = 77 characters.\n */\nexport const ALPHAS =\n ' ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?';\n\n/** One quadrant: the selector byte that switches to it, and its 77 entries. */\nexport interface Quadrant {\n /** EBCDIC byte the host sends, and we send, to select this quadrant. */\n readonly selector: number;\n /** ALPHAS index -> the data byte it stands for. 0 means \"not in this quadrant\". */\n readonly xlate: readonly number[];\n}\n\n/**\n * The four quadrants: `static struct { ... } conv[NQ]` (ft_cut.c:64-104).\n *\n * TRANSCRIPTION PROVENANCE: these 308 bytes were extracted from ft_cut.c by\n * script and emitted in the C's own 12-per-line, 4-per-group layout so a diff\n * against the source is a visual no-op. The test file re-checks the properties\n * that a single wrong byte would break (lengths, selectors, the exhaustive\n * round trip, and the disjointness structure).\n *\n * A zero entry means \"this quadrant does not encode a byte at this index\",\n * EXCEPT at index 0 of OTHER_2 and of quadrant 3, where 0x00 is the real data\n * byte NULL -- which is exactly why hostToLocal needs the OTHER_2 and\n * XLATE_NULL special cases at ft_cut.c:181-182.\n */\nexport const QUADRANTS: readonly Quadrant[] = [\n {\n selector: 0x5e, // ';'\n xlate: [\n 0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,\n 0xd3,0xd4,0xd5,0xd6, 0xd7,0xd8,0xd9,0xe2, 0xe3,0xe4,0xe5,0xe6,\n 0xe7,0xe8,0xe9,0x81, 0x82,0x83,0x84,0x85, 0x86,0x87,0x88,0x89,\n 0x91,0x92,0x93,0x94, 0x95,0x96,0x97,0x98, 0x99,0xa2,0xa3,0xa4,\n 0xa5,0xa6,0xa7,0xa8, 0xa9,0xf0,0xf1,0xf2, 0xf3,0xf4,0xf5,0xf6,\n 0xf7,0xf8,0xf9,0x6c, 0x50,0x6d,0x4d,0x5d, 0x4c,0x4e,0x6b,0x60,\n 0x4b,0x61,0x7a,0x6e, 0x6f,\n ],\n },\n {\n selector: 0x7e, // '='\n xlate: [\n 0x20,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, 0x48,0x49,0x4a,0x4b,\n 0x4c,0x4d,0x4e,0x4f, 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57,\n 0x58,0x59,0x5a,0x61, 0x62,0x63,0x64,0x65, 0x66,0x67,0x68,0x69,\n 0x6a,0x6b,0x6c,0x6d, 0x6e,0x6f,0x70,0x71, 0x72,0x73,0x74,0x75,\n 0x76,0x77,0x78,0x79, 0x7a,0x30,0x31,0x32, 0x33,0x34,0x35,0x36,\n 0x37,0x38,0x39,0x25, 0x26,0x27,0x28,0x29, 0x2a,0x2b,0x2c,0x2d,\n 0x2e,0x2f,0x3a,0x3b, 0x3f,\n ],\n },\n {\n selector: 0x5c, // '*'\n xlate: [\n 0x00,0x00,0x01,0x02, 0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0a,\n 0x0b,0x0c,0x0d,0x0e, 0x0f,0x10,0x11,0x12, 0x13,0x14,0x15,0x16,\n 0x17,0x18,0x19,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,\n 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,\n 0x00,0x00,0x00,0x00, 0x00,0x3c,0x3d,0x3e, 0x00,0xfa,0xfb,0xfc,\n 0xfd,0xfe,0xff,0x7b, 0x7c,0x7d,0x7e,0x7f, 0x1a,0x1b,0x1c,0x1d,\n 0x1e,0x1f,0x00,0x00, 0x00,\n ],\n },\n {\n selector: 0x7d, // '\\'' (apostrophe)\n xlate: [\n 0x00,0xa0,0xa1,0xea, 0xeb,0xec,0xed,0xee, 0xef,0xe0,0xe1,0xaa,\n 0xab,0xac,0xad,0xae, 0xaf,0xb0,0xb1,0xb2, 0xb3,0xb4,0xb5,0xb6,\n 0xb7,0xb8,0xb9,0x80, 0x00,0xca,0xcb,0xcc, 0xcd,0xce,0xcf,0xc0,\n 0x00,0x8a,0x8b,0x8c, 0x8d,0x8e,0x8f,0x90, 0x00,0xda,0xdb,0xdc,\n 0xdd,0xde,0xdf,0xd0, 0x00,0x00,0x21,0x22, 0x23,0x24,0x5b,0x5c,\n 0x00,0x5e,0x5f,0x00, 0x9c,0x9d,0x9e,0x9f, 0xba,0xbb,0xbc,0xbd,\n 0xbe,0xbf,0x9a,0x9b, 0x00,\n ],\n },\n];\n\n/**\n * x3270's fixed EBCDIC-to-ASCII table, `ebc2asc0[256]` (Common/tables.c:41-73).\n *\n * WHY THIS IS NOT `CodePage`/cp037. `ft_cut.c` uses `ebc2asc0`/`asc2ebc0` for\n * exactly one purpose: locating a character in TABLE6 or ALPHAS. The comment at\n * ft_cut.c:344-348 explains the model -- \"The host uses a fixed EBCDIC-to-ASCII\n * translation table, which was derived empirically into i_ft2asc/i_asc2ft.\n * Invert that so that when the host applies its conversion, it gets the right\n * EBCDIC code.\"\n *\n * These tables are single-byte ASCII, and differ from cp037 in 66 of 256\n * entries: `ebc2asc0` maps every EBCDIC control byte below 0x40 to 0x20 (space)\n * rather than to a control character, except X'1C' -> '*' and X'1E' -> ';'.\n * cp037 is a faithful character-set mapping and (correctly, for its purpose)\n * does not do that.\n *\n * Only ONE of those 66 differences can reach this codec: EBCDIC X'41', which\n * `ebc2asc0` maps to 0x20 (space, ALPHAS index 0) while cp037 maps to U+00A0\n * NO-BREAK SPACE (not in ALPHAS at all). Using cp037 would make X'41' a\n * conversion error where x3270 decodes it as ALPHAS[0]. Over the 64 TABLE6\n * characters and the 77 ALPHAS characters, in both directions, the two tables\n * otherwise agree exactly -- the test asserts all four of those agreements and\n * pins the X'41' disagreement, so the claim is checked rather than asserted.\n *\n * Hence: cp037 is right for the screen and wrong here, and this module carries\n * its own 256-byte tables. They are a codec detail, not a code page, and are\n * deliberately not exported as one.\n */\nconst EBC2ASC: readonly number[] = [\n /* 00 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 08 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 10 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 18 */ 0x20, 0x20, 0x20, 0x20, 0x2a, 0x20, 0x3b, 0x20,\n /* 20 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 28 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 30 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 38 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n /* 40 */ 0x20, 0x20, 0xe2, 0xe4, 0xe0, 0xe1, 0xe3, 0xe5,\n /* 48 */ 0xe7, 0xf1, 0xa2, 0x2e, 0x3c, 0x28, 0x2b, 0x7c,\n /* 50 */ 0x26, 0xe9, 0xea, 0xeb, 0xe8, 0xed, 0xee, 0xef,\n /* 58 */ 0xec, 0xdf, 0x21, 0x24, 0x2a, 0x29, 0x3b, 0xac,\n /* 60 */ 0x2d, 0x2f, 0xc2, 0xc4, 0xc0, 0xc1, 0xc3, 0xc5,\n /* 68 */ 0xc7, 0xd1, 0xa6, 0x2c, 0x25, 0x5f, 0x3e, 0x3f,\n /* 70 */ 0xf8, 0xc9, 0xca, 0xcb, 0xc8, 0xcd, 0xce, 0xcf,\n /* 78 */ 0xcc, 0x60, 0x3a, 0x23, 0x40, 0x27, 0x3d, 0x22,\n /* 80 */ 0xd8, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,\n /* 88 */ 0x68, 0x69, 0xab, 0xbb, 0xf0, 0xfd, 0xfe, 0xb1,\n /* 90 */ 0xb0, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70,\n /* 98 */ 0x71, 0x72, 0xaa, 0xba, 0xe6, 0xb8, 0xc6, 0xa4,\n /* a0 */ 0xb5, 0x7e, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,\n /* a8 */ 0x79, 0x7a, 0xa1, 0xbf, 0xd0, 0xdd, 0xde, 0xae,\n /* b0 */ 0x5e, 0xa3, 0xa5, 0xb7, 0xa9, 0xa7, 0xb6, 0xbc,\n /* b8 */ 0xbd, 0xbe, 0x5b, 0x5d, 0xaf, 0xa8, 0xb4, 0xd7,\n /* c0 */ 0x7b, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,\n /* c8 */ 0x48, 0x49, 0xad, 0xf4, 0xf6, 0xf2, 0xf3, 0xf5,\n /* d0 */ 0x7d, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,\n /* d8 */ 0x51, 0x52, 0xb9, 0xfb, 0xfc, 0xf9, 0xfa, 0xff,\n /* e0 */ 0x5c, 0xf7, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,\n /* e8 */ 0x59, 0x5a, 0xb2, 0xd4, 0xd6, 0xd2, 0xd3, 0xd5,\n /* f0 */ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,\n /* f8 */ 0x38, 0x39, 0xb3, 0xdb, 0xdc, 0xd9, 0xda, 0x20,\n];\n\n/** x3270's fixed ASCII-to-EBCDIC table, `asc2ebc0[256]` (Common/tables.c:74-106). */\nconst ASC2EBC: readonly number[] = [\n /* 00 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 08 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 10 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 18 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 20 */ 0x40, 0x5a, 0x7f, 0x7b, 0x5b, 0x6c, 0x50, 0x7d,\n /* 28 */ 0x4d, 0x5d, 0x5c, 0x4e, 0x6b, 0x60, 0x4b, 0x61,\n /* 30 */ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,\n /* 38 */ 0xf8, 0xf9, 0x7a, 0x5e, 0x4c, 0x7e, 0x6e, 0x6f,\n /* 40 */ 0x7c, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,\n /* 48 */ 0xc8, 0xc9, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6,\n /* 50 */ 0xd7, 0xd8, 0xd9, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6,\n /* 58 */ 0xe7, 0xe8, 0xe9, 0xba, 0xe0, 0xbb, 0xb0, 0x6d,\n /* 60 */ 0x79, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,\n /* 68 */ 0x88, 0x89, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96,\n /* 70 */ 0x97, 0x98, 0x99, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,\n /* 78 */ 0xa7, 0xa8, 0xa9, 0xc0, 0x4f, 0xd0, 0xa1, 0x00,\n /* 80 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 88 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 90 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* 98 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n /* a0 */ 0x41, 0xaa, 0x4a, 0xb1, 0x9f, 0xb2, 0x6a, 0xb5,\n /* a8 */ 0xbd, 0xb4, 0x9a, 0x8a, 0x5f, 0xca, 0xaf, 0xbc,\n /* b0 */ 0x90, 0x8f, 0xea, 0xfa, 0xbe, 0xa0, 0xb6, 0xb3,\n /* b8 */ 0x9d, 0xda, 0x9b, 0x8b, 0xb7, 0xb8, 0xb9, 0xab,\n /* c0 */ 0x64, 0x65, 0x62, 0x66, 0x63, 0x67, 0x9e, 0x68,\n /* c8 */ 0x74, 0x71, 0x72, 0x73, 0x78, 0x75, 0x76, 0x77,\n /* d0 */ 0xac, 0x69, 0xed, 0xee, 0xeb, 0xef, 0xec, 0xbf,\n /* d8 */ 0x80, 0xfd, 0xfe, 0xfb, 0xfc, 0xad, 0xae, 0x59,\n /* e0 */ 0x44, 0x45, 0x42, 0x46, 0x43, 0x47, 0x9c, 0x48,\n /* e8 */ 0x54, 0x51, 0x52, 0x53, 0x58, 0x55, 0x56, 0x57,\n /* f0 */ 0x8c, 0x49, 0xcd, 0xce, 0xcb, 0xcf, 0xcc, 0xe1,\n /* f8 */ 0x70, 0xdd, 0xde, 0xdb, 0xdc, 0x8d, 0x8e, 0xdf,\n];\n\n/**\n * A byte the CUT codec cannot convert.\n *\n * host->local, this is x3270's `cut_abort(get_message(\"ftCutConversionError\"),\n * SC_ABORT_XMIT)` at ft_cut.c:156 and :162, which returns -1 and fails the\n * transfer. We throw, and the caller (the state machine, a later unit) turns it\n * into that same abort -- the design doc's rule is that a transfer fault ends\n * the transfer without killing the session.\n */\nexport class CutConversionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'CutConversionError';\n }\n}\n\n/**\n * TABLE6 character -> its index, i.e. decode one 6-bit value.\n *\n * `static unsigned from6(unsigned char c)` (ft_cut.c:583-594). Takes an EBCDIC\n * byte as it came off the wire, translates it with `ebc2asc0`, and looks it up:\n *\n * c = ebc2asc0[c];\n * p = strchr(table6, c);\n * if (p == NULL) {\n * return 0;\n * }\n *\n * NOTE THE ERROR SEMANTICS, which we match deliberately: an unrecognized byte\n * yields 0, indistinguishable from a genuine 0. That is lossy, and we keep it\n * because from6 decodes sequence numbers and lengths whose framing x3270 has\n * validated for decades -- diverging here would change frame handling on a\n * protocol we cannot yet test against the live host. 192 of the 256 possible\n * bytes take this path.\n *\n * There is one subtlety worth stating: `strchr` on a C string also matches the\n * NUL terminator, so in x3270 a byte translating to 0x00 would return 64, one\n * past the end. It cannot arise, because ebc2asc0 never produces 0x00 (verified\n * over all 256 entries), so JS's `indexOf` -- which does not have that quirk --\n * is faithful, not merely close.\n */\nexport function from6(byte: number): number {\n const ascii = EBC2ASC[byte & 0xff]!;\n const ix = TABLE6.indexOf(String.fromCharCode(ascii));\n return ix === -1 ? 0 : ix;\n}\n\n/**\n * A 6-bit value -> the EBCDIC byte encoding it. The inverse of from6.\n *\n * x3270 has no `to6` function; it inlines this as\n * `asc2ebc0[(int)table6[cs & 0x3f]]` (ft_cut.c:554, the checksum) and\n * `asc2ebc0[(int)table6[count & 0x3f]]` (ft_cut.c:556, the length). Naming it\n * lets one place own the `& 0x3f` mask.\n */\nexport function to6(value: number): number {\n return ASC2EBC[TABLE6.charCodeAt(value & 0x3f)]!;\n}\n\n/**\n * XOR of every byte, masked to 6 bits.\n *\n * ft_cut.c:550-554, the local->host frame checksum:\n *\n * cs = 0;\n * for (i = 0; i < count; i++) {\n * cs ^= ea_buf[O_UP_DATA + i].ec;\n * }\n * ctlr_add(O_UP_CSUM, asc2ebc0[(int)table6[cs & 0x3f]], 0);\n *\n * Returns the RAW 6-bit value; the caller encodes it with `to6` when writing it\n * into a frame. Splitting those apart keeps this function testable by hand and\n * lets the host->local direction reuse it to verify the checksum the host sent\n * (which the design doc has us trace but never treat as fatal, since x3270\n * never even reads O_DT_CSUM).\n *\n * Note the mask is applied ONCE at the end, not per byte. Same result, because\n * XOR is bitwise, but it matters that it is the encoded value that is 6 bits.\n */\nexport function checksum(bytes: Iterable<number>): number {\n let cs = 0;\n for (const b of bytes) cs ^= b & 0xff;\n return cs & 0x3f;\n}\n\n/**\n * The quadrant state that persists across bytes within one transfer.\n *\n * WHY AN OBJECT RATHER THAN A MODULE VARIABLE. x3270 uses a file-static\n * `static int quadrant = -1` (ft_cut.c:108), reset per transfer at\n * ft_cut.c:435. That has two consequences we refuse to inherit: two concurrent\n * transfers would corrupt each other's encoding, and tests would be\n * order-dependent -- a test that left the quadrant set would change the output\n * of the next one, which is precisely the bug class this codec is most likely\n * to hide.\n *\n * A class, rather than a state object threaded through free functions, because\n * the state is one small field with exactly two operations on it, and because\n * \"one instance per transfer\" is then enforced by construction instead of by\n * everyone remembering to pass the right object.\n */\nexport class CutCodec {\n /**\n * Current quadrant, or -1 for \"not yet established\".\n *\n * -1 rather than undefined to keep the comparisons `< 0` / `!== OTHER_2`\n * readable against the C, since the retry logic depends on both.\n */\n private quadrant = -1;\n\n /** Reset to the start-of-transfer state (x3270 does this at ft_cut.c:435). */\n reset(): void {\n this.quadrant = -1;\n }\n\n /** The current quadrant, or -1 if unset. Exposed for tests and tracing. */\n get currentQuadrant(): number {\n return this.quadrant;\n }\n\n /**\n * host -> local: decode frame data bytes into file bytes.\n *\n * `upload_convert` (ft_cut.c:134-200) -- which, per the naming trap above, is\n * the direction a user calls a DOWNLOAD (IND$FILE GET). Binary mode only; the\n * ASCII/CRLF and DBCS tails of the C function are deferred.\n *\n * The loop, following the C including its `goto retry`:\n *\n * 1. If no quadrant is established, the byte MUST be a selector\n * (ft_cut.c:148-158). Found: set it and CONSUME the byte -- the C's\n * `continue`, so a selector produces no output. Not found: conversion\n * error.\n * 2. Otherwise the byte must be in 0x40..0xf9 (ft_cut.c:161-164), or it is a\n * conversion error. Note this check happens only once a quadrant is set,\n * so a selector below 0x40 could never be rejected by it.\n * 3. Translate to ASCII and find it in ALPHAS (ft_cut.c:167-168). Not there:\n * drop the quadrant and RETRY THE SAME BYTE (`goto retry`), which will\n * then read it as a selector.\n * 4. If the quadrant does not map that index -- `quadrant != OTHER_2 && c !=\n * XLATE_NULL && !conv[quadrant].xlate[ix]` (ft_cut.c:181-182) -- drop the\n * quadrant and retry likewise. The OTHER_2 exemption exists because a\n * zero xlate entry normally means \"absent\", but in OTHER_2 zero is the\n * genuine data byte NULL.\n *\n * FINDING: the `c != XLATE_NULL` half of that condition is DEAD CODE in\n * x3270 as shipped. It can only matter when the quadrant is not OTHER_2,\n * xlate[ix] is 0, and c is 0xc1 -- but ebc2asc0[0xc1] is 'A', always\n * ALPHAS index 1, and no non-OTHER_2 quadrant has a zero at index 1\n * (0xc1, 0x41, --, 0xa0). Ported anyway, since it costs nothing and the\n * quadrants are data; the test file asserts the unsatisfiability, so we\n * would learn if that ever changed.\n * 5. Emit `xlate[ix]` (ft_cut.c:188-189).\n *\n * The retry is a loop rather than recursion, and is bounded: each retry sets\n * quadrant to -1, and the -1 branch either consumes the byte or throws, so at\n * most two passes per input byte.\n */\n hostToLocal(input: Uint8Array | readonly number[]): Uint8Array {\n const out: number[] = [];\n const bytes = input instanceof Uint8Array ? input : Uint8Array.from(input);\n\n for (const c of bytes) {\n // The C's `retry:` label (ft_cut.c:147).\n for (;;) {\n if (this.quadrant < 0) {\n // Find the quadrant (ft_cut.c:148-158).\n const q = QUADRANTS.findIndex((quad) => quad.selector === c);\n if (q < 0) {\n throw new CutConversionError(\n `CUT conversion error: 0x${c.toString(16).padStart(2, '0')} is not a quadrant selector`,\n );\n }\n this.quadrant = q;\n break; // the C's `continue`: the selector itself emits nothing\n }\n\n // Make sure it's in a valid range (ft_cut.c:160-164).\n if (c < 0x40 || c > 0xf9) {\n throw new CutConversionError(\n `CUT conversion error: 0x${c.toString(16).padStart(2, '0')} is outside 0x40..0xf9`,\n );\n }\n\n // Translate to a quadrant index (ft_cut.c:168-173).\n const ix = ALPHAS.indexOf(String.fromCharCode(EBC2ASC[c]!));\n if (ix === -1) {\n this.quadrant = -1; // try a different quadrant\n continue;\n }\n\n // See if it's mapped by that quadrant, handling NULLs specially\n // (ft_cut.c:175-186).\n const mapped = QUADRANTS[this.quadrant]!.xlate[ix]!;\n if (this.quadrant !== OTHER_2 && c !== XLATE_NULL && !mapped) {\n this.quadrant = -1; // try a different quadrant\n continue;\n }\n\n // Map it (ft_cut.c:188-189).\n out.push(mapped);\n break;\n }\n }\n\n return Uint8Array.from(out);\n }\n\n /**\n * local -> host: encode file bytes into frame data bytes.\n *\n * `download_convert` (ft_cut.c:307-384) plus `store_download`\n * (ft_cut.c:270-304) -- the direction a user calls an UPLOAD (IND$FILE PUT),\n * which the design doc names as the primary use, since VM/370 has no TCP/IP.\n * Binary mode only.\n *\n * Two paths:\n *\n * - NULL is special-cased FIRST (ft_cut.c:320-333), before store_download is\n * ever reached: if the quadrant is not OTHER_2, switch to it and emit its\n * selector; then emit XLATE_NULL. NULL is in two quadrants' xlate at index\n * 0 (OTHER_2 and quadrant 3), so leaving it to the generic search would\n * make the encoding depend on which was found first.\n * - Otherwise `store_download`: if the current quadrant maps the byte, emit\n * one byte, `asc2ebc0[alphas[ix]]` (ft_cut.c:276-283). Otherwise search\n * the OTHER quadrants -- skipping the current one, which was just tried\n * (ft_cut.c:286-300) -- and on a hit emit the selector AND the character,\n * two bytes, having updated the current quadrant.\n *\n * Output length is therefore data-dependent: 1 byte per input byte while the\n * quadrant holds, 2 when it changes. That statefulness is the whole point of\n * the encoding, and the reason the tests exercise a deliberate mid-buffer\n * quadrant change rather than only single bytes.\n *\n * WHERE WE DIVERGE FROM x3270, deliberately: when no quadrant contains the\n * byte, the C sets `quadrant = -1`, prints \"Oops\" to stderr and returns 0\n * (ft_cut.c:301-303) -- silently dropping the byte and corrupting the file.\n * We throw. This is safe to make fatal rather than lossy because the branch\n * is unreachable: all 256 byte values are covered by the four tables\n * (verified exhaustively in the test file), so reaching it would mean the\n * tables had been damaged, and a loud failure beats a corrupt upload.\n */\n localToHost(input: Uint8Array | readonly number[]): Uint8Array {\n const out: number[] = [];\n const bytes = input instanceof Uint8Array ? input : Uint8Array.from(input);\n\n for (const c of bytes) {\n // Handle nulls separately (ft_cut.c:320-333).\n if (c === 0) {\n if (this.quadrant !== OTHER_2) {\n this.quadrant = OTHER_2;\n out.push(QUADRANTS[OTHER_2]!.selector);\n }\n out.push(XLATE_NULL);\n continue;\n }\n this.storeByte(c, out);\n }\n\n return Uint8Array.from(out);\n }\n\n /**\n * One byte, local->host: `store_download` (ft_cut.c:270-304).\n *\n * Appends to `out` rather than returning a count, since the C's return value\n * exists only to advance an output pointer.\n */\n private storeByte(c: number, out: number[]): void {\n // Quadrant already defined (ft_cut.c:275-283).\n if (this.quadrant >= 0) {\n const ix = QUADRANTS[this.quadrant]!.xlate.indexOf(c);\n if (ix !== -1) {\n out.push(ASC2EBC[ALPHAS.charCodeAt(ix)]!);\n return;\n }\n }\n\n // Locate a quadrant (ft_cut.c:285-300). `oq` skips the one just tried.\n const oq = this.quadrant;\n for (let q = 0; q < NQ; q++) {\n if (q === oq) continue;\n const ix = QUADRANTS[q]!.xlate.indexOf(c);\n if (ix === -1) continue;\n this.quadrant = q;\n out.push(QUADRANTS[q]!.selector);\n out.push(ASC2EBC[ALPHAS.charCodeAt(ix)]!);\n return;\n }\n\n // Unreachable: every byte 0x00-0xff is in some quadrant. See the class\n // comment on why this throws where x3270 prints \"Oops\" and drops the byte.\n this.quadrant = -1;\n throw new CutConversionError(\n `CUT conversion error: no quadrant encodes byte 0x${c.toString(16).padStart(2, '0')}`,\n );\n }\n}\n\n/**\n * Convenience wrappers for a whole buffer converted by a fresh codec.\n *\n * Correct ONLY for a complete buffer, because they start from quadrant -1.\n * A multi-frame transfer must hold one CutCodec across all its frames, or a\n * frame boundary would silently reset the quadrant and produce a leading\n * selector the host never asked for -- or, host->local, reject a first byte\n * that is data rather than a selector.\n */\nexport function hostToLocal(input: Uint8Array | readonly number[]): Uint8Array {\n return new CutCodec().hostToLocal(input);\n}\n\n/** See hostToLocal: whole-buffer only. */\nexport function localToHost(input: Uint8Array | readonly number[]): Uint8Array {\n return new CutCodec().localToHost(input);\n}\n", "/**\n * IND$FILE CUT-mode frame layout over a `Screen`.\n *\n * CUT is the 3270 screen used as a data pipe: the host plants a structured\n * field at the last cell of a 24x80 buffer and both sides read and write data at\n * fixed offsets inside that buffer. This module owns those offsets \u2014 reading a\n * frame out of a screen, and writing a response or an upload frame in. It is\n * pure with respect to I/O: no sockets, no session, no files. The codec is\n * `ft/cut.ts`; the state machine and the CLI action are later units. See\n * docs/superpowers/specs/2026-08-18-indfile-cut-transfer-design.md.\n *\n * Every constant below is cited to x3270 4.5 `include/ft_cut_ds.h`, which is 82\n * lines and documents the whole wire format, and every behavior to\n * `Common/ft_cut.c`. Where CUT is underspecified the design doc's governing\n * principle applies: do what x3270 does, because it is the client these hosts\n * have actually been driven with for decades.\n *\n * ## THE GEOMETRY COUPLING IS LOAD-BEARING\n *\n * `O_SF` is 1919, which is the last cell of a 24x80 buffer and nothing else.\n * Stage 2a pinned the advertised geometry to 24x80 as both default and alternate\n * size, so this works today, and the design doc's \"GEOMETRY COUPLING\" section\n * warns whoever implements alternate screen sizes that these two facts are\n * coupled. Every entry point here therefore calls `requireCutGeometry` and\n * THROWS on a differently-sized screen rather than reading or writing cells that\n * mean something else. See that function for why throwing beats returning false.\n *\n * ## ONE CODEC PER TRANSFER\n *\n * `writeUploadData` takes a `CutCodec` rather than constructing one. The\n * quadrant is persistent state and a selector byte is emitted only when it\n * changes, so a per-frame codec would emit a spurious leading selector on every\n * upload frame after the first. This is finding 3 in the design doc and it is\n * why no function in this file ever calls `new CutCodec()`.\n *\n * ## SEQUENCE NUMBERS AND CHECKSUMS ARE ECHOED RAW, NEVER RE-ENCODED\n *\n * A trap that would produce a wrong byte on the wire while every decoded value\n * looked right. x3270 echoes the frame sequence back as the RAW EBCDIC byte it\n * received \u2014 `ctlr_add(O_UP_FRAME_SEQ, seq, 0)` where `seq =\n * ea_buf[O_DR_FRAME_SEQ].ec` (ft_cut.c:500, :549), and likewise\n * `ctlr_add(RO_FRAME_SEQ, ea_buf[O_DT_FRAME_SEQ].ec, 0)` (ft_cut.c:670). It\n * never runs the decoded number back through `to6`. Because `from6`/`to6` are\n * inverses over the 64 real encodings the two agree for well-formed input, but\n * `from6` maps all 192 other bytes to 0 (ft_cut.c:590-592), so a host byte\n * outside the alphabet would come back as `to6(0)` = 'a' instead of itself.\n *\n * So the parsed frames carry BOTH forms \u2014 `seq` decoded for logic and display,\n * `rawSeq` for echoing \u2014 and the write functions take `rawSeq` under that name\n * precisely so that passing the wrong one does not typecheck as a mistake a\n * reader would miss.\n */\n\nimport { AID, FA } from '../constants.js';\nimport { cp037 } from '../codepage.js';\nimport type { Screen } from '../screen.js';\nimport { checksum, to6, from6, type CutCodec } from './cut.js';\n\n// ---------------------------------------------------------------------------\n// Wire constants, all from include/ft_cut_ds.h\n// ---------------------------------------------------------------------------\n\n/**\n * Offset to the CUT structured field. `#define O_SF 1919` (ft_cut_ds.h:33).\n *\n * This is the last cell of a 24x80 buffer. Its field attribute being auto-skip\n * is the whole of the frame-detection test; see `isCutFrame`.\n */\nexport const O_SF = 1919;\n\n/** The only screen size CUT works on: 24 * 80. See `requireCutGeometry`. */\nexport const CUT_SCREEN_SIZE = 1920;\n\n/** Offset to the frame type. `#define O_FRAME_TYPE 0` (ft_cut_ds.h:36). */\nexport const O_FRAME_TYPE = 0;\n\n/**\n * Frame types, all host->us except FT_DATA which the header marks\n * \"bidirectional\" (ft_cut_ds.h:37, :45, :49, :50).\n */\nexport const FrameType = {\n /** `FT_CONTROL_CODE 0xc3` (ft_cut_ds.h:37). */\n CONTROL_CODE: 0xc3,\n /** `FT_DATA_REQUEST 0xc2` (ft_cut_ds.h:45). */\n DATA_REQUEST: 0xc2,\n /** `FT_RETRANSMIT 0x4c` (ft_cut_ds.h:49). */\n RETRANSMIT: 0x4c,\n /** `FT_DATA 0xc1` (ft_cut_ds.h:50). */\n DATA: 0xc1,\n} as const;\n\n/** Control-code frame: offset to frame sequence. ft_cut_ds.h:38. */\nexport const O_CC_FRAME_SEQ = 1;\n/** Control-code frame: offset to the two-byte status code. ft_cut_ds.h:39. */\nexport const O_CC_STATUS_CODE = 2;\n/** Control-code frame: offset of the message text. ft_cut_ds.h:44. */\nexport const O_CC_MESSAGE = 4;\n\n/**\n * How many bytes of message text a control code carries.\n *\n * NOT in the header \u2014 it is the loop bound in x3270's `cut_control_code`,\n * `for (i = 0; i < 80; i++)` over `ea_buf[O_CC_MESSAGE + i].ec`\n * (ft_cut.c:460-469). One display line's worth, which is what the host paints.\n */\nexport const CC_MESSAGE_LENGTH = 80;\n\n/** Status codes inside a control-code frame (ft_cut_ds.h:40-43). */\nexport const StatusCode = {\n /** `SC_HOST_ACK 0x8181` \u2014 ack of the IND$FILE command (ft_cut_ds.h:40). */\n HOST_ACK: 0x8181,\n /** `SC_XFER_COMPLETE 0x8189` \u2014 file transfer complete (ft_cut_ds.h:41). */\n XFER_COMPLETE: 0x8189,\n /** `SC_ABORT_FILE 0x8194` \u2014 abort, file error (ft_cut_ds.h:42). */\n ABORT_FILE: 0x8194,\n /** `SC_ABORT_XMIT 0x8198` \u2014 abort, transmission error (ft_cut_ds.h:43). */\n ABORT_XMIT: 0x8198,\n} as const;\n\nconst KNOWN_STATUS_CODES: readonly number[] = [\n StatusCode.HOST_ACK,\n StatusCode.XFER_COMPLETE,\n StatusCode.ABORT_FILE,\n StatusCode.ABORT_XMIT,\n];\n\n/**\n * Whether a status code is one of the four the header names.\n *\n * `parseFrame` deliberately does NOT reject an unknown one, even though x3270\n * aborts with `ftCutUnknownControl` (ft_cut.c:487-490): aborting is a state\n * machine's decision and it needs the code to report, so the frame carries the\n * raw number and the caller uses this predicate. Contrast an unknown FRAME TYPE,\n * which is rejected here, because there is then no frame to hand anybody.\n */\nexport function isKnownStatusCode(status: number): boolean {\n return KNOWN_STATUS_CODES.includes(status);\n}\n\n/**\n * Data-request frame: offset to the start field. ft_cut_ds.h:46.\n *\n * The field attribute governing the upload data area. x3270 rewrites it\n * non-display after filling the area; see `writeUploadData`.\n */\nexport const O_DR_SF = 1;\n/**\n * Data-request frame: offset to the data code. ft_cut_ds.h:47.\n *\n * Defined by the header and read by nothing: `grep -rn O_DR_DATA_CODE` over the\n * whole x3270 4.5 tree returns only the `#define`. Exported for completeness,\n * and not consulted, exactly as x3270 does not consult it.\n */\nexport const O_DR_DATA_CODE = 2;\n/** Data-request frame: offset to frame sequence. ft_cut_ds.h:48. */\nexport const O_DR_FRAME_SEQ = 3;\n\n/** Data frame: offset to frame sequence. ft_cut_ds.h:51. */\nexport const O_DT_FRAME_SEQ = 1;\n/**\n * Data frame: offset to checksum. ft_cut_ds.h:52.\n *\n * x3270 NEVER READS THIS \u2014 `grep -rn O_DT_CSUM` over the tree returns only the\n * `#define`, a deliberate omission per the design doc. We read it because the\n * live TK5 capture proved the host populates it (6-bit 62, and our `checksum()`\n * over the same data computes 62), so a mismatch is a real diagnostic. The\n * design decision is \"trace it, never abort on it\": the protocol offers no\n * client-initiated retransmit to recover with, and MECAFF on VM/370 ignores the\n * field entirely on receive, so a mismatch must not fail a transfer x3270 would\n * have completed. `parseFrame` therefore reports the declared value and leaves\n * the comparison to the caller.\n */\nexport const O_DT_CSUM = 2;\n/**\n * Data frame: offset to the length. ft_cut_ds.h:53.\n *\n * TWO bytes forming a 12-bit value, `from6(ea_buf[O_DT_LEN].ec) << 6 |\n * from6(ea_buf[O_DT_LEN + 1].ec)` (ft_cut.c:615-616).\n */\nexport const O_DT_LEN = 3;\n/** Data frame: offset to the data. ft_cut_ds.h:54. */\nexport const O_DT_DATA = 5;\n\n/** Offset to the response area. `#define O_RESPONSE (O_SF-5)` = 1914, ft_cut_ds.h:57. */\nexport const O_RESPONSE = O_SF - 5;\n/** Response frame type. `(O_RESPONSE+1)` = 1915, ft_cut_ds.h:58. */\nexport const RO_FRAME_TYPE = O_RESPONSE + 1;\n/** Response frame sequence. `(O_RESPONSE+2)` = 1916, ft_cut_ds.h:61. */\nexport const RO_FRAME_SEQ = O_RESPONSE + 2;\n/**\n * Response reason code. `(O_RESPONSE+3)` = 1917, ft_cut_ds.h:62.\n *\n * TWO bytes, 1917 and 1918: x3270 writes `HIGH8(reason)` then `LOW8(reason)`\n * (ft_cut.c:671-672). 1918 is the last cell before `O_SF`, so the response area\n * exactly fills the gap the host leaves for it.\n */\nexport const RO_REASON_CODE = O_RESPONSE + 3;\n\n/** Response frame types (ft_cut_ds.h:59-60). */\nexport const ResponseFrameType = {\n /**\n * `RFT_RETRANSMIT 0x4c` (ft_cut_ds.h:59).\n *\n * Like `O_DR_DATA_CODE`, defined by the header and written by nothing in\n * x3270 \u2014 its only abort path writes `RFT_CONTROL_CODE`. Exported because the\n * header names it, not because we have a use for it yet.\n */\n RETRANSMIT: 0x4c,\n /** `RFT_CONTROL_CODE 0xc3` (ft_cut_ds.h:60) \u2014 what `cut_abort` writes. */\n CONTROL_CODE: 0xc3,\n} as const;\n\n/**\n * Upload area: offset to the data code. ft_cut_ds.h:76.\n *\n * The third of the header's write-only constants; nothing in x3270 reads or\n * writes it. Exported for completeness.\n */\nexport const O_UP_DATA_CODE = 2;\n/** Upload area: offset to frame sequence. ft_cut_ds.h:77. */\nexport const O_UP_FRAME_SEQ = 3;\n/** Upload area: offset to checksum. ft_cut_ds.h:78. */\nexport const O_UP_CSUM = 4;\n/** Upload area: offset to the two-byte length. ft_cut_ds.h:79. */\nexport const O_UP_LEN = 5;\n/** Upload area: offset to the start of data. ft_cut_ds.h:80. */\nexport const O_UP_DATA = 7;\n/**\n * Maximum bytes of ENCODED data in one upload frame.\n * `#define O_UP_MAX (O_SF-O_UP_DATA)` = 1912, ft_cut_ds.h:81.\n *\n * NOTE THE OVERLAP, which is x3270's and which we inherit deliberately: 1912\n * bytes at offset 7 reach address 1918, so a maximal upload frame writes over\n * `O_RESPONSE` (1914) and the whole response area. That is fine, and is what\n * `cut_data_request` does (ft_cut.c:515-522): a data-request screen's own field\n * attribute is at `O_DR_SF`, and the host is asking for data, not reading a\n * response. `O_SF` itself at 1919 is never touched, so frame detection survives.\n */\nexport const O_UP_MAX = O_SF - O_UP_DATA;\n\n/**\n * Maximum bytes of data in one DOWNLOAD frame: `O_RESPONSE - O_DT_DATA` = 1909.\n *\n * Not a header constant; it is the size of x3270's conversion buffer,\n * `static unsigned char cvbuf[O_RESPONSE - O_DT_DATA]` (ft_cut.c:602), and the\n * bound its oversize check uses (ft_cut.c:617-620). The live TK5 capture sends\n * exactly `data[1909]` in a full frame, so the host agrees with the derivation.\n */\nexport const MAX_DOWNLOAD_DATA = O_RESPONSE - O_DT_DATA;\n\n/** First byte of the two-byte end-of-file sentinel. ft_cut_ds.h:65. */\nexport const EOF_DATA1 = 0x5c;\n/** Second byte of the two-byte end-of-file sentinel. ft_cut_ds.h:66. */\nexport const EOF_DATA2 = 0xa9;\n\n/**\n * The five acknowledgement AIDs (ft_cut_ds.h:69-73), resolved against our own\n * `AID` table rather than restated as numbers.\n *\n * The header writes these as `AID_ENTER`, `AID_PF1`, `AID_CLEAR`, `AID_PA2`,\n * `AID_PF2`; the numeric values live in our constants.ts, verified there against\n * GA23-0059 Table 3-4. RESYNC differing between VM and TSO is the dialect seam\n * the design doc anticipated, and is why these are two separate entries rather\n * than one \"resync\".\n */\nexport const AckAid = {\n /** `ACK_OK = AID_ENTER` (ft_cut_ds.h:69). */\n OK: AID.ENTER,\n /** `ACK_RETRANSMIT = AID_PF1` (ft_cut_ds.h:70). */\n RETRANSMIT: AID.PF1,\n /** `ACK_RESYNC_VM = AID_CLEAR` (ft_cut_ds.h:71). */\n RESYNC_VM: AID.CLEAR,\n /** `ACK_RESYNC_TSO = AID_PA2` (ft_cut_ds.h:72). */\n RESYNC_TSO: AID.PA2,\n /** `ACK_ABORT = AID_PF2` (ft_cut_ds.h:73). */\n ABORT: AID.PF2,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * A frame we refuse to parse, or a screen we refuse to touch.\n *\n * `abortStatus` carries the status code x3270 aborts with, so the state machine\n * can send the response the host expects without re-deriving which fault it was.\n * It is absent for the geometry error, which is a local configuration bug rather\n * than anything the host did wrong.\n */\nexport class CutFrameError extends Error {\n readonly abortStatus?: number;\n\n constructor(message: string, abortStatus?: number) {\n super(message);\n this.name = 'CutFrameError';\n if (abortStatus !== undefined) this.abortStatus = abortStatus;\n }\n}\n\n/**\n * Refuse to operate on anything but a 24x80 buffer.\n *\n * WHY THIS THROWS instead of making `isCutFrame` return false. A silent false on\n * a 43x80 screen would mean \"the transfer never progresses and nothing says\n * why\", which is the worst of the three outcomes; corrupting memory at offsets\n * that mean something else on a bigger buffer is worse still. Throwing turns the\n * design doc's coupling warning into a failure a developer cannot miss. It is\n * safe to be loud because the caller is a transfer, and the established error\n * rule is that a transfer fault ends the transfer without killing the session.\n */\nfunction requireCutGeometry(screen: Screen): void {\n if (screen.size !== CUT_SCREEN_SIZE) {\n throw new CutFrameError(\n `CUT file transfer requires a ${CUT_SCREEN_SIZE}-cell (24x80) screen; ` +\n `this screen is ${screen.rows}x${screen.cols} = ${screen.size} cells. ` +\n `O_SF is ${O_SF}, the last cell of a 24x80 buffer, and the frame offsets ` +\n `are meaningless on any other geometry.`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Detection\n// ---------------------------------------------------------------------------\n\n/**\n * Does this screen hold a CUT frame?\n *\n * x3270's whole test, `ft_cut_data` at ft_cut.c:394:\n *\n * if (ea_buf[O_SF].fa && FA_IS_SKIP(ea_buf[O_SF].fa)) {\n *\n * `.fa` non-zero means \"there is a field attribute at this cell\" (x3270's\n * `ctlr_add_fa` ORs in `FA_PRINTABLE` so the stored byte is never zero,\n * ctlr.c:2812); ours is `attributeAt(addr) !== null`. `FA_IS_SKIP(c)` is\n * `((c) & FA_PROTECT) && ((c) & FA_NUMERIC)` (3270ds.h:207), i.e. protected AND\n * numeric \u2014 the same computation `Screen.makeField` does for `Field.autoSkip`.\n *\n * `attributeAt` rather than `fieldAt`: `fieldAt(O_SF)` scans backwards for the\n * attribute GOVERNING 1919, which on most screens is some other field's, so its\n * `autoSkip` would answer a different question. It is also O(size) where this is\n * O(1), and this runs on every screen update while a transfer is active.\n *\n * Masks, never equality: the live TK5 frame plants 0x7c here (protect | numeric\n * | zero-intensity | printable-ish bits), x3270 would store 0xfc for the same\n * field, and both are auto-skip. See the `Field.attr` comment in screen.ts.\n */\nexport function isCutFrame(screen: Screen): boolean {\n requireCutGeometry(screen);\n const attr = screen.attributeAt(O_SF);\n if (attr === null) return false;\n return (attr & FA.PROTECT) !== 0 && (attr & FA.NUMERIC) !== 0;\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\n/** A control code from the host: `cut_control_code`, ft_cut.c:419-492. */\nexport interface CutControlCodeFrame {\n kind: 'controlCode';\n /** Frame sequence, decoded through `from6`. */\n seq: number;\n /** The raw byte at `O_CC_FRAME_SEQ`; echo THIS, never `to6(seq)`. */\n rawSeq: number;\n /** The 16-bit status code; compare against `StatusCode`. */\n status: number;\n /** Host message text, for humans. Empty when the host sent only blanks. */\n message: string;\n}\n\n/** A request for the next upload frame: `cut_data_request`, ft_cut.c:497-568. */\nexport interface CutDataRequestFrame {\n kind: 'dataRequest';\n /** Frame sequence, decoded through `from6`. */\n seq: number;\n /** The raw byte at `O_DR_FRAME_SEQ`; this is what `writeUploadData` echoes. */\n rawSeq: number;\n}\n\n/**\n * \"Re-send the previous frame\": `cut_retransmit`, ft_cut.c:573-578.\n *\n * Carries nothing \u2014 the frame type is the whole message. x3270 answers this by\n * giving up (`cut_abort(get_message(\"ftCutRetransmit\"), SC_ABORT_XMIT)`, and its\n * own comment calls that \"(Improperly) process a retransmit\"). We surface it as\n * a frame kind instead, because the design doc requires upload to RETAIN the\n * last encoded block and actually re-send it. That divergence lives in the state\n * machine; this module's job is only to say a retransmit arrived.\n */\nexport interface CutRetransmitFrame {\n kind: 'retransmit';\n}\n\n/** Data from the host: `cut_data`, ft_cut.c:599-647. */\nexport interface CutDataFrame {\n kind: 'data';\n /** Frame sequence, decoded through `from6`. */\n seq: number;\n /** The raw byte at `O_DT_FRAME_SEQ`; `writeResponse` echoes this on abort. */\n rawSeq: number;\n /**\n * The 6-bit checksum the host declared at `O_DT_CSUM`, already decoded.\n * Compare with `checksum(frame.data)` from ft/cut.ts. See `O_DT_CSUM` on why\n * a mismatch is a trace note and not an abort.\n */\n declaredChecksum: number;\n /** Declared payload length, the 12-bit value from the two `O_DT_LEN` bytes. */\n length: number;\n /**\n * The RAW 6-bit-encoded payload, NOT decoded.\n *\n * Decoding belongs to the caller because it owns the `CutCodec` whose quadrant\n * persists across frames \u2014 see the module comment. A copy, not a view, so a\n * later screen update cannot change a frame already parsed.\n */\n data: Uint8Array;\n}\n\nexport type CutFrame =\n | CutControlCodeFrame\n | CutDataRequestFrame\n | CutRetransmitFrame\n | CutDataFrame;\n\n/**\n * Read the frame at the start of the buffer.\n *\n * The dispatch is x3270's `ft_cut_data` switch (ft_cut.c:395-412). Callers\n * should gate on `isCutFrame` first; this function does not, because the two\n * questions are separable and a test wants to parse a synthetic frame without\n * also constructing the detection attribute.\n *\n * THROWS `CutFrameError` for the two faults x3270 aborts on, carrying the same\n * status code it uses:\n *\n * - an unknown frame type \u2014 `ftCutUnknownFrame` / `SC_ABORT_XMIT`,\n * ft_cut.c:408-411. Not a silent default: a frame type we do not recognize\n * means we have lost sync with the host, and guessing would write a wrong\n * file.\n * - a declared length past `MAX_DOWNLOAD_DATA` \u2014 `ftCutOversize` /\n * `SC_ABORT_XMIT`, ft_cut.c:617-620.\n */\nexport function parseFrame(screen: Screen): CutFrame {\n requireCutGeometry(screen);\n const at = (addr: number): number => screen.cellAt(addr).ebcdic;\n const frameType = at(O_FRAME_TYPE);\n\n switch (frameType) {\n case FrameType.CONTROL_CODE: {\n // `code = (ea_buf[O_CC_STATUS_CODE].ec << 8) |\n // ea_buf[O_CC_STATUS_CODE + 1].ec;` (ft_cut.c:428-429).\n const status = (at(O_CC_STATUS_CODE) << 8) | at(O_CC_STATUS_CODE + 1);\n const rawSeq = at(O_CC_FRAME_SEQ);\n return {\n kind: 'controlCode',\n seq: from6(rawSeq),\n rawSeq,\n status,\n message: readMessage(screen),\n };\n }\n\n case FrameType.DATA_REQUEST: {\n const rawSeq = at(O_DR_FRAME_SEQ);\n return { kind: 'dataRequest', seq: from6(rawSeq), rawSeq };\n }\n\n case FrameType.RETRANSMIT:\n return { kind: 'retransmit' };\n\n case FrameType.DATA: {\n // 12 bits across two 6-bit characters (ft_cut.c:615-616).\n const length = (from6(at(O_DT_LEN)) << 6) | from6(at(O_DT_LEN + 1));\n if (length > MAX_DOWNLOAD_DATA) {\n throw new CutFrameError(\n `CUT data frame declares ${length} bytes, more than the ` +\n `${MAX_DOWNLOAD_DATA}-byte maximum (O_RESPONSE - O_DT_DATA)`,\n StatusCode.ABORT_XMIT,\n );\n }\n const data = new Uint8Array(length);\n for (let i = 0; i < length; i++) data[i] = at(O_DT_DATA + i);\n const rawSeq = at(O_DT_FRAME_SEQ);\n return {\n kind: 'data',\n seq: from6(rawSeq),\n rawSeq,\n declaredChecksum: from6(at(O_DT_CSUM)),\n length,\n data,\n };\n }\n\n default:\n throw new CutFrameError(\n `unknown CUT frame type 0x${frameType.toString(16).padStart(2, '0')}`,\n StatusCode.ABORT_XMIT,\n );\n }\n}\n\n/**\n * The 80 bytes of host message text in a control-code frame.\n *\n * x3270's tail of `cut_control_code` (ft_cut.c:457-482): translate 80 bytes from\n * `O_CC_MESSAGE`, then strip trailing spaces, then one trailing '$', then\n * trailing spaces again. (The '$' is IND$FILE's own message terminator, and the\n * second strip exists because removing it can expose more padding.)\n *\n * cp037 IS right here, unlike in the codec. This is host text meant for a human,\n * so a faithful character-set mapping is exactly what is wanted; the codec's\n * private `ebc2asc0` tables exist only to locate characters in TABLE6/ALPHAS and\n * flatten the whole EBCDIC control range to space, which would mangle a message.\n *\n * One divergence, and it is about our screen rather than the protocol: untouched\n * cells hold 0x00, which cp037 maps to U+0000, and x3270 never sees those\n * because a real host paints the whole line. NULs are treated as blanks \u2014 the\n * same convention `Screen.rowText` uses \u2014 so a synthetic or partly-written frame\n * yields a trimmed message instead of a string full of NULs.\n */\nfunction readMessage(screen: Screen): string {\n // O_CC_MESSAGE + 80 = 84, nowhere near O_SF, so no bound check is needed.\n let text = '';\n for (let i = 0; i < CC_MESSAGE_LENGTH; i++) {\n const b = screen.cellAt(O_CC_MESSAGE + i).ebcdic;\n text += b === 0x00 ? ' ' : cp037.toUnicode(b);\n }\n let out = text.replace(/ +$/, '');\n if (out.endsWith('$')) out = out.slice(0, -1).replace(/ +$/, '');\n return out;\n}\n\n/**\n * Is this payload the two-byte end-of-file sentinel?\n *\n * `if (raw_length == 2 && cvbuf[0] == EOF_DATA1 && cvbuf[1] == EOF_DATA2)`\n * (ft_cut.c:625). The length check is part of the test, not a precondition: a\n * longer buffer that merely STARTS with 5c a9 is data, and treating it as EOF\n * would truncate the file. `5c` is also quadrant 2's selector and `a9` a\n * perfectly ordinary encoded character, so that is a reachable payload, not a\n * theoretical one.\n */\nexport function isEofData(data: Uint8Array | readonly number[]): boolean {\n return data.length === 2 && data[0] === EOF_DATA1 && data[1] === EOF_DATA2;\n}\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\nexport interface ResponseOptions {\n /** `ResponseFrameType.CONTROL_CODE` for an abort; see that enum. */\n frameType: number;\n /**\n * The RAW sequence byte from the frame being responded to \u2014 `frame.rawSeq`.\n * x3270 echoes `ea_buf[O_DT_FRAME_SEQ].ec` verbatim (ft_cut.c:670). See the\n * module comment on why this is not the decoded number.\n */\n rawSeq: number;\n /**\n * 16-bit reason, written high byte then low (ft_cut.c:671-672). Omit to leave\n * both reason bytes untouched, for a response type that carries none.\n */\n reason?: number;\n}\n\n/**\n * Write the response area, us -> host. x3270's `cut_abort`, ft_cut.c:662-678.\n *\n * Writes only 1915-1918. `O_RESPONSE` itself (1914) is NOT written: in the live\n * TK5 capture the host plants a field attribute there (`SBA(1914) SF(0xc1)`),\n * and 0xc1 already carries `FA.MODIFY`, so the field is pre-armed for Read\n * Modified and we neither need nor want to disturb it. That is also why this\n * function sets no MDT of its own \u2014 x3270 does not either.\n *\n * Sending the AID is the caller's job: x3270 follows this with\n * `run_action(AnPF, IA_FT, \"2\", NULL)`, i.e. `AckAid.ABORT`. Keeping the AID out\n * of here is what makes this module pure with respect to I/O.\n */\nexport function writeResponse(screen: Screen, opts: ResponseOptions): void {\n requireCutGeometry(screen);\n screen.setChar(RO_FRAME_TYPE, opts.frameType);\n screen.setChar(RO_FRAME_SEQ, opts.rawSeq);\n if (opts.reason !== undefined) {\n // HIGH8/LOW8 (ft_cut.c:671-672).\n screen.setChar(RO_REASON_CODE, (opts.reason >> 8) & 0xff);\n screen.setChar(RO_REASON_CODE + 1, opts.reason & 0xff);\n }\n}\n\nexport interface UploadDataOptions {\n /**\n * The RAW sequence byte from the data request \u2014 `frame.rawSeq`. Echoed\n * verbatim (ft_cut.c:500, :549).\n */\n rawSeq: number;\n /** Source file bytes still to send. Only a prefix will be consumed. */\n data: Uint8Array | readonly number[];\n /**\n * The transfer's ONE codec, held across every frame. See the module comment:\n * constructing one here would emit a spurious selector at the start of every\n * frame after the first.\n */\n codec: CutCodec;\n}\n\nexport interface UploadDataResult {\n /**\n * How many SOURCE bytes were consumed. The caller cannot compute this \u2014 see\n * `writeUploadData`.\n */\n consumed: number;\n /** How many ENCODED bytes were written at `O_UP_DATA`, i.e. the frame length. */\n encodedLength: number;\n}\n\n/**\n * Fill the upload data area, us -> host. x3270's `cut_data_request`,\n * ft_cut.c:513-556.\n *\n * WHY THIS RETURNS A CONSUMED COUNT. Encoding expands: a byte the current\n * quadrant maps costs one output byte, a byte in another quadrant costs two,\n * because the selector has to be emitted too (`store_download`,\n * ft_cut.c:277-299). The frame holds at most `O_UP_MAX` ENCODED bytes, so how\n * many SOURCE bytes fit depends on the data and on the codec's quadrant at the\n * moment the frame starts. The caller therefore cannot know it in advance, and\n * this function decides and reports.\n *\n * WHERE WE DIVERGE FROM x3270, and why it is safe. x3270 fills to exactly\n * `O_UP_MAX` by pulling one ENCODED byte at a time from `xlate_getc`, which\n * converts a source byte and buffers the leftover in `xlate_buf`\n * (ft_cut.c:696-702, :751-760) \u2014 so a selector can land in one frame and its\n * character in the next. We stop while fewer than 2 bytes of room remain, which\n * never splits a byte's encoding and so needs no cross-frame buffer. It costs at\n * most ONE unused byte per frame out of 1912, and it is safe because the host\n * concatenates frame payloads: where a frame boundary falls inside the encoded\n * stream has no protocol meaning. The alternative \u2014 encode, discover it does not\n * fit, put it back \u2014 is not available, since a `CutCodec`'s quadrant has already\n * moved by then and there is no rollback.\n *\n * Callers must handle the `consumed === 0` case. It means the frame has no room\n * (only possible with an empty `data`), NOT that the file has ended; end of file\n * is `writeUploadEof`.\n */\nexport function writeUploadData(screen: Screen, opts: UploadDataOptions): UploadDataResult {\n requireCutGeometry(screen);\n const src = opts.data instanceof Uint8Array ? opts.data : Uint8Array.from(opts.data);\n\n const encoded: number[] = [];\n let consumed = 0;\n // One source byte at a time: the maximum expansion is 2 bytes, so with 2 bytes\n // of room the next byte is guaranteed to fit and no speculative encoding \u2014 and\n // hence no impossible rollback \u2014 is ever needed.\n //\n // A `localToHost` call per byte, which allocates, and that is a deliberate\n // trade: the alternative is to encode a whole slice and discard the tail, but\n // a `CutCodec`'s quadrant would already have moved past the discard point and\n // there is no way to wind it back. Correctness over allocations; a full frame\n // is 1912 calls and a transfer is bounded by the file size.\n const one = new Uint8Array(1);\n while (consumed < src.length && O_UP_MAX - encoded.length >= 2) {\n one[0] = src[consumed]!;\n for (const b of opts.codec.localToHost(one)) encoded.push(b);\n consumed++;\n }\n // The loop invariant, asserted rather than trusted: overrunning here would\n // scribble past O_SF and destroy the frame-detection attribute, which would\n // present as \"the transfer silently stops\" rather than as an encoding bug.\n if (encoded.length > O_UP_MAX) {\n throw new CutFrameError(\n `internal error: encoded ${encoded.length} bytes for an upload frame, ` +\n `over the ${O_UP_MAX}-byte maximum`,\n );\n }\n\n writeUploadFrame(screen, opts.rawSeq, encoded);\n return { consumed, encodedLength: encoded.length };\n}\n\n/**\n * Write an end-of-file upload frame: the two-byte sentinel as the data.\n *\n * ft_cut.c:541-546:\n *\n * if (!count && cut_eof) {\n * ctlr_add(O_UP_DATA, EOF_DATA1, 0);\n * ctlr_add(O_UP_DATA+1, EOF_DATA2, 0);\n * count = 2;\n * }\n *\n * A separate function rather than a flag on `writeUploadData`, because these two\n * bytes go on the wire RAW and must not pass through the codec: `0x5c` is\n * quadrant 2's selector, so encoding it would emit something else entirely and\n * would additionally move the quadrant. The checksum and length are computed\n * over the sentinel exactly as over data, since x3270 falls through to the same\n * code.\n */\nexport function writeUploadEof(screen: Screen, rawSeq: number): void {\n requireCutGeometry(screen);\n writeUploadFrame(screen, rawSeq, [EOF_DATA1, EOF_DATA2]);\n}\n\n/**\n * The shared tail of both upload writers: data, sequence, checksum, length, and\n * the display hack. ft_cut.c:520, :549-561.\n */\nfunction writeUploadFrame(screen: Screen, rawSeq: number, encoded: readonly number[]): void {\n for (let i = 0; i < encoded.length; i++) {\n screen.setChar(O_UP_DATA + i, encoded[i]!);\n }\n\n // `ctlr_add(O_UP_FRAME_SEQ, seq, 0)` -- the raw byte (ft_cut.c:549).\n screen.setChar(O_UP_FRAME_SEQ, rawSeq);\n\n // `ctlr_add(O_UP_CSUM, asc2ebc0[(int)table6[cs & 0x3f]], 0)` (ft_cut.c:554),\n // over the ENCODED bytes as they sit in the buffer (ft_cut.c:551-553). The\n // 6-bit mask lives in ft/cut.ts's checksum/to6.\n screen.setChar(O_UP_CSUM, to6(checksum(encoded)));\n\n // The length is the ENCODED byte count, as two 6-bit characters, high then\n // low (ft_cut.c:555-556). O_UP_MAX is 1912, which needs 11 bits, so 12 bits\n // is always enough.\n const count = encoded.length;\n screen.setChar(O_UP_LEN, to6((count >> 6) & 0x3f));\n screen.setChar(O_UP_LEN + 1, to6(count & 0x3f));\n\n // \"XXX: Change the data field attribute so it doesn't display.\"\n // (ft_cut.c:558-561) -- purely cosmetic, and it preserves MDT, which matters:\n //\n // attr = ea_buf[O_DR_SF].fa;\n // attr = (attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL;\n // ctlr_add_fa(O_DR_SF, attr, 0);\n //\n // Ported because a screenful of encoded bytes flashing past is a real\n // annoyance, and because it is one line. x3270 does this unconditionally,\n // reading `.fa` as 0 when there is no field there and thereby CREATING one;\n // we skip instead, since inventing a field attribute the host did not send\n // would change the buffer's field structure behind the host's back.\n const attr = screen.attributeAt(O_DR_SF);\n if (attr !== null) {\n screen.setFieldAttribute(O_DR_SF, (attr & ~FA.INTENSITY) | FA.INT_ZERO_NSEL);\n }\n}\n", "import { CUT_SCREEN_SIZE, isCutFrame } from './frames.js';\nimport type { Screen } from '../screen.js';\n\n/**\n * Does this screen LOOK like a CUT frame? Never throws, whatever the geometry.\n *\n * ## WHY THIS IS NOT JUST `isCutFrame`\n *\n * `isCutFrame` calls `requireCutGeometry`, which THROWS `CutFrameError` on any screen\n * that is not 24x80 (`frames.ts:312`). That is right for STEPPING a transfer: `O_SF` is\n * 1919, the last cell of a 24x80 buffer and of nothing else, so the frame offsets are\n * meaningless on another geometry and computing them anyway would read fields that mean\n * something else. The throw turns a silent corruption into a failure a developer cannot\n * miss.\n *\n * But it makes `isCutFrame` unable to answer the question a protocol DECIDER has to ask.\n * **The host chooses the protocol, not the client** \u2014 x3270's `ft_running` merely\n * REPORTS which one arrived (`ft.c:556`) and it has no selection logic at all \u2014 so we do\n * not learn whether the geometry is legal until the host has already answered. A decider\n * must therefore be askable at 43x80 and get `false` rather than an exception.\n *\n * Hence: **new function for DECIDING, `isCutFrame` unchanged for STEPPING.** Once CUT has\n * won, the 24x80 demand is real again and the throw is how existing code refuses to\n * compute offsets it cannot.\n *\n * ## WHY IT DELEGATES RATHER THAN RE-IMPLEMENTING\n *\n * The geometry check runs FIRST and returns `false`, so `isCutFrame` is only ever reached\n * on a screen it accepts \u2014 which means the attribute test itself is not duplicated. Two\n * copies of \"what is a CUT frame\" would drift, and the reasoning in `isCutFrame` is\n * load-bearing and not obvious: it masks rather than compares, because a live TK5 frame\n * plants 0x7c at `O_SF` where x3270 would store 0xfc for the same field, and both are\n * auto-skip.\n */\nexport function looksLikeCutFrame(screen: Screen): boolean {\n if (screen.size !== CUT_SCREEN_SIZE) return false;\n return isCutFrame(screen);\n}\n", "/**\n * IND$FILE CUT-mode transfer state machine: frames in, intents out.\n *\n * This is the third of the four units the design doc lays out\n * (docs/superpowers/specs/2026-08-18-indfile-cut-transfer-design.md): the codec\n * is `ft/cut.ts`, the frame layout is `ft/frames.ts`, and the `Transfer()` CLI\n * action that drives a `Session` is a later unit. This module does NO I/O. It\n * never touches a socket, never opens a file, and does not import `Session`. It\n * is handed a `Screen` that already holds a CUT frame, it may write a response\n * or an upload frame INTO that screen, and it returns the AID to press and\n * whether the transfer has ended. That is what makes a whole scripted transfer\n * testable against synthetic screens \u2014 see packages/core/test/ft/transfer.test.ts.\n *\n * Ported from x3270 4.5 `Common/ft_cut.c`: `ft_cut_data` (:391-414) is the\n * dispatch, and `cut_control_code` (:419-492), `cut_data_request` (:497-568),\n * `cut_retransmit` (:573-578), `cut_data` (:599-647), `cut_ack` (:652-657) and\n * `cut_abort` (:662-678) are the five handlers. Every branch below cites the\n * line it came from, and every deliberate divergence says so.\n *\n * ## FOUR CONSTRAINTS, EACH OF WHICH HAS ALREADY COST THIS PROJECT SOMETHING\n *\n * **1. ONE `CutCodec` FOR THE WHOLE TRANSFER.** Finding 3 in the design doc.\n * The quadrant is persistent state and a selector byte is emitted only when it\n * changes, so a per-frame codec would put a spurious leading selector on every\n * upload frame after the first, and would reject a download frame that\n * legitimately starts with data rather than a selector. This class constructs\n * exactly one codec, in its constructor, and resets it only where x3270 does\n * (on `SC_HOST_ACK`, ft_cut.c:435).\n *\n * **2. A RETRANSMIT RE-SENDS THE PREVIOUS BLOCK BYTE-IDENTICALLY,** and the way\n * that is achieved is the central design decision here. See `RetainedBlock`.\n *\n * **3. A MAXIMAL UPLOAD FRAME OVERWRITES THE RESPONSE AREA.** `O_UP_DATA` (7) +\n * `O_UP_MAX` (1912) = 1919, so the data runs to 1918 and the response area\n * begins at 1914. A response and a maximal upload frame therefore cannot\n * coexist in one screen, and this machine never writes both: the abort path and\n * the upload path are mutually exclusive branches of a single `step`. `O_SF`\n * itself is never written by either, so frame detection survives regardless.\n *\n * **4. SEQUENCE NUMBERS ARE ECHOED RAW.** `frames.ts` exposes both `seq`\n * (through `from6`) and `rawSeq` (the literal byte) because `to6(from6(b))` is\n * not the identity for the 192 bytes outside the alphabet. Everything written\n * back to the host here uses `rawSeq`, or reads the byte straight out of the\n * screen as x3270 does.\n */\n\nimport { FA } from '../constants.js';\nimport type { Screen } from '../screen.js';\nimport { CutCodec, CutConversionError, checksum } from './cut.js';\nimport {\n AckAid,\n CutFrameError,\n O_DR_SF,\n O_DT_FRAME_SEQ,\n O_UP_DATA,\n O_UP_FRAME_SEQ,\n ResponseFrameType,\n StatusCode,\n isEofData,\n isKnownStatusCode,\n parseFrame,\n writeResponse,\n writeUploadData,\n writeUploadEof,\n type CutFrame,\n} from './frames.js';\n\n/**\n * Which way the file is going, named from the LOCAL side \u2014 the opposite of\n * x3270's internal vocabulary, which names both directions from the host's.\n *\n * `receive` is host->local, `IND$FILE GET`, and uses `hostToLocal`.\n * `send` is local->host, `IND$FILE PUT`, and uses `localToHost`. The design doc\n * calls this out as naming trap 1 and it is why no identifier in this module is\n * called \"upload\" or \"download\" without an offset constant's name attached: the\n * `O_UP_*` offsets are x3270's, and those really are the local->host ones.\n */\nexport type TransferDirection = 'send' | 'receive';\n\nexport interface TransferOptions {\n direction: TransferDirection;\n /**\n * For `send`, the bytes to put on the host. Required, and a `send` without it\n * throws from the constructor rather than transferring an empty file.\n * For `receive`, must be omitted: passing data to a download is a caller\n * mistake worth failing loudly on.\n */\n data?: Uint8Array | readonly number[];\n}\n\n/**\n * How a transfer ended.\n *\n * `status` on failure is the `StatusCode` involved \u2014 the one the host sent in\n * its control code, or the one we put in the response area when we aborted. It\n * is what a trace wants and what a test can assert on without matching prose.\n */\nexport type TransferResult =\n | { ok: true; data?: Uint8Array }\n | { ok: false; error: string; status?: number };\n\nexport interface TransferStep {\n /**\n * The AID to press, if any. Present on every step until the transfer has\n * ended; absent only when `step` is called again after completion.\n *\n * Note that a HOST-initiated abort is acknowledged with Enter, not PF2:\n * `cut_control_code` calls `cut_ack()` for `SC_ABORT_FILE`/`SC_ABORT_XMIT`\n * (ft_cut.c:452) before completing the transfer as failed. PF2\n * (`AckAid.ABORT`) is only for an abort WE initiate.\n */\n ack?: number;\n /** Present exactly once, on the step that ends the transfer. */\n done?: TransferResult;\n}\n\n/**\n * The previous local->host frame, retained verbatim so a `FT_RETRANSMIT` can\n * re-send it byte for byte.\n *\n * ## WHY THE ENCODED BYTES AND NOT THE SOURCE RANGE\n *\n * This is the design decision the task singles out, and re-encoding is not an\n * option. The codec is stateful: `localToHost` emits a selector only when the\n * quadrant changes, so encoding the same source bytes again \u2014 with the quadrant\n * now sitting wherever the frame LEFT it rather than where the frame STARTED \u2014\n * produces a different byte string. Concretely, a frame whose first byte needed\n * a selector would not emit one the second time, and the host would decode the\n * first character against the wrong quadrant. That is a silent file corruption,\n * not a visible failure.\n *\n * The alternatives were:\n *\n * - **Snapshot and restore the codec's quadrant.** `CutCodec` deliberately\n * exposes only `currentQuadrant` (read) and `reset()`, because a settable\n * quadrant is a footgun; faking a restore by encoding a priming byte would\n * emit bytes nobody asked for. And even done cleanly it buys nothing: it\n * reproduces the same output only by re-deriving it, so it is strictly more\n * machinery for a strictly weaker guarantee.\n * - **Retain the bytes.** Byte-identity holds BY CONSTRUCTION rather than by\n * an argument about determinism, and it costs at most 1916 bytes.\n *\n * So we retain the bytes. And we retain them by reading them back out of the\n * screen `frames.ts` just wrote, not by recomputing them: the whole frame\n * `writeUploadData`/`writeUploadEof` writes is the CONTIGUOUS range\n * `O_UP_FRAME_SEQ`(3), `O_UP_CSUM`(4), `O_UP_LEN`(5,6), `O_UP_DATA`(7)..\n * `O_UP_DATA + count - 1`, so one loop captures the sequence byte, the\n * checksum, the two length characters and the payload together. Nothing in this\n * module recomputes a checksum or re-encodes a length, which means a retransmit\n * cannot drift from a first transmission even if `frames.ts` changed how it\n * computes either.\n *\n * The source range is retained too, per the design, but only as a record: the\n * offset is NOT rewound on a retransmit, because the block was already accounted\n * for when it was first sent and the host is asking for the same block again,\n * not for a different one.\n */\ninterface RetainedBlock {\n /** First address written: `O_UP_FRAME_SEQ`. */\n addr: number;\n /** The frame's bytes, `addr` onwards, exactly as they went to the host. */\n bytes: Uint8Array;\n /** Where in the source this block started. A record, not used to rewind. */\n sourceOffset: number;\n /** How many SOURCE bytes it covered. 0 for the end-of-file frame. */\n sourceLength: number;\n /** Was this the end-of-file sentinel frame rather than data? */\n eof: boolean;\n}\n\n/**\n * x3270's own message strings, from `Common/fb-common:35-41`, so a user reading\n * our failure sees the text they would have seen from wc3270.\n */\nconst MSG = {\n /** `ftHostCancel`, substituted when the host's abort carried no text\n * (ft_cut.c:480-482). */\n HOST_CANCEL: 'Transfer canceled by host',\n /** `ftCutUnknownControl` (ft_cut.c:489). */\n UNKNOWN_CONTROL: 'Unknown FT control code from host',\n /** `ftCutRetransmit` (ft_cut.c:577). */\n RETRANSMIT: 'Transmission error',\n /** `ftUserCancel` (fb-common:35), used by `cancel` \u2014 x3270's own wording for an\n * operator cancellation, as at ft_cut.c:509 and :610. */\n USER_CANCEL: 'Transfer canceled by user',\n} as const;\n\n/**\n * The subset of `MSG` that DFT shares with CUT.\n *\n * Exported rather than copied into `dft.ts`: `ftUserCancel` is ONE x3270 message\n * and both engines report it, so two string literals could drift and a front end\n * comparing them would then see two different \"canceled by user\" texts. The rest of\n * `MSG` stays private because every other entry is CUT-specific \u2014 a DFT abort\n * carries the host's own text, not a control-code name.\n */\nexport const FT_MSG = {\n USER_CANCEL: MSG.USER_CANCEL,\n} as const;\n\n/**\n * The CUT transfer state machine.\n *\n * One instance per transfer, and it is single-use: once `step` has returned a\n * `done`, the transfer is over and further steps are inert. A second transfer\n * needs a second instance, which is also what keeps the codec's quadrant from\n * leaking between transfers (the bug x3270's file-static `quadrant` has and we\n * declined to inherit \u2014 see the `CutCodec` class comment).\n */\nexport class CutTransfer {\n readonly direction: TransferDirection;\n\n /**\n * THE ONE CODEC. Constructed here, held for the life of the transfer, reset\n * only on `SC_HOST_ACK` where x3270 resets its quadrant (ft_cut.c:435).\n */\n private readonly codec = new CutCodec();\n\n /** local->host: the source bytes. Empty for a `receive`. */\n private readonly source: Uint8Array;\n\n /** local->host: how many source bytes have been sent. */\n private offset = 0;\n\n /** local->host: the previous frame, for a retransmit. See `RetainedBlock`. */\n private lastBlock: RetainedBlock | undefined;\n\n /** local->host: has the end-of-file sentinel gone out? */\n private eofSent = false;\n\n /** host->local: decoded file bytes, kept in chunks and joined once. */\n private readonly chunks: Uint8Array[] = [];\n private receivedLength = 0;\n\n /** host->local: has the two-byte EOF sentinel arrived? Reported, not required. */\n private eofSeen = false;\n\n private readonly warningList: string[] = [];\n\n private outcome: TransferResult | undefined;\n\n constructor(opts: TransferOptions) {\n this.direction = opts.direction;\n if (opts.direction === 'send') {\n if (opts.data === undefined) {\n throw new TypeError(\"a 'send' transfer needs the bytes to send\");\n }\n this.source = opts.data instanceof Uint8Array ? opts.data : Uint8Array.from(opts.data);\n } else {\n if (opts.data !== undefined) {\n throw new TypeError(\"a 'receive' transfer takes no data; the host supplies it\");\n }\n this.source = new Uint8Array(0);\n }\n }\n\n /** The result, once the transfer has ended; `undefined` while it is running. */\n get result(): TransferResult | undefined {\n return this.outcome;\n }\n\n /** Has the transfer ended, either way? */\n get complete(): boolean {\n return this.outcome !== undefined;\n }\n\n /**\n * Non-fatal oddities noticed along the way, in order.\n *\n * Today this holds exactly one kind of entry: a download checksum that did not\n * match. See `verifyChecksum` for why that warns rather than aborts. Exposed\n * so a CLI can surface it, because a warning nobody can see is not a warning.\n *\n * A COPY, so a caller cannot append to or truncate our log. `readonly` in the\n * type only stops the honest mistake.\n */\n get warnings(): readonly string[] {\n return [...this.warningList];\n }\n\n /**\n * File bytes moved so far \u2014 decoded bytes written for a `receive`, source\n * bytes sent for a `send`. x3270's `ft_update_length` progress display\n * (ft_cut.c:565, :644) reads the same two counters.\n */\n get bytesTransferred(): number {\n return this.direction === 'receive' ? this.receivedLength : this.offset;\n }\n\n /** Did the host send the two-byte EOF sentinel? host->local only. */\n get sawEof(): boolean {\n return this.eofSeen;\n }\n\n /**\n * What a `FT_RETRANSMIT` would re-send, or `undefined` before the first\n * local->host frame has gone out.\n *\n * This is what makes the retained source range (see `RetainedBlock`) more than\n * bookkeeping: \"re-sent 956 bytes from offset 1912\" is the trace line a\n * retransmit needs, and neither number is recoverable from the transfer's\n * current position, because the position has already moved past the block.\n */\n get lastSentBlock():\n | { sourceOffset: number; sourceLength: number; frameLength: number; eof: boolean }\n | undefined {\n const b = this.lastBlock;\n if (b === undefined) return undefined;\n return {\n sourceOffset: b.sourceOffset,\n sourceLength: b.sourceLength,\n frameLength: b.bytes.length,\n eof: b.eof,\n };\n }\n\n /**\n * Process the CUT frame currently on `screen`, which may be mutated with a\n * response or an upload frame.\n *\n * The caller gates on `isCutFrame(screen)`; this does not, for the same reason\n * `parseFrame` does not \u2014 the two questions are separable, and a test wants to\n * drive a synthetic frame without also constructing the detection attribute.\n *\n * Faults are contained. A malformed frame ends the TRANSFER, never the\n * session, which is the design doc's error rule and matches how a program\n * check reports without dropping the connection. The single exception is a\n * screen of the wrong geometry: `frames.ts` throws a `CutFrameError` with no\n * `abortStatus` for that, because it is a local configuration bug rather than\n * anything the host did, and we rethrow it rather than reporting a transfer\n * failure that would send whoever debugs it looking at the host.\n */\n step(screen: Screen): TransferStep {\n if (this.outcome !== undefined) {\n // Idempotent after the end: no ack, no screen writes, the same result.\n return { done: this.outcome };\n }\n\n let frame: CutFrame;\n try {\n frame = parseFrame(screen);\n } catch (e) {\n if (e instanceof CutFrameError && e.abortStatus !== undefined) {\n // The two faults x3270 aborts on: an unknown frame type\n // (ft_cut.c:408-411) and an oversize declared length (ft_cut.c:617-620),\n // both with SC_ABORT_XMIT. `CutFrameError` carries which, so we do not\n // re-derive it, and its message is more specific than x3270's fixed\n // \"Illegal frame length\" / \"Unknown frame type from host\".\n return this.abort(screen, e.abortStatus, e.message);\n }\n throw e; // wrong geometry: loud, and not the host's fault\n }\n\n return this.direction === 'receive'\n ? this.stepReceive(screen, frame)\n : this.stepSend(screen, frame);\n }\n\n // -------------------------------------------------------------------------\n // host -> local (IND$FILE GET)\n // -------------------------------------------------------------------------\n\n private stepReceive(screen: Screen, frame: CutFrame): TransferStep {\n switch (frame.kind) {\n case 'controlCode':\n return this.controlCode(screen, frame.status, frame.message);\n\n case 'data': {\n // `if (raw_length == 2 && cvbuf[0] == EOF_DATA1 && cvbuf[1] ==\n // EOF_DATA2) { cut_ack(); return; }` (ft_cut.c:625-629). EOF is\n // acknowledged and then the host sends SC_XFER_COMPLETE; the sentinel\n // itself is not file content and must not be decoded.\n if (isEofData(frame.data)) {\n this.eofSeen = true;\n return { ack: AckAid.OK };\n }\n\n this.verifyChecksum(frame.seq, frame.declaredChecksum, frame.data);\n\n let decoded: Uint8Array;\n try {\n // THE ONE CODEC, carrying its quadrant across every frame. A fresh\n // one here would reject any frame whose first byte is data rather\n // than a selector \u2014 which is most frames after the first.\n decoded = this.codec.hostToLocal(frame.data);\n } catch (e) {\n if (e instanceof CutConversionError) {\n // `cut_abort(get_message(\"ftCutConversionError\"), SC_ABORT_XMIT)`\n // (ft_cut.c:156, :164), which is exactly the mapping the\n // `CutConversionError` class comment promises the state machine\n // would apply.\n return this.abort(screen, StatusCode.ABORT_XMIT, e.message);\n }\n throw e;\n }\n\n this.chunks.push(decoded);\n this.receivedLength += decoded.length;\n // `fts.length += conv_length; ft_update_length(); cut_ack();`\n // (ft_cut.c:643-645).\n return { ack: AckAid.OK };\n }\n\n case 'retransmit':\n // \"Re-send the previous frame\" \u2014 but host->local we have sent no frame,\n // only acknowledgements, so there is nothing to re-send and no\n // client-initiated recovery in the protocol. x3270 gives up here in\n // BOTH directions (`cut_abort(get_message(\"ftCutRetransmit\"),\n // SC_ABORT_XMIT)`, ft_cut.c:577), and for this direction that is simply\n // correct rather than the \"(Improperly)\" its own comment admits to.\n return this.abort(screen, StatusCode.ABORT_XMIT, MSG.RETRANSMIT);\n\n case 'dataRequest':\n // The host asking us for data during a GET means one of us has lost\n // track of the direction. x3270 has no such check \u2014 its\n // `cut_data_request` would read from a file it opened for writing \u2014 and\n // we would rather fail than write a corrupt local file.\n return this.abort(\n screen,\n StatusCode.ABORT_XMIT,\n 'host requested data during a receive transfer',\n );\n }\n }\n\n /**\n * Compare the checksum the host declared with the one we compute.\n *\n * VERIFY BUT WARN, NEVER ABORT \u2014 the design doc's decision, made at the user's\n * request and resting on evidence rather than on principle. x3270 never reads\n * `O_DT_CSUM` at all (its only mention in the tree is the `#define`), and\n * MECAFF on VM/370 ignores the field on receive; but the live TK5 capture\n * proves TK5's IND$FILE POPULATES it \u2014 host sent 6-bit 62, our `checksum()`\n * over the same 1904 bytes computed 62 \u2014 so a mismatch here is real\n * information rather than a spurious warning on every frame.\n *\n * It must not be fatal for two reasons: the protocol offers no\n * client-initiated retransmit to recover with (`ACK_RETRANSMIT` exists in the\n * header and x3270 never sends it), and failing a transfer that x3270 would\n * have completed would be a regression against the de facto protocol.\n */\n private verifyChecksum(seq: number, declared: number, data: Uint8Array): void {\n const computed = checksum(data);\n if (computed !== declared) {\n this.warningList.push(\n `frame ${seq}: checksum mismatch, host declared ${declared} but ` +\n `${data.length} data bytes compute to ${computed}`,\n );\n }\n }\n\n // -------------------------------------------------------------------------\n // local -> host (IND$FILE PUT)\n // -------------------------------------------------------------------------\n\n private stepSend(screen: Screen, frame: CutFrame): TransferStep {\n switch (frame.kind) {\n case 'controlCode':\n return this.controlCode(screen, frame.status, frame.message);\n\n case 'dataRequest': {\n // `cut_data_request` (ft_cut.c:497-568). x3270 discovers end of file by\n // reading it (`xlate_getc() == EOF`, :516-519) and then sends the\n // sentinel on the NEXT pass when `count` came out 0 (:541-546); we know\n // the length up front, so the same two frames come out of one test.\n if (this.offset >= this.source.length) {\n if (this.eofSent) {\n // The host asked for more after we said end of file. Nothing left\n // to give, and re-sending the sentinel forever is worse than saying\n // so.\n return this.abort(\n screen,\n StatusCode.ABORT_XMIT,\n 'host requested data after end of file',\n );\n }\n writeUploadEof(screen, frame.rawSeq);\n this.eofSent = true;\n this.retain(screen, 2, this.offset, 0, true);\n return { ack: AckAid.OK };\n }\n\n const sourceOffset = this.offset;\n let written;\n try {\n written = writeUploadData(screen, {\n rawSeq: frame.rawSeq,\n data: this.source.subarray(sourceOffset),\n codec: this.codec, // THE ONE CODEC again: see the class comment\n });\n } catch (e) {\n if (e instanceof CutConversionError) {\n // Unreachable with intact quadrant tables \u2014 all 256 byte values are\n // covered (codec finding 2) \u2014 but if it ever fires, note that\n // `writeUploadData` builds its whole encoding before writing any of\n // it, so the screen is untouched and there is nothing to clean out.\n // x3270 does have to zero what it wrote (ft_cut.c:529-532).\n return this.abort(screen, StatusCode.ABORT_XMIT, e.message);\n }\n throw e;\n }\n\n if (written.consumed === 0) {\n // Cannot happen: the worst-case expansion is 2 encoded bytes per\n // source byte and a frame holds 1912, so a non-empty remainder always\n // consumes at least one byte. Asserted anyway, because the failure\n // mode is an infinite ack loop against a live host rather than\n // anything a reader would spot.\n return this.abort(\n screen,\n StatusCode.ABORT_XMIT,\n `internal error: no source bytes fit in an upload frame with ` +\n `${this.source.length - sourceOffset} still to send`,\n );\n }\n\n this.offset += written.consumed;\n this.retain(screen, written.encodedLength, sourceOffset, written.consumed, false);\n // `run_action(AnEnter, IA_FT, NULL, NULL)` (ft_cut.c:567).\n return { ack: AckAid.OK };\n }\n\n case 'retransmit': {\n // WHERE WE DELIBERATELY DIVERGE FROM x3270, and the reason this frame\n // kind exists in `frames.ts` at all. x3270 aborts\n // (`cut_abort(get_message(\"ftCutRetransmit\"), SC_ABORT_XMIT)`,\n // ft_cut.c:577) under a comment that calls its own handling\n // \"(Improperly) process a retransmit\". The design doc requires the real\n // behavior: re-send the previous block, byte for byte.\n //\n // The AID is Enter, not PF1: `AckAid.RETRANSMIT` is what a client would\n // press to ask the HOST to re-send, whereas here the host has asked US,\n // so this is an ordinary data frame going up \u2014 the same\n // `run_action(AnEnter)` that ends `cut_data_request` (ft_cut.c:567).\n if (this.lastBlock === undefined) {\n // A retransmit before we ever sent a frame \u2014 nothing exists to\n // re-send, so x3270's abort is the only honest answer.\n return this.abort(screen, StatusCode.ABORT_XMIT, MSG.RETRANSMIT);\n }\n this.replay(screen, this.lastBlock);\n return { ack: AckAid.OK };\n }\n\n case 'data':\n // The host sending us data during a PUT: same lost-direction fault as a\n // data request during a GET, and the same answer.\n return this.abort(screen, StatusCode.ABORT_XMIT, 'host sent data during a send transfer');\n }\n }\n\n /**\n * Snapshot the frame `frames.ts` has just written, for a possible retransmit.\n *\n * The written region is contiguous \u2014 `O_UP_FRAME_SEQ` 3, `O_UP_CSUM` 4,\n * `O_UP_LEN` 5-6, `O_UP_DATA` 7 onwards (ft_cut_ds.h:77-80) \u2014 so one loop\n * captures the sequence byte, the checksum, both length characters and the\n * payload as one string of bytes. See `RetainedBlock` for why this reads the\n * screen back instead of recomputing any of it.\n *\n * `O_UP_DATA_CODE` (2) is excluded because nothing in x3270 writes it, so\n * copying whatever happens to be at offset 2 would put a byte on the wire that\n * the first transmission did not.\n */\n private retain(\n screen: Screen,\n encodedLength: number,\n sourceOffset: number,\n sourceLength: number,\n eof: boolean,\n ): void {\n const end = O_UP_DATA + encodedLength; // exclusive\n const bytes = new Uint8Array(end - O_UP_FRAME_SEQ);\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = screen.cellAt(O_UP_FRAME_SEQ + i).ebcdic;\n }\n this.lastBlock = { addr: O_UP_FRAME_SEQ, bytes, sourceOffset, sourceLength, eof };\n }\n\n /**\n * Write a retained frame into a new screen, byte for byte.\n *\n * The one thing NOT replayed is the field attribute at `O_DR_SF`: that is\n * x3270's cosmetic \"change the data field attribute so it doesn't display\"\n * (ft_cut.c:558-561), and it is a property of THIS screen's field structure\n * rather than of the frame we are re-sending, so it is recomputed from the\n * attribute the host planted here. As in `frames.ts`, an absent attribute is\n * left absent rather than invented.\n */\n private replay(screen: Screen, block: RetainedBlock): void {\n for (let i = 0; i < block.bytes.length; i++) {\n screen.setChar(block.addr + i, block.bytes[i]!);\n }\n const attr = screen.attributeAt(O_DR_SF);\n if (attr !== null) {\n screen.setFieldAttribute(O_DR_SF, (attr & ~FA.INTENSITY) | FA.INT_ZERO_NSEL);\n }\n }\n\n // -------------------------------------------------------------------------\n // shared\n // -------------------------------------------------------------------------\n\n /**\n * `cut_control_code` (ft_cut.c:419-492) \u2014 identical in both directions, which\n * is why it is not duplicated per direction.\n */\n private controlCode(screen: Screen, status: number, message: string): TransferStep {\n switch (status) {\n case StatusCode.HOST_ACK:\n // ft_cut.c:431-441. The host has accepted the IND$FILE command; this is\n // the transfer's real start, and x3270 resets ALL its per-transfer state\n // here: `expanded_length = 0; quadrant = -1; xlate_buffered = 0;\n // xlate_buf_ix = 0; cut_eof = false;`. We do the same, which for us\n // means resetting the codec's quadrant and everything derived from it.\n // Warnings survive, because they are a log rather than transfer state.\n this.codec.reset();\n this.offset = 0;\n this.eofSent = false;\n this.eofSeen = false;\n this.lastBlock = undefined;\n this.chunks.length = 0;\n this.receivedLength = 0;\n return { ack: AckAid.OK };\n\n case StatusCode.XFER_COMPLETE:\n // `cut_ack(); cut_xfer_in_progress = false; ft_complete(NULL);`\n // (ft_cut.c:442-447). Note the ack comes FIRST and then the transfer\n // completes, so the terminal step carries both.\n return this.finish(AckAid.OK, this.success());\n\n case StatusCode.ABORT_FILE:\n case StatusCode.ABORT_XMIT:\n // ft_cut.c:448-486. ACKNOWLEDGED WITH ENTER, not PF2: `cut_ack()` at\n // :452, because the host has already decided and is not being asked to\n // do anything. The message is the host's own text from `O_CC_MESSAGE`,\n // already stripped by `frames.ts`; an empty one becomes\n // `ftHostCancel`(:480-482).\n return this.finish(AckAid.OK, {\n ok: false,\n error: message.length > 0 ? message : MSG.HOST_CANCEL,\n status,\n });\n\n default:\n // `cut_abort(get_message(\"ftCutUnknownControl\"), SC_ABORT_XMIT)`\n // (ft_cut.c:487-490). `frames.ts` deliberately parses an unknown status\n // rather than rejecting it, precisely so this decision could live here\n // and could name the code it rejected.\n // A known code reaching the default branch means someone added a fifth\n // entry to `StatusCode` and to `isKnownStatusCode` without teaching this\n // switch about it. Reporting it to the host as \"unknown control code\"\n // would be a lie that reads as a host fault, so it throws instead: this\n // is the one failure here that is OURS.\n if (isKnownStatusCode(status)) {\n throw new Error(`internal error: unhandled known status code 0x${status.toString(16)}`);\n }\n return this.abort(\n screen,\n StatusCode.ABORT_XMIT,\n `${MSG.UNKNOWN_CONTROL} (0x${status.toString(16).padStart(4, '0')})`,\n );\n }\n }\n\n /**\n * Cancel at the operator's request: abort the transfer and tell the host.\n *\n * ## WHY THIS WRAPS `abort` RATHER THAN `abort` BEING MADE PUBLIC\n *\n * `abort` is the INTERNAL error path. Its callers each already hold a\n * `CutFrameError`'s status and message, so its signature takes both \u2014 and an\n * operator canceling has neither. Exposing `abort` would put two arguments in\n * the public API that no caller outside this file can sensibly supply, and the\n * natural wrong guess (status 0, an empty message) is one the host would read\n * as a protocol fault rather than a cancellation.\n *\n * ## THE STATUS IS `SC_ABORT_FILE`, NOT `SC_ABORT_XMIT`\n *\n * x3270 uses `SC_ABORT_FILE` at BOTH of its user-cancel sites \u2014\n * `cut_abort(get_message(\"ftUserCancel\"), SC_ABORT_FILE)` at ft_cut.c:509 and\n * :610 \u2014 and reserves `SC_ABORT_XMIT` for transmission faults, which is what\n * every other abort in this file is. Reporting a transmission error when the\n * operator simply changed their mind misstates the cause in the host's own log.\n *\n * `AckAid.ABORT` (PF2) is the AID: `run_action(AnPF, IA_FT, \"2\", NULL)`\n * (ft_cut.c:674). Note the asymmetry recorded at the top of this file \u2014 a\n * HOST-initiated abort is acknowledged with Enter, not PF2; PF2 is only for an\n * abort WE initiate, which is exactly this.\n *\n * ## WE SEND IMMEDIATELY WHERE x3270 DEFERS\n *\n * x3270's `ft_do_cancel` (ft.c:1209-1225) sets `FT_ABORT_WAIT` and sends\n * nothing until the host's NEXT frame arrives, which is what its two call sites\n * above are. That suits a client whose transfer loop owns the screen; ours is\n * called from a front end closing a form, which has no later frame to wait for\n * and must not leave the host primed. So this writes the response and returns\n * the AID in one step, and the front end sends it.\n *\n * IDEMPOTENT, on the buffer as well as the return value. A closed form racing a\n * completing transfer must not put a second PF2 on the wire: the host has\n * already left transfer mode and would read it as input into whatever panel it\n * painted next. The post-completion return matches `step`'s own\n * (`{ done: this.outcome }`), so a caller reading `done` sees the real outcome\n * \u2014 which for a transfer that finished normally is a SUCCESS, not this\n * cancellation.\n */\n cancel(screen: Screen): TransferStep {\n if (this.outcome !== undefined) return { done: this.outcome };\n return this.abort(screen, StatusCode.ABORT_FILE, MSG.USER_CANCEL);\n }\n\n /**\n * We abort: write the response area and press PF2. `cut_abort`\n * (ft_cut.c:662-678), minus the AID, which is the caller's to send.\n *\n * THE SEQUENCE BYTE IS READ STRAIGHT OUT OF THE SCREEN, at offset 1, exactly\n * as x3270 does: `ctlr_add(RO_FRAME_SEQ, ea_buf[O_DT_FRAME_SEQ].ec, 0)`\n * (ft_cut.c:670), regardless of which frame type is actually there. Offset 1\n * is `O_DT_FRAME_SEQ`, `O_CC_FRAME_SEQ` and `O_DR_SF` all at once, so for a\n * data request x3270 echoes the field attribute cell rather than the sequence\n * at offset 3 \u2014 faithfully reproduced, because whatever the host makes of that\n * byte it has made of it for decades. It is also the only thing available when\n * `parseFrame` threw before producing a frame at all.\n *\n * Constraint 3 in the module comment: this writes 1915-1918, so it must never\n * run in the same `step` as a maximal upload frame, whose data reaches 1918.\n * It does not \u2014 every path here either aborts or writes upload data.\n */\n private abort(screen: Screen, status: number, error: string): TransferStep {\n writeResponse(screen, {\n frameType: ResponseFrameType.CONTROL_CODE,\n rawSeq: screen.cellAt(O_DT_FRAME_SEQ).ebcdic,\n reason: status,\n });\n // `run_action(AnPF, IA_FT, \"2\", NULL)` (ft_cut.c:674) = ACK_ABORT = PF2.\n return this.finish(AckAid.ABORT, { ok: false, error, status });\n }\n\n /** The successful result, with the accumulated file for a `receive`. */\n private success(): TransferResult {\n if (this.direction !== 'receive') return { ok: true };\n const data = new Uint8Array(this.receivedLength);\n let at = 0;\n for (const chunk of this.chunks) {\n data.set(chunk, at);\n at += chunk.length;\n }\n return { ok: true, data };\n }\n\n private finish(ack: number, result: TransferResult): TransferStep {\n this.outcome = result;\n return { ack, done: result };\n }\n}\n", "/**\n * DFT (Distributed Function Terminal) file-transfer wire constants.\n *\n * Every value is transcribed from x3270's `include/ft_dft_ds.h`, which is the\n * only place they are written down \u2014 the IBM manual documents the DDM Query\n * Reply that ENABLES this protocol but not these request codes.\n *\n * ## THE OFFSET TRAP, AND ITS ONE EXCEPTION\n *\n * x3270 overlays `struct data_buffer` on the START of the structured field, so\n * its offsets count the two length bytes and the SFID. `parseStructuredFields`\n * hands us the parameters only, so **offsets read through `data_bufr` are x3270's\n * minus 3** \u2014 the request type, and everything in a `Data Insert`. Confirmed\n * against a live host: TK5 sent field lengths 0x29 and 0x23 and our parser\n * reported 38- and 32-byte payloads (41-3, 35-3).\n *\n * **THE `Open` IS THE EXCEPTION AND ITS OFFSETS ARE UNCHANGED.**\n * `dft_open_request` is handed `cp`, not the struct base, and `cp` already points\n * at struct offset 3 \u2014 the same byte our payload starts at. **This file got that\n * wrong first time and its tests passed anyway**, because the test helper wrote\n * the name at the same wrong offset the parser read. Full derivation and the\n * measurement at `NAME_AT_SHORT` below; do not re-apply the -3 here.\n */\n\nimport { AID, Sfid } from '../constants.js';\n\n/**\n * x3270 overlays its offsets on the RAW structured field; ours start 3 bytes\n * later, because parseStructuredFields has already stripped the two length\n * bytes and the SFID. See \"THE OFFSET TRAP\" above.\n */\nexport const X3270_HEADER_LEN = 3;\n\n/** Host request types: `TR_*_REQ` and `TR_DATA_INSERT`. */\nexport const DftRequest = {\n /** `TR_OPEN_REQ` \u2014 open a file or announce a message. */\n OPEN: 0x0012,\n /** `TR_CLOSE_REQ`. */\n CLOSE: 0x4112,\n /**\n * `TR_SET_CUR_REQ` \u2014 set cursor. `dft_set_cur_req` (`ft_dft.c:414-419`) only\n * traces; it calls `net_output()` nowhere, so no reply frame goes out at all\n * \u2014 not even a bare acknowledgement.\n */\n SET_CUR: 0x4511,\n /** `TR_GET_REQ` \u2014 host wants data FROM us: this is the upload path. */\n GET: 0x4611,\n /**\n * `TR_INSERT_REQ`. x3270's handler (`ft_dft.c:193-198`) is the same shape as\n * `SET_CUR`'s: it traces and returns, with no `net_output()` call.\n */\n INSERT: 0x4711,\n /** `TR_DATA_INSERT` \u2014 host is giving us data: the download path. */\n DATA_INSERT: 0x4704,\n} as const;\n\n/** Replies we send. `TR_*_REPLY`. */\nexport const DftReply = {\n /** `TR_GET_REPLY` \u2014 our data, answering a GET. */\n GET: 0x4605,\n /** `TR_NORMAL_REPLY` \u2014 an acknowledgement carrying a record number. */\n NORMAL: 0x4705,\n /**\n * `TR_ERROR_REPLY`, and note it is **8 bits**, not 16: x3270 writes\n * `HIGH8(code)` then this byte, so the reply type's high byte is borrowed from\n * whatever request failed (`ft_dft.c:703-704`, `:645-646`).\n */\n ERROR: 0x08,\n /** `TR_CLOSE_REPLY`. */\n CLOSE: 0x4109,\n} as const;\n\n/** Sub-headers inside a frame: `TR_*_HDR`. */\nexport const DftHeader = {\n /** `TR_RECNUM_HDR`, followed by a 32-bit record number. */\n RECNUM: 0x6306,\n /** `TR_ERROR_HDR`, followed by a 16-bit error code. */\n ERROR: 0x6904,\n /** `TR_NOT_COMPRESSED`. We never compress, so this is the only value we send. */\n NOT_COMPRESSED: 0xc080,\n /** `TR_BEGIN_DATA`, a single byte introducing the length-prefixed payload. */\n BEGIN_DATA: 0x61,\n} as const;\n\n/** Error codes: `TR_ERR_*`. */\nexport const DftError = {\n /** `TR_ERR_EOF` \u2014 a GET past end of file. Not a failure: it ends an upload. */\n EOF: 0x2200,\n /** `TR_ERR_CMDFAIL` \u2014 what `dft_abort` always sends (`ft_dft.c:706`). */\n CMDFAIL: 0x0100,\n} as const;\n\n/**\n * `OPEN_MSG` (`ft_dft.c:53`). An `Open` whose trimmed name equals this is the\n * host announcing a MESSAGE, not a file \u2014 x3270 sets `message_flag` and pointedly\n * does NOT call `ft_running`, so it must not start a transfer.\n */\nexport const OPEN_MSG = 'FT:MSG';\n\n/** A malformed DFT frame. A transfer fault, never a session fault. */\nexport class DftFrameError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'DftFrameError';\n }\n}\n\n/** A parsed DFT frame: the request type, and the payload it came from. */\nexport interface DftFrame {\n /** 16-bit request type from payload offset 0. One of `DftRequest`'s values. */\n requestType: number;\n /**\n * The whole payload, INCLUDING the request type. Handlers read their own\n * fields at their own offsets, so they need the original bytes rather than a\n * subarray whose offsets differ again.\n */\n payload: Uint8Array;\n}\n\n/**\n * Extract a `SF_TRANSFER_DATA` payload's request type, without disturbing the\n * payload.\n *\n * `payload` is what `parseStructuredFields` yields: the parameters, with the\n * length bytes and the SFID already removed. So the request type x3270 reads at\n * `cp+3` is at **0** here. See the offset trap in this module's header comment.\n */\nexport function parseDftFrame(payload: Uint8Array): DftFrame {\n if (payload.length < 2) {\n throw new DftFrameError(\n `DFT frame needs at least 2 bytes for a request type, got ${payload.length}`,\n );\n }\n return { requestType: (payload[0]! << 8) | payload[1]!, payload };\n}\n\n/**\n * The two legal `Open` payload lengths, which are x3270's two legal FIELD\n * lengths minus the 3-byte header we never see.\n *\n * `dft_open_request` tests `len == 0x23` and `len == 0x29` on the field length\n * (`ft_dft.c:146-148`). TK5 sent both in one session, arriving here as 32 and 38\n * bytes, which is what confirmed the subtraction against a real host.\n */\nconst OPEN_SHORT = 0x23 - X3270_HEADER_LEN; // 32\nconst OPEN_LONG = 0x29 - X3270_HEADER_LEN; // 38\n\n/**\n * ## THE OPEN OFFSETS ARE *NOT* x3270's MINUS 3 \u2014 THIS IS THE ONE EXCEPTION\n *\n * **CORRECTED 2026-09-25 after empirical measurement; the first version of this\n * file was WRONG here and its tests passed anyway.** The `-3` rule holds for the\n * payload *length* and for `Data Insert`'s offsets, because those are read through\n * `data_bufr`, which overlays the field from byte 0. It does **not** hold for the\n * `Open`, because `dft_open_request` is not given the struct base.\n *\n * `GET16` does **not** advance its pointer (`include/3270ds.h:341-344` \u2014 it reads\n * `*(ptr)` and `*(ptr+1)` and assigns nothing back). So at `ft_dft.c:108` `cp` is\n * set to `data_bufr->sf_request_type` and is *still* pointing there when\n * `:114` calls `dft_open_request(data_length, cp)`. `sf_request_type` is at struct\n * offset **3** (verified by `offsetof`), which is exactly where OUR payload\n * starts.\n *\n * So `cp` and our `payload` point at THE SAME BYTE, and the Open's internal\n * offsets carry over **unchanged**:\n *\n * | field | x3270 | ours |\n * |---|---|---|\n * | name, `len == 0x23` | `cp + 25` (`:147`) | **25** |\n * | record size, `len == 0x29` | `cp + 27` (`:151`) | **27** |\n * | name, `len == 0x29` | `cp + 31` (`:153`) | **31** |\n *\n * Measured, not reasoned: a synthetic `0x23` field with `memcpy(cp + 25, \"FT:DATA\", 7)`\n * puts the name at field index 28 = payload index 25, and payload index 22 reads\n * zeros. The live-probe evidence (field lengths `0x29`/`0x23` arriving as 38/32-byte\n * payloads) confirms the LENGTH subtraction only, and was wrongly generalised to\n * these offsets.\n *\n * **Why the original error was invisible:** the test helper wrote the name at the\n * same wrong offset the parser read it from, so the pair agreed with each other and\n * not with the wire \u2014 and the plan itself warns this failure is quiet, because a\n * wrong name simply is not `FT:MSG`, so a MESSAGE frame silently starts a file\n * transfer. `openPayload` now writes at the x3270 offsets and is checked against a\n * byte-for-byte literal frame.\n */\nconst NAME_AT_SHORT = 25;\nconst NAME_AT_LONG = 31;\nconst RECSZ_AT = 27;\n\n/** How many bytes the name field occupies. `memcpy(namebuf, name, 7)` (`ft_dft.c:159`). */\nconst NAME_LENGTH = 7;\n\n/** A parsed `Open`. */\nexport interface DftOpen {\n /** The 7-byte name with trailing spaces trimmed. */\n name: string;\n /**\n * `true` when the name is `FT:MSG`: the host is sending a MESSAGE, not opening\n * a file. x3270 sets `message_flag` and does not call `ft_running`\n * (`ft_dft.c:171-176`), so this must not start a transfer.\n */\n isMessage: boolean;\n /**\n * Record size, present only in the long form. Absent rather than 0 when the\n * host did not send one, because 0 is what x3270 uses as \"no value\" and an\n * optional property says so in the type.\n */\n recordSize?: number;\n}\n\n/**\n * Parse an `Open` request.\n *\n * The name is compared as ASCII. That looks wrong for a 3270 data stream and is\n * not: x3270 does `strcmp(namebuf, OPEN_MSG)` on the raw bytes with no EBCDIC\n * translation (`ft_dft.c:171`), and TK5's own frames carry ASCII `FT:DATA` \u2014\n * observed in the 2026-09-24 probe trace. The name is metadata the host writes in\n * the PC's alphabet, not screen data.\n */\nexport function parseDftOpen(payload: Uint8Array): DftOpen {\n let nameAt: number;\n let recordSize: number | undefined;\n\n if (payload.length === OPEN_SHORT) {\n nameAt = NAME_AT_SHORT;\n } else if (payload.length === OPEN_LONG) {\n nameAt = NAME_AT_LONG;\n recordSize = (payload[RECSZ_AT]! << 8) | payload[RECSZ_AT + 1]!;\n } else {\n throw new DftFrameError(\n `unknown Open length: ${payload.length} payload bytes `\n + `(expected ${OPEN_SHORT} or ${OPEN_LONG}, i.e. field length 0x23 or 0x29)`,\n );\n }\n\n // The name ENDS EXACTLY AT THE PAYLOAD'S LAST BYTE in both forms: 25+7 = 32 and\n // 31+7 = 38, the two lengths checked just above. That exact fit is corroborating\n // evidence for the corrected offsets rather than a coincidence -- x3270's\n // `memcpy(namebuf, name, 7)` consumes the field to its end, which is why the\n // short form's legal length is 0x23 and not more. So this is provably in bounds,\n // and an off-by-one in either direction would run off the end.\n let name = '';\n for (let i = 0; i < NAME_LENGTH; i++) name += String.fromCharCode(payload[nameAt + i]!);\n // Trailing spaces only, matching x3270's backwards walk from namebuf[6]\n // (ft_dft.c:161-164). trimEnd() would also eat tabs and newlines, which are\n // legal name bytes; a host sending one would silently get a different name.\n name = name.replace(/ +$/, '');\n\n // `recordSize: undefined` would not typecheck under exactOptionalPropertyTypes\n // against an optional property, so the key is added only when it has a value.\n // Same spread-when-defined idiom as bind.ts:116.\n return { name, isMessage: name === OPEN_MSG, ...(recordSize === undefined ? {} : { recordSize }) };\n}\n\n/**\n * Big-endian 16-bit. **Exported because `ft/dft.ts` needs it in Task 6** \u2014 the\n * alternative is a second copy in that file, and two hand-rolled big-endian\n * writers that could disagree is exactly the kind of drift this project has been\n * bitten by. Matches x3270's `SET16` macro (`include/3270ds.h:337-340`).\n *\n * Deliberately NOT shared with `queryreply.ts`'s module-private `u16`: that one\n * range-checks and throws, because a Query Reply builds a self-describing\n * structure where a bad length corrupts the whole unit. These writers are called\n * with our own constants and a record counter we increment ourselves, so a shared\n * helper would couple two unrelated wire modules to buy nothing.\n */\nexport function u16(n: number): [number, number] {\n return [(n >> 8) & 0xff, n & 0xff];\n}\n\n/**\n * Big-endian 32-bit, for the record number. Exported for the same reason as\n * `u16`; matches `SET32` (`include/3270ds.h:345-350`).\n *\n * `>>>` is used for the high byte, but note it is NOT load-bearing: `& 0xff`\n * truncates a sign-extended result to the same byte, measured across\n * `0x80000000` and `0xffffffff`. It is written unsigned because that is what the\n * value IS, not because `>>` would produce different bytes here.\n */\nexport function u32(n: number): [number, number, number, number] {\n return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];\n}\n\n/**\n * Every reply starts `AID_SF, L, L, SF_TRANSFER_DATA`, where L is the structured\n * field length COUNTING ITSELF AND THE SFID BUT NOT THE AID \u2014 x3270 writes\n * `SET16(obptr, 5)` before a 6-byte buffer (`ft_dft.c:184`, `space3270out(6)` at\n * `:184`).\n *\n * So `length` here is `body.length + 3`: two length bytes, one SFID, and the\n * caller's body. The AID is outside the count. This is the same +3 relationship\n * as `X3270_HEADER_LEN` on the INBOUND side, and it is the same three bytes \u2014 but\n * it is written as its own arithmetic here rather than reusing that constant,\n * because one is \"what the parser stripped from a field we received\" and the other\n * is \"what we must count when declaring a field we send\". Tying them together\n * would make a future change to either silently change the other.\n */\nfunction reply(...body: number[]): Uint8Array {\n return Uint8Array.of(AID.SF, ...u16(body.length + 3), Sfid.TRANSFER_DATA, ...body);\n}\n\n/**\n * Acknowledge an `Open`. `ft_dft.c:181-189`.\n *\n * Sent for BOTH an `FT:DATA` open and an `FT:MSG` open: x3270 acknowledges before\n * it looks at `message_flag` (`:171-176` precedes `:181`), so the message branch\n * gets the same 6 bytes.\n */\nexport function buildOpenAck(): Uint8Array {\n // The literal 0x0009 is x3270's, and it is NOT one of the TR_ constants -- it\n // has no name in ft_dft_ds.h. Left as a number with this note rather than\n // given an invented name.\n return reply(...u16(0x0009));\n}\n\n/** Acknowledge a `Close`. `ft_dft.c:680-687`. */\nexport function buildCloseAck(): Uint8Array {\n return reply(...u16(DftReply.CLOSE));\n}\n\n/**\n * Acknowledge received data, carrying the record number. `ft_dft.c:206-214`.\n *\n * The counter is the CALLER's: x3270 keeps `recnum` static and increments it here,\n * but a builder that owned it could not construct two acks for the same record,\n * which is what a retransmit needs. Task 5's state machine holds it.\n */\nexport function buildDataAck(recordNumber: number): Uint8Array {\n return reply(...u16(DftReply.NORMAL), ...u16(DftHeader.RECNUM), ...u32(recordNumber));\n}\n\n/**\n * Report a failure. `ft_dft.c:698-706`.\n *\n * `failedRequest` supplies the high byte of the reply type, which is why\n * `DftReply.ERROR` is 8 bits: x3270 writes `HIGH8(code)` then the error byte, so\n * a failed GET (`0x4611`) yields reply type `0x4608`. The code sent is always\n * `TR_ERR_CMDFAIL` \u2014 every one of `dft_abort`'s five call sites passes a different\n * REQUEST but the same error code (`:155`, `:227`, `:401`, `:577`, `:619`).\n * `TR_ERR_EOF` is used only by the upload's own EOF frame, not here.\n */\nexport function buildDftError(failedRequest: number): Uint8Array {\n return reply(\n (failedRequest >> 8) & 0xff,\n DftReply.ERROR,\n ...u16(DftHeader.ERROR),\n ...u16(DftError.CMDFAIL),\n );\n}\n", "/**\n * The DFT file-transfer state machine.\n *\n * ## WHY THIS IS A SEPARATE ENGINE FROM `CutTransfer`\n *\n * CUT is screen-shaped: it takes a `Screen`, parses a frame out of fixed offsets\n * in a 1920-cell buffer, writes a response back into it, and returns an AID to\n * press. That is why CUT works only at 24x80 and why `frames.ts` throws on any\n * other geometry.\n *\n * DFT is record-shaped. **This module imports no `Screen` and no `Session`**, and\n * that is the feature, not an accident of layering: it is what lets a transfer run\n * at 43x80, which is the user's own working geometry and the reason this stage\n * exists. `ft_dft.c` has zero screen-buffer references against 25 in `ft_cut.c`.\n *\n * One instance per transfer, single-use like `CutTransfer` \u2014 once `handle` has\n * returned a `done`, the transfer is over and further calls are inert.\n */\nimport { AID, Sfid } from '../constants.js';\nimport { boundDftBufferSize, DFT_BUF_DEFAULT } from '../queryreply.js';\nimport {\n DftError,\n DftHeader,\n DftReply,\n DftRequest,\n buildCloseAck,\n buildDataAck,\n buildDftError,\n buildOpenAck,\n parseDftFrame,\n parseDftOpen,\n u16,\n u32,\n} from './dftFrames.js';\nimport { FT_MSG, type TransferDirection, type TransferResult } from './transfer.js';\n\n/** `END_TRANSFER` (`ft_dft.c:54`): the host's own \"transfer complete\" message. */\nconst END_TRANSFER = 'TRANS03';\n\n/**\n * Offsets into a `Data Insert` payload.\n *\n * **These ARE x3270's minus 3, unlike the `Open`'s** \u2014 `dft_data_insert` is handed\n * `data_bufr` itself, the struct base, so `struct data_buffer`'s own offsets apply\n * and our stripped 3 bytes come off them. The `Open` is the exception because it\n * receives a pointer already 3 bytes in; see the long note in `dftFrames.ts`, which\n * exists because getting that backwards shipped a real bug.\n *\n * Offsets are into `struct data_buffer` (`ft_dft.c:59-67`): `data_length` at 8,\n * `data` at 10, both verified with `offsetof`.\n */\nconst DATA_LENGTH_AT = 8 - 3; // 5\nconst DATA_AT = 10 - 3; // 7\n/** The declared data length counts 5 bytes of header. `ft_dft.c:236`. */\nconst LENGTH_OVERHEAD = 5;\n\n/**\n * Bytes of one buffer that are NOT data. `ft_dft.c:583` reads\n * `dft_buffersize - 27`, with the comment \"always read 5 bytes less than we're\n * allowed\" \u2014 the 27 covers the 17-byte frame header, the 5-byte margin, and the\n * remainder x3270 does not itemise.\n */\nconst UPLOAD_OVERHEAD = 27;\n\n/**\n * Bytes before the data in an upload frame: `AID`, 2 length, `SFID`, reply type,\n * recnum header, 4 recnum, compression, begin-data, 2 data length.\n *\n * **The declared SF length is this MINUS ONE.** x3270 writes\n * `SET16(bufptr, obptr - (obuf + 1))` (`ft_dft.c:654-655`) \u2014 total bytes excluding\n * the AID \u2014 while the data still lands at index 17 (`bufptr = obuf + 17`, `:584`).\n * Both numbers are right and they differ by the AID; an earlier draft of the plan\n * used 17 for both, which would have overstated every frame's length by one.\n */\nconst UPLOAD_HEADER_LEN = 17;\n\nexport interface DftOptions {\n direction: TransferDirection;\n /** For `send`, the bytes to put on the host. Required for `send`. */\n data?: Uint8Array | readonly number[];\n /**\n * The DFT buffer size, which bounds one upload frame. Defaults to x3270's\n * `DFT_BUF` 16384 and is CLAMPED to [256, 32767] by `boundDftBufferSize`, the\n * same function the DDM Query Reply uses.\n *\n * Reusing that clamp is a correctness requirement, not tidiness: the size we\n * ADVERTISE to the host in the DDM unit and the size we CHUNK by here must be\n * the same number, or we promise one frame size and send another.\n */\n bufferSize?: number;\n}\n\n/** What one inbound frame produced. */\nexport interface DftStep {\n /**\n * The reply to send, as a complete inbound record starting with `AID_SF`.\n *\n * **Absent is a real answer, not a gap.** Set Cursor, Insert and an unknown\n * request type all produce no reply, because x3270's handlers for all three\n * trace and return. Sending something would put unsolicited bytes on the wire\n * mid-transfer.\n */\n reply?: Uint8Array;\n /** Present exactly once, on the frame that ends the transfer. */\n done?: TransferResult;\n /**\n * The request type, when it was one we do not implement. For the caller to\n * trace \u2014 x3270 logs `Unsupported(0x%04x)` and carries on (`ft_dft.c:131-133`),\n * and a silent drop with nothing in the log is how an unimplemented request\n * becomes an unexplainable stall.\n */\n unsupported?: number;\n}\n\nexport class DftTransfer {\n readonly direction: TransferDirection;\n\n /** local->host: the source bytes. Empty for a `receive`. */\n private readonly source: Uint8Array;\n /** local->host: how many source bytes have gone out. Task 6 advances it. */\n private offset = 0;\n\n /** host->local: received chunks, joined once at the end. */\n private readonly chunks: Uint8Array[] = [];\n private receivedLength = 0;\n\n /** The record number on the next acknowledgement. `recnum` starts at 1. */\n private recordNumber = 1;\n\n /**\n * Did the host open `FT:MSG` rather than a file? Exposed because the session\n * needs it: a message transfer must not be reported as a file transfer.\n */\n private messageFlag = false;\n\n /** Has the operator asked to cancel? Checked on the inbound edge. */\n private cancelRequested = false;\n\n /** One upload frame's ceiling, clamped. Readable so a caller can advertise it. */\n readonly bufferSize: number;\n\n /**\n * The last upload frame, verbatim, for `dft_read_modified` to re-send.\n *\n * x3270 keeps this in `dft_savebuf` (`ft_dft.c:657-663`) and replays it when a\n * Read Modified arrives with `AID_SF`. Retaining the BYTES rather than the\n * source range is the same decision `CutTransfer` documents for retransmits:\n * re-deriving a frame can produce different bytes, and here it would also\n * double-count `offset`.\n *\n * The copy in x3270 sits AFTER the data/EOF if-else, so an EOF frame is retained\n * exactly as a data frame is \u2014 a Read Modified after EOF must re-send the EOF,\n * not the last data frame, which the host already has.\n */\n private savedFrame: Uint8Array | undefined;\n\n private outcome: TransferResult | undefined;\n\n constructor(opts: DftOptions) {\n this.direction = opts.direction;\n if (opts.direction === 'send') {\n if (opts.data === undefined) {\n throw new TypeError(\"a 'send' transfer needs the bytes to send\");\n }\n this.source = opts.data instanceof Uint8Array ? opts.data : Uint8Array.from(opts.data);\n } else {\n if (opts.data !== undefined) {\n throw new TypeError(\"a 'receive' transfer takes no data; the host supplies it\");\n }\n this.source = new Uint8Array(0);\n }\n this.bufferSize = boundDftBufferSize(opts.bufferSize ?? DFT_BUF_DEFAULT);\n }\n\n get result(): TransferResult | undefined { return this.outcome; }\n get complete(): boolean { return this.outcome !== undefined; }\n get isMessage(): boolean { return this.messageFlag; }\n /** The last upload frame, for the Read Modified hook. */\n get retainedFrame(): Uint8Array | undefined { return this.savedFrame; }\n /** Bytes transferred so far, for a progress display. */\n get transferred(): number {\n return this.direction === 'send' ? this.offset : this.receivedLength;\n }\n\n /**\n * Ask for the transfer to stop. Idempotent, and deliberately does NOT send\n * anything: DFT defers, checking the flag on the next inbound frame\n * (`ft_dft.c:225-228`, `:576-579`). CUT sends its abort immediately because a\n * front end closing a form has no later frame to wait on; a DFT transfer always\n * has a next inbound frame, so the deferral is free and matches x3270.\n */\n cancel(): void {\n this.cancelRequested = true;\n }\n\n /** Feed one `SF_TRANSFER_DATA` payload in; get the reply out. */\n handle(payload: Uint8Array): DftStep {\n if (this.outcome !== undefined) return { done: this.outcome };\n\n const frame = parseDftFrame(payload);\n\n // The cancellation edge: x3270 tests this in dft_data_insert and\n // dft_get_request, before processing either (ft_dft.c:225, :576). NOT for a\n // message frame, which is how the host's own final message still gets read.\n if (this.cancelRequested && !this.messageFlag\n && (frame.requestType === DftRequest.DATA_INSERT || frame.requestType === DftRequest.GET)) {\n return this.fail(FT_MSG.USER_CANCEL, frame.requestType);\n }\n\n switch (frame.requestType) {\n case DftRequest.OPEN: return this.onOpen(frame.payload);\n case DftRequest.DATA_INSERT: return this.onDataInsert(frame.payload);\n case DftRequest.CLOSE: return this.onClose();\n case DftRequest.GET: return this.onGet();\n // NEITHER SENDS ANYTHING. Both x3270 handlers trace and return -- they are\n // empty functions, `dft_insert_request` at ft_dft.c:193-198 and\n // `dft_set_cur_req` at :414-419, each carrying a \"doesn't currently do\n // anything\" comment. So the correct reply is NO reply.\n //\n // An earlier draft of the plan had these acknowledge with an Open ack,\n // which would put six unsolicited bytes on the wire to a live mainframe\n // mid-transfer. Returning {} is not laziness; it is what the reference does.\n case DftRequest.SET_CUR:\n case DftRequest.INSERT: return {};\n default:\n // ALSO SILENT. x3270's default arm traces \"Unsupported(0x%04x)\" and\n // breaks (ft_dft.c:131-133) -- it does not send an error reply, and\n // inventing one would answer a frame the host may not expect an answer to.\n // The caller traces; this returns nothing.\n return { unsupported: frame.requestType };\n }\n }\n\n /**\n * An `Open`. Acknowledged either way \u2014 x3270 acks at `ft_dft.c:181`, which is\n * AFTER it has set `message_flag` at `:171-176` but unconditional on it, so a\n * message open gets the same six bytes a file open does.\n *\n * `message_flag` is a single flag RESET by every Open, which is what lets one\n * transfer be a file and then a message. The received chunks must survive that\n * \u2014 see `onClose` for the sequence, and note there is a test asserting the\n * chunks are not discarded here, because clearing them would deliver an empty\n * file while reporting success.\n */\n private onOpen(payload: Uint8Array): DftStep {\n const open = parseDftOpen(payload);\n this.messageFlag = open.isMessage;\n // recnum = 1 at ft_dft.c:178, in dft_open_request and nowhere else.\n this.recordNumber = 1;\n return { reply: buildOpenAck() };\n }\n\n private onDataInsert(payload: Uint8Array): DftStep {\n const declared = (payload[DATA_LENGTH_AT]! << 8) | payload[DATA_LENGTH_AT + 1]!;\n const length = declared - LENGTH_OVERHEAD;\n const data = payload.subarray(DATA_AT, DATA_AT + Math.max(0, length));\n\n if (this.messageFlag) return this.onMessage(data);\n\n // x3270 guards the file write with `if (my_length > 0)` (ft_dft.c:284) and\n // acknowledges OUTSIDE that block (:410, after it closes at :407), so an empty\n // frame is acked rather than treated as an error.\n if (data.length > 0) {\n // Copied, not retained: `payload` is a view into the inbound record's buffer\n // and the caller is free to reuse it. A subarray here would alias a buffer\n // that the next read overwrites, which would corrupt every chunk but the last.\n this.chunks.push(Uint8Array.from(data));\n this.receivedLength += data.length;\n }\n const reply = buildDataAck(this.recordNumber);\n this.recordNumber++;\n return { reply };\n }\n\n /**\n * A message frame. **This is the completion channel** \u2014 the host's text ends the\n * transfer either way, and it is the only path on which `ft_complete` is called.\n * On a download the received bytes are handed over here, not on the `Close`.\n *\n * The frame is acknowledged as well as completing: `dft_data_ack()` runs at\n * `ft_dft.c:250`, before the text is inspected at `:267`.\n */\n private onMessage(data: Uint8Array): DftStep {\n const reply = buildDataAck(this.recordNumber);\n this.recordNumber++;\n\n let text = '';\n for (const b of data) text += String.fromCharCode(b);\n // A '$' terminates the message (ft_dft.c:258-265).\n const dollar = text.indexOf('$');\n if (dollar >= 0) text = text.slice(0, dollar);\n\n // A PREFIX test, matching `memcmp(msgp, END_TRANSFER, strlen(END_TRANSFER))`\n // at ft_dft.c:268 -- NOT an equality test. A real host sends \"TRANS03\" followed\n // by its own wording, so an equality test would report every successful\n // transfer as a failure whose error text is the success message.\n if (text.startsWith(END_TRANSFER)) {\n this.outcome = this.direction === 'receive'\n ? { ok: true, data: this.join() }\n : { ok: true };\n } else {\n this.outcome = { ok: false, error: text };\n }\n return { reply, done: this.outcome };\n }\n\n /**\n * A `Close` acknowledges and **does NOT end the transfer.**\n *\n * MEASURED FROM THE SOURCE, and it is the opposite of the obvious guess:\n * `ft_complete` is called from exactly three places, all inside the message\n * branch of `dft_data_insert` (`ft_dft.c:270`, `:273`, `:276`).\n * `dft_close_request` (`:672-688`) only writes a CloseAck. The real sequence is:\n *\n * Open(\"FT:DATA\") -> Data... -> Close -> Open(\"FT:MSG\") -> Data(TRANS03)\n *\n * and it is that last message that completes the transfer. `message_flag` is one\n * flag reset by every `Open` (`:171-175`), which is what lets a single transfer\n * be a file and then a message.\n *\n * So this instance MUST survive its own Close. Ending here would report success\n * before the host said `TRANS03` \u2014 i.e. before the host has committed the file \u2014\n * and would discard the host's error text on a failure, which is the one channel\n * that carries it.\n */\n private onClose(): DftStep {\n return { reply: buildCloseAck() };\n }\n\n /**\n * The host is asking us for data: this is the upload path.\n *\n * Either a data frame or, when the source is exhausted, an EOF frame. **EOF is\n * not a failure** \u2014 `TR_ERR_EOF` is how an upload ends, and the host answers it\n * with a Close and then its own `FT:MSG`. Reporting it as an error would fail\n * every successful upload; completing on it would claim success before the host\n * had committed the file.\n */\n private onGet(): DftStep {\n const room = this.bufferSize - UPLOAD_OVERHEAD;\n const chunk = this.source.subarray(this.offset, this.offset + room);\n\n if (chunk.length === 0) {\n // 10 bytes, SF length 9 -- the same SHAPE as dft_abort's frame\n // (ft_dft.c:698-706), differing only in the final code: TR_ERR_EOF where an\n // abort sends TR_ERR_CMDFAIL. The high byte is HIGH8(TR_GET_REQ) = 0x46,\n // because this answers a GET (ft_dft.c:645-650).\n const eof = Uint8Array.of(\n AID.SF, ...u16(9), Sfid.TRANSFER_DATA,\n (DftRequest.GET >> 8) & 0xff, DftReply.ERROR,\n ...u16(DftHeader.ERROR), ...u16(DftError.EOF),\n );\n this.savedFrame = eof;\n // `offset` is deliberately NOT advanced: `transferred` feeds a progress\n // display, and an EOF that moved it would overstate what the host received.\n return { reply: eof };\n }\n\n this.offset += chunk.length;\n const frame = Uint8Array.of(\n AID.SF, ...u16(chunk.length + UPLOAD_HEADER_LEN - 1), Sfid.TRANSFER_DATA,\n ...u16(DftReply.GET),\n ...u16(DftHeader.RECNUM), ...u32(this.recordNumber),\n ...u16(DftHeader.NOT_COMPRESSED), DftHeader.BEGIN_DATA,\n ...u16(chunk.length + LENGTH_OVERHEAD),\n ...chunk,\n );\n this.recordNumber++;\n this.savedFrame = frame;\n return { reply: frame };\n }\n\n private fail(error: string, failedRequest: number): DftStep {\n this.outcome = { ok: false, error };\n return { reply: buildDftError(failedRequest), done: this.outcome };\n }\n\n private join(): Uint8Array {\n const out = new Uint8Array(this.receivedLength);\n let at = 0;\n for (const c of this.chunks) { out.set(c, at); at += c.length; }\n return out;\n }\n}\n", "/**\n * Split a host argument into prefixes, LU names, host and port.\n *\n * s3270's documented shape is `[prefix:][LUname@]hostname[:port]`, with prefixes\n * stacked one colon each \u2014 `S:C:host` \u2014 and multiple LUs comma-separated and tried in\n * order as rejections come back (`telnet.c` `setup_lus`/`next_lu`).\n *\n * Its own file rather than an addition to `tls.ts`, because `tls.ts` is about TLS and\n * an LU name is not. Both consume prefixes, so prefix splitting lives here and the\n * TLS-relevant letters are handed on.\n *\n * PREFIX RECOGNITION IS RESTRICTED TO SINGLE LETTERS FROM s3270'S OWN SET,\n * deliberately. s3270 requires a colon per prefix and treats `SC:host` as a syntax\n * error (\"double ':'\"). Matching that keeps us from silently applying prefixes an\n * operator could not have got from s3270, and it is also what stops a one-character\n * hostname being eaten as a prefix.\n */\n\n/**\n * The prefix letters s3270 accepts, and nothing else: `pfxstr` is\n * `\"AaCcLlNnPpSsBbYyTt\"` at Common/split_host.c:38, and the meanings are the\n * `host_flags_t` enum in include/split_host.h.\n *\n * A broader class was wrong in both directions. `Z:host` became prefix `Z` plus host\n * `host`, where s3270 treats the whole thing as a host and then fails on the port \u2014\n * so a typo silently changed the target instead of being reported.\n */\nconst PREFIX_LETTERS = 'ACLNPSBYT';\nexport interface HostSpec {\n readonly host: string;\n /** LU names in the order written; not sorted, not de-duplicated. */\n readonly lus: readonly string[];\n /** Upper-cased single-letter prefixes, in the order written. */\n readonly prefixes: readonly string[];\n /** Left as text: the caller owns port validation and defaulting. */\n readonly portText: string | undefined;\n}\n\nexport function parseHostSpec(raw: string): HostSpec {\n let rest = raw;\n const prefixes: string[] = [];\n for (;;) {\n const m = /^([A-Za-z]):(.*)$/.exec(rest);\n if (m === null) break;\n const letter = m[1]!.toUpperCase();\n if (!PREFIX_LETTERS.includes(letter)) break;\n prefixes.push(letter);\n rest = m[2]!;\n }\n\n const lus: string[] = [];\n // The LAST @, so an LU name containing one cannot swallow the host.\n const at = rest.lastIndexOf('@');\n if (at !== -1) {\n for (const n of rest.slice(0, at).split(',')) {\n // An empty entry would become a CONNECT with no name, which is a request for a\n // nameless resource rather than for the default one.\n if (n === '') throw new Error('empty LU name in the LU list');\n lus.push(n);\n }\n rest = rest.slice(at + 1);\n }\n\n // A bracketed IPv6 literal owns its colons; only a colon AFTER the bracket is a port\n // separator. Without this, `[::1]:3271` parses as several unknown prefixes and an\n // empty host.\n if (rest.startsWith('[')) {\n const close = rest.indexOf(']');\n if (close === -1) throw new Error('unterminated [ in the host argument');\n const host = rest.slice(1, close);\n if (host === '') throw new Error('no host in the host argument');\n const tail = rest.slice(close + 1);\n return {\n host, lus, prefixes,\n portText: tail.startsWith(':') ? tail.slice(1) : undefined,\n };\n }\n\n const colon = rest.lastIndexOf(':');\n const host = colon === -1 ? rest : rest.slice(0, colon);\n if (host === '') throw new Error('no host in the host argument');\n return {\n host, lus, prefixes,\n portText: colon === -1 ? undefined : rest.slice(colon + 1),\n };\n}\n\n/** A host argument with every prefix applied and the port resolved. */\nexport interface ResolvedHost {\n readonly host: string;\n readonly port: number;\n readonly lus: readonly string[];\n /**\n * `false` from an `N:` prefix; `undefined` when the argument said nothing, so a\n * `-tn3270e` flag or the session default still decides. Not defaulted to `true`\n * here: \"the operator asked for TN3270E\" and \"the operator said nothing\" have to\n * stay distinguishable, or the contradiction check below cannot fire.\n */\n readonly tn3270e: boolean | undefined;\n /** `L:` was given. TLS is already our default, so this only matters against `-insecure`. */\n readonly tlsRequested: boolean;\n}\n\n/**\n * What each prefix we do not implement would do in s3270, and the flag to reach the\n * nearest thing we have.\n *\n * REFUSED RATHER THAN IGNORED, every one. Each changes what s3270 puts on the wire, so\n * dropping one silently hands the operator a session they did not ask for and cannot\n * see in the trace \u2014 the same reasoning that makes `L:` alongside `-insecure` an error\n * rather than a downgrade. `B:` is absent from this table because it is \"now a no-op\"\n * in s3270 itself (split_host.h), so accepting and ignoring it matches its behavior\n * exactly.\n */\nconst UNIMPLEMENTED_PREFIXES: Readonly<Record<string, string>> = Object.freeze({\n A: 'means an NVT/ANSI session rather than 3270 (ANSI_HOST). This client is 3270-only.',\n C: 'suppresses the login wait (NO_LOGIN_HOST). We never wait for a login, so the '\n + 'prefix would be a no-op here -- but it is refused rather than ignored, because '\n + 'a script that relies on it is relying on s3270 behavior we have not verified.',\n P: 'connects through a telnet passthru proxy (PASSTHRU_HOST), which is not implemented.',\n S: 'asks for the standard data stream, not the extended one (STD_DS_HOST). Use a '\n + 'terminal type without the -E suffix instead, e.g. -model 3278-2.',\n T: 'disables telnet negotiation entirely (NO_TELNET_HOST), which is not implemented.',\n Y: 'skips certificate verification (NO_VERIFY_CERT_HOST). Use -noverifycert.',\n});\n\n/**\n * Apply the prefixes and resolve the port.\n *\n * ONE function rather than a rule mirrored into each front end. The two arg parsers\n * already diverged once over `-insecure` \u2014 the fix is pinned by\n * `packages/tui/test/harness-flags.test.ts` \u2014 and a prefix that disabled TN3270E in\n * the TUI but not the CLI would be that bug again. The TUI calls this from its arg\n * parser and the CLI's runner calls it from `Connect()`.\n *\n * The error constructor is injected so each front end raises its own type: the TUI's\n * `UsageError` prints a usage line, while the runner's plain `Error` becomes an\n * s3270 `error` reply.\n */\nexport function resolveHostSpec(\n raw: string,\n mkError: (message: string) => Error,\n): ResolvedHost {\n let spec: HostSpec;\n try {\n spec = parseHostSpec(raw);\n } catch (e) {\n // Re-raised through the caller's constructor so a malformed host argument is\n // reported the same way as an unsupported prefix, rather than as an internal fault.\n throw mkError(e instanceof Error ? e.message : String(e));\n }\n\n for (const p of spec.prefixes) {\n const why = UNIMPLEMENTED_PREFIXES[p];\n if (why !== undefined) throw mkError(`the ${p}: host prefix ${why}`);\n }\n\n return {\n host: spec.host,\n port: resolvePort(spec.portText, raw, mkError),\n lus: spec.lus,\n // Only ever false or undefined: there is no prefix that turns TN3270E on.\n tn3270e: spec.prefixes.includes('N') ? false : undefined,\n tlsRequested: spec.prefixes.includes('L'),\n };\n}\n\n/**\n * The port, defaulting to 23.\n *\n * VALIDATED, unlike the `Number()` in the `splitTarget` this replaces: that turned\n * `host:abc` into port `NaN` and handed it to `net.connect`, which reports something\n * about an invalid option rather than about the argument the operator typed. A strict\n * digits-only test rather than `Number()`, because `Number` accepts `' 23 '`, `0x17`,\n * `1e3` and `''` \u2014 none of which s3270 would take, and the last of which is what\n * `host:` produces.\n *\n * 23 even for `L:`, matching s3270. Redirecting to 992 would open a connection\n * somewhere the operator did not type.\n */\nfunction resolvePort(\n portText: string | undefined,\n raw: string,\n mkError: (message: string) => Error,\n): number {\n if (portText === undefined) return 23;\n if (!/^\\d+$/.test(portText)) {\n throw mkError(\n `${JSON.stringify(raw)} has no usable port: ${JSON.stringify(portText)} is not a number. `\n + 'The shape is [prefix:][LU,LU@]host[:port].');\n }\n const port = Number(portText);\n if (port < 1 || port > 65535) {\n throw mkError(`port ${port} in ${JSON.stringify(raw)} is outside 1-65535`);\n }\n return port;\n}\n", "import { createConnection } from 'node:net';\nimport { connect as tlsConnect } from 'node:tls';\nimport { readFileSync } from 'node:fs';\nimport type { Connection } from '@tn3270/core';\n\n/**\n * TLS for the host connection, and the flags that select it.\n *\n * Separate from runner.ts for two reasons: runner.ts is already long and has one\n * job (command semantics), and this file needs `node:fs` to read a CA file,\n * which runner.ts deliberately does without so its commands stay testable with\n * no temp directory.\n *\n * `packages/core` is untouched by all of this. `Session` takes an injected\n * `connect: (host, port) => Connection` (core/src/session.ts), so the protocol\n * layer never learns that TLS exists \u2014 it is handed a `Connection` that happens\n * to be encrypted. Design: docs/superpowers/specs/2026-08-25-tls-support-design.md\n */\n\n/**\n * How the socket is made. A discriminated union, so that once flags are\n * resolved, a contradictory combination cannot be represented at all.\n */\nexport type TlsOptions =\n | { readonly kind: 'plaintext' }\n | { readonly kind: 'tls'; readonly verify: boolean; readonly caFile?: string };\n\n/** TLS on, verified against the system trust store. The product default. */\nexport const DEFAULT_TLS: TlsOptions = Object.freeze({ kind: 'tls', verify: true });\n\n/**\n * How long the TLS handshake may stall before we give up and say so.\n *\n * NOT optional, and not a nicety. A plaintext 3270 host does not reject a TLS\n * handshake \u2014 IT HANGS. Hercules writes `IAC DO TERMINAL-TYPE` (ff fd 18) and\n * waits; OpenSSL reads that leading 0xff as a record content type and blocks for\n * a length that never arrives. Measured 2026-08-25 and pinned by\n * test/tls-harness.test.ts.\n *\n * An earlier version of this comment named the first option TN3270E (40). That\n * was wrong, and remeasuring on 2026-08-27 corrected it: NEITHER Hercules system\n * ever sends option 40 at all \u2014 both open with TERMINAL-TYPE (24) and go on to\n * BINARY and EOR. The hang mechanism is unaffected, since it turns on the leading\n * 0xff rather than on which option follows it, but the byte was misreported. See\n * docs/superpowers/specs/2026-08-27-stage2b-tn3270e-design.md for the full\n * negotiation capture from both hosts.\n * Without this deadline, `tn3270 localhost:3270` under a TLS default appears to\n * do nothing whatsoever.\n *\n * 10 s because a handshake is two round trips and these hosts are on a LAN or\n * loopback, while a genuinely slow link should not be called dead too early.\n */\nexport const HANDSHAKE_TIMEOUT_MS = 10_000;\n\n/** Raw flag state, before conflicts are resolved. */\nexport interface TlsFlags {\n insecure?: boolean;\n /** Undefined means \"not mentioned\", which resolves to verifying. */\n verify?: boolean;\n caFile?: string;\n}\n\n/** Usage text for the TLS flags, shared so the two front ends cannot drift. */\nexport const TLS_USAGE = '[-insecure] [-noverifycert] [-verifycert] [-cafile FILE]';\n\n/**\n * Consumes a TLS flag if `flag` is one, returning how many extra argv entries it\n * ate; returns undefined if the flag is none of ours, so the caller's switch can\n * fall through to its own handling.\n *\n * Both front ends delegate here rather than each spelling the flags out, because\n * two copies of these rules is two things to keep in step. `resolveHostSpec` exists\n * for the same reason, on the other half of the same argument.\n */\nexport function takeTlsFlag(\n flags: TlsFlags,\n flag: string,\n value: string | undefined,\n usageError: (message: string) => Error,\n): number | undefined {\n switch (flag) {\n case '-insecure':\n flags.insecure = true;\n return 0;\n // s3270's spelling is `-noverifycert` (include/resources.h:583), which we\n // adopt for free compatibility; `-no-verify` is accepted because it is what\n // a person guesses.\n case '-noverifycert':\n case '-no-verify':\n flags.verify = false;\n return 0;\n case '-verifycert':\n flags.verify = true;\n return 0;\n case '-cafile':\n if (value === undefined) {\n throw usageError('-cafile needs a PEM file, e.g. -cafile /etc/ssl/host.pem');\n }\n flags.caFile = value;\n return 1;\n default:\n return undefined;\n }\n}\n\n/**\n * Turns raw flags into a `TlsOptions`, rejecting contradictions.\n *\n * Contradictions are usage errors rather than precedence rules on purpose. If\n * `-insecure -cafile x.pem` quietly picked a winner, half the people who typed\n * it would get the opposite of what they meant, and one of those two outcomes is\n * an unencrypted connection. Better to refuse.\n *\n * `usageError` is injected because each front end has its own `UsageError` class\n * and each maps it to exit status 2; throwing our own would need both catch\n * blocks widened.\n */\nexport function resolveTls(flags: TlsFlags, usageError: (message: string) => Error): TlsOptions {\n if (flags.insecure === true) {\n if (flags.verify !== undefined || flags.caFile !== undefined) {\n throw usageError(\n '-insecure disables TLS, so it cannot be combined with -verifycert, -noverifycert or -cafile',\n );\n }\n return { kind: 'plaintext' };\n }\n if (flags.verify === false && flags.caFile !== undefined) {\n throw usageError(\n '-noverifycert and -cafile contradict each other: -cafile verifies against that PEM, -noverifycert verifies nothing',\n );\n }\n const verify = flags.verify !== false;\n return flags.caFile !== undefined\n ? { kind: 'tls', verify, caFile: flags.caFile }\n : { kind: 'tls', verify };\n}\n\n/**\n * How the session was started, which decides how a TLS remedy is worded. `'flag'` is a command\n * line, where `-insecure` is the fix; `'dialog'` is the GUI's Connect dialog, where there is no\n * command line to add a flag to and the fix is its TLS checkbox (spec\n * 2026-11-05-packaging-design.md section 2, \"TLS hang\").\n */\nexport type TlsRemedy = 'flag' | 'dialog';\n\n/**\n * Explains a TLS failure in terms of the flag that would change it.\n *\n * This mapping is most of the feature's usability. A TLS failure against a\n * plaintext host is otherwise indistinguishable from a host being down, and the\n * fix \u2014 one flag \u2014 is not guessable from `ECONNRESET`.\n *\n * `remedy: 'dialog'` rewords only the two plaintext-host cases, the only ones whose fix the Connect\n * dialog has a control for; the certificate cases name flags the dialog cannot set, which is still\n * the truth about how to fix them. THE DIALOG WORDING PUTS THE REMEDY FIRST, because a message is\n * read by someone who may see only its start (memory note *fixed-width-messages-lose-their-point*).\n * The `'flag'` wording is left exactly as it shipped.\n */\nexport function describeTlsError(\n code: string | undefined, host: string, port: number, remedy: TlsRemedy = 'flag',\n): string {\n const dialog = remedy === 'dialog';\n switch (code) {\n case 'HANDSHAKE_TIMEOUT':\n return dialog\n ? `Untick TLS in the Connect dialog: ${host}:${port} accepted the connection but never `\n + 'completed a TLS handshake, so it probably does not speak TLS \u2014 a Hercules or other '\n + 'vintage system.'\n : `${host}:${port} accepted the connection but never completed a TLS handshake. `\n + 'If it does not speak TLS \u2014 a Hercules or other vintage system \u2014 use -insecure.';\n case 'ERR_SSL_WRONG_VERSION_NUMBER':\n case 'ERR_SSL_PACKET_LENGTH_TOO_LONG':\n return dialog\n ? `Untick TLS in the Connect dialog: ${host}:${port} answered with something that is not `\n + 'TLS, so it is almost certainly a plaintext host.'\n : `${host}:${port} answered with something that is not TLS. `\n + 'It is almost certainly a plaintext host: use -insecure.';\n case 'DEPTH_ZERO_SELF_SIGNED_CERT':\n case 'SELF_SIGNED_CERT_IN_CHAIN':\n case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':\n return `${host}:${port} presented a certificate that no trusted CA vouches for. `\n + 'Prefer -cafile FILE with a copy of its certificate, which still authenticates the '\n + 'host; -noverifycert also connects but authenticates nothing.';\n case 'ERR_TLS_CERT_ALTNAME_INVALID':\n return `${host}:${port} presented a valid certificate issued for a different name. `\n + 'Connect using the name on the certificate, or -noverifycert to ignore the mismatch.';\n case 'CERT_HAS_EXPIRED':\n return `${host}:${port} presented an expired certificate. `\n + 'Use -cafile with a current copy, or -noverifycert to ignore the expiry.';\n default:\n return `TLS connection to ${host}:${port} failed${code !== undefined ? ` (${code})` : ''}.`;\n }\n}\n\n/** Socket codes worth saying in words; anything else keeps Node's own message. */\nconst CONNECT_WORDS: Readonly<Record<string, string>> = {\n ECONNREFUSED: 'connection refused',\n ENOTFOUND: 'no such host',\n ETIMEDOUT: 'timed out',\n EHOSTUNREACH: 'host unreachable',\n};\n\n/**\n * Explains a PLAINTEXT connect failure -- one where TLS was never involved, so `describeTlsError`'s\n * \"TLS connection to ... failed\" would send the reader to a setting that was not on.\n *\n * The action and the target come first (`cannot connect to host:port`), for the same truncation\n * reason the dialog's TLS wording leads with its remedy. A code this does not know keeps the\n * underlying message when there is one, rather than inventing words for it.\n */\nexport function describeConnectError(\n code: string | undefined, host: string, port: number, message?: string,\n): string {\n const target = `cannot connect to ${host}:${port}`;\n const words = code !== undefined ? CONNECT_WORDS[code] : undefined;\n if (words !== undefined) return `${target}: ${words} (${code})`;\n if (message !== undefined && message !== '') return `${target}: ${message}`;\n return code !== undefined ? `${target} (${code})` : target;\n}\n\n/** Wraps a socket as the `Connection` the core session consumes. */\nfunction adapt(sock: import('node:net').Socket): Connection {\n const conn: Connection = {\n write: (b) => { sock.write(b); },\n close: () => { sock.destroy(); },\n onData: undefined,\n onClose: undefined,\n onError: undefined,\n };\n sock.on('data', (b: Buffer) => conn.onData?.(new Uint8Array(b)));\n sock.on('close', () => conn.onClose?.());\n return conn;\n}\n\n/**\n * Opens a connection to the host, encrypted or not.\n *\n * The plaintext path is byte-for-byte the behavior that shipped before TLS\n * existed, so `-insecure` is a true escape hatch rather than a differently\n * configured TLS path.\n */\nexport function tcpConnect(\n host: string,\n port: number,\n options: TlsOptions = DEFAULT_TLS,\n handshakeMs: number = HANDSHAKE_TIMEOUT_MS,\n): Promise<Connection> {\n if (options.kind === 'plaintext') {\n return new Promise((resolve, reject) => {\n const sock = createConnection({ host, port });\n const conn = adapt(sock);\n sock.on('error', (e: Error) => { conn.onError?.(e); reject(e); });\n sock.on('connect', () => resolve(conn));\n });\n }\n\n let ca: Buffer | undefined;\n if (options.caFile !== undefined) {\n try {\n ca = readFileSync(options.caFile);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return Promise.reject(new Error(`-cafile ${options.caFile} cannot be read: ${msg}`));\n }\n }\n\n return new Promise((resolve, reject) => {\n const sock = tlsConnect({\n host,\n port,\n rejectUnauthorized: options.verify,\n ...(ca !== undefined ? { ca: [ca] } : {}),\n // Doubles as the handshake deadline: measured to fire on a stalled\n // handshake, so no second timer is needed.\n timeout: handshakeMs,\n });\n const conn = adapt(sock);\n let settled = false;\n const fail = (code: string | undefined, err?: Error) => {\n if (settled) return;\n settled = true;\n const e: Error & { code?: string } = new Error(describeTlsError(code, host, port), { cause: err });\n // THE CODE RIDES ALONG so a front end can re-word the remedy for how the session was started\n // (`describeTlsError`'s `remedy`); the message above is the command-line wording. Our own\n // HANDSHAKE_TIMEOUT has no underlying error, so `code` itself is it; otherwise only a real\n // library code is copied -- `fail` is also handed a bare message when there is none, and that\n // is not a code.\n const realCode = err !== undefined ? (err as NodeJS.ErrnoException).code : code;\n if (realCode !== undefined) e.code = realCode;\n conn.onError?.(e);\n sock.destroy();\n reject(e);\n };\n\n sock.on('timeout', () => { fail('HANDSHAKE_TIMEOUT'); });\n sock.on('error', (e: NodeJS.ErrnoException) => {\n // After a successful handshake this is an ordinary mid-session socket\n // error, which belongs to the session, not to connect().\n if (settled) { conn.onError?.(e); return; }\n fail(e.code ?? e.message, e);\n });\n sock.on('secureConnect', () => {\n // MANDATORY. `timeout` is an INACTIVITY timer, not a handshake timer: a\n // 3270 session is idle for as long as the user is reading the screen, so\n // leaving it armed would drop live sessions at think-time. There is a\n // regression test for exactly this, because it is the line an implementer\n // would naturally omit.\n sock.setTimeout(0);\n settled = true;\n resolve(conn);\n });\n });\n}\n", "import { parseOversize, OversizeError, type Oversize } from '@tn3270/core';\n\n/**\n * `-oversize`, shared by every front end -- mirrors `tls.ts`'s `takeTlsFlag`, for the\n * same reason that file states: two copies of one rule is two things to keep in step.\n *\n * Parsing itself (`<cols>x<rows>`, `auto`, `none`) lives in `@tn3270/core`'s\n * `parseOversize` -- this is only the argv plumbing around it.\n */\n\n/** Usage text for the flag, shared so the front ends cannot drift. */\nexport const OVERSIZE_USAGE = '[-oversize <cols>x<rows>|auto|none]';\n\n/**\n * Consumes `-oversize` if `flag` is it, returning how many extra argv entries it ate\n * (always 1, for the value); returns undefined if the flag is none of ours, so the\n * caller's switch can fall through to its own handling -- same contract as `takeTlsFlag`.\n */\nexport function takeOversizeFlag(\n out: { oversize?: Oversize },\n flag: string,\n value: string | undefined,\n usage: (message: string) => Error,\n): number | undefined {\n if (flag !== '-oversize') return undefined;\n if (value === undefined) {\n throw usage('-oversize needs a value: <cols>x<rows>, auto or none');\n }\n try {\n out.oversize = parseOversize(value);\n } catch (e) {\n if (e instanceof OversizeError) throw usage(e.message);\n throw e;\n }\n return 1;\n}\n", "import {\n OversizeError, resolveTerminalType, resolveAlternateSize,\n type GeometrySetting, type Oversize,\n} from '@tn3270/core';\n\n/**\n * ONE model-or-oversize choice from a list, and the rule for applying it. Shared by every front\n * end that offers the choices as a list -- the GUI's Model menu and the web gateway's `Alt-M`\n * overlay -- so the two cannot drift on what a click does. Moved here from `gui/src/menu.ts`.\n */\n\n/** What one list item asks for: ONE field, because one click changes one thing. */\nexport type GeometryChoice = { readonly model: string } | { readonly oversize: Oversize };\n\n/** The model as the host will see it: core's resolution, `IBM-` stripped. */\nfunction modelName(model: string): string {\n return resolveTerminalType({ model }).replace(/^IBM-/, '');\n}\n\n/**\n * Why `current.oversize` cannot go with `model`, as a notice for the operator, or `undefined` if\n * that is not the reason. Decided from the SETTING, not by parsing core's message.\n *\n * NEITHER TEXT QUOTES CORE'S REMEDY, which is written for a command line: `use -model 3278-4-E`\n * names a flag, and `use at least 132x27` names a size, and a list offers neither. Each says what\n * happened first (the status line truncates), then a choice the list DOES offer.\n */\nfunction explainReset(current: Oversize, model: string): string | undefined {\n const name = modelName(model);\n if (!name.endsWith('-E')) {\n return `oversize is now None: model ${name} has no extended data stream; `\n + `pick ${name}-E for an oversize`;\n }\n const alt = resolveAlternateSize({ model });\n if (current.kind === 'fixed' && alt !== undefined\n && (current.rows < alt.rows || current.cols < alt.cols)) {\n return `oversize is now None: ${current.cols}x${current.rows} is smaller than model ${name}; `\n + 'pick Auto, or a model it fits';\n }\n return undefined;\n}\n\n/**\n * Apply one choice through `setGeometry`, and THE ORDER-DEPENDENCE RULE.\n *\n * A list changes one thing per click, and core validates model and oversize TOGETHER -- the\n * CLI's `Set()` found that some single steps are refused (commit `1a4713e`): from a fixed 100x30,\n * model 3278-5-E alone is refused. The CLI's answer, both in one call, is not available to a\n * radio item. So: A MODEL CHOICE WINS. If the CURRENT oversize is what refuses it -- the model has\n * no extended data stream, or a fixed size is below the model -- the model is applied with\n * oversize None and the returned notice says so.\n *\n * WHY THE MODEL WINS AND NOT THE OVERSIZE: refusing would leave models unreachable from a list\n * whose only fixed size is the startup one. None always fits a known model, so every model stays\n * reachable, and the startup size comes back by picking a model it fits and then the size.\n *\n * AN OVERSIZE CHOICE IS NEVER FIXED UP by changing the model behind it: that would change the\n * thing the operator did not touch. Its refusal, and every refusal that is not explained above\n * (`disconnect first`, `wait for the connection attempt`, an unknown model), is thrown unchanged.\n * If the retry with None is refused too, that second refusal is the one thrown.\n *\n * `setGeometry` is a parameter so this is testable with no session.\n */\nexport function applyGeometryChoice(\n current: GeometrySetting, choice: GeometryChoice, setGeometry: (next: GeometrySetting) => void,\n): string | undefined {\n if (!('model' in choice)) {\n setGeometry({ ...current, oversize: choice.oversize });\n return undefined;\n }\n const next: GeometrySetting = { ...current, model: choice.model };\n try {\n setGeometry(next);\n return undefined;\n } catch (err) {\n if (!(err instanceof OversizeError) || current.oversize.kind === 'none') throw err;\n const notice = explainReset(current.oversize, choice.model);\n if (notice === undefined) throw err;\n setGeometry({ ...next, oversize: { kind: 'none' } });\n return notice;\n }\n}\n", "import { Session, type GeometrySetting, type Size } from '@tn3270/core';\nimport { tcpConnect, DEFAULT_TLS, type TlsOptions } from './tls.js';\n\n/**\n * A session whose socket is TLS unless told otherwise.\n *\n * `tls` defaults to `DEFAULT_TLS` \u2014 verified TLS \u2014 because that is the product\n * default, and a default argument that meant plaintext would make every caller\n * that forgot the parameter silently insecure. `-insecure` passes\n * `{ kind: 'plaintext' }` explicitly.\n */\nexport function defaultSession(\n terminalType?: string,\n tls: TlsOptions = DEFAULT_TLS,\n /**\n * The model's ALTERNATE screen size, from `resolveAlternateSize`. Absent leaves\n * the session a model 2, where the alternate size equals the default 24x80.\n */\n alternate?: { readonly rows: number; readonly cols: number },\n /**\n * Offer TN3270E. Absent means the product default, which is ON -- matching x3270,\n * and safe because the negotiation backs off to traditional tn3270 on a reject.\n */\n tn3270e?: boolean,\n /**\n * Request the BIND-IMAGE function. Absent means the product default, ON.\n * `-bind-image off` (Task 11) passes `false`.\n */\n bindImage?: boolean,\n /**\n * Range-check a BIND's geometry against the model. Absent means the product\n * default, ON, matching x3270's `bind_limit`. `-bind-limit off` (Task 11) passes\n * `false`.\n */\n bindLimit?: boolean,\n /**\n * Device-name template for NEW-ENVIRON's DEVNAME uservar. Absent means the session\n * refuses telnet option 39 entirely -- see `SessionOptions.devname`. `-devname`\n * (Task 7) passes the template string.\n */\n devname?: string,\n /**\n * Advertise Query Reply (DDM), which lets a host choose DFT file transfer.\n *\n * **ABSENT MEANS ON.** This said \"the product default, which is OFF\" until 2026-10-27, and\n * that had been false since the default flipped on 2026-09-29. `Session` decides it with\n * `this.opts.ddm !== false` in TWO places that must agree -- the capabilities build\n * (`core/src/session.ts`, the Query Reply) and the `ddmAdvertised` getter a front end uses to\n * tell the operator \"DDM was not advertised\" -- so `undefined` advertises and only an explicit\n * `-ddm off` suppresses it.\n *\n * THE STALE VERSION COST REAL DIAGNOSIS TIME: a live TSO transfer stalled at 0 bytes, and this\n * comment was read as evidence that DDM had not been offered, which would have made the host's\n * fallback to CUT the explanation. The wire said otherwise. A default's polarity is exactly the\n * kind of claim that outlives its flip, because nothing executes a comment.\n */\n ddm?: boolean,\n /**\n * Geometry mode (Task 2's `SessionOptions.geometry`): the setting to resolve at every\n * connect, and the `auto` measure to resolve it with. When given, `terminalType` and\n * `alternate` above MUST be undefined -- `Session`'s constructor throws otherwise,\n * geometry being one source of truth or none (see its \"ONE SOURCE OF GEOMETRY\" comment).\n */\n geometry?: { readonly setting: GeometrySetting; readonly measure?: () => Size | undefined },\n): Session {\n return new Session({\n connect: (h, p) => tcpConnect(h, p, tls),\n ...(terminalType ? { terminalType } : {}),\n ...(tn3270e === undefined ? {} : { tn3270e }),\n ...(alternate !== undefined\n ? { alternateRows: alternate.rows, alternateCols: alternate.cols }\n : {}),\n ...(bindImage === undefined ? {} : { bindImage }),\n ...(bindLimit === undefined ? {} : { bindLimit }),\n ...(devname === undefined ? {} : { devname }),\n ...(ddm === undefined ? {} : { ddm }),\n ...(geometry !== undefined ? { geometry: geometry.setting } : {}),\n ...(geometry?.measure !== undefined ? { measure: geometry.measure } : {}),\n });\n}\n", "/**\n * Terminal byte sequences to NAMED 3270 actions.\n *\n * This module decides nothing about 3270 semantics. It answers one question --\n * \"which named action did the user just press?\" -- and `Keyboard` in core owns\n * everything that follows: field-aware typing, tab, EraseEOF, insert mode, and\n * the keyboard-lock rules. So the action names here are deliberately the same\n * ones the CLI's command table uses, and adding a key is a table entry.\n *\n * ## WHERE THESE SEQUENCES CAME FROM\n *\n * All of them were measured with `tput -T xterm-256color <cap>` on the\n * development box rather than taken from memory or from the plan. Two\n * corrections to the plan came out of that, both the same shape:\n *\n * **The arrow keys and Home have TWO encodings each, and terminfo reports the\n * one the plan did not.** `tput kcuu1` gives `\\x1bOA` (SS3) and `tput khome`\n * gives `\\x1bOH`, while the plan's tests used `\\x1b[A` and `\\x1b[H` (CSI). Both\n * are correct: xterm sends CSI in normal cursor mode and SS3 once DECCKM is set,\n * and `smkx` is exactly `\\x1b[?1h\\x1b=`. terminfo documents the application-mode\n * form because it assumes `smkx` has been sent. Since any layer -- us, tmux,\n * screen -- can flip that mode, BOTH are in the table. Supporting one would lose\n * the arrow keys outright, depending on the terminal.\n *\n * The function keys needed no correction: `kf1`-`kf24` matched the plan exactly,\n * including the irregularity worth knowing about -- F7-F11 are `18/19/20/21/23~`,\n * with **no `22~`** -- and shifted Fn is `;2` with PF(n+12), the c3270\n * convention.\n *\n * ## PARTIAL VERSUS NULL IS THE INTERESTING PART\n *\n * A lone ESC is both a legal keypress and the first byte of every function key,\n * so it cannot be resolved from the buffer alone. This module reports that fact\n * and refuses to guess; `app.ts` owns what happens next -- arming a timer for a\n * lone ESC just as it does for a genuinely unfinished multi-byte sequence like\n * `\\x1b[`, but PROMOTING it to a Meta prefix for PA1/PA2/PA3 on expiry rather\n * than discarding it. Guessing here would either eat the following keystroke\n * or emit a spurious PA.\n */\n\nimport { PF_AIDS } from '@tn3270/core';\n\nexport type Action =\n | { kind: 'pf'; n: number }\n | { kind: 'pa'; n: number }\n | { kind: 'enter' }\n | { kind: 'clear' }\n | { kind: 'reset' }\n | { kind: 'up' }\n | { kind: 'down' }\n | { kind: 'left' }\n | { kind: 'right' }\n | { kind: 'home' }\n | { kind: 'tab' }\n | { kind: 'backTab' }\n | { kind: 'backspace' }\n | { kind: 'delete' }\n | { kind: 'eraseEOF' }\n | { kind: 'eraseInput' }\n | { kind: 'attn' }\n | { kind: 'sysreq' }\n // Dup and Field Mark are TYPED CHARACTERS, not AIDs -- see `applyAction` and\n // `Keyboard.dup`/`Keyboard.fieldMark` in core.\n | { kind: 'dup' }\n | { kind: 'fieldMark' }\n | { kind: 'toggleInsert' }\n // NO CHORD, IN EITHER FRONT END, AND THAT IS A MEASURED DECISION RATHER THAN AN OVERSIGHT.\n // c3270 binds Newline to Ctrl-J in BOTH its keymaps -- `Ctrl<Key>j: Newline()` at\n // Common/fb-c3270:190 in the non-Windows table a terminal build reads, and `Ctrl <Key>j:\n // Newline()` at :100 in the _WIN32 one -- but Ctrl-J IS `\\n` (0x0a), which `buildTable` below\n // already maps to `enter` for terminals that send LF for Return. ONE BYTE CANNOT BE BOTH, and\n // Enter is the AID that submits the screen; quietly turning Return into a cursor move is not a\n // trade this feature gets to make. x3270's Shift-Return is no help either: terminals do not\n // report modifiers on Return. Recorded as the user's call on 2026-09-14 --\n // \"leave SysReq and Newline to the keypad, where a button has no spelling problem\" --\n // and this member is the other half of that decision, not a reversal of it: the keypad button\n // and the TUI overlay are Newline's routes, exactly as they are Sys Req's.\n | { kind: 'newline' }\n // Not a 3270 key at all: it shows or hides the front end's own virtual keypad, and\n // `applyAction` throws on it exactly as it does on `quit`. It lives in this union because\n // the chord that opens the keypad, the GUI's Alt-K and the `BINDING_INTENT` row must all\n // name it the way every other action is named.\n //\n // NOT because a keypad BUTTON needs it: `KEYPAD_KEYS` must contain neither this nor\n // `quit`, since a drawn button for either would be a button whose only effect is to throw.\n | { kind: 'toggleKeypad' }\n // The front end's own FULL 122-KEY KEYBOARD surface, `Ctrl-B`, added 2026-10-22. `applyAction`\n // throws on it exactly as it does on `toggleKeypad` and `quit`: what a surface looks like is the\n // front end's business. It lives in this union so the chord, the GUI's menu item and the\n // browser's key mapper can all name it the way every other action is named.\n //\n // TWO OF THE FOUR FRONT ENDS IMPLEMENT IT, and that asymmetry is the design rather than an\n // omission: the TUI renders `KEYPAD_KEYS` as a 29-column name-and-chord LIST because it cannot\n // draw a keyboard -- c3270's own keypad is 78 columns wide and that front end refuses to draw\n // below 24x80 -- so `buildTable` below deliberately does NOT map `0x02`. The CLI has no\n // interactive surface at all.\n //\n // NEITHER `KEYPAD_KEYS` NOR `KEYBOARD_3279` MAY CONTAIN IT, for a sharper version of the reason\n // `toggleKeypad` gives: a drawn button for this would be a button that toggles the surface it is\n // drawn on. Both tables' tests pin that.\n | { kind: 'toggleKeyboard' }\n // The front end's MODEL/OVERSIZE CHOOSER, `Alt-M`, added 2026-11-04: the GUI's Model menu and the\n // web page's picker overlay, each choosing the geometry the NEXT connect uses. `applyAction`\n // throws on it exactly as it does on `toggleKeyboard`: what a menu or an overlay looks like is the\n // front end's business. It lives in this union so the chord, both DOM front ends and the GUI's\n // child-window funnel can name it the way every other action is named.\n //\n // THE CHOICE DOES NOT TRAVEL AS THIS ACTION. This only OPENS the chooser; what the operator picks\n // is a separate message per front end (the web page's `setGeometry`), because a model is not a\n // keystroke and this union carries no field for one.\n //\n // TWO OF THE FOUR FRONT ENDS IMPLEMENT IT, as with `toggleKeyboard`, and for a different reason:\n // the TUI changes SIZE by being resized and changes MODEL only by flag, so it has no chooser and\n // `buildTable` below maps no sequence to this kind. The CLI has `Set(model,...)` instead.\n //\n // NEITHER `KEYPAD_KEYS` NOR `KEYBOARD_3279` MAY CONTAIN IT: a 3279 had no model key, and a button\n // for it would be a button whose only effect is to throw.\n | { kind: 'geometryMenu' }\n // Opens the front end's file-transfer dialog, and `applyAction` throws on it for the same\n // reason it throws on `toggleKeypad`: what a dialog looks like is the front end's business.\n //\n // THE ONE ACTION THAT CANNOT BE COMPLETED BY THE KEY THAT STARTS IT. Every other member of\n // this union is self-contained -- an AID, a cursor move, a character -- but a transfer needs\n // a local path, a direction and a host file name, which is why it opens a form instead of\n // doing something. That is also why it is the first member `KEYPAD_KEYS` carries despite\n // `applyAction` throwing on it: unlike `toggleKeypad`, whose button would be a button that\n // toggles the surface it is drawn on, a transfer button is a perfectly sensible thing to\n // click -- the front end intercepts it exactly as it intercepts the chord. See the note on\n // `KeypadKey.action`.\n | { kind: 'transferForm' }\n // THE RECTANGLE, NOT THE TEXT, and that is forced rather than chosen: the renderer only ever\n // receives a `DrawList`, whose `DrawCell` carries a CG-order atlas glyph and NO character\n // (`canvas/src/drawlist.ts:111-121`). So the front end that holds the `Session` extracts the\n // text from `resolve(snapshot)`, where `text` and `hidden` both live. Sending coordinates is\n // also the discipline `keypadButtonCenter` already documents -- it keeps the seam honest.\n //\n // `applyAction` THROWS on this, like `quit`, `toggleKeypad` and `transferForm`: what a clipboard\n // is belongs to the front end, and the GUI has an OS clipboard where the gateway has the\n // operator's browser.\n //\n // SPELLED INLINE AND NOT IMPORTED AS `CellRect`, WHICH IS NOT AN OVERSIGHT: `CellRect` lives in\n // `@tn3270/canvas`, and the graph is `core <- frontend <- { cli, tui, canvas, gui, web }` --\n // `frontend` importing from `canvas` would INVERT it and create a cycle (verified from the\n // package.json files: `canvas` depends on `frontend`, not the reverse). The four fields are\n // structurally identical, so `extractText(cells, cols, action.rect)` typechecks without a cast.\n // Do not \"tidy\" this into an import.\n | { kind: 'copy'; rect: { top: number; left: number; bottom: number; right: number } }\n /**\n * THE OPERATOR ASKED TO COPY, CARRYING NO RECT. 2026-10-27, for the web copy gesture.\n *\n * SEPARATE FROM `copy` BECAUSE A KEY MAPPER CANNOT SEE A SELECTION. The spec for this feature\n * first said the chord should raise `{kind:'copy', rect}` straight from `canvas/src/keys.ts`, and\n * that is not implementable: `copy` above requires the four rect fields, while `actionForKey(e:\n * KeyLike)` (`canvas/src/keys.ts:164`) is a pure function of one key event, and the selection\n * lives in `canvas/src/renderer.ts`'s module-level `anchor`/`focus` (`:88-89`). So the keymap\n * raises the INTENT and the renderer -- the one place that holds the rectangle -- turns it into a\n * real `copy`.\n *\n * BOTH HALVES NOW EXIST, and this paragraph used to say the opposite. Written in `7f2f52e` --\n * which added this member, `applyAction`'s throw and the gateway's refusal, but NOTHING that\n * produces the kind -- it read \"That conversion is a LATER TASK of this feature; this member is\n * the vocabulary it needs, and nothing raises the kind yet\". True for exactly two commits.\n * `9e14d86` made `actionForKey` RAISE it from `Ctrl-Shift-C` / `Cmd-Shift-C`\n * (`canvas/src/keys.ts`, the branch above the case-folded `CTRL` lookup), and `9a6e53e` made\n * `canvas/src/renderer.ts`'s keydown listener CONVERT it, intercepting the kind before\n * `clearSelection()` and calling `copySelection()`. `4d56edb` then added the Electron GUI's\n * `requestCopy` drop, which is what keeps the kind out of `applyAction` in a child window.\n *\n * CITED BY COMMIT AND BY NAME RATHER THAN BY LINE, because the line numbers in the paragraph\n * above this one have already been falsified once on this branch -- see the note on\n * `renderer.ts`'s `__tn3270Copy` global, where two citations drifted within one commit of being\n * written. A \"not yet\" is the shape that rots fastest: nothing fails when it comes true.\n *\n * THE SEVENTH KIND `applyAction` THROWS ON, so the front end that raises it must own it, and a\n * front end that forgot gets a loud failure instead of a chord that silently does nothing.\n *\n * THAT IS THE ONLY RESPECT IN WHICH IT RESEMBLES `toggleKeypad`, and the distinction is worth\n * drawing because the spec and the plan both got it wrong: the two-step CONVERSION above is\n * `transferForm`'s shape, not `toggleKeypad`'s. `toggleKeypad` is terminally intercepted --\n * `web/src/bridgecore.ts` calls `deps.toggleKeypad?.()` and returns, converting it to nothing --\n * whereas `transferForm` opens an overlay that LATER emits different messages, which is exactly\n * what `requestCopy` does in becoming a `copy`.\n *\n * IT MUST NEVER REACH A `Session`, AND THE GATEWAY REFUSES IT AT DECODE for a reason that does\n * not expire the way `copy`'s and `paste`'s did (`web/src/protocol.ts`): the server does not know\n * which cells the operator dragged, so there is no message it could send back that would make\n * this kind actionable there. `copy` carries the rectangle and is answered with `copyText`; this\n * carries nothing, and the browser is the only party that can supply what is missing. So it takes\n * `quit`'s branch of that file's two-branch rule -- a REJECTION -- rather than the interception\n * branch. (`toggleKeypad` was itself rejected there once, in `c94979f`, before `2fb921c`\n * replaced that stop-gap with the interception it has now; so \"rejection\" and \"interception\" are\n * branches a kind can move between, not permanent labels. What makes this one permanent is that\n * the server can never acquire the rectangle.)\n *\n * NO FIELDS MEANS NOTHING TO SHAPE-CHECK, which is the one way it is easier than `copy`: the\n * refusal is unconditional, so there is no malformed-field path to bound.\n */\n | { kind: 'requestCopy' }\n | { kind: 'type'; text: string }\n /**\n * PASTE, 2026-10-23, AND IT IS DELIBERATELY NOT `type` -- BUT NOT FOR THE REASON THE PLAN GAVE.\n *\n * The plan for this feature said that carrying a paste as `type` would send every `\\n` as\n * **Enter, an AID that submits a half-filled panel to a live host** and every `\\f` as **Clear,\n * which wipes the screen**. THAT IS TRUE OF x3270 AND FALSE OF THIS CODE, and the difference was\n * MEASURED rather than reasoned: `applyAction` dispatches `type` to `Keyboard.typeString`, which\n * is a bare loop over `type(ch)`; `Keyboard` contains no reference to `AID` or `sendAID` at all,\n * and `type` routes every character through `CodePage.fromUnicode`. Driven directly,\n * `typeString('AB\\nC\\fD')` leaves cells `c1 c2 25 c3 0c c4` -- the `\\n` and `\\f` land as the\n * EBCDIC bytes 0x25 and 0x0c IN THE BUFFER, and nothing reaches the wire.\n *\n * SO THE REAL DEFECT IS CORRUPTION, NOT SUBMISSION, and it is still a defect worth a separate\n * kind. Those two control bytes are written into fields as though they were characters, the MDT\n * is set on each field they touch, and the next AID the operator does send carries them to the\n * host as field data. `pasteString` instead gives `\\n` a newline (suppressed after a wrap), `\\f`\n * a SPACE, drops `\\r` so CRLF text gains no stray character per line, aborts on a keyboard lock\n * or a wrap past the start, and returns `{typed, dropped, reason?}` -- none of which `typeString`\n * has. Those rules are measured from x3270's `Common/kybd.c`; the line numbers are tabulated in\n * `paste.ts`'s header and are deliberately NOT repeated here, there being no second copy to keep\n * in step and no reference checkout in this repo to re-verify them against.\n *\n * SO THE TEXT IS THE SAME AND THE SEMANTICS ARE NOT, which is what earns a separate kind.\n *\n * `applyAction` THROWS on this, like `copy` and the four before it: a clipboard belongs to the\n * front end, and for `paste` the text comes from the OPERATOR's machine -- the OS clipboard in\n * the GUI, a browser `paste` event at the far end of a socket in the gateway.\n */\n | { kind: 'paste'; text: string }\n | { kind: 'quit' };\n\n/**\n * Sentinel: these bytes are a valid PREFIX of a longer sequence, so the caller\n * must wait for more input rather than acting or discarding.\n *\n * Returning `null` instead would conflate \"wait\" with \"impossible\", and the\n * caller's two correct responses to those are opposite: keep buffering, versus\n * throw the bytes away. A bare ESC is the case that matters -- it is a legal key\n * AND the start of every function key, and this module cannot tell them apart\n * from bytes alone. app.ts owns what happens next -- timing it out the same as\n * an unfinished sequence, but promoting it to a Meta prefix on expiry rather\n * than discarding it; this module stays pure.\n */\nexport const PARTIAL = Symbol('partial');\n\n/**\n * The PF sequences, `tput kf1`..`kf24`. PF(n) for F(n), PF(n+12) for Shift+F(n).\n * Note the gap: there is no `22~`.\n */\nconst PF_KEYS: readonly (readonly [string, number])[] = [\n ['\\x1bOP', 1], ['\\x1bOQ', 2], ['\\x1bOR', 3], ['\\x1bOS', 4],\n ['\\x1b[15~', 5], ['\\x1b[17~', 6], ['\\x1b[18~', 7], ['\\x1b[19~', 8],\n ['\\x1b[20~', 9], ['\\x1b[21~', 10], ['\\x1b[23~', 11], ['\\x1b[24~', 12],\n ['\\x1b[1;2P', 13], ['\\x1b[1;2Q', 14], ['\\x1b[1;2R', 15], ['\\x1b[1;2S', 16],\n ['\\x1b[15;2~', 17], ['\\x1b[17;2~', 18], ['\\x1b[18;2~', 19], ['\\x1b[19;2~', 20],\n ['\\x1b[20;2~', 21], ['\\x1b[21;2~', 22], ['\\x1b[23;2~', 23], ['\\x1b[24;2~', 24],\n];\n\nfunction buildTable(): Map<string, Action> {\n const t = new Map<string, Action>();\n for (const [seq, n] of PF_KEYS) t.set(seq, { kind: 'pf', n });\n\n // Cursor keys and Home, BOTH encodings -- see the header note on DECCKM.\n t.set('\\x1b[A', { kind: 'up' });\n t.set('\\x1b[B', { kind: 'down' });\n t.set('\\x1b[C', { kind: 'right' });\n t.set('\\x1b[D', { kind: 'left' });\n t.set('\\x1bOA', { kind: 'up' });\n t.set('\\x1bOB', { kind: 'down' });\n t.set('\\x1bOC', { kind: 'right' });\n t.set('\\x1bOD', { kind: 'left' });\n t.set('\\x1b[H', { kind: 'home' });\n t.set('\\x1bOH', { kind: 'home' });\n\n t.set('\\x1b[Z', { kind: 'backTab' }); // tput kcbt\n t.set('\\x1b[3~', { kind: 'delete' }); // tput kdch1\n t.set('\\x7f', { kind: 'backspace' }); // tput kbs\n t.set('\\t', { kind: 'tab' });\n t.set('\\r', { kind: 'enter' });\n t.set('\\n', { kind: 'enter' }); // some terminals send LF for Return\n\n // `\\x1b[4~` is vt220 Select/End. Bound to EraseEOF as a deliberate CHOICE,\n // following c3270; it is not a terminfo-derived mapping.\n t.set('\\x1b[4~', { kind: 'eraseEOF' });\n\n // The Insert key toggles insert mode, as x3270 does (fb-x3270:210). MEASURED:\n // `tput kich1` is `\\x1b[2~` on the development box.\n t.set('\\x1b[2~', { kind: 'toggleInsert' });\n\n // Control keys. Ctrl-C is Clear, not interrupt: Clear is an AID a 3270 user\n // needs constantly (it dismisses MORE...), and Ctrl-] is the way out.\n // Attn is c3270's Ctrl-A (Common/fb-c3270:83). It is a Telnet BREAK, not an AID.\n t.set('\\x01', { kind: 'attn' });\n t.set('\\x03', { kind: 'clear' });\n t.set('\\x12', { kind: 'reset' });\n t.set('\\x15', { kind: 'eraseInput' });\n t.set('\\x1d', { kind: 'quit' });\n\n // Dup and Field Mark are c3270's own Ctrl-D and Ctrl-F, and BOTH of its keymaps agree:\n // `Ctrl<Key>d: Dup()` / `Ctrl<Key>f: FieldMark()` at Common/fb-c3270:186-187 (the\n // non-Windows table, which a terminal build reads) and the same pair at :88 and :93 in the\n // _WIN32 one. They are TYPED CHARACTERS, not AIDs -- see the note on the Action union.\n t.set('\\x04', { kind: 'dup' });\n t.set('\\x06', { kind: 'fieldMark' });\n // The keypad/overlay toggle, ALSO c3270's own: `Ctrl<Key>k: Keypad()`, fb-c3270:191. Its\n // _WIN32 keymap spells the same command Alt-K (:48), which would arrive here as `ESC k` --\n // and new bindings do not go down the ESC path, whose two regressions are recorded in\n // app.ts. The canvas front ends, which read a real KeyboardEvent and so have no ESC\n // ambiguity, accept both. Sys Req gets NO chord: c3270 defines none in either keymap, which\n // is why the TUI's overlay is its only keyboard route.\n t.set('\\x0b', { kind: 'toggleKeypad' });\n // The transfer form. NOT c3270's -- it binds no transfer key at all, in either half of\n // fb-c3270, so there is no reference spelling to match and the choice is ours. `0x14` is free\n // in both of its keymaps and was free here: the control bytes this table already takes are\n // 01 03 04 06 0b 12 15 1d 7f.\n t.set('\\x14', { kind: 'transferForm' });\n\n // The PA keys have no terminal equivalent, so ESC-digit, as c3270 does.\n t.set('\\x1b1', { kind: 'pa', n: 1 });\n t.set('\\x1b2', { kind: 'pa', n: 2 });\n t.set('\\x1b3', { kind: 'pa', n: 3 });\n return t;\n}\n\nconst TABLE: ReadonlyMap<string, Action> = buildTable();\n\n/** Every prefix of every table key, so PARTIAL is a lookup and not a scan. */\nconst PREFIXES: ReadonlySet<string> = (() => {\n const s = new Set<string>();\n for (const key of TABLE.keys()) {\n for (let i = 1; i < key.length; i++) s.add(key.slice(0, i));\n }\n return s;\n})();\n\n/** Printable ASCII, the only thing a 3270 field accepts as typed text. */\nfunction isPrintable(b: number): boolean {\n return b >= 0x20 && b <= 0x7e;\n}\n\n/**\n * How many leading bytes are printable ASCII; 0 if the first one is not.\n *\n * Exported so `app.ts` can consume a typed run in one pass instead of probing\n * prefix by prefix, which is O(n^2) on a paste. The printability rule stays here,\n * with the table, rather than being restated by the caller.\n */\nexport function printableRun(bytes: Uint8Array): number {\n let n = 0;\n while (n < bytes.length && isPrintable(bytes[n]!)) n++;\n return n;\n}\n\n/**\n * The longest sequence in the table, so a caller can bound its prefix scan.\n *\n * DERIVED, not written down: a longer key added below raises this automatically,\n * where a literal 7 would silently stop matching the new key's last byte.\n */\nexport const MAX_SEQUENCE_LENGTH: number =\n Math.max(...[...TABLE.keys()].map((k) => k.length));\n\n/**\n * The action for a buffer of terminal input.\n *\n * `PARTIAL` means \"a longer sequence may still arrive\"; `null` means \"discard\".\n * The caller must treat those differently -- see the note on PARTIAL.\n */\nexport function lookup(bytes: Uint8Array): Action | typeof PARTIAL | null {\n // An empty buffer is not a prefix of anything; returning PARTIAL here would\n // park the reader in a waiting state that only a timeout could leave.\n if (bytes.length === 0) return null;\n\n const seq = String.fromCharCode(...bytes);\n const exact = TABLE.get(seq);\n if (exact !== undefined) return exact;\n\n // A complete match is checked FIRST, so a key that is also a live prefix\n // resolves rather than waits.\n //\n // NO TEST CAN CURRENTLY PIN THIS ORDER, verified by mutation: swapping these\n // two blocks leaves all 25 tests green, because no key in TABLE is a proper\n // prefix of another one. (The near misses are not prefixes: `\\x1b[15~` and\n // `\\x1b[15;2~` diverge at `~` versus `;`, and PA1's `\\x1b1` differs from\n // `\\x1b[1;2P` at the second byte.) The order is kept because it becomes\n // load-bearing the moment a terminal needs a key that IS a prefix of another\n // -- then exact-first is the difference between acting and hanging until the\n // ESC timer fires. Do not write a test claiming to pin it as it stands.\n if (PREFIXES.has(seq)) return PARTIAL;\n\n // A run of printable bytes is typed text. Handling the whole run rather than\n // one byte keeps paste working without the caller re-entering per byte.\n let printable = true;\n for (const b of bytes) {\n if (!isPrintable(b)) { printable = false; break; }\n }\n if (printable) return { kind: 'type', text: seq };\n\n return null;\n}\n\n/**\n * Is this a PF number the architecture defines? Guards a hand-built action.\n *\n * The upper bound is `PF_AIDS.length`, NOT a literal 24: core's table of AID\n * bytes is the authority on how many PF keys exist, and a second hardcoded 24\n * here would be a copy to keep in step for no benefit.\n */\nexport function isValidPf(n: number): boolean {\n return Number.isInteger(n) && n >= 1 && n <= PF_AIDS.length;\n}\n", "/**\n * Which keys the virtual keypad and the TUI overlay offer, as DATA.\n *\n * ## ONE TABLE, TWO FRONT-END FAMILIES\n *\n * This lives in `frontend` because both package graphs reach it: `canvas` already imports from\n * here (`canvas/src/keys.ts:1` takes `Action`, `canvas/src/drawlist.ts:46` takes `schemeRgb`), and\n * so the Electron GUI and the web gateway get it; `tui` imports from here too. A copy in `canvas`\n * would be out of the TUI's reach, and two lists of 47 keys drift silently.\n *\n * Nothing here is geometry in PIXELS and nothing here draws: `row` and `col` are in CELLS, and\n * `canvas/src/keypad.ts` owns the cells-to-pixels arithmetic and the button rectangles.\n *\n * ## WHERE THE KEY SET COMES FROM\n *\n * c3270 ships a character-cell keypad, and its callback table is the authoritative list of what a\n * 3270 keypad is expected to carry: `Common/c3270/keypad.callbacks:1-44` is exactly PA1-3, Attn,\n * Erase EOF, Erase Input, Sys Req, Clear, Home, Cursor Select, Compose, Insert, Delete, Dup,\n * Field Mark, Tab, Reset, Back Tab, Newline, Enter and PF1-24 -- 44 keys. This table is that set\n * with two dropped and five added: 44 - 2 + 5 = 47.\n *\n * **Dropped by decision.** *Cursor Select*: `AID.SELECT` exists, but the key needs the\n * field-intensity parse and belongs with the light-pen work. *Compose*: it is x3270's own input\n * method, not a 3270 key. Those two are now the whole of it: the arithmetic above read `44 - 3 + 5\n * = 46` while Newline was the third dropped key, and it is dropped no longer.\n *\n * **Newline is HERE, and it was the branch's one open question.** It is on c3270's keypad,\n * `Keyboard.newline()` has existed in core (`core/src/keyboard.ts:348`) and the CLI could already\n * call it as `Newline()` (the `Newline` case in `cli/src/runner.ts`) -- but no interactive front end could reach it,\n * and this table inherited its absence from a spec that dropped it without noticing. That is the\n * same shape as `Session.sysreq()` before this branch: a capability with no interactive route,\n * which is precisely what this feature exists to fix, so its absence was never a decision like the\n * two above. Raised with the user 2026-09-16 and decided by them: add it. Row 4 ended at column 36\n * with `BkSp`, so it takes column 42 for a right edge of 48, well inside 72 and needing no new row\n * -- which is why the Electron window does not resize and only the keypad golden moved.\n *\n * **It has NO CHORD, in any front end.** c3270 binds Ctrl-J (`Common/fb-c3270:190`, and `:100` in\n * its _WIN32 keymap), and Ctrl-J is `\\n` -- already `enter` in the terminal keymap, for terminals\n * that send LF for Return. See the note on the `newline` member of the `Action` union in\n * `keymap.ts`, and the user's 2026-09-14 call recorded there. So the keypad button and the TUI\n * overlay ARE its keyboard route, and its chord column in the overlay is deliberately blank --\n * exactly as Sys Req's is, and for a documented reason rather than an omission.\n *\n * **Added.** The four cursor arrows and Backspace. c3270's keypad has none of these -- there are\n * no cursor callbacks in `keypad.callbacks`, and the arrow glyphs its layout draws are Tab, Back\n * Tab and Newline (`Common/c3270/keypad.full:8-11`), not cursor keys. c3270 can leave them out\n * because it is driven from a real keyboard that has them; a keypad that can be driven by MOUSE\n * ALONE cannot, or a mouse user could never move the cursor.\n *\n * That same misreading is why `NewLn` is LETTERS: the glyph c3270 draws for Newline is `<-+`, which\n * this table's first version took for a cursor arrow. Every label here is an abbreviation in words\n * -- `ErEOF`, `BkTab`, `FldMk`, `SysRq` -- so `NewLn` is the house style, and `NL` would be the one\n * label a reader has to be told the meaning of. Both fit the 5-character limit; this one is legible.\n *\n * No typewriter keys, though: a keypad earns its space by offering what a PC keyboard lacks, and a\n * clickable QWERTY would duplicate the hardware and eat the area the 3270 display needs.\n *\n * ## WHERE THE ARRANGEMENT COMES FROM\n *\n * A 122-key tn327x keyboard, in the way that matters for muscle memory: PF keys in a 2x12 block\n * across the top, modal keys in a left-hand cluster, cursor and edit keys on the right. PF13-24\n * sits ABOVE PF1-12, which is c3270's order too (`Common/c3270/keypad.labels:2` and `:4`).\n *\n * The TUI overlay ignores `row` and `col` and lists the keys in table order.\n */\n\nimport type { Action } from './keymap.js';\n\nexport interface KeypadKey {\n /** At most 5 characters: see the test. Longer labels overflow into the next key. */\n readonly label: string;\n /**\n * Never `quit` or `toggleKeypad`. `applyAction` throws on both, so a button for either would be\n * a button whose only effect is to throw -- and `applyAction`'s swallow would hide it. See the\n * note on `toggleKeypad` in keymap.ts, and the test that pins it.\n */\n readonly action: Action;\n /** For the TUI overlay and for accessibility: what this key is called in words. */\n readonly name: string;\n}\n\nconst pfRow = (from: number): KeypadKey[] => Array.from({ length: 12 }, (_, i) => ({\n label: `PF${from + i}`,\n action: { kind: 'pf', n: from + i },\n name: `PF${from + i}`,\n}));\n\nexport const KEYPAD_KEYS: readonly KeypadKey[] = Object.freeze([\n ...pfRow(13),\n ...pfRow(1),\n\n { label: 'PA1', action: { kind: 'pa', n: 1 }, name: 'PA1' },\n { label: 'PA2', action: { kind: 'pa', n: 2 }, name: 'PA2' },\n { label: 'PA3', action: { kind: 'pa', n: 3 }, name: 'PA3' },\n { label: 'Home', action: { kind: 'home' }, name: 'Home' },\n { label: '^', action: { kind: 'up' }, name: 'Cursor up' },\n { label: 'Ins', action: { kind: 'toggleInsert' }, name: 'Insert mode' },\n { label: 'Dup', action: { kind: 'dup' }, name: 'Dup' },\n { label: 'Reset', action: { kind: 'reset' }, name: 'Reset' },\n\n { label: 'Attn', action: { kind: 'attn' }, name: 'Attention' },\n { label: 'SysRq', action: { kind: 'sysreq' }, name: 'System Request' },\n { label: 'Clear', action: { kind: 'clear' }, name: 'Clear' },\n { label: '<', action: { kind: 'left' }, name: 'Cursor left' },\n { label: 'v', action: { kind: 'down' }, name: 'Cursor down' },\n { label: '>', action: { kind: 'right' }, name: 'Cursor right' },\n { label: 'FldMk', action: { kind: 'fieldMark' }, name: 'Field Mark' },\n { label: 'Enter', action: { kind: 'enter' }, name: 'Enter' },\n\n { label: 'ErEOF', action: { kind: 'eraseEOF' }, name: 'Erase EOF' },\n { label: 'ErInp', action: { kind: 'eraseInput' }, name: 'Erase Input' },\n { label: 'Tab', action: { kind: 'tab' }, name: 'Tab' },\n { label: 'BkTab', action: { kind: 'backTab' }, name: 'Back Tab' },\n { label: 'Del', action: { kind: 'delete' }, name: 'Delete' },\n { label: 'BkSp', action: { kind: 'backspace' }, name: 'Backspace' },\n // The 47th key, and the only one on this row past `BkSp`: row 4 ended at column 36, so this\n // takes 42 for a right edge of 48 -- inside 72, and NO NEW ROW, which is what keeps the Electron\n // window the same size and leaves the two non-keypad goldens byte-identical.\n { label: 'NewLn', action: { kind: 'newline' }, name: 'Newline' },\n // The 48th key, and the FIRST whose action `applyAction` throws on -- see the note on\n // `KeypadKey.action`, which this key made it necessary to rewrite. A transfer needs arguments,\n // so the button opens a form; the front end intercepts it exactly as it intercepts Ctrl-T.\n //\n // `NewLn` ended at 48, so this takes 48 for a right edge of 54 -- still inside 72 and still NO\n // NEW ROW, so the Electron window keeps its size and only the keypad golden moves. The gap at\n // column 18 is NOT free space: it separates the Tab/BkTab clusters deliberately.\n { label: 'Xfer', action: { kind: 'transferForm' }, name: 'File Transfer' },\n]);\n", "import type { Action } from './keymap.js';\n\n/**\n * Which key means which action, stated once in words.\n *\n * NOT A CODE GENERATOR, and deliberately so. The terminal keymap is a table of measured\n * byte sequences and the GUI's mapper will match Chromium `KeyboardEvent`s; deriving\n * either from this would mean re-expressing a live-verified table in a form nothing has\n * tested. What this gives instead is one place to read the INTENT, plus a test that the\n * terminal keymap agrees with it -- so a key added to one front end is visibly absent\n * from the other.\n *\n * `terminal` is the byte sequence the terminal keymap should return this action for, or\n * `undefined` where the binding is GUI-only or where the terminal encodes it in more\n * than one way. The arrows and Home have BOTH SS3 (`ESC O A`) and CSI (`ESC [ A`) forms\n * because any layer -- us, tmux, screen -- can flip DECCKM, so naming one of the two here\n * would assert less than `keymap.test.ts` already does and would imply the other is\n * wrong.\n *\n * Ctrl-C is Clear and NOT an interrupt, which is correct for a 3270 and surprising to\n * everyone: a 3270 user needs Clear constantly to dismiss VM's `MORE...` state. Ctrl-]\n * is therefore the way out, and every front end must say so on startup -- an\n * undocumented escape hatch is no escape hatch.\n */\nexport interface Binding {\n /** How a user would describe the key. */\n readonly key: string;\n readonly action: Action;\n /** The terminal byte sequence, where a single canonical one exists. */\n readonly terminal?: string;\n /** Why this binding, where it is not obvious. */\n readonly note?: string;\n}\n\nexport const BINDING_INTENT: readonly Binding[] = Object.freeze([\n {\n key: 'Enter', action: { kind: 'enter' }, terminal: '\\r',\n note: 'the Enter AID -- EXCEPT while disconnected, when it RECONNECTS to the same host '\n + 'instead of sending anything. VM prints \"Press Enter or Clear to continue\" as a LOGOFF '\n + 'drops the line, and both keys used to do nothing at all. A deliberate divergence: '\n + 'x3270 binds no key to `Reconnect()`. See `applyAction`, and the OIA, which says so',\n },\n {\n key: 'Ctrl-C', action: { kind: 'clear' }, terminal: '\\x03',\n note: 'the Clear AID, not an interrupt: it dismisses VM\\'s MORE... state. Also RECONNECTS '\n + 'while disconnected, exactly as Enter does -- VM names both keys',\n },\n {\n key: 'Ctrl-]', action: { kind: 'quit' }, terminal: '\\x1d',\n note: 'the way out, because Ctrl-C is Clear. Say so on startup.',\n },\n { key: 'Ctrl-R', action: { kind: 'reset' }, terminal: '\\x12' },\n { key: 'Ctrl-U', action: { kind: 'eraseInput' }, terminal: '\\x15' },\n { key: 'Tab', action: { kind: 'tab' }, terminal: '\\t' },\n { key: 'Shift-Tab', action: { kind: 'backTab' }, terminal: '\\x1b[Z' },\n { key: 'Backspace', action: { kind: 'backspace' }, terminal: '\\x7f' },\n { key: 'Delete', action: { kind: 'delete' }, terminal: '\\x1b[3~' },\n {\n key: 'End', action: { kind: 'eraseEOF' }, terminal: '\\x1b[4~',\n note: 'vt220 Select/End, bound to EraseEOF as a CHOICE following c3270 -- not a '\n + 'terminfo-derived mapping',\n },\n { key: 'F1', action: { kind: 'pf', n: 1 }, terminal: '\\x1bOP' },\n { key: 'F3', action: { kind: 'pf', n: 3 }, terminal: '\\x1bOR' },\n {\n key: 'F12', action: { kind: 'pf', n: 12 }, terminal: '\\x1b[24~',\n note: 'the function keys are irregular: F7-F11 are 18/19/20/21/23~, with no 22~',\n },\n {\n key: 'Shift-F1', action: { kind: 'pf', n: 13 }, terminal: '\\x1b[1;2P',\n note: 'Shift+F(n) is PF(n+12), the c3270 convention',\n },\n {\n key: 'Alt-1', action: { kind: 'pa', n: 1 }, terminal: '\\x1b1',\n note: 'the PA keys have no terminal equivalent, so ESC-digit, as c3270 does. The GUI '\n + 'matches e.code (Digit1), NOT e.key: on macOS Option-1 reports key \"\u00A1\".',\n },\n { key: 'Alt-2', action: { kind: 'pa', n: 2 }, terminal: '\\x1b2' },\n { key: 'Alt-3', action: { kind: 'pa', n: 3 }, terminal: '\\x1b3' },\n {\n key: 'Ctrl-A', action: { kind: 'attn' }, terminal: '\\x01',\n note: 'c3270\\'s own default (Common/fb-c3270:83). A Telnet BREAK, not an AID.',\n },\n {\n key: 'Insert', action: { kind: 'toggleInsert' }, terminal: '\\x1b[2~',\n note: 'x3270\\'s Toggle(insertMode) (fb-x3270:210); `tput kich1` measured \\\\x1b[2~',\n },\n {\n key: 'Ctrl-D', action: { kind: 'dup' }, terminal: '\\x04',\n note: 'c3270\\'s own binding (Common/fb-c3270:186, and :88 in its _WIN32 keymap). Writes '\n + 'EBCDIC 0x1c and then TABS to the next unprotected field: kybd.c:1435 suppresses '\n + '`key_Character`\\'s auto-skip for a keyboard Dup, but `Dup_action` moves the cursor '\n + 'itself at kybd.c:2790, so the tab happens exactly ONCE. The manual says so outright '\n + '(p. 7-12, \"a Tab key operation to be performed\")',\n },\n {\n key: 'Ctrl-F', action: { kind: 'fieldMark' }, terminal: '\\x06',\n note: 'c3270\\'s own binding (Common/fb-c3270:187, and :93 in its _WIN32 keymap). Writes '\n + 'EBCDIC 0x1e and auto-skips normally, as any typed character does -- the Dup '\n + 'suppression above is specific to EBC_dup',\n },\n {\n key: 'Ctrl-K', action: { kind: 'toggleKeypad' }, terminal: '\\x0b',\n note: 'shows the keypad (GUI, browser) or the special-keys overlay (TUI). ALSO c3270\\'s '\n + 'own: `Ctrl<Key>k: Keypad()` at Common/fb-c3270:191. Its _WIN32 keymap spells the same '\n + 'command Alt-K (:48), which would be ESC k in a terminal and so is not bound there; the '\n + 'canvas front ends accept both. NOT a 3270 key -- `applyAction` throws on it and each '\n + 'front end owns its own display',\n },\n {\n key: 'Ctrl-B', action: { kind: 'toggleKeyboard' },\n note: 'opens the FULL 122-key 3279 keyboard -- THE CANVAS FRONT ENDS ONLY, which is why this '\n + 'row gives NO `terminal` sequence. The TUI renders the keypad as a LIST because it '\n + 'cannot draw a keyboard inside 24x80, so `buildTable` deliberately leaves 0x02 unmapped '\n + 'and this entry documents an intent two of the four front ends honour. Stating a '\n + 'sequence here would be a lie the table\\'s own test catches, since that test demands '\n + 'every `terminal` resolve in the terminal keymap. NOT c3270\\'s either: it binds no such '\n + 'key in either keymap, so the spelling is ours. Invisible to the TUI overlay, which '\n + 'looks chords up BY ACTION from KEYPAD_KEYS -- a table this kind may never appear in, '\n + 'since a button for it would toggle the surface it is drawn on',\n },\n {\n key: 'Alt-M', action: { kind: 'geometryMenu' },\n note: 'opens the MODEL/OVERSIZE chooser -- the GUI\\'s Model menu, the browser\\'s picker overlay '\n + '-- whose choice applies at the next connect. THE CANVAS FRONT ENDS ONLY, which is why this '\n + 'row gives NO `terminal` sequence, as Ctrl-B\\'s does not: the TUI changes size by being '\n + 'RESIZED and model only by flag, so there is no chooser for a sequence to open, and Alt-M '\n + 'would be ESC m there besides. Matched on e.code (KeyM), NOT e.key: on macOS Option-M '\n + 'reports key \"\u00B5\", the Alt-K trap again. NOT c3270\\'s: it binds no such key. NOT a 3270 key '\n + '-- `applyAction` throws on it and each front end owns its own display',\n },\n {\n key: 'Ctrl-T', action: { kind: 'transferForm' }, terminal: '\\x14',\n note: 'opens the IND$FILE transfer form (TUI). NOT c3270\\'s -- it binds NO transfer key at '\n + 'all, in either half of Common/fb-c3270, so there is no reference spelling to match and '\n + '0x14 is free in both. NOT a 3270 key -- `applyAction` throws on it, because a transfer '\n + 'needs arguments and each front end owns its own dialog',\n },\n\n {\n key: 'Ctrl-Shift-C', action: { kind: 'requestCopy' },\n note: 'copies the selected cells. NO `terminal` sequence, and for a stronger reason than '\n + 'Ctrl-B\\'s: the TUI has neither a cell selection nor a clipboard of any kind -- no OSC '\n + '52, no selection state over the screen -- so there is nothing for a sequence to do, '\n + 'where Ctrl-B at least names a surface the TUI draws differently. A CANVAS-FRONT-END '\n + 'binding, and Cmd-Shift-C is its macOS spelling: this table names ONE key per action, so '\n + 'the pair is asserted directly in canvas/test/keys.test.ts. THE SHIFTED FORM IS '\n + 'DELIBERATE -- plain Ctrl-C is the Clear AID and stays Clear on every platform, which is '\n + 'why canvas/src/keys.ts matches this chord BEFORE its case-folded Ctrl table. NOT a 3270 '\n + 'key -- `applyAction` throws on it, because only the renderer knows which cells were '\n + 'dragged',\n },\n\n // Sequence-less on purpose: two encodings each. See the header note on DECCKM.\n { key: 'Up', action: { kind: 'up' } },\n { key: 'Down', action: { kind: 'down' } },\n { key: 'Left', action: { kind: 'left' } },\n { key: 'Right', action: { kind: 'right' } },\n { key: 'Home', action: { kind: 'home' } },\n]);\n", "/**\n * The 48 keys of `KEYPAD_KEYS` grouped for a DOM layout.\n *\n * ## WHY A SECOND GROUPING RATHER THAN `KeypadKey.row`/`col`\n *\n * (THOSE FIELDS NO LONGER EXIST -- deleted 2026-10-22, see below. This section is kept in the past\n * tense because it is the ARGUMENT for this file existing, and that argument is still live.)\n *\n * They WERE cell coordinates for a blitter. `KeypadKey.col` is a left edge in character\n * cells, `KEYPAD_KEY_WIDTH` cells per key (6, so twelve of them span 72 and fit inside an\n * 80-column screen -- `keypad.ts`'s own note on the constant), and `row` is an index into\n * `canvas/src/keypad.ts`'s `DRAWN_ROW` = `[0, 2, 4, 6, 8]`, which spreads the five table rows\n * over nine drawn ones so that no two buttons touch vertically. Both numbers exist to satisfy a\n * character grid that a DOM layout does not have: there is no column budget, and the blank\n * separator row exists because a one-cell-tall inverse-video button would otherwise merge into\n * the one above it (measured, same file) -- a problem CSS does not have either.\n *\n * Read the other way round, the cell coordinates ENCODE the clusters without naming them -- the\n * gap at column 18 on rows 2-4 \"is NOT free space: it separates the Tab/BkTab clusters\n * deliberately\" (`keypad.ts`, on `Xfer`). This file names what those gaps mean, so a reader does\n * not have to reconstruct the intent from column numbers, and so the blocks survive the deletion\n * of the canvas keypad that Task 9 of this feature performs.\n *\n * `row`/`col` ARE GONE FROM `KeypadKey`, DELETED 2026-10-22 -- as is the `KEYPAD_ROWS` /\n * `KEYPAD_KEY_WIDTH` pair they were checked against. This header used to say they were \"LEFT\n * ALONE ... live fields, not legacy ones\", because `canvas/src/keypad.ts` still read them, and\n * that they \"become dead only when that file is deleted\". That file WAS deleted with the canvas\n * keypad, so they did, and the full-keyboard work removed them.\n *\n * Three tests went with them (`keypad.test.ts`'s row grouping, whole-key column boundary and\n * overlap cases). They were not bad tests -- one of them says outright that \"Task 5 turns `col`\n * into pixels\" -- they went because their SUBJECT went: a blitter that no longer exists. The\n * equivalent arithmetic now lives in `keyboard3279.test.ts`, over the geometry that IS live.\n *\n * THE BLOCKS BELOW ARE NOW THE ONLY LAYOUT INFORMATION `KEYPAD_KEYS` CARRIES, which is why the\n * paragraphs above about naming the clusters matter more than they did: nothing else records that\n * `Tab` and `BkTab` belong to different groups.\n *\n * ## THE GROUPING IS WRITTEN DOWN, NOT DERIVED\n *\n * Deriving blocks from `col` gaps would have made this agree with the canvas layout by\n * construction, and so unable to disagree -- the same argument `KEYPAD_ROWS` made for itself\n * before it too was deleted. (It would also have tied a DOM layout to a table that the canvas\n * keypad's deletion stopped being the authority on -- which is exactly what happened.) Written down, a key that falls out of every block is a test failure, and a key\n * in two blocks is a test failure too -- see `keypadView.test.ts`, whose first case is the whole\n * protection against a dropped or doubled button.\n *\n * The blocks are therefore ALLOWED to disagree with the canvas rows, and they deliberately do.\n * The canvas has `Reset` at row 2 column 60 and `Enter` at row 3 column 60 -- a right-hand column\n * holding two keys with nothing in common but their column -- and `Xfer` at row 4 column 48,\n * placed there because, in its own note's words, `NewLn` \"ended at 48, so this takes 48 ... still\n * inside 72 and still NO NEW ROW\". That is a column budget talking, not a grouping. Here `Reset`\n * joins the modal keys whose state it clears, and `Enter`/`Xfer` get a block of their own.\n *\n * ## NO DOM, NO ELECTRON\n *\n * Pure data and pure functions, for the same reason `transferForm.ts` is: `vitest.config.ts` sets\n * `environment: 'node'`, there is no `document` in any test in this repo and jsdom is not a\n * dependency. Everything worth testing about the grouping is testable without an element. The\n * views that consume this (the GUI's keypad window, the web gateway's overlay) take their DOM\n * injected.\n */\n\nimport { KEYPAD_KEYS, type KeypadKey } from './keypad.js';\nimport { BINDING_INTENT } from './bindings.js';\n\nexport interface KeypadBlock {\n /**\n * Stable identifier, used as a DOM id suffix and in tests. Lowercase and hyphenated because it\n * ends up in an `id` attribute and in a CSS selector.\n */\n readonly id: string;\n /** Visible heading, in words, shown above the block's buttons. */\n readonly title: string;\n readonly keys: readonly KeypadKey[];\n}\n\n/**\n * The one key with this label, or a throw.\n *\n * THROWS RATHER THAN SKIPS. A typo'd label here would otherwise silently drop a button from the\n * window, and the test's \"exactly once\" case would report it as a count mismatch somewhere in a\n * flattened list of 48 rather than at the typo. For `SysRq` and `NewLn` a dropped button is worse\n * than cosmetic: those two have NO chord in ANY front end -- `keymap.ts` records \"Sys Req gets NO\n * chord: c3270 defines none in either keymap\", and `keypad.ts` records the same for Newline, whose\n * c3270 chord Ctrl-J is already `enter` in the terminal keymap -- so the keypad button and the\n * TUI overlay are their only interactive route at all.\n *\n * `Dup` and `FldMk` are often listed with those two (the spec's \"four otherwise-unreachable\n * keys\"), but they are not in the same position TODAY: both have Ctrl-D/Ctrl-F in the terminal\n * keymap (`keymap.ts:190-191`, from `Common/fb-c3270:186-187`) and in the GUI/web key mapper\n * (`canvas/src/keys.ts:77-78`). Losing their button would cost them the mouse, not the keyboard.\n */\nconst byLabel = (label: string): KeypadKey => {\n const key = KEYPAD_KEYS.find((k) => k.label === label);\n if (key === undefined) throw new Error(`no keypad key labelled ${label}`);\n return key;\n};\n\n/** Twelve consecutive PF keys, by label, so a renumbering of the table cannot reorder them. */\nconst pfRange = (from: number): readonly KeypadKey[] =>\n Array.from({ length: 12 }, (_, i) => byLabel(`PF${from + i}`));\n\nexport const KEYPAD_BLOCKS: readonly KeypadBlock[] = Object.freeze([\n // PF13-24 ABOVE PF1-12. That is `KEYPAD_KEYS`' own order -- it opens `pfRow(0, 13)` then\n // `pfRow(1, 1)` -- and its header cites c3270 for the same arrangement\n // (`Common/c3270/keypad.labels:2` and `:4`). Reversing it here would be a silent relearn for\n // anyone used to the shipped keypad, which is why the test pins the first label of each row\n // rather than just the count.\n { id: 'pf-high', title: 'PF13-24', keys: pfRange(13) },\n { id: 'pf-low', title: 'PF1-12', keys: pfRange(1) },\n {\n // The INTERRUPT keys: each one reaches the host OUTSIDE the ordinary read-modified exchange.\n // PA1-3 and Clear go as short-read AIDs; `SysRq` is Telnet IAC AO on a TN3270E session that\n // agreed the SYSREQ function and a TEST REQUEST READ on a classic one (`actions.ts:109-120`\n // records both forms); and `Attn` is TELNET BREAK.\n //\n // `Attn` IS `IAC BREAK` AND NOT `IAC IP`. Stated this loudly because the first version of\n // this comment said IP, and because the two are a real pair of Telnet commands that a reader\n // can plausibly swap. MEASURED: `Telnet.sendAttn()` is `Uint8Array.of(T.IAC, T.BREAK)` with\n // the trace label `'Attn (IAC BREAK)'` (`core/src/telnet.ts:225-230`), both it and\n // `Session.sendAttn()` (`core/src/session.ts:1616`) carry the docstring \"The 3270 Attn key is\n // Telnet BREAK (RFC 1576 \u00A78), not an AID\", three tests assert the exact two bytes\n // (`core/test/telnet.test.ts:448`, `core/test/session.test.ts:493`,\n // `cli/test/runner.test.ts:373`) and a fourth pins that it never reaches `sendAID`\n // (`frontend/test/actions.test.ts:120`, with the `sendAID` assertion at `:128`), and `T.IP` is\n // referenced NOWHERE in this repository (`grep -rn 'T\\.IP\\b' packages/*/src` -> no hits). IP\n // is not a loose synonym for BREAK: they are two distinct Telnet commands, and a reader who\n // swaps them has the wrong byte on the wire.\n //\n // BUT THAT IS OURS, NOT x3270's, AND WE DIVERGE FROM IT. An earlier draft of this comment\n // claimed x3270 \"draws the same distinction\", with `net_break` for Attn and `IAC IP` behind a\n // separate `Interrupt()` action. x3270 does close to the OPPOSITE, read from\n // `Common/kybd.c:978-1002`: `Attn_action`'s docstring is \"ATTN key, per RFC 2355. Sends IP,\n // regardless.\" and its FIRST branch is `if (IN_E) { if (net_bound()) net_interrupt(0); ... }`.\n // `net_break` is only the `IN_3270` fallthrough for a CLASSIC session. And `Interrupt_action`\n // is not a separate key at all any more: its docstring ends \"This is now the same as the Attn\n // action\" (`:1008`), so x3270 says the two have CONVERGED.\n //\n // A PA1-BEFORE-BREAK CLAIM WAS ALSO DRAFTED HERE AND IS REMOVED, BECAUSE IT IS NOT TRUE OF\n // THE VERSION THIS PROJECT CITES. A newer upstream `Attn_action` does send\n // `ctlr_read_modified(AID_PA1, false)` before `net_break(0)`, commented \"This is what PCOMM\n // does in plain TN3270 mode\" -- but that line does not exist in SUITE3270 4.5, which is the\n // tree in `~/src/suite3270-4.5` that every other citation in this repo is measured against\n // (`grep -rn 'ctlr_read_modified(AID_PA1' Common/*.c` -> zero hits there; the `IN_3270` arm is\n // a bare `net_break(0)`). Mixing versions is how a wrong wire constant gets in: cite the tree\n // we build and test against, and if a newer one diverges say WHICH version did what.\n //\n // ## FINDING: OUR ATTN DIVERGES FROM x3270 IN TN3270E MODE, AND IS UNTESTED THERE\n //\n // `Session.sendAttn()` (`core/src/session.ts:1617-1620`) is three lines with NO branch on\n // `inTn3270e()`: it calls `Telnet.sendAttn()`, which sends `{IAC, BREAK}` unconditionally. So\n // on a TN3270E session x3270 sends IP and we send BREAK. No test covers Attn in E mode --\n // the only three are classic (cited above) -- and nothing else in the repo records this.\n //\n // THE ASYMMETRY WITH `sysreq()` IS THE REASON TO THINK THIS IS AN OVERSIGHT RATHER THAN A\n // DECISION: `sysreq()` sits IMMEDIATELY ABOVE `sendAttn()` in the same file (`:1596`), it\n // branches on `inTn3270e()` as its first act, and its docstring opens \"TWO COMPLETELY\n // DIFFERENT THINGS ON THE WIRE, chosen by whether the session is TN3270E\" and cites x3270's\n // `IN_E` split for exactly that. The neighbouring key got the branch; this one did not, and\n // no comment anywhere says it was considered.\n //\n // DELIBERATELY NOT FIXED HERE. Changing what `Attn` puts on the wire is protocol work, it is\n // outside a keypad-presentation feature, and it wants a live TN3270E witness this project\n // does not have -- both of its live hosts answer `IAC WILL TN3270E` with DONT, measured three\n // times (`session.ts`, on `sysreq`). Recorded rather than silently repaired or silently\n // ignored. `actions.ts:108` is a bare `case 'attn': session.sendAttn(); break;` with no wire\n // note, which is why the citation for this half points at core and not at the dispatch.\n //\n // None of the six is the ordinary \"submit this screen\" that `Enter` is, which is why `Enter`\n // is not in this block despite also being an AID. `Reset` is the odd one, and it is here\n // deliberately: `Keyboard.reset()` (`core/src/keyboard.ts:434`) only calls `oia.reset()`, and\n // the input-inhibit state it clears is the state a host error and the AID keys leave you in.\n //\n // THAT IS AN AFFINITY, NOT A RULE, and the stronger version of it is false: `Enter` and all\n // 24 PF keys raise `SystemWait` through the same `sendAID` path from OTHER blocks\n // (`session.ts:1553`), the three operator-error inhibits come from typing into a protected or\n // numeric field rather than from any key here, and `Attn` -- which IS in this block -- does\n // not touch the OIA at all (`session.ts:1617-1620` goes straight to the Telnet layer). Reset\n // is grouped with the keys whose kind of trouble it answers; it is not claimed that this\n // block is the only way to need it.\n id: 'attention',\n title: 'Attention',\n keys: ['PA1', 'PA2', 'PA3', 'Attn', 'SysRq', 'Clear', 'Reset'].map(byLabel),\n },\n {\n // Everything that MOVES THE CURSOR without altering the buffer. `NewLn` is a cursor move in\n // 3270 terms and nothing else: `Keyboard.newline()` (`core/src/keyboard.ts:348`) is documented\n // \"first unprotected cell at or after the start of the next line\" and its whole body assigns\n // `s.cursor`. It belongs here, not beside `Enter`, which its name invites a reader to assume.\n //\n // The four arrows are `^ v < >` because that is what `KEYPAD_KEYS` labels them, and the\n // labels are the button text. `keypad.ts` records why they exist at all: c3270's keypad has\n // no cursor callbacks, but a keypad drivable by MOUSE ALONE needs them or a mouse user could\n // never move the cursor -- which is exactly the DOM keypad's case too.\n id: 'cursor',\n title: 'Cursor',\n keys: ['Home', '^', 'v', '<', '>', 'Tab', 'BkTab', 'NewLn'].map(byLabel),\n },\n {\n // Everything that CHANGES THE BUFFER locally, with no transmission to the host. `Dup` and\n // `FldMk` are here because they are TYPED CHARACTERS and not AIDs -- see the note on the\n // `Action` union in `keymap.ts` -- so a user hunting them under \"Attention\" beside PA1 would\n // be looking in the wrong place.\n //\n // `Ins` toggles a mode rather than editing, and it is the mode that decides what `Dup` and\n // `FldMk` write into -- the only two keys here that go through `writeControl`\n // (`core/src/keyboard.ts:127` and `:165`). NOT \"what every other key in this block does\",\n // which an earlier draft of this comment claimed: `insertMode` is read in exactly two places,\n // `type()` (`keyboard.ts:74`) and `writeControl()` (`:223`), and `Del` (`:413`), `BkSp`\n // (`:398`), `ErEOF` (`:369`) and `ErInp` (`:393`) never read it. Four of the six are\n // unaffected by the toggle, so `Ins` earns its place here by editing the buffer like the\n // rest and not by governing them.\n id: 'editing',\n title: 'Editing',\n keys: ['Ins', 'Del', 'BkSp', 'ErEOF', 'ErInp', 'Dup', 'FldMk'].map(byLabel),\n },\n {\n // The two keys that SUBMIT OR START SOMETHING: `Enter` transmits the modified fields, `Xfer`\n // starts a file transfer. Both reach the host, neither is an interrupt, which is what keeps\n // them out of the `attention` block.\n //\n // `Xfer` IS THE ONE KEY IN THIS TABLE WHOSE ACTION `applyAction` REFUSES -- `actions.ts:57`,\n // \"applyAction does not handle transferForm: the front end owns its own dialog\" -- so a\n // button wired straight through to `applyAction` is a button that only throws. Whoever\n // renders this block must give it the same treatment that front end already gives Ctrl-T,\n // and THAT DIFFERS BY FRONT END rather than being one rule: the Electron GUI opens a\n // window (`gui/src/main.ts:924`) and the TUI opens an overlay (`tui/src/app.ts:672`), but\n // the web gateway deliberately REJECTS it with an error frame naming the kind\n // (`web/src/protocol.ts:146`), because a browser-initiated transfer would write to the\n // gateway's filesystem and not the operator's. The web keypad therefore still shows the\n // button -- the rejection is the feedback, and `protocol.ts` says so in as many words.\n id: 'send',\n title: 'Send',\n keys: ['Enter', 'Xfer'].map(byLabel),\n },\n]);\n\n/**\n * Tooltip text for a key: its name, plus the binding note when there is one.\n *\n * ## THE DATA IS UNEVEN IN TWO DIFFERENT WAYS, AND BOTH HAVE TO BE HANDLED\n *\n * MEASURED 2026-10-06:\n *\n * - `BINDING_INTENT` has NO ENTRY AT ALL for 22 of the 48 keys -- PF14-24, PF2-11, `SysRq`\n * and `NewLn`.\n * - It has an entry with NO `note` for a further 15 -- PF1, PF3, PA2, PA3, `Home`, all four\n * arrows, `Reset`, `ErInp`, `Tab`, `BkTab`, `Del`, `BkSp`. `note` is OPTIONAL on `Binding`\n * (`bindings.ts:32`).\n *\n * SO \"FOUND A BINDING\" AND \"HAS PROSE\" ARE DIFFERENT QUESTIONS, and conflating them is not a\n * cosmetic slip: the first draft of this function tested only `binding === undefined` and then\n * read `binding.note.startsWith(...)`, which throws on all 15 -- including `Tab`, `Home` and\n * every arrow, i.e. most of the cursor cluster. `note === undefined` is the condition that\n * matters, and it subsumes the missing-entry case through the optional chain below.\n *\n * `name` IS THEREFORE THE FLOOR AND THE NOTE IS THE BONUS. Nothing is ever blank, and no prose\n * has to be invented to fill a table -- which matters because the 22 with no entry are mostly PF\n * keys, whose meaning is HOST-DEPENDENT and cannot honestly be described beyond their number.\n *\n * ## MATCHED ON THE ACTION, NOT THE LABEL\n *\n * `BINDING_INTENT`'s `key` field is a keyboard chord -- `'Ctrl-C'`, `'Enter'` -- and not a keypad\n * label. Matching on it would silently find nothing for most keys, and \"silently finds nothing\"\n * is indistinguishable here from \"has no note\": every tooltip would quietly degrade to the bare\n * name and no test asserting non-emptiness would notice.\n */\nexport function tooltipFor(key: KeypadKey): string {\n const wanted = JSON.stringify(key.action);\n const note = BINDING_INTENT.find((b) => JSON.stringify(b.action) === wanted)?.note;\n if (note === undefined) return key.name;\n // Do not repeat the name when the note already opens with it: several do, and\n // \"Enter -- the Enter AID\" must not become \"Enter -- Enter -- the Enter AID\".\n if (note.startsWith(key.name)) return note;\n return `${key.name} -- ${note}`;\n}\n", "import { AID, pfAID, paAID, type Session } from '@tn3270/core';\nimport type { Action } from './keymap.js';\n\n/**\n * Apply one named action to a session.\n *\n * ## THIS FILE DECIDES NOTHING ABOUT 3270 SEMANTICS\n *\n * The field-aware typing rules, the tab order, the keyboard lock and the AID semantics\n * all live in core and are tested there. If a branch here grows logic, that logic is in\n * the wrong package. It is shared because it is the one translation every front end\n * needs and none of them should own: the CLI's command table, the TUI's keymap and the\n * GUI's KeyboardEvent mapper all produce these same names.\n *\n * ## `quit` AND `toggleKeypad` ARE NOT HANDLED HERE, AND THROW RATHER THAN BEING IGNORED\n *\n * Teardown is the front end's: the TUI restores raw mode on every exit path, the GUI\n * closes a window. Silently ignoring `quit` would leave a front end that forgot to check\n * for it simply unquittable, and that failure would show up as a hang rather than an\n * error. Callers test for it before delegating.\n *\n * `toggleKeypad` is the same shape. Showing or hiding a keypad is a display decision no\n * `Session` knows anything about: the Electron main process and the gateway's server each\n * hold the flag and rebuild the frame, and the TUI opens a list overlay instead. The\n * switch below treats an unrecognized kind as a NO-OP, so ignoring it would leave a front\n * end that forgot to intercept it presenting a button or a chord that does nothing at\n * all -- the least diagnosable outcome available. So it throws.\n *\n * ## `enter` AND `clear` DEPEND ON THE CONNECTION STATE, WHICH NOTHING ELSE HERE DOES\n *\n * On a DISCONNECTED session they reconnect to the last host instead of sending their AID. That is a\n * divergence from x3270 and the one piece of state-dependent dispatch in this file, so it has its own\n * commentary: see `reconnectInstead` below, and do not touch either case without reading it.\n *\n * ## A REJECTED ACTION IS SWALLOWED\n *\n * Not connected, or a program check, is normal operation rather than a crash \u2014 and the\n * OIA already says why, so the caller's redraw shows it. `typeString` is different again:\n * it REPORTS refusal by returning false rather than throwing, so there is nothing to\n * catch and nothing to do.\n */\nexport function applyAction(session: Session, action: Action): void {\n if (action.kind === 'quit') {\n throw new Error('applyAction does not handle quit: the front end owns its own teardown');\n }\n // THE SAME REASONING AS `quit`, and the same failure mode if it were ignored instead --\n // see the docstring above. Note this is OUTSIDE the `try`, as `quit` is: the catch-all\n // below would swallow the throw and hand back the silent no-op it exists to prevent.\n if (action.kind === 'toggleKeypad') {\n throw new Error('applyAction does not handle toggleKeypad: the front end owns its own display');\n }\n // THE THIRD OF THE SAME SHAPE, and outside the `try` for the same reason as the two above.\n // A transfer needs arguments, so this action opens a form rather than doing anything, and\n // what a form looks like is the front end's business -- a terminal draws an overlay, a GUI\n // will draw a window. A front end that bound the chord and forgot the dialog gets a loud\n // failure instead of a key that appears to do nothing.\n if (action.kind === 'transferForm') {\n throw new Error('applyAction does not handle transferForm: the front end owns its own dialog');\n }\n // THE FOURTH OF THE SAME SHAPE, and outside the `try` for the reason the three above give: the\n // catch-all would swallow the throw and hand back the silent no-op it exists to prevent.\n //\n // What a clipboard IS differs per front end, which is why this cannot be dispatched here: the\n // Electron GUI has an OS clipboard, the gateway has the operator's browser at the far end of a\n // socket, and the CLI and TUI have neither. The action carries a RECTANGLE rather than text\n // because the renderer that raises it has no characters to send -- see the union member.\n if (action.kind === 'copy') {\n throw new Error('applyAction does not handle copy: the front end owns its own clipboard');\n }\n // THE FIFTH OF THE SAME SHAPE, 2026-10-22, and outside the `try` for the reason the four above\n // give. The full 122-key keyboard is a surface, and which surface -- an Electron window or an\n // in-pane overlay -- is the front end's business, exactly as the keypad's is. Two of the four\n // front ends have one at all; the TUI cannot draw a keyboard inside 24x80 and the CLI has no\n // interactive surface.\n if (action.kind === 'toggleKeyboard') {\n throw new Error(\n 'applyAction does not handle toggleKeyboard: the front end owns its own display',\n );\n }\n /*\n THE SIXTH OF THE SAME SHAPE, 2026-10-23, and outside the `try` for the reason the five above\n give: a kind whose meaning depends on WHOSE MACHINE it lands on, so the front end owns it.\n `copy` reads that machine's clipboard; `paste` writes from it.\n\n IT COULD BE HANDLED HERE AND IS NOT, WHICH IS A POLICY CHOICE RATHER THAN A CONSTRAINT -- said\n plainly because the spec for this feature first claimed the opposite. `pasteString` needs a\n `Screen` as well as a `Keyboard`, and this function HAS both: `session` carries them, and\n `gui/src/main.ts` already calls `pasteString(session.keyboard, session.screen, text)`. What\n rules it out is the pattern -- `quit`, `toggleKeypad`, `toggleKeyboard`, `transferForm` and\n `copy` all throw here for the same reason, and a front end that forgot the arm fails loudly\n rather than being silently inert.\n\n AND THERE IS A SECOND, CONCRETE REASON, which the policy one should not be allowed to hide:\n `pasteString` RETURNS `{typed, dropped, reason?}`, and `applyAction` returns `void` inside a\n `try` that swallows everything. Dispatching it here would discard the one report that says a\n paste stopped early -- on a keyboard lock, or a wrap past the start -- which is the whole\n reason that return type exists.\n\n BOTH DOM FRONT ENDS THEN SHARE ONE HELPER (`pasteAction.ts`, Task 2), so the GUI's rules stop\n being private to its menu handler and the browser is not a second copy of them.\n */\n if (action.kind === 'paste') {\n throw new Error('applyAction does not handle paste: the front end owns its own clipboard');\n }\n /*\n THE SEVENTH OF THE SAME SHAPE, 2026-10-27, and outside the `try` for the reason the six above\n give: the catch-all would swallow the throw and hand back the silent no-op it exists to prevent.\n\n ITS REASON IS NOT \"WHOSE MACHINE\", WHICH IS WHAT MAKES IT A SEVENTH GUARD RATHER THAN A COPY OF\n THE FIFTH AND SIXTH. `copy` and `paste` are the front end's because a clipboard belongs to the\n operator; this kind is the front end's because the SELECTION does. It carries no rect at all,\n and the rectangle is exactly what a copy needs: it lives in `canvas/src/renderer.ts`'s\n module-level `anchor`/`focus`, which this function cannot reach and which `actionForKey` --\n pure, given one key event -- cannot reach either. So the chord raises the intent and the\n renderer converts it to a real `copy`.\n\n SO THIS IS AN UNSATISFIABLE REQUEST HERE AND NOT MERELY AN UNWELCOME ONE, which is why it\n throws rather than being handled: there is no argument this function could supply.\n */\n if (action.kind === 'requestCopy') {\n throw new Error(\n 'applyAction does not handle requestCopy: only the renderer knows the selection',\n );\n }\n /*\n THE EIGHTH OF THE SAME SHAPE, 2026-11-04, and outside the `try` for the reason the seven above\n give: the catch-all would swallow the throw and hand back the silent no-op it exists to prevent.\n\n ITS REASON IS `toggleKeyboard`'S: the model/oversize chooser is a SURFACE -- a native menu in the\n Electron GUI, a DOM overlay in the browser -- and which surface is the front end's business. The\n TUI and CLI have none (one resizes, the other has `Set(model,...)`), so no `Session` ever needs\n to see the kind. What the operator then CHOOSES is not this action at all.\n */\n if (action.kind === 'geometryMenu') {\n throw new Error(\n 'applyAction does not handle geometryMenu: the front end owns its own display',\n );\n }\n const k = session.keyboard;\n try {\n switch (action.kind) {\n case 'type': k.typeString(action.text); break;\n // ENTER AND CLEAR MEAN TWO DIFFERENT THINGS, AND WHICH ONE IS DECIDED BY `isConnected()`.\n // Read the block comment below before touching either line: \"Enter submits the screen,\n // except when it doesn't\" is the whole of what a maintainer needs to be told here.\n case 'enter':\n if (reconnectInstead(session)) break;\n session.sendAID(AID.ENTER);\n break;\n case 'clear':\n if (reconnectInstead(session)) break;\n session.sendAID(AID.CLEAR);\n break;\n // `pfAID`/`paAID`, NOT `PF_AIDS[action.n - 1]!`. The index form put a bogus 0x00 AID on the\n // wire to a live mainframe for an out-of-range `n`, because `undefined` coerces to 0 inside\n // `Uint8Array.from`. These throw instead, and the `catch` below means nothing is sent at all.\n case 'pf': session.sendAID(pfAID(action.n)); break;\n case 'pa': session.sendAID(paAID(action.n)); break;\n case 'reset': k.reset(); break;\n case 'left': k.left(); break;\n case 'right': k.right(); break;\n case 'up': k.up(); break;\n case 'down': k.down(); break;\n case 'home': k.home(); break;\n case 'tab': k.tab(); break;\n case 'backTab': k.backTab(); break;\n // BESIDE `tab`/`backTab` because it is the same kind of thing: a local cursor move, not an\n // AID. `Keyboard.newline()` has existed since stage 1 (`core/src/keyboard.ts:348`) and the\n // CLI could already call it as `Newline()` (that case in `cli/src/runner.ts`) -- this\n // case is what makes it reachable from an interactive front end, the same shape of gap\n // `Session.sysreq()` had. It has NO CHORD: see the note on the union member.\n case 'newline': k.newline(); break;\n case 'backspace': k.backspace(); break;\n case 'delete': k.deleteChar(); break;\n case 'eraseEOF': k.eraseEOF(); break;\n case 'eraseInput': k.eraseInput(); break;\n case 'attn': session.sendAttn(); break;\n // `Session.sysreq()` has existed since stage 2b with no front end able to call it;\n // this case is what makes it reachable. TWO FORMS, and `Session.sysreq` picks: Telnet\n // IAC AO on a TN3270E session that agreed the SYSREQ function, and a TEST REQUEST READ\n // -- the four bytes SOH `%` `/` STX plus modified field data -- on a classic one. The\n // classic form is what both of this project's live hosts get, since neither offers\n // TN3270E. Refused silently either way; see that method for every refusal.\n //\n // SYSREQ IS AN AID ARCHITECTURALLY -- `AID.SYSREQ` 0xf0 in `@tn3270/core`'s constants,\n // x3270 `3270ds.h:307` -- and x3270 does route it through `key_AID` on a non-E session\n // (`kybd.c:2864`). IT IS STILL NEVER THE BYTE ON THE WIRE FOR THIS KEY: the AID selects\n // the heading and is then discarded. Cited by name, not line: `constants.ts` moves.\n case 'sysreq': session.sysreq(); break;\n // Typed CHARACTERS, not AIDs: they write one EBCDIC control byte into the buffer and\n // set MDT -- see the keyboard methods and kybd.c:2788,2825.\n //\n // What routing them through `sendAID` would ACTUALLY do, traced rather than assumed:\n // 0x1c and 0x1e are not in `AID`, `VALID_AIDS` is built from its values\n // (`constants.ts`, by name: the line moves), and `sendAID` throws `RangeError` before it\n // even checks for a connection (`Session.sendAID`'s AID-byte check, by name: the line\n // moves) -- which the `catch` below swallows. So the outcome is NOT a wrong byte on the\n // wire but a key that does nothing at all, silently. Same class of defect, worse\n // diagnosability.\n case 'dup': k.dup(); break;\n case 'fieldMark': k.fieldMark(); break;\n // Read-then-set rather than a stored flag: `Keyboard.insertMode` is the one truth and\n // the OIA reads it too, so a second copy here could disagree with what is displayed.\n case 'toggleInsert': k.setInsertMode(!k.insertMode); break;\n // EXHAUSTIVENESS, WITH NO RUNTIME FOOTPRINT. `satisfies never` is a type-level assertion\n // that every member has been handled above: it emits `default: action;` and nothing else, so\n // an unrecognized `kind` arriving from untrusted JSON is still the silent no-op that\n // `web/src/protocol.ts` relies on. What it buys is two compile errors instead of two silent\n // defects: a new `Action` member with no case, and -- the one that matters -- DELETING EITHER\n // GUARD ABOVE without adding a case, which would otherwise turn `quit` or `toggleKeypad` back\n // into the silent no-op this file's docstring calls the least diagnosable outcome available.\n // Prose alone did not prevent that; TS1360 does.\n default: action satisfies never;\n }\n } catch (err) {\n void err;\n }\n}\n\n/**\n * ON A DISCONNECTED SESSION, ENTER AND CLEAR RECONNECT TO THE SAME HOST INSTEAD OF SUBMITTING.\n *\n * Returns `true` when it has TAKEN OVER the keypress, i.e. when the caller must not send its AID.\n *\n * ## THIS IS THE SUBTLE THING IN THIS FILE. READ IT BEFORE CHANGING EITHER CASE\n *\n * One key with two meanings is exactly the kind of thing that surprises a maintainer, so state it\n * plainly: while `session.isConnected()` is false, `enter` and `clear` open a socket and send\n * NOTHING, and while it is true they are the AID senders they have always been. No other action\n * kind is state-dependent, and none should become so without a reason as concrete as this one.\n *\n * WHY IT IS WORTH IT. VM/370 prints \"Press Enter or Clear to continue\" the instant a LOGOFF drops\n * the connection, and that message is on the LAST SCREEN THE HOST EVER SENT, still sitting there\n * under an `X Disconnected` OIA. Both keys previously did nothing at all: `sendAID` throws 'not\n * connected' and the catch above swallows it, so the operator's own reading of the screen was\n * simply wrong, with no feedback of any kind. The OIA now says so too (`X Disconnected -- press\n * Enter to reconnect`, gated on there being a host to go back to).\n *\n * ## A DELIBERATE DIVERGENCE FROM x3270\n *\n * x3270 has no such binding, and that was checked rather than assumed: `Enter_action` and\n * `Clear_action` (`Common/kybd.c`) branch only on the keyboard lock and on NVT mode, and the word\n * \"reconnect\" appears nowhere in that file. x3270 reaches this behavior two other ways \u2014 the\n * `Reconnect()` ACTION (`Common/host.c`, which we also expose, as the CLI's `Reconnect()`), and the\n * `reconnect` RESOURCE (`Common/glue.c`) that redials automatically. We follow x3270 where it has an\n * answer; here it has one for scripts and none for a keyboard, and this is the gap that fills.\n *\n * ## WHY HERE, AND NOT IN EACH FRONT END\n *\n * `applyAction` is the one translation the TUI, the GUI and the browser share, and the behavior is\n * identical in all three; three copies would be three chances for one to drift. IT IS DELIBERATELY\n * NOT IN THE CLI, which does not use this function: a script's `Enter()` must not silently open a\n * socket, and s3270 spells the deliberate act `Reconnect()`.\n *\n * ## THE PROMISE, WHICH IS THE PART THAT CAN BREAK A PROCESS\n *\n * `applyAction` is SYNCHRONOUS and `Session.reconnect()` is not. The promise is deliberately not\n * returned and not awaited \u2014 a signature change would reach every caller in four front ends, and\n * two of them (`gui/src/main.ts`'s `ipcMain` handler, `web/src/main.ts`'s `onText`) are event\n * handlers with nothing above them to await it \u2014 so the rejection MUST be contained here. An\n * unhandled rejection terminates the process on modern Node, and in the gateway that means every\n * other operator's session goes with it: `web/src/wsserver.ts` records that a throw in a socket\n * handler ends the process, and this project has already hung one process on an unawaited promise.\n * `web/src/main.ts` contains its own initial connect the same way, `void session.connect(...)\n * .catch(...)`.\n *\n * SWALLOWED, LIKE EVERY OTHER REFUSAL IN THIS FILE, and the reason it is not silent: `reconnect()`\n * records the failure in the session's `lastError()` and traces it before rethrowing, and the OIA\n * still reads `press Enter to reconnect` \u2014 which remains the operator's correct next move, because\n * a host that refused a socket once may take one now. The two synchronous refusals `reconnect()`\n * raises (already connected, no previous host) arrive as a rejected promise too, since it is\n * `async`, so this one `catch` covers all three and the front ends' unconditional repaint is\n * unaffected either way.\n */\nfunction reconnectInstead(session: Session): boolean {\n if (session.isConnected()) return false;\n // `void` on the call as well as the `catch`, so no lint rule can be satisfied by removing the\n // catch instead of the void -- the catch is the load-bearing half.\n void session.reconnect().catch((err: unknown) => { void err; });\n return true;\n}\n", "import type { Keyboard, Screen } from '@tn3270/core';\n\n/** What a paste did. `reason` is present only when the paste stopped early. */\nexport interface PasteResult {\n readonly typed: number;\n readonly dropped: number;\n readonly reason?: 'keyboard locked' | 'wrapped past the start';\n}\n\nexport interface PasteOptions {\n /** Observability hook for tests; production passes nothing. */\n readonly onAid?: (aid: number) => void;\n}\n\n/**\n * Paste text into the screen, with x3270's measured semantics.\n *\n * ## IT LIVES IN `frontend` BECAUSE EVERY FRONT END WANTS THE SAME RULES\n *\n * The graph is `core <- frontend <- { cli, tui, canvas, gui, web }`, so `frontend` is the one place\n * all four front ends can consume. Keeping these rules here is what stops them being written twice,\n * and the keyboard is INJECTED so this is testable with no Electron and no Session.\n *\n * ## THE RULES ARE MEASURED FROM `Common/kybd.c`, NOT RECALLED\n *\n * Every line below was read in the reference, not remembered:\n *\n * | Input | While pasting | Outside pasting | Line |\n * |---|---|---|---|\n * | `\\n` | Newline, i.e. next unprotected field | **Enter, an AID** | `:3928`, `:3957` |\n * | `\\n` after a wrap | **suppressed** | -- | `:3929` |\n * | `\\b` | Left | Left | `:3914` |\n * | `\\f` | a **SPACE** | **Clear, an AID** | `:3918`, `:3920` |\n * | `\\r` | **dropped** | Newline | `:3963-3967` |\n * | `\\t` | Tab | Tab | `:3968` |\n *\n * The two rows with a different meaning inside a paste are the ones that matter, and both would\n * have been guessed wrong: `\\n` as Enter would SUBMIT a half-filled panel to a live host, and `\\f`\n * as Clear would WIPE the screen mid-paste. `\\r` is dropped so that CRLF text does not gain a\n * stray character per line -- the `\\n` after it carries the meaning.\n *\n * This is the `auto_skip` default path; OVERLAY-PASTE MODE IS NOT IMPLEMENTED, and not asking for\n * it is what keeps that branch unreachable -- the same reasoning that kept BIND-IMAGE unrequested\n * in stage 2b.\n *\n * ## NO ENTER IS EVER SYNTHESIZED\n *\n * `\\n` becomes `Enter_action` in the reference ONLY WHEN NOT PASTING (`:3957`). Enter submits the\n * screen; quietly submitting a half-filled panel to a live host is not a trade this makes. The\n * test asserts it as an ABSENCE, through `onAid`, because \"no AID was sent\" is the property.\n */\nexport function pasteString(\n keyboard: Keyboard, screen: Screen, text: string, opts: PasteOptions = {},\n): PasteResult {\n const cols = screen.cols;\n const startAddr = screen.cursor;\n let lastAddr = screen.cursor;\n let lastRow = Math.floor(screen.cursor / cols);\n let justWrapped = false;\n let typed = 0;\n\n const chars = [...text];\n for (let i = 0; i < chars.length; i++) {\n // THE KEYBOARD LOCK IS FATAL TO THE REMAINDER, matching the reference: \"it isn't possible to\n // unlock the keyboard from a string, so if the keyboard is locked, it's fatal\"\n // (`kybd.c:3874-3880`). We report how many got in rather than dropping it silently.\n if (keyboard.oia.isInhibited()) {\n return { typed, dropped: chars.length - i, reason: 'keyboard locked' };\n }\n // A CURSOR THAT WRAPPED PAST WHERE IT STARTED ABORTS (`kybd.c:3886`), or a long paste\n // circles the screen and overwrites what it already typed.\n if (screen.cursor < startAddr && i > 0) {\n return { typed, dropped: chars.length - i, reason: 'wrapped past the start' };\n }\n\n // ROW TRACKING FOR `justWrapped` (`kybd.c:3898-3906`): if the cursor moved to a different row\n // on its own -- because filling a field wrapped it -- the wrap has already done a newline's\n // job, and the next `\\n` must be swallowed.\n if (lastAddr !== screen.cursor) {\n lastAddr = screen.cursor;\n const row = Math.floor(screen.cursor / cols);\n justWrapped = row !== lastRow;\n lastRow = row;\n }\n\n const ch = chars[i]!;\n switch (ch) {\n case '\\n':\n // SUPPRESSED AFTER A WRAP. Without this every line after the first FULL one lands ONE\n // FIELD LATE, which presents as a core keyboard fault rather than a paste bug.\n if (!justWrapped) keyboard.newline();\n lastRow = Math.floor(screen.cursor / cols);\n justWrapped = false;\n break;\n case '\\b':\n keyboard.left();\n break;\n case '\\f':\n if (!keyboard.type(' ')) return { typed, dropped: chars.length - i };\n typed += 1;\n break;\n case '\\t':\n keyboard.tab();\n break;\n case '\\r':\n // DROPPED, not typed and not Enter: CRLF text would otherwise put a stray character in\n // every field. The `\\n` that follows carries the meaning.\n break;\n default:\n if (!keyboard.type(ch)) return { typed, dropped: chars.length - i };\n typed += 1;\n break;\n }\n }\n return { typed, dropped: 0 };\n}\n", "import type { Session } from '@tn3270/core';\nimport { pasteString } from './paste.js';\n\n/** What a paste did, plus the one line an operator should be shown about it. */\nexport interface PasteOutcome {\n readonly typed: number;\n readonly dropped: number;\n /** Present only when something was lost. At most one, and it is already operator-facing text. */\n readonly warning?: string;\n}\n\n/**\n * Apply a clipboard paste to a session, and decide what to tell the operator about it.\n *\n * ## ONE COPY OF THE RULES, BECAUSE TWO FRONT ENDS NEED THE SAME DECISIONS\n *\n * `pasteString` already owns the x3270 semantics -- which control character means what while\n * pasting. What it does NOT own is the policy on top: which outcome deserves a warning, and in\n * which words. Those lived inline in the Electron GUI's `onPaste` menu handler, and the web\n * gateway's browser half is about to need them identically. Two copies of a message that must\n * agree is how they diverge: this project has already watched a fix reach the prose body of a\n * message and never the heading that summarised it. So the GUI now calls this, and the browser\n * will call this, and there is exactly one place where the wording can be changed.\n *\n * ## THE WARNING IS REMEDY-FIRST, AND THAT IS NOT A STYLE PREFERENCE\n *\n * The status line TRUNCATES, and this project has already shipped a message whose only actionable\n * phrase fell past the cut. `paste stopped: keyboard locked ...` tells the operator the thing they\n * can act on -- the host holds the keyboard, wait or reset -- before the arithmetic. The counts\n * come second because they are diagnosis, not remedy.\n *\n * A silently half-applied paste is the worst outcome available here: the operator cannot see how\n * much of a dataset name arrived, and a SHORT one may be a VALID DIFFERENT NAME. That is why the\n * `dropped > 0` arm warns at all, rather than treating a partial paste as success.\n *\n * ## IT DOES NOT REPAINT, AND THE CALLER MUST\n *\n * A paste is a LOCAL action: it emits no `screen` event, so a caller that does not repaint shows\n * nothing until the host next speaks. Measured on the same path for `tab` in the web gateway,\n * where the missing repaint cost a 5-second test timeout. The GUI's `send()` after this call is\n * that repaint and is deliberately UNCONDITIONAL -- the characters that did land must be visible\n * even on the warning paths.\n *\n * In the browser the repaint carries a second job: `onError` BLANKS THE CANVAS to draw its text,\n * and `onFrame` is what clears that state. So the frame is also what dismisses the warning this\n * function returns. Returning the string instead of sending it keeps that ordering the caller's\n * decision, since only the caller knows its own transport.\n *\n * ## WHY THIS IS NOT IN `applyAction`\n *\n * `applyAction` deliberately THROWS on `{kind:'paste'}` -- the clipboard is owned by whoever has\n * one, and in the browser reading it is async and permissioned while `applyAction` is neither.\n * Each front end intercepts the kind before dispatch, reads its own clipboard, and calls this.\n */\nexport function applyPaste(session: Session, text: string): PasteOutcome {\n const r = pasteString(session.keyboard, session.screen, text);\n const total = r.typed + r.dropped;\n if (r.reason !== undefined) {\n return {\n typed: r.typed, dropped: r.dropped,\n warning: `paste stopped: ${r.reason} after ${r.typed} of ${total} characters`,\n };\n }\n if (r.dropped > 0) {\n return {\n typed: r.typed, dropped: r.dropped,\n warning: `paste short: ${r.typed} of ${total} characters (field full or protected)`,\n };\n }\n return { typed: r.typed, dropped: r.dropped };\n}\n", "import { type Color3279, type Rgb, PALETTE_3279 } from '@tn3270/core';\n\n/**\n * The display palettes every front end draws from, and the ANSI slots they quantise to.\n *\n * ## WHY THIS IS NOT CORE'S TABLE\n *\n * Core's `PALETTE_3279` answers \"which color IS code F1\" \u2014 the architected meaning, pinned\n * to GA23-0059 with notes on the OCR damage in its tables. It is not a presentation choice,\n * and its own comment says the RGB values are \"OUR OWN CHOICE, DELIBERATELY NOT X3270'S\":\n * saturated primaries, picked so that seven base colors survive quantisation to sixteen\n * ANSI slots. On a black background its pure `#0000ff` blue is close to illegible, which is\n * what a user reported from the Electron GUI on 2026-09-14. The GUI had been drawing from\n * core while the TUI had quietly had its own gentler table since it shipped.\n *\n * So this module owns presentation for ALL front ends, and core keeps the meaning. Core's\n * table is not orphaned by that: it is the `3279` scheme's data.\n *\n * ## A SCHEME CARRIES ITS SLOT MAP, NOT JUST RGB\n *\n * `green` is what forces this, and the RGB-only shape looks sufficient right up to the\n * moment it fails: with one shared slot map, a green session would render green at\n * truecolor and blue/red/yellow on a sixteen-color terminal. The slot map is part of a\n * scheme's identity, which is why it is scheme data here rather than the TUI's private\n * `ANSI_16` table.\n *\n * Quantisation ITSELF stays in the TUI \u2014 `detectDepth`, the 6x6x6 cube, the SGR strings.\n * That is genuinely terminal-specific. Which slot a color belongs in is not.\n *\n * ## WHY THE SLOTS ARE A TABLE AND NOT NEAREST-RGB\n *\n * Nearest-RGB made the palette responsible for sixteen-color distinctness, and with any\n * pleasant palette blue and turquoise both land nearest ANSI cyan and collapse into one\n * slot \u2014 measured for both references: x3270's `#1e90ff` and zti's `(120,144,240)` each\n * quantise to bright cyan, exactly like their turquoise. Deciding the slot explicitly\n * separates \"what color is this\" from \"which of sixteen slots does it occupy\".\n */\n\n/** `[ansiIndex, bright]`. */\nexport type Slot = readonly [number, boolean];\n\nexport interface Scheme {\n readonly rgb: Readonly<Record<number, Rgb>>;\n readonly ansi16: Readonly<Record<number, Slot>>;\n}\n\n/**\n * The slot map the three COLOR schemes share.\n *\n * Keyed by color code rather than derived from RGB, so it is correct for all three\n * regardless of how saturated their values are.\n */\nconst COLOR_SLOTS: Readonly<Record<number, Slot>> = Object.freeze({\n 0xf0: [0, false], 0xf1: [4, true], 0xf2: [1, true], 0xf3: [5, true],\n 0xf4: [2, true], 0xf5: [6, true], 0xf6: [3, true], 0xf7: [7, true],\n 0xf8: [0, false], 0xf9: [4, false], 0xfa: [3, true], 0xfb: [5, false],\n 0xfc: [2, true], 0xfd: [6, false], 0xfe: [7, false], 0xff: [7, true],\n});\n\n/**\n * `default`: zti's colors for F0-F7, x3270's for F8-FF.\n *\n * F0-F7 are **zti's own values**, read from `tnz/zti.py:2813-2820` where they are declared\n * in curses' 0-1000 scale and converted here to 0-255 (`green_rgb = (141, 847, 188)` ->\n * `(36, 216, 48)`). Independently confirmed on the wire: a captured zti session emits\n * `38;2;35;215;47` for green, `38;2;120;144;239` for blue and `38;2;87;239;239` for\n * turquoise, which is this table to within rounding.\n *\n * **zti defines only those eight**, because it advertises only F1-F7 in its Color Query\n * Reply (`tnz/tnz.py:4329-4340`, `NP = 8`) and so is never sent the rest. We advertise all\n * sixteen, so F8-FF come from x3270's `rgbmap` (`c3270/screen.c:213-229`).\n *\n * NOTE F0 NEUTRAL-BLACK IS PURE BLACK HERE, as it is in zti; core keeps `0x1a1a1a`. This is\n * why nothing needs a divergence to get a black-looking background: the default background\n * resolves to F0, and F0 renders black.\n */\nconst DEFAULT_RGB: Readonly<Record<number, Rgb>> = Object.freeze({\n 0xf0: [0, 0, 0], // zti: neutral black, PURE black\n 0xf1: [120, 144, 240], // zti blue\n 0xf2: [240, 24, 24], // zti red\n 0xf3: [255, 0, 255], // zti pink\n 0xf4: [36, 216, 48], // zti green\n 0xf5: [88, 240, 240], // zti turquoise\n 0xf6: [255, 255, 0], // zti yellow\n 0xf7: [255, 255, 255], // zti neutral white\n 0xf8: [47, 79, 79], // x3270 black (0x2f4f4f)\n 0xf9: [0, 0, 205], // x3270 deep blue\n 0xfa: [255, 165, 0], // x3270 orange\n 0xfb: [160, 32, 240], // x3270 purple\n 0xfc: [144, 238, 144], // x3270 pale green\n 0xfd: [150, 205, 205], // x3270 pale turquoise\n 0xfe: [119, 136, 153], // x3270 grey\n 0xff: [245, 245, 245], // x3270 white\n});\n\n/**\n * `x3270`: that emulator's `rgbmap` in full, `c3270/screen.c:213-229`.\n *\n * For someone comparing our output against x3270, or simply used to its look. Note blue is\n * dodger blue and green is limegreen \u2014 x3270 is not saturated either. `default`'s F8-FF are\n * these same values, so only F0-F7 differ between the two.\n */\nconst X3270_RGB: Readonly<Record<number, Rgb>> = Object.freeze({\n 0xf0: [0x1a, 0x1a, 0x1a], // neutral black\n 0xf1: [0x1e, 0x90, 0xff], // blue (dodger blue)\n 0xf2: [0xff, 0x00, 0x00], // red\n 0xf3: [0xff, 0x00, 0xff], // pink\n 0xf4: [0x32, 0xcd, 0x32], // green (limegreen)\n 0xf5: [0x00, 0xff, 0xff], // turquoise\n 0xf6: [0xff, 0xff, 0x00], // yellow\n 0xf7: [0xff, 0xff, 0xff], // neutral white\n 0xf8: [0x2f, 0x4f, 0x4f], // black (\"alas, this may be gray\" -- x3270's own comment)\n 0xf9: [0x00, 0x00, 0xcd], // deep blue\n 0xfa: [0xff, 0xa5, 0x00], // orange\n 0xfb: [0xa0, 0x20, 0xf0], // purple\n 0xfc: [0x90, 0xee, 0x90], // pale green\n 0xfd: [0x96, 0xcd, 0xcd], // pale turquoise\n 0xfe: [0x77, 0x88, 0x99], // grey\n 0xff: [0xf5, 0xf5, 0xf5], // white\n});\n\n/** x3270's GreenScreen shades: `#21a021` normal, `lime` for emphasis. */\nconst GREEN: Rgb = [0x21, 0xa0, 0x21];\nconst LIME: Rgb = [0x00, 0xff, 0x00];\nconst DARK: Rgb = [0, 0, 0];\n\n/**\n * `green`: a monochrome 3278, which is the only \"authentic\" option here.\n *\n * A 3278 has a green phosphor and no color at all \u2014 color is a 3279 feature \u2014 and we\n * advertise `IBM-3278-2-E`. x3270 ships this as `GreenScreen` (`x3270/fb-x3270:98`).\n *\n * ## IT CANNOT BE TRANSCRIBED FROM X3270, AND THE REASON IS STRUCTURAL\n *\n * x3270's scheme format has a SEPARATE screen background. Verified in\n * `xfer_color_scheme` (`x3270/screen.c:4119-4180`): tokens 0-15 are the IBM colors, 16 is a\n * fallback, **17 is the screen background**, 18 select background, 19-22 attribute colors.\n * So `GreenScreen` sets F0 neutral-black to `#21a021` \u2014 green \u2014 and gets its dark screen\n * from `grey10` at token 17.\n *\n * **We resolve the background FROM F0**, so copying that table literally would give us a\n * green background. F0 and F8 therefore stay black here. That is a deliberate deviation,\n * not a transcription slip, and `palette.test.ts` pins it.\n *\n * Which codes get LIME follows x3270's own assignment \u2014 F2, F7 and FF \u2014 i.e. the ones a\n * host uses for emphasis.\n */\nconst GREEN_RGB: Readonly<Record<number, Rgb>> = Object.freeze({\n 0xf0: DARK, 0xf1: GREEN, 0xf2: LIME, 0xf3: GREEN,\n 0xf4: GREEN, 0xf5: GREEN, 0xf6: GREEN, 0xf7: LIME,\n 0xf8: DARK, 0xf9: GREEN, 0xfa: GREEN, 0xfb: GREEN,\n 0xfc: GREEN, 0xfd: GREEN, 0xfe: GREEN, 0xff: LIME,\n});\n\n/**\n * Green's own slots: everything visible on ANSI green, the emphasis codes bright.\n *\n * Sharing `COLOR_SLOTS` would make a sixteen-color terminal render blue and red from a\n * scheme whose whole point is that it does not have any.\n */\nconst GREEN_SLOTS: Readonly<Record<number, Slot>> = Object.freeze({\n 0xf0: [0, false], 0xf1: [2, false], 0xf2: [2, true], 0xf3: [2, false],\n 0xf4: [2, false], 0xf5: [2, false], 0xf6: [2, false], 0xf7: [2, true],\n 0xf8: [0, false], 0xf9: [2, false], 0xfa: [2, false], 0xfb: [2, false],\n 0xfc: [2, false], 0xfd: [2, false], 0xfe: [2, false], 0xff: [2, true],\n});\n\nexport const SCHEMES: Readonly<Record<string, Scheme>> = Object.freeze({\n default: Object.freeze({ rgb: DEFAULT_RGB, ansi16: COLOR_SLOTS }),\n // Core's architected table, which is what makes it a scheme rather than an orphan.\n '3279': Object.freeze({ rgb: PALETTE_3279, ansi16: COLOR_SLOTS }),\n x3270: Object.freeze({ rgb: X3270_RGB, ansi16: COLOR_SLOTS }),\n green: Object.freeze({ rgb: GREEN_RGB, ansi16: GREEN_SLOTS }),\n});\n\n/**\n * Declaration order, which is also the order the usage text lists them.\n *\n * NOT `Object.keys(SCHEMES)`: the key `'3279'` is a canonical numeric string, and JS\n * enumerates integer-like keys in ascending numeric order BEFORE other string keys\n * regardless of insertion order -- `Object.keys` would yield `['3279', 'default', ...]`.\n * Measured, not theoretical: this is exactly the failure the first test run hit.\n */\nexport const SCHEME_NAMES: readonly string[] = Object.freeze(['default', '3279', 'x3270', 'green']);\n\nexport const DEFAULT_SCHEME = 'default';\n\n/**\n * x3270's spelling for the monochrome scheme, accepted so a user coming from it can type\n * what they already know.\n */\nconst ALIASES: Readonly<Record<string, string>> = Object.freeze({ greenscreen: 'green' });\n\n/**\n * A scheme by name, defaulting to the readable one.\n *\n * THROWS on an unknown name rather than falling back. A silent fallback would draw the\n * wrong palette and leave the user doubting what they typed.\n */\nexport function resolveScheme(name?: string): Scheme {\n if (name === undefined) return SCHEMES[DEFAULT_SCHEME]!;\n const key = name.toLowerCase();\n const resolved = SCHEMES[ALIASES[key] ?? key];\n if (resolved === undefined) {\n throw new RangeError(\n `unknown color scheme ${JSON.stringify(name)}; use one of ${SCHEME_NAMES.join(', ')}`,\n );\n }\n return resolved;\n}\n\n/**\n * RGB for a color identification within a scheme. Throws rather than guessing, matching\n * core's `colorRgb` contract, which `drawlist.ts` already depends on.\n */\nexport function schemeRgb(scheme: Scheme, code: Color3279): Rgb {\n const rgb = scheme.rgb[code];\n if (rgb === undefined) {\n throw new RangeError(`0x${code.toString(16)} is not a 3279 color (expected 0xF0-0xFF)`);\n }\n return rgb;\n}\n", "/**\n * `Transfer()` keyword parsing and IND$FILE command construction.\n *\n * The fourth and last of the units the design doc lays out\n * (docs/superpowers/specs/2026-08-18-indfile-cut-transfer-design.md): the codec\n * is `core/ft/cut.ts`, the frame layout `core/ft/frames.ts`, the state machine\n * `core/ft/transfer.ts`, and this is the operator-facing end \u2014 keywords in, a\n * validated request and a host command string out.\n *\n * NO I/O HAPPENS HERE. This module decides *what* to do; `runner.ts` drives the\n * session and `main.ts` supplies the file system through `TransferFiles`. That is\n * the same split Replay() already uses, and it is what makes every string this\n * module produces assertable in a unit test.\n *\n * Ported from x3270 4.5 `Common/ft.c`: `parse_ft_keywords` (:829-1000) is the\n * keyword table and its validation, and `ft_go` (:683-772) is the command\n * builder. Both are cited by line below, and every deliberate divergence says so.\n *\n * ## THE DIALECT SEAM\n *\n * The design doc's \"Dialect seam\" names exactly three host-dependent things:\n * command construction, the resync AID, and the `$` escape. The first two are the\n * `Dialect` interface here; the third is settled once for both dialects \u2014 see\n * `IND_FILE`. Everything else (codec, frames, state machine) is host-independent,\n * so adding CICS later is a third `Dialect` object and nothing else.\n *\n * TSO is the lead dialect today, not VM: the design doc's \"MVS/TSO HOST FOUND\"\n * and \"PROTOCOL CONFIRMED\" sections record that TK5's IND$FILE speaks CUT, needs\n * no Query List work, and has already had a real frame decoded out of it, while\n * VM/CMS remains blocked on Query List support. Hence `Host=tso` is the default\n * even though x3270's own default is also TSO for unrelated reasons (ft.c:320).\n */\n\nimport {\n AckAid, boundDftBufferSize, DFT_BUF_MAX, DFT_BUF_MIN, type TransferDirection,\n} from '@tn3270/core';\n\n/**\n * The file system, injected.\n *\n * `runner.ts` stays free of `node:fs` exactly as it is free of file I/O for\n * Replay() \u2014 main.ts owns that (see its comment at the `Replay` branch) \u2014 and a\n * test supplies an in-memory implementation instead of a temp directory. The four\n * operations are the whole of what a transfer needs.\n */\nexport interface TransferFiles {\n /** Does the destination already exist? Governs the `Exist` check. */\n exists(path: string): boolean;\n /** Read the whole source file for a `send`. Throws if unreadable. */\n read(path: string): Uint8Array;\n /** Create or truncate. Used for `Exist=keep` (on a missing file) and `replace`. */\n write(path: string, bytes: Uint8Array): void;\n /** Append. Used for `Exist=append`. */\n append(path: string, bytes: Uint8Array): void;\n}\n\n/** `Host=` \u2014 which host command dialect to speak. ft.c:147, `HT_TSO`/`HT_VM`. */\nexport type FtHostType = 'tso' | 'vm';\n/** `Mode=` \u2014 ft.c:148. BINARY IS OUR DEFAULT; see `parseTransferKeywords`. */\nexport type FtMode = 'binary' | 'ascii';\n/** `Cr=` \u2014 ft.c:149 (x3270 spells the list `auto remove add keep`). */\nexport type FtCr = 'auto' | 'remove' | 'add' | 'keep';\n/** `Exist=` \u2014 ft.c:151. */\nexport type FtExist = 'keep' | 'replace' | 'append';\n/** `Recfm=` \u2014 ft.c:152, minus its `default` spelling which means \"unspecified\". */\nexport type FtRecfm = 'fixed' | 'variable' | 'undefined';\n\n/**\n * A validated transfer request: every keyword resolved, every default applied,\n * and every combination that cannot be expressed already rejected.\n *\n * The optional members are optional in the \"the operator did not say\" sense, and\n * absence is meaningful: no `recfm` means no `RECFM(...)` in the command, i.e.\n * let the host choose, which is x3270's `DEFAULT_RECFM` (ft_private.h:44).\n */\nexport interface TransferRequest {\n direction: TransferDirection;\n localFile: string;\n hostFile: string;\n host: FtHostType;\n mode: FtMode;\n cr: FtCr;\n exist: FtExist;\n recfm?: FtRecfm;\n lrecl?: number;\n blksize?: number;\n /**\n * The DFT frame size, clamped to 256..32767. **Not a host keyword** -- it never\n * appears in the `IND$FILE` command. It sizes OUR frames, and the host learns it\n * from the DDM Query Reply instead, which is why it applies in BOTH directions and\n * on BOTH hosts where the DCB keywords do not.\n */\n bufferSize?: number;\n /**\n * Things the operator should be told about their own request, having got a valid\n * transfer anyway. Absent when there is nothing to say.\n *\n * ## WHY A WARNING AND NOT AN ERROR\n *\n * The one case today is a clamped `BufferSize`. Rejecting it would diverge from\n * x3270, whose `set_dft_buffersize` silently returns the bound\n * (`ft_dft.c:740-747`) -- but clamping SILENTLY is worse than either, because the\n * number changes both the frames we put on the wire and the size we advertise to\n * the host, and an operator who tuned it deliberately would never learn it was\n * ignored. Same principle this module already applies to keywords: a request that\n * quietly does something other than what was asked is the failure mode to avoid.\n *\n * SHAPED LIKE `CutTransfer.warnings` on purpose (`ft/transfer.ts:277`), which the\n * CLI already drains into `data:` lines -- so surfacing these costs the caller one\n * loop in the same idiom rather than a new mechanism.\n */\n warnings?: readonly string[];\n}\n\n/**\n * `Transfer()` failed before anything reached the host.\n *\n * A distinct class so the runner can tell \"the operator typed something\n * impossible\" from \"the host refused\", and so a test can assert the difference.\n * Both end up as an `error` reply; only the second one leaves the host in\n * transfer mode.\n */\nexport class TransferOptionError extends Error {\n constructor(message: string) {\n // \"Transfer():\" prefix as x3270 does, e.g. `popup_an_error(AnTransfer\n // \"(): Unknown option: '%s'\"` (ft.c:918).\n super(`Transfer(): ${message}`);\n this.name = 'TransferOptionError';\n }\n}\n\n/**\n * The program name, with the `$` that has to be EBCDIC `0x5B`.\n *\n * ## HOW THE `$` WAS VERIFIED, because a wrong byte here fails at the host with\n * no diagnostic\n *\n * x3270 does not type a literal `$`. It emits `\"IND\\\\e005BFILE ...\"` (ft.c:685)\n * and hands it to `emulate_input`, whose `\\e` escape means \"the following hex\n * digits are an EBCDIC code point\" (kybd.c:4180, the `EBC` lexer state) \u2014 so\n * x3270 puts the byte `0x5B` on the wire and never consults a code page. The\n * design doc's dialect seam calls this out as item 3 and requires us to verify\n * the byte rather than trust a character round-trip.\n *\n * Verified three ways, all reproducible:\n *\n * 1. The generated table: `CP037_TO_UNICODE[0x5b]` is `0x0024`, i.e. `$`\n * (packages/core/src/codepages/cp037.ts, row for bytes 0x58-0x5f).\n * 2. It is the ONLY entry that is `0x0024` \u2014 scanned all 256 \u2014 so\n * `CodePage`'s reverse map cannot resolve `$` to anything else, whatever\n * order it is built in. (That matters: the reverse map is built high byte to\n * low so the lowest EBCDIC byte wins for characters with several encodings,\n * codepage.ts:30-35. With a unique entry that rule never comes into play.)\n * 3. A test asserts `cp037.encode('IND$FILE')[3] === 0x5b`, and a second one\n * asserts the byte that actually lands in the screen buffer after the\n * keyboard has typed the command \u2014 the real wire path, since `Keyboard.type`\n * is what `String()` and this action both go through.\n *\n * So the literal `$` here is safe, and there is a test that will fail if the code\n * page is ever swapped for one where it is not (cp1047, cp500 and friends all\n * keep `$` at 0x5B, but that is a fact about them, not a guarantee).\n */\nexport const IND_FILE = 'IND$FILE';\n\n/**\n * The host-dependent half of a transfer: command syntax and the resync AID.\n *\n * The design doc's \"Dialect seam\" section is the specification for this\n * interface. Deliberately NOT a class hierarchy \u2014 there is no state and no\n * inheritance to share, and two object literals read better than two subclasses.\n */\nexport interface Dialect {\n readonly name: FtHostType;\n /** The full command line to type, WITHOUT a trailing newline. */\n buildCommand(req: TransferRequest): string;\n /**\n * The AID that resynchronises a confused transfer: Clear on VM,\n * PA2 on TSO (`ACK_RESYNC_VM`/`ACK_RESYNC_TSO`, ft_cut_ds.h:71-72).\n *\n * NOTHING SENDS THIS YET, and x3270 does not either \u2014 `grep -rn ACK_RESYNC`\n * over the whole 4.5 tree returns only the two `#define`s. It lives here\n * because the design doc names it as one of the three things a dialect owns,\n * and because a future resync path must not have to rediscover that the two\n * hosts differ.\n */\n readonly resyncAid: number;\n}\n\n/**\n * Shared body of both dialects' `buildCommand`.\n *\n * x3270 builds the whole line with one format string and three substitutions:\n *\n * vb_appendf(&r, \"IND\\e005BFILE %s %s %s\",\n * p->receive_flag? \"GET\": \"PUT\",\n * p->host_filename,\n * (p->host_type != HT_TSO)? \"(\": \"\"); // ft.c:685-688\n *\n * \u2014 and then appends each option, the FIRST with no leading space and the rest\n * with one. That is where the two dialects' shapes come from, and they are not\n * symmetrical:\n *\n * TSO `IND$FILE GET 'A.B' ASCII CRLF RECFM(F) LRECL(80)`\n * VM `IND$FILE GET A B C (ASCII CRLF RECFM V LRECL 80`\n *\n * Note `(ASCII` with no space on VM: x3270's third substitution is the `(` and\n * the first option is appended straight onto it (`vb_appends(&r, \"ASCII\")`,\n * ft.c:690). Reproduced rather than tidied, because this is the string decades of\n * CMS hosts have been parsing.\n *\n * ONE COSMETIC DIVERGENCE: with no options at all, TSO's third substitution is\n * the empty string and x3270's line therefore ends in a space \u2014\n * `\"IND$FILE GET 'A.B' \"`. We trim it. A trailing blank is invisible, is\n * discarded by every host command parser, and would make every expected string in\n * a test carry an unexplained space. It also costs one cell of the input field,\n * which `Runner`'s field-size pre-flight would then have to allow for. VM's line\n * ends in `(` and is unaffected.\n */\nfunction buildIndFileCommand(req: TransferRequest, dialect: FtHostType): string {\n // GET is host->local. Naming trap 1 in the design doc: x3270's `receive_flag`\n // is the same direction as our `receive`, but its internal \"upload\"/\"download\"\n // vocabulary is the host's and is inverted from ordinary usage.\n const verb = req.direction === 'receive' ? 'GET' : 'PUT';\n const options: string[] = [];\n\n // `if (p->ascii_flag) vb_appends(&r, \"ASCII\")` (ft.c:689-690). BINARY IS THE\n // ABSENCE OF THE WORD, on both our hosts: the TK5 usage text lists only\n // `ASCII` among its options (design doc, \"MVS/TSO HOST FOUND\"), and x3270\n // emits an explicit `BINARY` only for CICS (ft.c:691-692), a dialect we do not\n // implement.\n if (req.mode === 'ascii') options.push('ASCII');\n if (wantsCrlf(req)) options.push('CRLF');\n\n // `if (p->append_flag && !p->receive_flag)` (ft.c:699-701). APPEND is a HOST\n // instruction, so it only makes sense on a PUT; for a GET, `Exist=append`\n // means \"append to the local file\" and never reaches the host.\n if (req.exist === 'append' && req.direction === 'send') options.push('APPEND');\n\n // `if (!p->receive_flag) { ... }` (ft.c:702-768). The whole DCB group is\n // send-only: on a GET the dataset already exists and its attributes are its\n // own. `parseTransferKeywords` rejects these keywords on a receive rather than\n // dropping them here, which is where x3270 silently drops them.\n if (req.direction === 'send' && req.recfm !== undefined) {\n if (dialect === 'tso') {\n // ft.c:704-726: \" RECFM(\" + letter + \")\", then LRECL and BLKSIZE, each\n // parenthesised. Confirmed independently by the live TK5 usage text:\n // \"RECFM(F|V|U) LRECL(nn) BLKSIZE(nn)\".\n options.push(`RECFM(${recfmLetter(req.recfm)})`);\n if (req.lrecl !== undefined) options.push(`LRECL(${req.lrecl})`);\n if (req.blksize !== undefined) options.push(`BLKSIZE(${req.blksize})`);\n } else {\n // ft.c:750-765: \" RECFM \" + letter, then \" LRECL %d\". BARE, no parens, and\n // NO BLKSIZE \u2014 the VM branch has no blksize case at all, and\n // `parseTransferKeywords` therefore refuses `Blksize` with `Host=vm`\n // instead of accepting a keyword that would vanish.\n options.push(`RECFM ${recfmLetter(req.recfm)}`);\n if (req.lrecl !== undefined) options.push(`LRECL ${req.lrecl}`);\n }\n }\n\n // THE HOST FILE NAME GOES THROUGH VERBATIM, quotes and all. x3270 does the\n // same \u2014 `p->host_filename` is substituted unmodified (ft.c:687) from whatever\n // the operator supplied (ft.c:361) \u2014 so the caller owns the quoting.\n //\n // That matters on TSO, where quoting is SEMANTIC rather than syntactic. The\n // live TK5 usage text says so:\n //\n // Usage: IND$FILE {GET|PUT} 'dataset.name' options\n // If the dataset name is specified without quotes, then the TSO userid\n // will be prepended to the dataset as 'userid.dataset.name'.\n //\n // So `HostFile=SYS1.PARMLIB(IEASYS00)` fetches HERC02.SYS1.PARMLIB(IEASYS00)\n // and fails, while `HostFile='SYS1.PARMLIB(IEASYS00)'` fetches the real one.\n // Both are legitimate requests and only the operator knows which is meant, so\n // adding quotes here would break the userid-relative form that TSO users\n // expect. Documented instead of guessed at.\n const open = dialect === 'tso' ? '' : '(';\n return `${IND_FILE} ${verb} ${req.hostFile} ${open}${options.join(' ')}`.trimEnd();\n}\n\n/**\n * Does the command carry `CRLF`?\n *\n * `if (p->ascii_flag && p->cr_flag) vb_appends(&r, \" CRLF\")` (ft.c:694-695), with\n * `cr_flag` set by ft.c:940-952: `auto` means \"follow the mode\" (`p->cr_flag =\n * p->ascii_flag`), and any explicit value gives `remove || add`. So in ascii mode\n * every value except `keep` asks for CRLF, and in binary mode nothing does.\n *\n * `remove` and `add` produce the SAME command because the direction already says\n * which one the host will do \u2014 CRLF means \"the other side uses CRLF line ends\",\n * and a GET removes them while a PUT adds them. x3270 collapses them for exactly\n * this reason and we keep both spellings so an s3270 script reads correctly.\n */\nfunction wantsCrlf(req: TransferRequest): boolean {\n return req.mode === 'ascii' && req.cr !== 'keep';\n}\n\n/** F, V or U (ft.c:707-719 for TSO; the VM branch has no U \u2014 see the caller). */\nfunction recfmLetter(recfm: FtRecfm): string {\n switch (recfm) {\n case 'fixed': return 'F';\n case 'variable': return 'V';\n case 'undefined': return 'U';\n }\n}\n\n/**\n * MVS/TSO. The lead dialect: proven to speak CUT against a live TK5 host, with a\n * real frame decoded end to end (design doc, \"PROTOCOL CONFIRMED\").\n */\nexport const TSO_DIALECT: Dialect = {\n name: 'tso',\n buildCommand: (req) => buildIndFileCommand(req, 'tso'),\n resyncAid: AckAid.RESYNC_TSO,\n};\n\n/**\n * VM/CMS. Implemented and unit-tested, NOT yet exercised against a host: VM/370\n * CE needs Query List support before MECAFF will paint fullscreen at all (design\n * doc, \"BLOCKER RESOLVED\"). The seam exists so that when that lands, this object\n * is all that has to be right.\n */\nexport const VM_DIALECT: Dialect = {\n name: 'vm',\n buildCommand: (req) => buildIndFileCommand(req, 'vm'),\n resyncAid: AckAid.RESYNC_VM,\n};\n\nexport function dialectFor(host: FtHostType): Dialect {\n return host === 'tso' ? TSO_DIALECT : VM_DIALECT;\n}\n\n// ---------------------------------------------------------------------------\n// Keyword parsing\n// ---------------------------------------------------------------------------\n\n/**\n * The keyword names we accept, lower-cased for lookup.\n *\n * A Set rather than a bare list of `if`s so an unknown keyword can be reported\n * with the full accepted set \u2014 the operator's next move is to retype it, and\n * \"unknown option 'Recfmt'\" without the list is a guessing game.\n *\n * x3270's table (ft.c:142-166) has eight more, of which SEVEN are deliberately\n * absent and absent LOUDLY (see `parseTransferKeywords`): `Remap` governs ascii\n * remapping we have not built, `Allocation`/`PrimarySpace`/`SecondarySpace`/\n * `Avblock` need `SPACE(n,n)` and `TRACKS|CYLS` command syntax nobody has tested\n * against the host yet, `WindowsCodePage` is not ours to honor, and\n * `OtherOptions` is a hole through which any of them could arrive unvalidated.\n *\n * **`BufferSize` IS THE EIGHTH AND IT IS NOW ACCEPTED**, because the behavior it\n * governs -- DFT -- exists. It is the one keyword here that never reaches the host\n * command: it sizes our own frames and the host learns it from the DDM Query Reply,\n * so it applies to a receive and to VM, where the DCB keywords do not.\n */\nconst KEYWORDS = [\n 'direction', 'localfile', 'hostfile', 'host', 'mode', 'cr', 'exist',\n 'recfm', 'lrecl', 'blksize', 'buffersize',\n] as const;\n\nconst KEYWORD_SET = new Set<string>(KEYWORDS);\n\n/**\n * Resolve one enumerated value, case-insensitively and EXACTLY.\n *\n * WHERE WE ARE STRICTER THAN s3270, on purpose. x3270 prefix-matches values \u2014\n * `strncasecmp(value, tp[i].keyword[k], strlen(value))` (ft.c:880-882) \u2014 so\n * `Mode=a` means ascii and `Cr=a` means `auto`, silently, because `auto` comes\n * first in its list and `add` never gets a chance. A one-letter abbreviation that\n * resolves to the wrong option is precisely the class of failure this action must\n * not have: `Mode=b` for binary and `Mode=a` for ascii differ by a silently\n * corrupted MODULE. Full spellings only, and an unrecognized one names the set.\n */\nfunction enumValue<T extends string>(\n keyword: string,\n value: string,\n allowed: readonly T[],\n): T {\n const lower = value.toLowerCase();\n const found = allowed.find((a) => a === lower);\n if (found === undefined) {\n throw new TransferOptionError(\n `invalid ${keyword} value '${value}' (expected ${allowed.join(', ')})`,\n );\n }\n return found;\n}\n\n/**\n * A positive integer, as x3270 requires for these keywords: `l = strtol(value,\n * &ptr, 10); if (l <= 0 || ptr == value || *ptr)` is an error (ft.c:894-900).\n *\n * `Number()` alone would accept `80.5`, `0x50`, `1e3` and ` 80 `, all of which\n * would reach the host as something the operator did not type, so the digits are\n * checked with a regexp first.\n */\nfunction positiveInt(keyword: string, value: string): number {\n if (!/^[0-9]+$/.test(value)) {\n throw new TransferOptionError(`${keyword} must be a positive integer, got '${value}'`);\n }\n const n = Number(value);\n if (n <= 0) throw new TransferOptionError(`${keyword} must be greater than zero`);\n return n;\n}\n\n/**\n * Parse `Transfer()` arguments into a validated request.\n *\n * ## SYNTAX: `keyword=value` ONLY\n *\n * s3270 also accepts `keyword,value` as two arguments (ft.c:865-877). We do not,\n * and cannot usefully: `commands.ts` splits arguments on commas AND spaces\n * (splitArgs, commands.ts:84-88), so `Transfer(Direction,send)` and\n * `Transfer(Direction=send, Mode=ascii)` arrive as indistinguishable token lists\n * and a parser that guessed would misread one of them. A bare token therefore\n * gets an error that says what to type instead.\n *\n * ## DEFAULTS, and the one that matters\n *\n * `Mode=binary` IS OUR DEFAULT AND IS NOT x3270's. x3270 defaults `ascii_flag`\n * to true (ft.c:321) \u2014 a text-first default from a world of card-image datasets.\n * The design doc's scope item 3 reverses it: \"Binary by default, `Mode=ascii`\n * opt-in. A wrong default silently corrupts a MODULE; a wrong explicit choice is\n * visible in the command.\" The live TK5 IND$FILE agrees, defaulting to binary\n * itself and listing `ASCII` as an option. Everything else follows x3270:\n * `Direction=receive` (ft.c:319), `Host=tso` (ft.c:320), `Exist=keep`\n * (ft.c:324-325), `Cr=auto` (ft.c:322), and no RECFM/LRECL/BLKSIZE\n * (ft.c:326-329).\n *\n * ## AN UNKNOWN KEYWORD IS AN ERROR\n *\n * Not a warning, not a skip. x3270 agrees for a genuinely unknown keyword\n * (ft.c:917-919) but merely warns and continues for an invalid *resource* value\n * (`xs_warning(\"Invalid %s '%s', ignoring\")`, ft.c:415). Dropping a keyword the\n * operator typed produces a transfer that does something they did not ask for \u2014\n * a `Recfm` that never reached the host is a dataset with the wrong DCB, found\n * days later.\n */\nexport function parseTransferKeywords(args: readonly string[]): TransferRequest {\n const given = new Map<string, string>();\n\n for (const arg of args) {\n if (arg === '') continue; // a trailing comma; harmless\n const eq = arg.indexOf('=');\n if (eq < 0) {\n throw new TransferOptionError(\n `option '${arg}' needs a value, as keyword=value ` +\n `(e.g. Direction=send); the s3270 'keyword,value' form is not accepted`,\n );\n }\n // `if (eq == argv[j] || !*(eq + 1))` \u2014 neither side may be empty\n // (ft.c:858-863).\n const keyword = arg.slice(0, eq);\n const value = arg.slice(eq + 1);\n if (keyword === '') throw new TransferOptionError(`option '${arg}' has no keyword`);\n if (value === '') throw new TransferOptionError(`option '${keyword}' has no value`);\n\n const lower = keyword.toLowerCase();\n if (!KEYWORD_SET.has(lower)) {\n throw new TransferOptionError(\n `unknown option '${keyword}' (accepted: ${KEYWORDS.join(', ')})`,\n );\n }\n // A repeated keyword is an error rather than last-wins. x3270 overwrites\n // silently (`tp[i].value = NewString(value)`, ft.c:911), which means\n // `Mode=ascii,Mode=binary` transfers in binary with no hint that the first\n // one was discarded.\n if (given.has(lower)) {\n throw new TransferOptionError(`option '${keyword}' given more than once`);\n }\n given.set(lower, value);\n }\n\n const get = (k: string): string | undefined => given.get(k);\n\n const localFile = get('localfile');\n const hostFile = get('hostfile');\n // \"Check for required values\" (ft.c:993-1000), reported one at a time and\n // LocalFile first, as x3270 does.\n if (localFile === undefined) throw new TransferOptionError(\"missing 'LocalFile' option\");\n if (hostFile === undefined) throw new TransferOptionError(\"missing 'HostFile' option\");\n\n /*\n `send`, AND THIS IS THE SECOND DELIBERATE DIVERGENCE FROM x3270 IN THIS BLOCK.\n\n x3270 defaults to RECEIVE: `receive_flag` is initialised true (`ft.c:319`). We default to\n `send`, for the reason the user gave when they asked for it: **people are far more likely to\n use IND$FILE to get something INTO a historic S/370 system than to get it out.** Until VM has\n a TCP/IP stack, this client's main job is putting software onto these machines -- few people\n develop software locally on a VM or MVS guest.\n\n IT MATCHES THE FORM, which opens on `send` too (`transferForm.ts`'s `newTransferForm`). An\n earlier revision deliberately kept the two APART -- form `send`, parser `receive` -- on the\n grounds that an unattended script has no label to read. The user overruled that, and their\n argument is the stronger one: a default should reflect what the tool is FOR, and a divergence\n between the form and the parser is its own trap for anyone who learns the tool through the\n form and then writes a script.\n\n ## THE RISK, STATED PLAINLY RATHER THAN SOFTENED\n\n A SCRIPT WRITTEN AGAINST x3270's DEFAULT, OR AGAINST OUR OWN EARLIER ONE, NOW MEANS THE\n OPPOSITE. A `Transfer(LocalFile=...,HostFile=...)` that downloaded will UPLOAD, and with\n `Exist=replace` that overwrites the host dataset. This is the same hazard the form's\n `directionLabel` exists to prevent, and a script has no form to look at.\n\n MEASURED WHEN THIS CHANGED: 39 of the 48 `Transfer(...)` calls in `cli/test/runner.test.ts`\n omitted `Direction=` and silently relied on `receive` -- which is exactly how much code can\n depend on a default without naming it. They were made EXPLICIT rather than left to track\n whatever the default happens to be. Every `Transfer(...)` in `packages/cli/scripts/` already\n named its direction, so no shipped script changed meaning.\n\n README's *File transfer* section documents this divergence for operators. **If you are reading\n this because a script did the wrong thing: name `Direction=` explicitly. Always.**\n\n The sibling divergence, for symmetry: `Mode` below defaults to `binary` where x3270 sets\n `ascii_flag` true (`ft.c:321`), because a wrong ascii default silently CORRUPTS a module while\n a wrong explicit choice is visible in the command.\n */\n const direction = enumValue('Direction', get('direction') ?? 'send', ['receive', 'send'] as const);\n const host = enumValue('Host', get('host') ?? 'tso', ['tso', 'vm'] as const);\n const mode = enumValue('Mode', get('mode') ?? 'binary', ['binary', 'ascii'] as const);\n const exist = enumValue('Exist', get('exist') ?? 'keep', ['keep', 'replace', 'append'] as const);\n\n const crText = get('cr');\n const cr = enumValue('Cr', crText ?? 'auto', ['auto', 'remove', 'add', 'keep'] as const);\n // `if (!p->ascii_flag) { popup_an_error(AnTransfer \"(): Invalid 'Cr' option\n // for ASCII mode\"); return NULL; }` (ft.c:944-947) \u2014 x3270's message reads\n // backwards but the rule is right: an explicit Cr in binary mode is a\n // contradiction, because CRLF translation only exists in ascii mode. `auto` is\n // exempt because it means \"follow the mode\", which binary mode answers.\n if (crText !== undefined && cr !== 'auto' && mode !== 'ascii') {\n throw new TransferOptionError(\n `Cr=${crText} needs Mode=ascii; in binary mode no line-end translation happens`,\n );\n }\n\n // `Recfm=default` is x3270's spelling for \"unspecified\" (ft.c:152, and\n // DEFAULT_RECFM at ft_private.h:44). Accepted so an s3270 script that says so\n // explicitly still runs, and mapped to absence.\n const recfmText = get('recfm');\n const recfmValue = recfmText === undefined\n ? undefined\n : enumValue('Recfm', recfmText, ['default', 'fixed', 'variable', 'undefined'] as const);\n const recfm: FtRecfm | undefined = recfmValue === undefined || recfmValue === 'default'\n ? undefined\n : recfmValue;\n\n const lreclText = get('lrecl');\n const blksizeText = get('blksize');\n const lrecl = lreclText === undefined ? undefined : positiveInt('Lrecl', lreclText);\n const blksize = blksizeText === undefined ? undefined : positiveInt('Blksize', blksizeText);\n\n // CLAMPED WITH THE FUNCTION CORE ALREADY USES, not with a second copy of the bounds.\n // `boundDftBufferSize` is what `ddmCapability` applies to the advertisement\n // (`queryreply.ts:487`) and what `DftTransfer`'s constructor applies to the chunking\n // (`dft.ts:172`), so reusing it here is what makes all three agree BY CONSTRUCTION\n // rather than by three matching literals. Clamping rather than rejecting is x3270's\n // `set_dft_buffersize` (`ft_dft.c:740-747`).\n //\n // `positiveInt` FIRST, so the clamp does not become a swallow-everything: `BufferSize=big`\n // must fail rather than quietly become the 256 minimum.\n const bufferSizeText = get('buffersize');\n const bufferSizeAsked = bufferSizeText === undefined\n ? undefined\n : positiveInt('BufferSize', bufferSizeText);\n const bufferSize = bufferSizeAsked === undefined\n ? undefined\n : boundDftBufferSize(bufferSizeAsked);\n\n // WARN ONLY WHEN THE CLAMP ACTUALLY MOVED IT. Comparing the two values rather than\n // re-testing the bounds is what keeps this honest if `boundDftBufferSize` ever changes:\n // there is one definition of \"out of range\" and it is the function's own behavior.\n // Both numbers are in the message because either alone is unactionable -- \"clamped to 256\"\n // does not say what was ignored, and naming only the input does not say what ran.\n const warnings: string[] = [];\n if (bufferSizeAsked !== undefined && bufferSize !== bufferSizeAsked) {\n warnings.push(\n `BufferSize ${bufferSizeAsked} is outside the DFT range `\n + `${DFT_BUF_MIN}..${DFT_BUF_MAX}; using ${bufferSize}`,\n );\n }\n\n // THREE COMBINATIONS x3270 ACCEPTS AND SILENTLY DROPS. Each one is rejected\n // here instead, because the visible symptom of the drop is a host dataset with\n // attributes nobody chose.\n //\n // 1. The DCB group on a receive: the whole block is inside\n // `if (!p->receive_flag)` (ft.c:702), so a GET discards it.\n if (direction === 'receive') {\n for (const [k, present] of [['Recfm', recfm !== undefined], ['Lrecl', lrecl !== undefined], ['Blksize', blksize !== undefined]] as const) {\n if (present) {\n throw new TransferOptionError(\n `${k} applies to Direction=send only; on a receive the host dataset already exists`,\n );\n }\n }\n }\n // 2. Lrecl or Blksize without Recfm: x3270 nests both inside\n // `if (p->recfm != DEFAULT_RECFM)` (ft.c:704, :721-726), so `Lrecl=80`\n // alone emits nothing at all.\n if (recfm === undefined && (lrecl !== undefined || blksize !== undefined)) {\n throw new TransferOptionError(\n 'Lrecl and Blksize need Recfm as well; the host ignores them without it',\n );\n }\n // 3. Blksize on VM, and Recfm=undefined on VM: the VM branch emits neither\n // (ft.c:749-766 has no blksize case, and its recfm switch has only F and\n // V \u2014 an undefined recfm there would emit the bare word `RECFM` with no\n // letter after it, which is a malformed command rather than a default).\n if (host === 'vm') {\n if (blksize !== undefined) {\n throw new TransferOptionError('Blksize is a TSO option; CMS files have no block size');\n }\n if (recfm === 'undefined') {\n throw new TransferOptionError(\"Recfm=undefined is a TSO option; CMS supports fixed and variable\");\n }\n }\n\n return {\n direction,\n localFile,\n hostFile,\n host,\n mode,\n cr,\n exist,\n // Conditional spreads, not `recfm: recfm`: with exactOptionalPropertyTypes an\n // explicit `undefined` is not the same as absence and does not typecheck.\n ...(recfm !== undefined ? { recfm } : {}),\n ...(lrecl !== undefined ? { lrecl } : {}),\n ...(blksize !== undefined ? { blksize } : {}),\n ...(bufferSize !== undefined ? { bufferSize } : {}),\n // Absent rather than empty, so `warnings` reads as \"there is something to say\".\n ...(warnings.length > 0 ? { warnings } : {}),\n };\n}\n\n/**\n * Parse, then build the command \u2014 the two halves the runner needs, so that a\n * test can assert the exact string an argument list produces.\n */\nexport function transferCommand(args: readonly string[]): { request: TransferRequest; command: string } {\n const request = parseTransferKeywords(args);\n return { request, command: dialectFor(request.host).buildCommand(request) };\n}\n", "/**\n * Drive a CUT transfer event-driven rather than blocking, for any front end that cannot\n * block: the TUI today, and the GUI and web gateway when they grow a transfer UI.\n *\n * ## WHY IT LIVES IN `frontend` AND NOT IN `tui`\n *\n * It was written for the TUI and sat in `packages/tui` for that reason alone. But its\n * justification below -- \"a TUI cannot block\" -- is equally true of a GUI and of a web\n * gateway, and both already declare `@tn3270/frontend`. There are TWO genuine control\n * flows here, not four: the CLI's blocking poll-until-done (the s3270 line protocol\n * cannot report a completion arriving after its `ok`) and this one, which the other three\n * front ends all want. Keeping it here is what stops the DFT arm being written four\n * times.\n *\n * ## WHY NOT REUSE THE CLI'S LOOP\n *\n * `Runner.runTransferFrames` is `async` and polls inside a `for(;;)` with a 10ms sleep,\n * which suits a script that blocks until the transfer ends -- the s3270 line protocol has\n * no way to report a completion arriving after its `ok`. A TUI cannot block: the screen\n * underneath must keep repainting, and in CUT mode a transfer IS screen traffic, so the\n * user watches the frames go by. Same `CutTransfer`, same steps, driven from the `screen`\n * event instead of a sleep.\n *\n * ## THE ORDER OF OPERATIONS IS THE ERROR-HANDLING RULE\n *\n * Everything checkable locally is checked BEFORE the host is told anything: 3270 mode, the\n * keyboard lock, the source file, the destination. A local error after the host has been\n * primed leaves it sitting in transfer mode waiting for a client that has already given up\n * -- which the operator then has to break out of by hand.\n *\n * The checks are in the CLI's order. Geometry is NO LONGER AMONG THEM: under\n * host-chooses-the-protocol we cannot know CUT was chosen until the host answers, so the\n * 24x80 demand moved to the decision point (see `docs/superpowers/specs/\n * 2026-09-25-transfer-protocol-selection-design.md`). The accepted cost is that a CUT-only\n * host at 43x80 is now primed before we find out; measured on VM/370, MECAFF refuses with\n * its own text in about a second and CMS recovers itself, so nothing wedges.\n *\n * ## WHAT IS DELIBERATELY NOT HERE\n *\n * No abort synthesised from a timeout. The CLI's comment gives the reason and it still\n * holds: an abort writes the response area and presses PF2 from a frame `CutTransfer` has\n * parsed, so inventing one here would put bytes on the wire that no captured session\n * contains. A timeout reports honestly that the host may still be mid-transfer. What IS\n * available now, and was not when the CLI was written, is `CutTransfer.cancel` for an abort\n * the OPERATOR asks for -- which is a different thing from one a deadline infers.\n */\n\nimport {\n AID, CUT_SCREEN_SIZE, CutTransfer, DftTransfer, looksLikeCutFrame,\n type Session, type TransferResult,\n} from '@tn3270/core';\nimport type { TransferFiles, TransferRequest } from './transfer.js';\n\n/** The CLI's own defaults (`runner.ts:43-44`), so the two front ends wedge alike. */\nconst FRAME_TIMEOUT_MS = 30_000;\nconst TOTAL_TIMEOUT_MS = 600_000;\n\nexport interface TransferRun {\n readonly ok: boolean;\n readonly error?: string;\n /** Abort and tell the host. Absent when the run never started. */\n readonly cancel?: () => void;\n}\n\nexport interface StartTransferOptions {\n /** `Session` from `@tn3270/core`, which is what `App` already holds. */\n session: Session;\n files: TransferFiles;\n request: TransferRequest;\n command: string;\n onProgress: (text: string) => void;\n onDone: (result: { ok: boolean; error?: string; bytes?: number }) => void;\n frameMs?: number;\n totalMs?: number;\n}\n\n/**\n * Start a transfer, or refuse it.\n *\n * Returns `{ ok: false, error }` for anything checkable locally, in which case nothing has\n * reached the host and `onDone` is never called -- the caller shows the error on the form.\n * Returns `{ ok: true, cancel }` once the command has been typed and Enter sent, after\n * which the outcome arrives through `onDone` exactly once.\n */\nexport function startTransfer(opts: StartTransferOptions): TransferRun {\n const { session, files, request, command, onProgress, onDone } = opts;\n const frameMs = opts.frameMs ?? FRAME_TIMEOUT_MS;\n const totalMs = opts.totalMs ?? TOTAL_TIMEOUT_MS;\n\n if (!session.is3270Mode()) {\n // x3270's `ftUnableNot3270`, \"not in 3270 mode\" (fb-common:47).\n return { ok: false, error: 'not in 3270 mode' };\n }\n\n // The local side. For a send this reads the whole file into memory, which is what the state\n // machine wants anyway: `CutTransfer` TAKES THE BYTES UP FRONT SO IT CAN ANSWER A RETRANSMIT\n // without re-reading. That, and not throughput, is the reason this is not streamed.\n //\n // AN EARLIER VERSION OF THIS COMMENT SAID a file big enough to matter \"would take hours over\n // CUT regardless\", which is false and predated any measurement. From `docs/live-testing.md:413`\n // -- ~15 ms/frame locally and a 1.727x codec expansion -- CUT moves ~72 KiB/s and DFT ~1065\n // KiB/s, so 10 MB is ~2.4 MINUTES and ~10 seconds respectively, not hours. Recomputed\n // 2026-10-07 from those two figures plus the frame capacities (`O_UP_MAX` 1912 encoded bytes,\n // DFT's default 16384 buffer).\n //\n // THE RATE IS FRAME-LATENCY-BOUND, NOT BANDWIDTH-BOUND, which is why the 15 ms matters more than\n // any byte count: every frame is a screen round trip. That figure is Hercules on one machine, so\n // a genuinely remote host at ~100 ms RTT is roughly 6x slower again -- ~16 min on CUT, ~1 min on\n // DFT. Quote the mechanism rather than the number if you cite this.\n let source: Uint8Array | undefined;\n if (request.direction === 'send') {\n try {\n source = files.read(request.localFile);\n } catch (err) {\n return {\n ok: false,\n error: `cannot read local file ${request.localFile}: `\n + `${err instanceof Error ? err.message : String(err)}`,\n };\n }\n } else if (request.exist === 'keep' && files.exists(request.localFile)) {\n // `if (p->receive_flag && !p->append_flag && !p->allow_overwrite)` (ft.c:666-674), with\n // x3270's own \"File exists\" wording plus the path.\n return { ok: false, error: `file exists: ${request.localFile} (use Exist=replace or append)` };\n }\n\n // The keyboard and the field, which can also refuse -- and must, before the host is told.\n const primed = primeAndType(session, command);\n if (primed !== undefined) return { ok: false, error: primed };\n\n // BOTH ENGINES, over the SAME source buffer. The host chooses the protocol, not us\n // (x3270's `ft_running` merely REPORTS which arrived, ft.c:556), so we cannot know\n // which we need until the first frame lands. Both need their source bytes up front\n // anyway -- CUT to answer a retransmit, DFT to answer a GET -- so this is one extra\n // object over one Uint8Array, not a second copy.\n const cut = new CutTransfer({\n direction: request.direction,\n ...(source !== undefined ? { data: source } : {}),\n });\n const dft = new DftTransfer({\n direction: request.direction,\n ...(source !== undefined ? { data: source } : {}),\n // `BufferSize=N` REACHES THE ENGINE, and this line is the one that closes a real bug\n // rather than adding a feature: without it the engine always took its 16384 default\n // while the DDM Query Reply advertised whatever the session was given, so the two\n // could differ by 32x -- promising the host one frame size and sending another.\n // `Session.startDftTransfer` now reads the size back OFF this engine for the\n // advertisement, so there is one number and both sides read it.\n ...(request.bufferSize === undefined ? {} : { bufferSize: request.bufferSize }),\n });\n\n // REGISTERED BEFORE THE HOST IS TOLD ANYTHING, and the order is load-bearing: a fast\n // host's first 0xd0 arrives from inside `handleRecord`, before any driver code runs\n // again, and `handleTransferData` needs a registered transfer at that moment. Doing\n // this after sendAID(ENTER) would lose the first frame of a fast transfer to a race.\n session.startDftTransfer(dft);\n\n let ended = false;\n let frameTimer: ReturnType<typeof setTimeout> | undefined;\n let totalTimer: ReturnType<typeof setTimeout> | undefined;\n\n /**\n * Which protocol the host chose, once it has said. `undefined` means still waiting.\n *\n * Read by `cancel`, which has to abort a CUT transfer and merely discard a DFT one.\n */\n let committed: 'cut' | 'dft' | undefined;\n\n /**\n * Did the host paint ANY screen that was not a CUT frame?\n *\n * Only the timeout message reads it, and it earns its keep there: \"nothing arrived at all\"\n * and \"screens arrived, none of them a frame\" want different diagnoses, and the operator\n * cannot tell them apart from a bare deadline.\n */\n let sawNonFrameScreen = false;\n\n const clearTimers = (): void => {\n if (frameTimer !== undefined) { clearTimeout(frameTimer); frameTimer = undefined; }\n if (totalTimer !== undefined) { clearTimeout(totalTimer); totalTimer = undefined; }\n };\n\n /**\n * How many bytes moved, from WHICHEVER ENGINE RAN.\n *\n * ## WHY THIS CANNOT JUST READ `committed`\n *\n * `committed` becomes `'dft'` only inside `onTransferEnd` (and `'cut'` on the first CUT frame),\n * so on every path that ends a transfer EARLY it is still `undefined` -- which is precisely the\n * timeout path. `cancel` already documents this and reaches for the engine's own byte count for\n * the same reason; this is that reasoning in one place instead of two.\n *\n * ## THE BUG IT FIXES, AND IT COST A WHOLE DIAGNOSIS RATHER THAN A COSMETIC LINE\n *\n * `timeout` interpolated `cut.bytesTransferred` unconditionally, and on a DFT transfer that\n * counter is ZERO BY CONSTRUCTION because the CUT engine never ran. So a DFT transfer that\n * stalled after moving 150 KB still reported `0 bytes` -- and the TSO/DFT 200 KB failure was\n * recorded on 2026-10-02 as \"no progress events at all ... zero bytes moved, so this is not the\n * timer\" ON THE STRENGTH OF THAT NUMBER. The message could not have said anything else.\n *\n * `finish` had the same hole by a different route: it reads `committed`, which the timeout path\n * leaves `undefined`, so the `bytes` handed to `onDone` was also CUT's zero. One helper, both\n * sites -- the one-path-reads-it-right-and-another-does-not shape this file's history carries\n * twice over (`Session.e`, `tn3270eNegotiated`).\n *\n * A nonzero `dft.transferred` is proof DFT ran: the CUT path releases the DFT registration on\n * its first frame (`onScreen`), so the two counters can never both be nonzero.\n */\n const bytesMoved = (): number => (\n dft.transferred > 0 ? dft.transferred : cut.bytesTransferred\n );\n\n /**\n * End the run exactly once: drop the listener, kill both timers, report.\n *\n * `ended` guards against every way two endings could race -- a frame completing as a\n * deadline fires, or a cancel arriving as the last frame lands. Without it a canceled run\n * would later report a timeout, OVERWRITING \"canceled\" on a form the operator has already\n * closed, and `onDone` would run twice for one transfer.\n */\n const finish = (result: TransferResult): void => {\n if (ended) return;\n ended = true;\n // A LEAKED LISTENER PER TRANSFER is invisible except as wasted CPU, which is why\n // `Session.listenerCount` exists at all. Removed here because this is the one place a\n // run ends -- the same discipline as `Session.handleClose` owning TN3270E teardown.\n session.off('screen', onScreen);\n session.off('transferEnd', onTransferEnd);\n // REMOVED WITH THE OTHERS, or a finished run keeps a listener on a live session: the next\n // transfer's frames would re-arm a timer belonging to a run that had ended, and this file's own\n // history has the shape -- a stale deadline firing into a later run is what the generation stamp\n // in `gui/src/transferWindow.ts` exists to contain.\n session.off('transferProgress', onDftProgress);\n clearTimers();\n // NARROWED, not `result.error`: `TransferResult` is a DISCRIMINATED UNION\n // (`ft/transfer.ts:98`) and `error` exists only on the `ok: false` arm, so reading it\n // unconditionally does not typecheck. Caught by `npm run typecheck` with all 14 tests\n // green -- vitest does not typecheck, and a `result.error` on the success arm would have\n // been `undefined` at runtime anyway, so no test could have found it.\n onDone({\n ok: result.ok,\n ...(result.ok ? {} : { error: result.error }),\n // FROM WHICHEVER ENGINE RAN. Reporting `cut.bytesTransferred` unconditionally would\n // tell the operator a DFT transfer moved 0 bytes -- a success message contradicting\n // itself, and the one number they would check against the host's own listing. The\n // counters are per-engine and not interchangeable: `bytesTransferred` on CUT,\n // `transferred` on DFT (`dft.ts:181`).\n //\n // VIA `bytesMoved`, NOT `committed`: this runs on the TIMEOUT path too, where `committed` is\n // still `undefined` and the old expression therefore reported CUT's zero for a DFT transfer\n // that had moved real bytes. See `bytesMoved`.\n bytes: bytesMoved(),\n });\n };\n\n const timeout = (why: string): void => {\n // We deliberately do NOT abort here -- see the module comment. The operator's recovery\n // is Attn or Clear, as it would be from a real terminal, and the message says so because\n // otherwise their next keystroke goes into a host still waiting for a CUT frame.\n //\n // THE RECOVERY COMES FIRST, WHICH IS NOT COSMETIC. The TUI's status line is 54 columns\n // and truncates, so the CLI's word order -- reason, bytes, then \"(press Attn or Clear)\"\n // -- put the ONE ACTIONABLE PHRASE past the cut: the operator saw \"transfer did not\n // complete within 600s after 0 bytes; \" and nothing about what to do with a host still\n // in transfer mode. Measured: the message is 113 characters. Same shape as the help\n // string that did not fit in `transferOverlay.ts`, one layer down.\n // WHAT WAS OBSERVED, not what we guess. Two clauses, mutually exclusive by construction,\n // and the bare message when neither applies -- a 24x80 host painting non-frames has\n // ignored the command (wrong panel, IND$FILE absent), where naming the geometry would be\n // false and naming `-ddm` irrelevant.\n //\n // THE REASON NAMES THE ENGINE THAT WAS ACTUALLY OFFERED, 2026-10-27. `why` is built by\n // `armFrameTimer` and says \"no CUT frame\", which is the CUT engine's deadline -- but that\n // timer is re-armed by BOTH engines, so on a DDM-advertised session the sentence named a\n // protocol the host may never have been asked to speak. **This misread a live TSO stall:\n // the operator reasonably concluded \"it is not doing DFT at all\" from a message that only\n // ever says CUT.** It cannot say WHICH engine the host chose -- `committed` is still\n // `undefined` here by construction, and that is the whole point of the timeout -- so it\n // states what we OFFERED, which is a fact we hold, instead of implying what arrived.\n const offered = session.ddmAdvertised ? 'CUT or DFT' : 'CUT only';\n let extra = '';\n if (sawNonFrameScreen && session.screen.size !== CUT_SCREEN_SIZE) {\n // FALLBACK ONLY. Measured on VM/370: MECAFF answers `IND$FILE requires a MECAFF\n // connected 3270 terminal` and CMS recovers in about a second, so the HOST'S OWN TEXT\n // usually arrives and is better than ours. This row is for a host that goes quiet --\n // which is the case the deleted geometry gate used to pre-empt.\n //\n // IT NOW SAYS THE HOST ANSWERED, AND LEADS WITH THAT. 2026-10-27, after a live TSO stall\n // where this clause was the whole message an operator had to reason from and it offered\n // geometry as THE explanation. Geometry is a guess; `sawNonFrameScreen` is a MEASUREMENT,\n // and it is the more useful half -- a host that painted a screen has usually said why, on\n // that screen, in its own words. The real cause that day was `IND$FILE` typed into an\n // ISPF command field instead of TSO's `READY` prompt, which this phrasing points at and\n // the old one hid: TK5's IND$FILE is a plain TSO command and ISPF never runs it.\n extra = ` (the host ANSWERED but sent no transfer frame -- read its screen, which usually`\n + ` says why: a command run somewhere that cannot run it, or a host wanting 24x80 where`\n + ` this session is ${session.screen.rows}x${session.screen.cols})`;\n } else if (!sawNonFrameScreen && !session.ddmAdvertised) {\n // EARNS ITS PLACE: forgetting `-ddm on` will be the commonest failure once DFT works,\n // because a host that speaks only DFT cannot CHOOSE DFT unless we advertised QCODE\n // 0x95. Guarded on `!sawNonFrameScreen` so it is not printed at a host that plainly\n // answered -- there it would be noise, and noise in a message gets learned as ignorable.\n extra = ' (DDM was not advertised; a host that speaks only DFT needs -ddm on)';\n }\n finish({\n ok: false,\n // `bytesMoved()`, NOT `cut.bytesTransferred`: on a DFT transfer the CUT counter is zero by\n // construction, so this message used to report `0 bytes` however much DFT had moved -- and\n // that zero was read as evidence of a dead protocol rather than a stalled one. See\n // `bytesMoved`.\n error: `press Attn or Clear: host may still be transferring. `\n + `${why} (we offered ${offered}), ${bytesMoved()} bytes${extra}`,\n });\n };\n\n /**\n * A DFT transfer ended, however it ended.\n *\n * `Session` has already cleared its own reference and fired `transferEnd`\n * (`session.ts:1254-1257`), so the outcome is read from the engine reference kept above --\n * `session.dftTransfer` is `undefined` by the time this runs and cannot be used.\n *\n * THE RECEIVED BYTES COME OFF `result.data`, NOT off the engine. `DftTransfer` has no `data`\n * accessor at all; its surface is `result`/`complete`/`isMessage`/`retainedFrame`/\n * `transferred` (`dft.ts:175-183`), and a download's payload is the `data` field of\n * `TransferResult`'s success arm (`ft/transfer.ts:98-100`). Reading a non-existent accessor\n * would have yielded `undefined`, coalesced to an empty array, and written a ZERO-BYTE FILE\n * under a SUCCESS message -- the failure mode this project's live-transfer rule already\n * names: compare bytes, not the status line.\n */\n /**\n * One accepted DFT data frame.\n *\n * ## THIS RE-ARMS THE DEADLINE, AND WITHOUT IT A LONG DFT TRANSFER IS KILLED\n *\n * `armFrameTimer` is called from the CUT path only, so before this handler existed nothing\n * re-armed the 30-second per-frame timer during a DFT transfer -- and the timer fired. Measured\n * 2026-10-02 against MVS/TSO: a 249-byte file finished inside the window, and a 200 KB one died\n * with `stalled: no CUT frame from the host within 30s, 0 bytes` while the host was transferring\n * perfectly well. **The message was doubly wrong -- nothing was stalled and nothing was speaking\n * CUT -- which is why this took a live 200 KB run to find rather than a unit test.**\n *\n * ## AND IT IS WHY DFT NOW HAS A PROGRESS LINE AT ALL\n *\n * `onProgress` used to be reached only from the CUT frame handler, so a DFT transfer reported\n * nothing between its start and its end. That was documented as correct-by-design; it was really\n * the same silence as the bug above, seen from the operator's side.\n *\n * `committed` is NOT set here. The protocol race is decided by whichever side speaks first, and a\n * data frame is already proof DFT won -- but `onTransferEnd` is the one place that owns that\n * transition, and setting it in two places is how the two would eventually disagree.\n */\n const onDftProgress = (): void => {\n if (ended) return;\n armFrameTimer();\n onProgress(`${dft.transferred} bytes`);\n };\n\n const onTransferEnd = (): void => {\n if (ended) return;\n committed = 'dft';\n const result = dft.result ?? { ok: false, error: 'DFT transfer ended without a result' };\n if (result.ok && request.direction === 'receive') {\n const bytes = result.data ?? new Uint8Array(0);\n try {\n if (request.exist === 'append') files.append(request.localFile, bytes);\n else files.write(request.localFile, bytes);\n } catch (err) {\n // THE TRANSFER SUCCEEDED AND THE WRITE DID NOT, handled exactly as the CUT arm does:\n // a \"complete\" that left no file on disk is the one outcome an operator must not be\n // told, and the host is already out of transfer mode so there is nothing to abort.\n finish({\n ok: false,\n error: `transfer complete but could not write ${request.localFile}: `\n + `${err instanceof Error ? err.message : String(err)}`,\n });\n return;\n }\n }\n finish(result);\n };\n\n /** Re-armed on every frame: the per-frame deadline measures the GAP, not the total. */\n const armFrameTimer = (): void => {\n if (frameTimer !== undefined) clearTimeout(frameTimer);\n frameTimer = setTimeout(\n () => { timeout(`stalled: no CUT frame from the host within ${frameMs / 1000}s`); },\n frameMs,\n );\n };\n\n /**\n * One frame.\n *\n * NO `outputCount` EQUIVALENT IS NEEDED, and that is the real simplification over the CLI:\n * its loop polls, so it must ask \"has the host written since I last looked\" to avoid\n * re-processing the frame still sitting in the buffer. An event fires once per host write,\n * so arriving here IS the freshness signal.\n */\n const onScreen = (): void => {\n if (ended) return;\n // DECIDE WITH THE NON-THROWING DETECTOR. `isCutFrame` was correct here while the geometry\n // gate guaranteed 24x80; with the gate gone this handler runs at ANY geometry, and\n // `isCutFrame` THROWS rather than answering (`frames.ts:314`). Measured: at 43x80 the\n // first host write of any kind threw `CutFrameError` out of the screen listener. That is\n // the whole reason `looksLikeCutFrame` was built in Task 2.\n if (!looksLikeCutFrame(session.screen)) {\n // The host painted something that is not a CUT frame. Keep waiting -- a DFT host may\n // still be about to speak, and a CUT host often paints its prompt first -- but REMEMBER\n // it, because the timeout message distinguishes this from silence.\n sawNonFrameScreen = true;\n return;\n }\n // CUT HAS WON. Release the DFT registration before stepping, so a stray inbound read\n // cannot replay a retained frame at a host now running CUT.\n if (committed === undefined) {\n committed = 'cut';\n session.cancelDftTransfer();\n }\n armFrameTimer();\n // `step` may mutate the screen -- an abort response, or a whole upload frame -- and the\n // AID it returns is what sends those bytes, so the two must not be separated.\n const step = cut.step(session.screen);\n if (step.ack !== undefined) session.sendAID(step.ack);\n onProgress(`${cut.bytesTransferred} bytes`);\n if (step.done !== undefined) {\n if (step.done.ok && request.direction === 'receive') {\n // `result.data` is always present for a successful receive (`CutTransfer.success`),\n // but the type allows its absence and an empty file is a legitimate transfer.\n const bytes = step.done.data ?? new Uint8Array(0);\n try {\n if (request.exist === 'append') files.append(request.localFile, bytes);\n else files.write(request.localFile, bytes);\n } catch (err) {\n // THE TRANSFER SUCCEEDED AND THE WRITE DID NOT. Reported as a failure, because a\n // \"complete\" that left no file on disk is the one outcome an operator must not be\n // told; the host is already out of transfer mode, so there is nothing to abort.\n finish({\n ok: false,\n error: `transfer complete but could not write ${request.localFile}: `\n + `${err instanceof Error ? err.message : String(err)}`,\n });\n return;\n }\n }\n finish(step.done);\n }\n };\n\n // NOW the host is involved. Everything from here can leave it in transfer mode, which is\n // why nothing above could.\n //\n // TWO WAKE SIGNALS, because the two protocols are observable in different places. A CUT\n // transfer IS screen traffic, so `screen` sees every frame. A DFT transfer paints NOTHING --\n // it is structured fields through `handleTransferData`, and although a WSF record does fire\n // `screen` (`session.ts:897` is unconditional) the buffer is untouched, so every one of\n // those events is a non-frame indistinguishable from a host painting a menu. A driver waking\n // only on `screen` would therefore spin to its deadline on a perfectly healthy transfer.\n //\n // REGISTERED BEFORE `sendAID`, WHICH IS WHAT MAKES A SEPARATE STATE CHECK UNNECESSARY. A\n // whole DFT transfer really can begin and finish inside `sendAID`'s record handling -- one\n // of the tests drives exactly that -- but the listener is already in place when it does, so\n // `onTransferEnd` fires from within that call stack and the run completes synchronously.\n //\n // THE PLAN ASKED FOR AN `if (dft.complete)` CHECK HERE AND IT WAS DELETED AS DEAD CODE.\n // Nothing between `startDftTransfer` above and this line can reach the socket -- the\n // intervening statements are closure definitions and `let`s -- so `dft.complete` is\n // unfalsifiably false at this point. Its mutation check could not be made to fail, and the\n // spec's own rule governs: \"If nothing does, the call is decoration and should be deleted\n // rather than kept.\" `transferRun.test.ts` pins the ORDER instead, which is the real\n // invariant; reversing these two lines is what would break, and does.\n session.on('screen', onScreen);\n session.on('transferEnd', onTransferEnd);\n session.on('transferProgress', onDftProgress);\n session.sendAID(AID.ENTER);\n\n // IF IT ALREADY FINISHED, `finish` has run and these are no-ops that must not be armed: a\n // timer set after a completed run would fire into `ended` and clear itself, but it also\n // keeps the event loop alive for `totalMs`, which in the TUI is ten minutes per transfer.\n if (ended) return { ok: true };\n\n armFrameTimer();\n totalTimer = setTimeout(\n () => { timeout(`did not complete within ${totalMs / 1000}s`); },\n totalMs,\n );\n\n return {\n ok: true,\n cancel: (): void => {\n if (ended) return;\n\n // A DFT TRANSFER ALREADY UNDER WAY IS ABORTED, NOT DISCARDED, and telling the two\n // apart takes care. `committed` is NO USE here: it becomes `'dft'` only inside\n // `onTransferEnd`, which calls `finish` immediately after, so `ended` above has already\n // returned by the time it could be read. A mid-flight DFT transfer is therefore\n // indistinguishable from a host that has said nothing -- except by the engine's own\n // byte count, which is the signal used.\n //\n // The distinction is the difference between aborting and ABANDONING, which this module\n // refuses to do elsewhere: discarding an engine whose host is mid-transfer leaves that\n // host's program waiting for a frame that never comes. `DftTransfer.cancel` defers,\n // checking its flag on the next inbound frame (`dft.ts:186-188`, matching x3270's\n // `ft_dft.c:225-228`), so the registration MUST SURVIVE for that frame to arrive --\n // which is why `cancelDftTransfer` is not called on this path.\n if (dft.transferred > 0) {\n dft.cancel();\n finish({ ok: false, error: 'transfer canceled by user' });\n return;\n }\n\n // NOTHING AROSE FROM THE DFT ENGINE, so it is the loser of a protocol race and is\n // DISCARDED -- exactly `cancelDftTransfer`'s documented case (\"the loser of a protocol\n // race\", `session.ts:367`), which sends nothing because the host never addressed it.\n // Without this a canceled run leaves `session.dft` set and `answerRead` would replay\n // its retained frame at a host that has moved on. `handleClose` clears `dft` too, but a\n // form closed on a LIVE session never reaches it.\n session.cancelDftTransfer();\n\n // A CUT ABORT IS ONLY POSSIBLE AT 24x80, AND DELETING THE GEOMETRY GATE IS WHAT MADE\n // THAT REACHABLE -- a defect this task introduced and the plan did not predict.\n // `CutTransfer.cancel` writes the response area through `writeResponse`, which calls\n // `requireCutGeometry` and THROWS `CutFrameError` on any other geometry\n // (`frames.ts:314`). The gate used to guarantee this line was never reached at 43x80;\n // now it is, and the throw escaped uncaught into the TUI's form-close path.\n //\n // Reported, not aborted, and that is the honest answer rather than the convenient\n // one: there is no CUT frame layout to write into at this geometry, and the host has\n // not told us it is running CUT. Inventing an abort would put bytes on the wire that\n // no captured session contains -- the same rule this module already gives for\n // refusing to synthesise one from a timeout.\n if (session.screen.size !== CUT_SCREEN_SIZE) {\n finish({ ok: false, error: 'transfer canceled by user' });\n return;\n }\n // ABORT, NOT ABANDON: `cancel` writes the response area and returns PF2, so the host\n // leaves transfer mode. Walking away would leave its program waiting for a frame that\n // never comes. `CutTransfer.cancel` is itself idempotent, and `ended` means we do not\n // rely on that -- a second call here does nothing at all.\n const step = cut.cancel(session.screen);\n if (step.ack !== undefined) session.sendAID(step.ack);\n finish(step.done ?? { ok: false, error: 'transfer canceled by user' });\n },\n };\n}\n\n/**\n * Prime the field and type the command, or return why it could not be done.\n *\n * Ported from `Runner.primeAndType` (`runner.ts:551`) with its reasoning intact, returning a\n * message instead of throwing because this caller reports onto a form rather than into an\n * s3270 reply. **Every refusal here happens BEFORE the host is told anything**, which is why\n * it is called before `sendAID(ENTER)` and not merged into it.\n */\nfunction primeAndType(session: Session, command: string): string | undefined {\n const s = session.screen;\n const k = session.keyboard;\n\n if (session.oia.isInhibited()) {\n // x3270's `ftUnableLocked`, \"keyboard locked\" (fb-common:46).\n return 'cannot begin transfer: keyboard locked';\n }\n\n // An unformatted screen has no fields to erase and no capacity to measure -- x3270 guesses\n // at the run of nulls and spaces from the cursor (kybd.c:4389-4403) and leaves it to the\n // host to make sense of. We refuse instead: IND$FILE is typed at a command prompt, every\n // host that offers one paints it as a field, and an unformatted screen here means the\n // operator is somewhere they did not think they were -- a VM/370 logon banner, say.\n // Failing is recoverable; typing a transfer command into a logon screen is not.\n if (!s.isFormatted()) {\n return 'cannot begin transfer: no input field (screen is unformatted)';\n }\n\n const atCursor = s.fieldAt(s.cursor);\n const usable = atCursor !== null && !atCursor.protected && !s.isFieldAttribute(s.cursor)\n && atCursor.length > 0;\n if (usable) {\n k.moveCursor(atCursor.start);\n } else {\n k.home();\n const homed = s.fieldAt(s.cursor);\n if (homed === null || homed.protected) {\n // `ftUnableNoField`, \"no input field\" (fb-common:48).\n return 'cannot begin transfer: no input field';\n }\n }\n\n const field = s.fieldAt(s.cursor);\n if (field === null || field.length < command.length) {\n // `ftUnableTooSmall`, \"input field too small\" (fb-common:49), with both numbers, because\n // the operator's fix depends on which panel they are on.\n return `cannot begin transfer: input field too small `\n + `(${field?.length ?? 0} cells for a ${command.length}-character command)`;\n }\n\n // \"Erase it\" (kybd.c:4430-4435): the whole field is nulled before typing, so whatever the\n // operator or the host left there does not become part of the command.\n k.eraseEOF();\n if (!k.typeString(command)) {\n // typeString stops at the first refusal and the OIA says why. Cannot normally happen --\n // the lock and the capacity are both checked above -- but a half-typed command must be\n // reported, not sent.\n return `input inhibited while typing the command (${session.oia.toText()})`;\n }\n return undefined;\n}\n", "/**\n * The file-transfer form's MODEL: fields, values, cycle order and applicability.\n *\n * PURE, and the same split as the keypad: `frontend/src/keypad.ts` owns the button table\n * while `tui/src/keypadOverlay.ts` renders it. Here the model is the data and\n * `tui/src/transferOverlay.ts` draws it, which is what makes stage 3 (the GUI) cheap --\n * it writes a renderer and reuses all of this.\n *\n * ## THE CENTRAL RULE: THIS FORM NEVER VALIDATES\n *\n * It collects STRINGS and hands them to `parseTransferKeywords`. The applicability rules\n * below are ERGONOMICS -- they stop a user entering a doomed combination -- but authority\n * lives in exactly one place. If these rules and the validator's ever disagree, THE\n * VALIDATOR WINS and the user sees its message. That is what keeps the TUI and the CLI\n * from drifting on what is legal, and it means this form cannot invent a legal-looking\n * combination the engine rejects.\n *\n * ## WHY UNSET IS A VALUE\n *\n * `Cr` and `Recfm` start at `''`, and that is not the same as any of their values: an\n * unset `Recfm` emits no `RECFM` keyword at all and lets the host choose, which is the\n * current CLI behavior that works on both hosts. A two-state F/V toggle would make \"I did\n * not ask for record attributes\" unexpressible.\n */\n\n/** Which field. The ids are the tab order. */\nexport type TransferFieldId =\n | 'direction' | 'host' | 'localFile' | 'hostFile' | 'mode'\n | 'exist' | 'cr' | 'recfm' | 'lrecl' | 'blksize';\n\n/** How a field takes input. `cycle` uses left/right; the others take printables. */\nexport type TransferFieldKind = 'cycle' | 'text' | 'numeric';\n\nexport interface TransferField {\n readonly id: TransferFieldId;\n readonly label: string;\n readonly kind: TransferFieldKind;\n /**\n * For a cycle field, the values in cycle order, `''` included where unset is legal.\n * Empty for text and numeric fields.\n *\n * `recfm`'s list here is the TSO one; `cycleField` drops `undefined` for VM. That\n * asymmetry lives in the cycle function rather than in two tables, so the VM rule has\n * one home.\n */\n readonly values: readonly string[];\n /** Visible width for a text or numeric field. */\n readonly width: number;\n}\n\n/**\n * The fields in tab order.\n *\n * `lrecl` is 5 WIDE, NOT 3: `positiveInt` has no upper bound and TSO datasets\n * legitimately reach 32760, so a narrow field would silently prevent valid transfers.\n */\nexport const TRANSFER_FIELDS: readonly TransferField[] = [\n { id: 'direction', label: 'Direction', kind: 'cycle', values: ['receive', 'send'], width: 0 },\n { id: 'host', label: 'Host', kind: 'cycle', values: ['tso', 'vm'], width: 0 },\n { id: 'localFile', label: 'Local file', kind: 'text', values: [], width: 40 },\n { id: 'hostFile', label: 'Host file', kind: 'text', values: [], width: 40 },\n { id: 'mode', label: 'Mode', kind: 'cycle', values: ['binary', 'ascii'], width: 0 },\n { id: 'exist', label: 'Exist', kind: 'cycle', values: ['keep', 'replace', 'append'], width: 0 },\n { id: 'cr', label: 'Cr', kind: 'cycle', values: ['', 'auto', 'remove', 'add', 'keep'], width: 0 },\n { id: 'recfm', label: 'Recfm', kind: 'cycle', values: ['', 'fixed', 'variable', 'undefined'], width: 0 },\n { id: 'lrecl', label: 'Lrecl', kind: 'numeric', values: [], width: 5 },\n { id: 'blksize', label: 'Blksize', kind: 'numeric', values: [], width: 5 },\n];\n\n/**\n * The direction as the operator should READ it: both endpoints named, never a bare verb.\n *\n * ## THIS IS DISPLAY ONLY, AND THAT IS A HARD CONSTRAINT RATHER THAN A STYLE CHOICE\n *\n * `formKeywords` emits `Direction=${value}` from the STORED value, and `parseTransferKeywords`\n * validates against the literals `['receive', 'send']`. A renamed VALUE would emit\n * `Direction=send: local to host` and be refused by this project's own validator. So the stored\n * value stays `send`/`receive` and only what is DRAWN changes -- the same split the TUI's `shown()`\n * already uses for `''` -> `-`, and both `<select>` builders for `''` -> `(host default)`.\n *\n * ## WHY THIS EXISTS: IT COST A REAL FILE\n *\n * On 2026-10-07 the user -- an expert on this protocol, driving their own client -- read the bare\n * `send`/`receive` the wrong way round and OVERWROTE THEIR OWN `PROFILE EXEC A` on VM/370 under\n * `Exist=replace`. All four of this feature's guards defend against an EMPTY file reaching the\n * host; NONE covers a valid file hitting the wrong target. Naming the endpoints is the cheap half\n * of that gap. x3270 does not confirm a direction either, so the old behaviour was arguably\n * correct -- it was also one click from data loss.\n *\n * ## `local`, NOT `browser`\n *\n * The phrasing first proposed was `this browser`, which is right for the web gateway and WRONG for\n * the other two front ends: in the TUI and the Electron GUI, \"local\" is this machine's filesystem.\n * `local` is true in all three AND matches the `Local file` field label directly above it, so the\n * form reads as one statement rather than two vocabularies.\n *\n * ## NO ARROWS\n *\n * Fira Code and friends ligate `->` and `<-` -- `canvas/src/transferUi.ts` already took the arrows\n * out of a help line for exactly this -- and the TUI draws printable ASCII only (`tui/src/app.ts`),\n * so a real `->` or a U+2192 would force per-front-end text and defeat the point of one shared\n * mapping. `to` means the same thing in every renderer.\n *\n * AN UNRECOGNISED VALUE IS RETURNED UNCHANGED rather than falling through to `undefined`: every\n * renderer maps the values it offers through this, so a lookup without this property would draw a\n * blank menu entry if a third direction were ever added.\n */\nexport function directionLabel(value: string): string {\n if (value === 'send') return 'send: local to host';\n if (value === 'receive') return 'receive: host to local';\n return value;\n}\n\nconst FIELD_BY_ID = new Map(TRANSFER_FIELDS.map((f) => [f.id, f]));\n\n/** Every field's current string. `''` means unset, for every kind. */\nexport type TransferValues = Record<TransferFieldId, string>;\n\nexport interface TransferFormState {\n readonly values: TransferValues;\n /** Index into `TRANSFER_FIELDS`. */\n readonly selected: number;\n /**\n * The validator's message, when a submit was refused. Cleared on the next edit.\n *\n * Explicitly `| undefined` because `exactOptionalPropertyTypes` is on: without it the\n * spreads below cannot write `error: undefined` to clear it, and `app.ts` cannot\n * assign a `string | undefined` from a caught error into it either.\n */\n readonly error?: string | undefined;\n}\n\n/**\n * A fresh form.\n *\n * ## IT OPENS ON `send`, AND THE KEYWORD PARSER'S DEFAULT IS DELIBERATELY DIFFERENT\n *\n * The user's reason, recorded because it will look surprising to someone one day: **until VM has a\n * TCP/IP stack, this client is mostly used to GET SOFTWARE ONTO these systems** -- few people\n * actually develop software locally on a VM or MVS guest. Upload is the common case, so it is the\n * default.\n *\n * (The user's planned route to such a stack, which is the condition that would make this worth\n * revisiting: UTS -- v7 Unix -- plus an IUCV-alike backported to VM/370, a simple network driver\n * over a CTC pair cribbed from Linux on System/390 and z, and a small TCP stack running in UTS.\n * Hard on a PDP-11 for address-space reasons; not hard on S/370 with up to 16 MB. **If download\n * stops being the rare direction, reconsider this default.**)\n *\n * ## `parseTransferKeywords` STILL DEFAULTS TO `receive`, AND MUST\n *\n * That default applies when a SCRIPT's `Transfer(...)` omits `Direction=`. Flipping it would\n * silently invert every such script -- a run that downloaded would UPLOAD, with `Exist=replace`\n * reachable and no form to look at -- which is the same data-destruction shape this change exists\n * to prevent, moved somewhere worse. It is also x3270's own default, which this project matches\n * absent a documented reason to differ.\n *\n * **The asymmetry is the point:** an interactive form with a visible, self-describing control\n * (`directionLabel`) is a different safety situation from an omitted keyword in an unattended\n * script. `transfer.test.ts` pins the parser's default so this cannot be \"tidied\" into agreement.\n *\n * ## ONE KNOCK-ON, AND IT IS VISIBLE\n *\n * `applicable` shows `Recfm` only on a send, so a fresh form now draws ONE MORE FIELD than it used\n * to. The TUI's box height is computed from the applicable count, so this is a layout change\n * there, not merely a value change. 27 tests across four files asserted the old default; each had\n * its SETUP updated rather than its assertion weakened.\n */\nexport function newTransferForm(): TransferFormState {\n return {\n values: {\n direction: 'send', host: 'tso', localFile: '', hostFile: '',\n mode: 'binary', exist: 'keep', cr: '', recfm: '', lrecl: '', blksize: '',\n },\n selected: 0,\n };\n}\n\n/**\n * The values a cycle field offers, given the rest of the form.\n *\n * The ONE state-dependent case is `recfm` on VM: `transfer.ts` records that \"CMS supports\n * fixed and variable\", so `undefined` is not offered there. Kept here rather than as a\n * second table so the rule has one home.\n */\nfunction valuesFor(id: TransferFieldId, values: TransferValues): readonly string[] {\n const field = FIELD_BY_ID.get(id);\n if (field === undefined) return [];\n if (id === 'recfm' && values.host === 'vm') {\n return field.values.filter((v) => v !== 'undefined');\n }\n return field.values;\n}\n\n/**\n * Move a cycle field by `delta`, wrapping, and clear anything the move made inapplicable.\n *\n * A no-op on a text or numeric field, so a caller may route left/right unconditionally.\n */\nexport function cycleField(\n state: TransferFormState, id: TransferFieldId, delta: number,\n): TransferFormState {\n const field = FIELD_BY_ID.get(id);\n if (field === undefined || field.kind !== 'cycle') return state;\n const options = valuesFor(id, state.values);\n const at = options.indexOf(state.values[id]);\n // A value the current options no longer contain (Recfm=U when Host flipped to vm)\n // reads as index -1, and `(-1 + 1) % n` is 0 -- so it lands on the first option\n // rather than throwing. That is the same repair `clearInapplicable` performs.\n const next = options[(((at + delta) % options.length) + options.length) % options.length];\n return clearInapplicable({\n ...state,\n values: { ...state.values, [id]: next ?? '' },\n error: undefined,\n });\n}\n\n/** Replace a text or numeric field's contents. A no-op on a cycle field. */\nexport function setFieldText(\n state: TransferFormState, id: TransferFieldId, text: string,\n): TransferFormState {\n const field = FIELD_BY_ID.get(id);\n if (field === undefined || field.kind === 'cycle') return state;\n // A numeric field takes digits only. Refusing the character is better than accepting it\n // and failing at submit: the validator's `positiveInt` would reject it, but the user\n // would not find out until they pressed Enter.\n if (field.kind === 'numeric' && !/^[0-9]*$/.test(text)) return state;\n if (text.length > field.width && field.kind === 'numeric') return state;\n return { ...state, values: { ...state.values, [id]: text }, error: undefined };\n}\n\n/**\n * Is this field meaningful, given the rest of the form?\n *\n * All five rules are derived from the validator's own behavior, cited where they come\n * from. An inapplicable field is not drawn and cannot be selected.\n */\nexport function applicable(id: TransferFieldId, values: TransferValues): boolean {\n switch (id) {\n // On a receive the dataset already exists, and x3270 silently drops these\n // (`ft.c:702`). Silently dropping is exactly what a form should make visible.\n case 'recfm':\n return values.direction === 'send';\n // `Lrecl=80` with no `Recfm` \"emits nothing at all\" (`ft.c:704,721-726`), so the\n // field would be a lie while Recfm is unset.\n case 'lrecl':\n return values.direction === 'send' && values.recfm !== '';\n // CMS files have no block size.\n case 'blksize':\n return values.direction === 'send' && values.recfm !== '' && values.host !== 'vm';\n // Carriage-return translation is meaningless on a binary transfer.\n case 'cr':\n return values.mode === 'ascii';\n default:\n return true;\n }\n}\n\n/**\n * Clear every value a state change has made inapplicable.\n *\n * INAPPLICABLE VALUES ARE CLEARED, NOT RETAINED INVISIBLY. A hidden value that breaks a\n * later submit is the worse failure: the user cannot see the field, cannot edit it, and\n * gets a validator error naming a keyword they never typed.\n *\n * ITERATED TO A FIXED POINT, because the rules CHAIN: clearing `recfm` (on a flip to\n * receive) makes `lrecl` and `blksize` inapplicable in turn, and a single pass reading\n * the values it is midway through mutating gets the wrong answer depending on table\n * order. `blksize` also depends on `recfm`, which is cleared LATER in tab order, so a\n * one-pass loop leaves it set. Two passes suffice for the current rules; the loop does\n * not assume that.\n */\nfunction clearInapplicable(state: TransferFormState): TransferFormState {\n const values = { ...state.values };\n let changed = true;\n while (changed) {\n changed = false;\n for (const f of TRANSFER_FIELDS) {\n if (!applicable(f.id, values) && values[f.id] !== '') {\n // Cycle fields whose unset value is not `''` (direction, host, mode, exist) are\n // never inapplicable, so this only ever clears an optional field.\n values[f.id] = '';\n changed = true;\n }\n }\n // Recfm=U surviving a flip to Host=vm is the other repair: the value stays legal for\n // TSO, so `applicable` does not catch it, but the VM dialect cannot express it.\n // Inside the loop because clearing it can cascade to Lrecl on the next pass.\n if (values.host === 'vm' && values.recfm === 'undefined') {\n values.recfm = '';\n changed = true;\n }\n }\n return { ...state, values };\n}\n\n/**\n * The form as `Transfer()` keywords, ready for `parseTransferKeywords`.\n *\n * Only applicable, non-empty fields are emitted, so an unset `Recfm` contributes nothing\n * and the host chooses -- which is the whole reason unset is a distinct state.\n */\nexport function formKeywords(state: TransferFormState): string[] {\n const out: string[] = [];\n const keyword: Record<TransferFieldId, string> = {\n direction: 'Direction', host: 'Host', localFile: 'LocalFile', hostFile: 'HostFile',\n mode: 'Mode', exist: 'Exist', cr: 'Cr', recfm: 'Recfm',\n lrecl: 'Lrecl', blksize: 'Blksize',\n };\n for (const f of TRANSFER_FIELDS) {\n const value = state.values[f.id];\n if (value === '' || !applicable(f.id, state.values)) continue;\n out.push(`${keyword[f.id]}=${value}`);\n }\n return out;\n}\n\n/** The next selectable field index in `delta`'s direction, skipping inapplicable ones. */\nexport function moveField(state: TransferFormState, delta: number): TransferFormState {\n const n = TRANSFER_FIELDS.length;\n let at = state.selected;\n for (let i = 0; i < n; i++) {\n at = (((at + delta) % n) + n) % n;\n const field = TRANSFER_FIELDS[at];\n if (field !== undefined && applicable(field.id, state.values)) {\n return { ...state, selected: at };\n }\n }\n return state;\n}\n", "import type { Action } from './keymap.js';\n\n/**\n * The 122-key 3279 keyboard, as DATA.\n *\n * ## WHY A SECOND TABLE RATHER THAN AN EXTENSION OF `KEYPAD_KEYS`\n *\n * The two answer different questions. `KEYPAD_KEYS` is \"which keys are hard to type\" -- 48 entries,\n * consumed by all three front ends, and the TUI renders it as a name-and-chord LIST in a\n * 29-column overlay. This is \"what does a 3279 keyboard look like\" -- ~122 entries with real\n * geometry, consumed only by the two DOM front ends, because the TUI cannot draw a keyboard:\n * c3270's own keypad is 78 columns wide and that front end refuses to draw below 24x80. The user\n * said so directly when asking for this: \"the TUI on the other hand is much more restrictive, so\n * I am not suggesting a full virtual keyboard for it.\"\n *\n * `keypadView.ts` set this precedent for the same reason -- `KEYPAD_KEYS`' cell coordinates were\n * the wrong model for a DOM layout -- and its header's argument applies here with more force: the\n * layout is WRITTEN DOWN, not derived, because deriving it would make it agree with itself by\n * construction and be unable to catch a change.\n *\n * ## THE GEOMETRY IS IN KEY UNITS, NOT PIXELS\n *\n * One unit is one standard cap. `Enter`, the spacebar and several others are wider, so a cap\n * carries `x` and `width` in units and the CSS multiplies by whatever pitch it picks. Pixels here\n * would freeze a font size into a data table.\n *\n * ## THE TRANSCRIPTION WAS AUDITED AND CORRECTED AGAINST A SECOND KEYBOARD\n *\n * **The user spotted the hole on 2026-10-22: \"the keyboard picture is clearly missing the 0/) so\n * maybe some of the other keys are also supposed to have caps that have been lost.\"** They were\n * right, and the audit it triggered found FIVE wrong pairs plus one physically blank cap. They\n * then supplied a sharp photograph of an **IBM 4978** -- a different 122-key 3279 layout with the\n * same typewriter character set -- which settled every one.\n *\n * **WHAT WAS WRONG, because the pattern of error is the useful part:**\n *\n * - **`6` carried a NOT SIGN; it is a CENT SIGN.** I read a blurred cap and then justified it\n * with EBCDIC reasoning about the logical-not operator. The reasoning was sound and still\n * produced the wrong glyph, because it explained a reading instead of testing one.\n * - **The cap right of `P` carried `cent`/`!`; it is `!`/`NOT SIGN`.** So both glyphs were real\n * and both were in the wrong places -- the most misleading kind of error, since each looked\n * defensible alone.\n * - **The home row's third cap carried `(`/`)`; it is `{`/`}`.** Flagged as suspect because it\n * duplicated `9`/`0`.\n * - **The bottom row carried `,`/`;` and `.`/`:`; they are `,`/`'` and `.`/`:`.** Flagged for\n * duplicating the home row's `;`/`:`.\n * - **`1` and `\\` both claimed a solid `|`.** The backslash cap is a BROKEN bar (U+00A6), which\n * EBCDIC distinguishes.\n * - **`0`'s cap is genuinely BLANK on the first unit** -- no legend at all under maximum zoom,\n * while `_`/`-` beside it is legible, so it is missing or worn smooth. `)` was inferred from\n * the `9`/`(` pattern and the 4978 then confirmed it.\n *\n * **TWO LESSONS WORTH KEEPING.** First, a glyph pair appearing TWICE on one keyboard is a\n * reliable detector: it found three of the five. Second, **the detector's PREDICTIONS were\n * unreliable** -- the audit guessed `,`/`<` and `.`/`>` from PC-adjacent habit and was wrong, so\n * guessing at a correction would have shipped a second error that no longer smelled. Duplication\n * tells you WHERE to look, not WHAT the answer is.\n *\n * ## A THIRD SOURCE, AND IT MATCHES THIS LAYOUT EXACTLY\n *\n * The user then supplied a photograph of an **IBM 3270 PC** whose keyboard is the SAME layout as\n * the first: PF13-24 above PF1-12, the two-column special bar, `Dup`/`PA1` and `Field Mark`/`PA2`\n * in the cursor cluster, the same arrow arrangement. From it:\n *\n * - **`PA3` was recorded as BLANK and is not** -- the cap reads `PA3`, completing the set beside\n * the PA1 and PA2 that ride the Dup and Field Mark caps.\n *\n * **AND IT CONTAINS A TRAP WORTH THE WARNING: its NUMERIC PAD IS A PC's.** `7`/`Home`, `8`/up,\n * `9`/`PgUp`, with `Esc`/`NumLk`/`ScrLk` above -- because a 3270 PC is a PC with 3270 emulation.\n * A keyboard that agrees everywhere else and differs silently in exactly the place you need is\n * the worst kind of cross-check, so NOTHING was taken from it there.\n *\n * **THE NUMERIC PAD WAS SETTLED FROM THE ORIGINAL PHOTOGRAPH INSTEAD, at 6x: `Space`, a FIELD TAB\n * arrow, `-` and `Enter` down the right-hand column.** Three of the five `unread` caps had been\n * resolvable all along -- **I gave up on the zoom too early**, which is the lesson that belongs\n * next to them.\n *\n * ## EVERY CAP IS NOW READ -- ZERO UNREADABLE, AND THE ROUTE IS THE LESSON\n *\n * The last two flanked `Home` and had survived all three photographs. They fell to a **10x crop of\n * the FIRST image with contrast and an unsharp mask**: an arrow pointing left into a bar and one\n * pointing right into a bar, the standard 3270 back-tab and field-tab glyphs, flanking Home\n * exactly as the cluster's logic predicts.\n *\n * **THE PIXELS WERE ALWAYS SUFFICIENT; THE PROCESSING WAS NOT.** Plain LANCZOS upscaling left them\n * a smudge through two rounds of \"still unreadable\", while sharpening resolved them in one. The\n * rule for next time: **exhaust the image you have before asking for another one.**\n *\n * Unreadable caps went 5 -> 2 -> 0 in a day, and the test that tracked the count is now inverted:\n * zero is the FLOOR, and an `unread` cap reappearing means a key was marked unreadable rather\n * than read.\n *\n * The 4978 also has keys this layout lacks (a `~`/`\\` cap, a bullet over plus-minus,\n * `LINE DUP`/`FIELD DUP`/`REP`), and none were transplanted: two units agree on the character set\n * where both have a key, and that is exactly how far a cross-check can be trusted.\n *\n * ## LEGENDS COME FROM THE PHOTOGRAPH, AND THIS IS NOT A PC KEYBOARD\n *\n * Transcribed from `s-l960.png`, an **IBM 5271 Converged Keyboard** -- the user identified it on\n * 2026-10-22, and the name matters more than it looks: \"122-key 3279 keyboard\" describes several\n * distinct units, while *Converged Keyboard* names ONE, a 122-key Model F sold with the 3270 PC\n * and the 3179/3180 terminals. Deskthority's Model F page confirms the family and says the layout\n * \"was arguably inspired by the DEC LK201, and seems to have been the basis for the layout of the\n * later Model M Enhanced Keyboard\" -- but carries no cap-level legends, so it cannot settle the\n * two caps still open.\n *\n * **SEARCH FOR `5271 Converged Keyboard` RATHER THAN FOR A DESCRIPTION** if a fourth source is\n * ever wanted. That is the lesson of the identification: the two caps still unread are unread\n * because I was searching by appearance, and a part number is a key into documentation that a\n * description is not. It is the 3270 TYPEWRITER\n * layout: the number row reads `| @ # $ % not & * ( )` over `1`-`0`, so `6` carries a NOT SIGN\n * where a PC has `^`, and `1` carries `|` where a PC has `!`. **Never infer a legend from a PC\n * keyboard.** Where the photograph could not be resolved, the cap is INERT with its `name` set and\n * says so at the entry -- an invented legend on historical hardware is exactly the\n * plausible-but-wrong detail this project keeps catching.\n *\n * ## WHAT IS DELIBERATELY NOT HERE\n *\n * No `toggleKeypad`, `toggleKeyboard`, `geometryMenu` or `quit` on any cap: `applyAction` throws\n * on all four, so a drawn button would be a button whose only effect is to throw -- and\n * `toggleKeyboard`'s would toggle the surface it is drawn on. (`geometryMenu` is not a 3279 key at\n * all: a terminal's model was its hardware.) Pinned by this table's own test.\n */\n\nexport const KEYBOARD_REGIONS = [\n 'pf', 'special-left', 'main', 'cursor', 'numpad',\n] as const;\n\nexport type KeyboardRegion = typeof KEYBOARD_REGIONS[number];\n\nexport interface KeyCap {\n /** What is printed on the cap, unshifted. `''` for a genuinely blank cap. */\n readonly label: string;\n /**\n * The shifted legend, on a dual-glyph cap only.\n *\n * ABSENT ON A LETTER, deliberately: `A` sends `A` unshifted on this keyboard, so a `shiftLabel`\n * of `A` would make Shift a no-op that still consumed its one-shot arm.\n */\n readonly shiftLabel?: string;\n /**\n * What the cap does, or `undefined` for an INERT cap -- a key that exists on the hardware and\n * will never do anything here.\n *\n * DRAWN RATHER THAN OMITTED, at the user's direction: a gap where `Esc` belongs is less honest\n * than a key that is plainly scenery. `ui.css` styles these in `GrayText` on a recessed cap\n * rather than with the house `opacity: 0.5`, and says there why.\n */\n readonly action?: Action | undefined;\n readonly region: KeyboardRegion;\n /** Row within the region, from 0 at the top. */\n readonly row: number;\n /** Left edge in KEY UNITS, within the region's row. */\n readonly x: number;\n /** Width in KEY UNITS. 1 is a standard cap. */\n readonly width: number;\n /**\n * Height in KEY UNITS, for a cap that spans rows. Absent means 1.\n *\n * ADDED 2026-10-22 FOR THE RETURN KEY, which the sharp reference photograph shows spanning the\n * QWERTY and home rows -- and that span is what makes the main block a RECTANGLE rather than a\n * ragged stack. Without it row 2 ends 1.25 units short of row 1.\n *\n * A TALL CAP IS DECLARED ON ITS TOP ROW and is simply absent from the rows beneath, so a row's\n * own width calculation does not have to know about it. `keyboardUi.ts` renders the span with a\n * grid-row rule; the row below leaves the space empty by having no cap at that `x`.\n */\n readonly height?: number;\n /** What this key is called in words: the tooltip, and the accessible name. */\n readonly name: string;\n}\n\n/**\n * A plain typed character.\n *\n * `{ kind: 'type' }` ALREADY EXISTS and is what the real-keyboard path produces\n * (`canvas/src/keys.ts`), so no new action kind was needed for any character cap -- which is most\n * of why this table was cheap to add.\n *\n * THE CONDITIONAL SPREAD IS REQUIRED, not stylistic: `exactOptionalPropertyTypes` is on in\n * `tsconfig.base.json`, under which `shiftLabel: undefined` does NOT satisfy `shiftLabel?: string`.\n */\nconst ch = (\n label: string, row: number, x: number, shiftLabel?: string,\n): KeyCap => ({\n label,\n ...(shiftLabel === undefined ? {} : { shiftLabel }),\n action: { kind: 'type', text: label },\n region: 'main', row, x, width: 1,\n name: shiftLabel === undefined ? label : `${shiftLabel} ${label}`,\n});\n\n/** A typed character outside the main block, with an explicit name. */\nconst ch2 = (\n label: string, region: KeyboardRegion, row: number, x: number, name: string,\n): KeyCap => ({\n label, action: { kind: 'type', text: label }, region, row, x, width: 1, name,\n});\n\n/** An inert cap: drawn, named, and does nothing. */\nconst dead = (\n label: string, region: KeyboardRegion, row: number, x: number, width: number, name: string,\n): KeyCap => ({ label, action: undefined, region, row, x, width, name });\n\n/**\n * The name BOTH Shift caps carry, and the way the view recognises them.\n *\n * ## SHIFT HAS NO `Action`, AND THAT IS THE DESIGN RATHER THAN A GAP\n *\n * It is PURE SURFACE STATE: arming it sends nothing to the host, nothing crosses the socket or the\n * IPC boundary, and the `Action` union needs no `shift` member. So its cap is INERT as far as this\n * table is concerned (`action: undefined`) and `canvas/src/keyboardUi.ts` gives it behaviour by\n * matching this exact name.\n *\n * MATCHED BY NAME RATHER THAN BY LABEL, because the left cap's printed legend in the photograph is\n * `>`/`<` rather than the word -- and because BOTH caps must drive the SAME one-shot state, neither\n * being distinguishable to us. `SHIFT_NAME` is exported so the view cannot drift from a string\n * literal typed twice.\n *\n * IT IS NOT STYLED AS AN INERT CAP even though `action` is `undefined`: the view sets\n * `data-shift=\"true\"` on it, and `ui.css` keys the grey scenery style off `[disabled]`, which the\n * view does NOT set here. An inert cap and an actionable-but-actionless cap look different on\n * purpose.\n */\nexport const SHIFT_NAME = 'Shift';\n\nconst shift = (row: number, x: number, width: number, side: string): KeyCap => ({\n // `Shft`, NOT `Shift`: the LEFT cap is 1.25 units, which is 31px usable, and five capitals at\n // the main block's 10px type need ~38px. The real keyboard prints `Shift` in full because its\n // cap is physically wider than one unit of this layout's pitch; four characters is the honest\n // abbreviation at this size. The NAME stays `Shift (...)`, which is what the view's `data-key`\n // and both harnesses address -- a label is presentation, a name is the identifier.\n label: 'Shft',\n action: undefined,\n region: 'main', row, x, width,\n // The side is in the name for the tooltip and for a screen reader, but `keyboardUi.ts` matches\n // on the PREFIX, so both caps drive one state.\n name: `${SHIFT_NAME} (${side}, one-shot)`,\n});\n\n/** An actionable cap that is not a typed character. */\nconst key = (\n label: string, action: Action, region: KeyboardRegion,\n row: number, x: number, width: number, name: string,\n): KeyCap => ({ label, action, region, row, x, width, name });\n\n/** `PF13`-`PF24` on top, `PF1`-`PF12` below -- see the note at the `pf` block. */\nconst pfRow = (row: number, first: number): KeyCap[] => Array.from(\n { length: 12 },\n (_, i) => key(\n `PF${first + i}`, { kind: 'pf', n: first + i }, 'pf', row, i, 1, `PF${first + i}`,\n ),\n);\n\n/** One row of letters, evenly spaced from `x`. */\nconst letters = (text: string, row: number, x: number): KeyCap[] =>\n [...text].map((c, i) => ch(c, row, x + i));\n\nexport const KEYBOARD_3279: readonly KeyCap[] = Object.freeze([\n /*\n THE PF STRIP: TWO ROWS ABOVE THE MAIN KEYBOARD, PF13-24 ON TOP OF PF1-12.\n\n ## THE POSITION IS A DECISION, NOT THE ONLY OPTION -- CONFIRMED BY THE USER 2026-10-22\n\n **\"There's also a common layout that puts PF13-24 in a block to the right of the numeric\n keypad but I prefer the two rows above the main keyboard.\"** Both arrangements shipped on real\n 3270-family hardware, and this one is what the reference 5271 has -- so the two-band layout in\n `ui.css` (PF strip across the top, the other four regions in a row beneath) is matching the\n photographed unit AND the user's stated preference, which happen to agree.\n\n **RECORDED BECAUSE IT WOULD OTHERWISE LOOK LIKE AN UNEXAMINED DEFAULT.** Someone who knows the\n right-hand-block variant could \"fix\" this toward it in good faith; they should know it was\n considered and declined. If it is ever wanted as an OPTION, the change is in the view's band\n assignment (`keyboardUi.ts` decides which band a region joins, from its name) and not in this\n table -- the geometry here is per-region, so a PF block beside the numpad needs no new\n coordinates, only a different container.\n\n ## THE ROW ORDER WITHIN THE STRIP\n\n PF13-24 above PF1-12 is the hardware's order in the photograph, and it matches both\n `KEYPAD_KEYS` (which opens `pfRow(0, 13)` then `pfRow(1, 1)`) and c3270\n (`Common/c3270/keypad.labels:2` and `:4`). Reversing it would be a silent relearn for anyone\n used to either. The table's test pins the first label of each row rather than only the count,\n which is what catches that.\n */\n ...pfRow(0, 13),\n ...pfRow(1, 1),\n\n /*\n THE LEFT SPECIAL BAR: two columns, five rows.\n\n `Attn` and `Clear` are real 3270 keys and work. `Cursr Sel` is a real 3270 key that does NOT:\n `AID.SELECT` exists in core, but `keypad.ts` records the key as dropped by decision because it\n \"needs the field-intensity parse and belongs with the light-pen work\". It is drawn inert, and\n its tooltip is the place that says it is deferred rather than impossible.\n\n `Esc`, `Print Screen`, `Scroll Lock` and `Pause` are PC keys present on this 122-key unit and\n are not 3270 functions. They get no action because they SHOULD not have one.\n\n THE TWO BLANK CAPS AT THE BOTTOM carry `Xfer` and nothing. The user spotted them -- \"since\n there are two unlabelled keys on the left hand special-key bar, one of those could get the\n `Xfer` label\" -- and there is precedent: `KEYPAD_KEYS` already carries `Xfer` as a deliberate\n non-3270 addition, and `keypad.ts` argues a transfer button \"is a perfectly sensible thing to\n click\" even though `applyAction` throws on the kind, because the front end intercepts it\n exactly as it intercepts the chord.\n */\n /*\n THE LEFT SPECIAL BAR -- REWRITTEN 2026-10-22, AND FOUR OF ITS CAPS WERE SIMPLY WRONG.\n\n I had `Esc`, `Print Screen`, `Scroll Lock` and `Pause` here: PC keys, read off a blurred\n photograph and believed because they looked like a plausible PC-ish column. The sharp reference\n (`DSC08836-scaled.jpeg`) shows what is actually there, and it is all 3270:\n\n Attn | Clear\n Cursr | (blank)\n Sel |\n Ex Sel| Erase EOF\n Print | Test <- `Test` is the SHIFTED legend on a blank cap\n (blk) | (blk)\n\n **TWO OF THEM ARE FUNCTIONS WE ALREADY IMPLEMENT**, so they become LIVE KEYS rather than grey\n scenery: `Erase EOF` is `eraseEOF`, which the compact keypad has carried all along as `ErEOF`.\n That is the most valuable thing this photograph bought -- not geometry, but two working keys\n where I had drawn dead ones.\n\n `Ex Sel` IS *NOT* IMPLEMENTED, and the distinction matters: it is Erase Input on some units and\n a field-select on others, and `keypad.ts` records Erase Input as `ErInp` separately. Rather\n than guess which this cap is, it stays inert with its real name -- the same discipline the\n unreadable caps got.\n\n `Print`'s SHIFTED LEGEND IS `Ident`, and the blank cap beside it carries `Test`. Both were read\n by the user on this photograph. Neither has an action here, so they are recorded as legends on\n inert caps: documented as existing and unimplemented rather than silently absent.\n\n `Attn`'s OWN SHIFTED LEGEND IS STILL UNREADABLE even here -- it is front-printed and blurred in\n every photograph so far -- as is the row-1 blank's. Those two remain unidentified.\n */\n key('Attn', { kind: 'attn' }, 'special-left', 0, 0, 1, 'Attention'),\n key('Clr', { kind: 'clear' }, 'special-left', 0, 1, 1, 'Clear'),\n dead('Cur\\nSel', 'special-left', 1, 0, 1, 'Cursor Select (not implemented: light-pen work)'),\n dead('', 'special-left', 1, 1, 1, 'blank, right of Cursor Select'),\n dead('Ex\\nSel', 'special-left', 2, 0, 1, 'Ex Sel (not implemented: ambiguous function)'),\n // A REAL 3270 FUNCTION WE IMPLEMENT -- the compact keypad has carried it as `ErEOF` all along.\n key('Ers\\nEOF', { kind: 'eraseEOF' }, 'special-left', 2, 1, 1, 'Erase EOF'),\n {\n label: 'Prnt',\n shiftLabel: 'Idnt',\n action: undefined,\n region: 'special-left', row: 3, x: 0, width: 1,\n name: 'Print (shifted: Ident, not implemented)',\n },\n {\n label: '',\n shiftLabel: 'Test',\n action: undefined,\n region: 'special-left', row: 3, x: 1, width: 1,\n name: 'blank (shifted: Test, not implemented)',\n },\n /*\n THE TWO BLANK CAPS AT THE BOTTOM, AND `Xfer` GOES BACK ON ONE OF THEM.\n\n It had moved to the cursor cluster's third cap on the user's reasoning that the cap had no\n unshifted function -- but this photograph IDENTIFIES that cap: it carries the\n `a`-with-a-squiggle glyph, which is delete-to-end-of-line. So it is a real key after all, and\n `Xfer` returns here, to a cap this file has CONFIRMED blank under maximum zoom.\n */\n key('Xfer', { kind: 'transferForm' }, 'special-left', 4, 0, 1, 'File transfer'),\n dead('', 'special-left', 4, 1, 1, 'blank, bottom right of the special bar'),\n\n /*\n THE MAIN BLOCK, five rows.\n\n ROW 0, the number row: `| @ # $ % not & * ( )` over `1`-`0`, then `_`/`-`, `+`/`=`, Backspace.\n THE NOT SIGN ON 6 IS THE PROOF THIS IS A 3270 LAYOUT and not a PC one; a PC has `^` there.\n `1` carries `|`, where a PC has `!`.\n */\n ch('1', 0, 0, '|'),\n ch('2', 0, 1, '@'),\n ch('3', 0, 2, '#'),\n ch('4', 0, 3, '$'),\n ch('5', 0, 4, '%'),\n /*\n `cent` OVER `6`, CORRECTED 2026-10-22 FROM A SECOND PHOTOGRAPH.\n\n I first read this as U+00AC NOT SIGN off a blurred cap, and reasoned about EBCDIC to justify\n it. A sharp photograph of another 122-key 3279 -- an IBM 4978, a DIFFERENT layout but the same\n typewriter character set -- shows `cent` unambiguously over `6`, and re-zooming the original\n cap is consistent with that rather than with a not-sign.\n\n TWO KEYBOARDS AGREEING BEATS ONE BLURRED CAP PLUS AN ARGUMENT, which is the lesson: the\n EBCDIC reasoning was sound and still produced the wrong glyph, because it explained a reading\n rather than testing it.\n */\n ch('6', 0, 5, '\u00A2'),\n ch('7', 0, 6, '&'),\n ch('8', 0, 7, '*'),\n ch('9', 0, 8, '('),\n /*\n `)` OVER `0` -- NO LONGER AN INFERENCE, CONFIRMED 2026-10-22.\n\n The cap is BLANK on the first reference unit (no legend at all under maximum zoom, while\n `_`/`-` beside it is legible, so it is missing or worn smooth), and `)` was originally guessed\n from the `9`/`(` pattern. A sharp photograph of an IBM 4978 shows `)` over `0` directly, so\n the guess is now a reading -- from a different keyboard, which is worth stating.\n */\n ch('0', 0, 9, ')'),\n ch('-', 0, 10, '_'),\n /*\n `+` OVER `=`, CONFIRMED by the 4978's own `+`/`=` cap -- though note that keyboard carries an\n EXTRA cap to the right of it, a bullet over plus-minus, which this one does not have. Two\n 122-key 3279 keyboards, two different furniture layouts; the CHARACTER SET agrees where both\n have the key, which is exactly how far a cross-check can be trusted.\n */\n ch('=', 0, 11, '+'),\n // BACKSPACE IS 3 UNITS, which is what closes row 0 at 15 -- and the photograph shows it as the\n // widest cap on that row, running to the right edge above the Return key.\n key('\u2190', { kind: 'backspace' }, 'main', 0, 12, 3, 'Backspace'),\n\n /*\n ROWS 1-4, AS A TRUE RECTANGLE -- REWRITTEN 2026-10-22 FROM A SHARP REFERENCE PHOTOGRAPH\n (`DSC08836-scaled.jpeg`), which is the same 5271 and finally legible.\n\n ## EVERY ROW ENDS AT EXACTLY 15 UNITS, AND THAT IS WHY\n\n The block used to end at 14, 15, 13.75, 13.5 and 14 units -- ragged by up to 1.5 units, which\n the user saw immediately: \"can we make the main keyboard an actual rectangle?\" A real keyboard\n is a rectangle because its leading keys STAGGER and its edge keys are WIDE, not because it has\n dead space, so that is what this now models.\n\n ## THE STAGGER IS MEASURED, NOT EYEBALLED\n\n `Z` MUST CENTRE BETWEEN `A` AND `S`, which the user named as the test. With Caps at 1.75 units\n `A` centres at 2.25 and `S` at 3.25, so `Z` must centre at 2.75 -- which a 2.25-unit left\n Shift gives exactly. Tab 1.5, Caps 1.75, Shift 2.25 is the classic ANSI-ish progression and it\n falls out of that one requirement rather than being copied from a PC.\n\n ## THE RETURN KEY SPANS TWO ROWS, WHICH IS WHAT CLOSES THE RECTANGLE\n\n The photograph shows the bent-arrow Return as one tall cap covering the QWERTY and home rows,\n with a SEPARATE small `Enter` on the bottom row. Without the span, row 2 ends 1.25 units short\n and no amount of widening the punctuation keys looks right. `height: 2` is declared on its top\n row and the home row simply has no cap at that `x` -- see `KeyCap.height`.\n\n IT IS DRAWN AS A RECTANGLE AND NOT AS THE INVERTED L THE HARDWARE HAS, which is the one place\n this table knowingly departs from the photograph. The L's overhanging half cannot be made\n clickable without covering the `{` below it, and the user's call (2026-10-23) was that a\n key-shaped dead region is worse than an oversized gap. The cap's own comment has the detail.\n */\n\n // ROW 1: Tab, QWERTY, two punctuation caps, and the TALL Return.\n key('\u2192|', { kind: 'tab' }, 'main', 1, 0, 1.5, 'Tab'),\n ...letters('QWERTYUIOP', 1, 1.5),\n // `!` over a NOT SIGN -- confirmed again on this photograph, right of `P`.\n ch('\u00AC', 1, 11.5, '!'),\n // A BROKEN BAR (U+00A6) over the backslash, distinct from the solid `|` on the `1` key.\n ch('\\\\', 1, 12.5, '\u00A6'),\n /*\n RETURN, TWO ROWS TALL AND 1.5 UNITS WIDE.\n\n `newline`, NOT `enter`, and that is the 3270 distinction this table has carried from the start:\n `Enter` is the AID that submits the screen and has its own cap on row 4 (and another on the\n numeric pad); this one moves the cursor to the next input field. `keymap.ts` records that no\n TUI chord could express Newline because Ctrl-J IS 0x0a -- a cap has no such spelling problem,\n which is half the reason this surface is worth having.\n */\n {\n label: '\u21B5',\n action: { kind: 'newline' },\n /*\n 1.25 UNITS WIDE, WHICH IS THE GEOMETRY THAT KEEPS THE `{` CLICKABLE.\n\n It was width 1.5 -- and that OVERLAPPED the `{` cap below it by 0.25 units, because the home\n row's content runs further right than the QWERTY row's (13.75 against 13.5, Caps being 1.75\n where Tab is 1.5). The user saw the `{` lose its border.\n\n A `clip-path` notch in `ui.css` was the first fix and it was NOT ENOUGH: a clip changes what\n is PAINTED and not what the box covers, so the Return still sat on top of the `{`. The\n harness said `OVERLAPS by 6px` once it compared boxes instead of hit-testing.\n\n 1.25 UNITS REMOVES THE OVERLAP AND KEEPS THE RECTANGLE: the home row is 13.75 + 1.25 = 15,\n and the QWERTY row is 13.5 of content, a 0.25-unit gap, then the same 1.25 = 15.\n\n THE `x` IS NOT WHAT DOES THAT, AND SAYING SO HERE IS THE POINT -- this comment claimed it was\n until a mutation (2026-10-23) showed otherwise. `ui.css` lifts a tall cap out of flow with\n `position: absolute; right: 0`, so its `x` is NEVER READ and the WIDTH alone sets its left\n edge. 13.75 is recorded because it is where the key really is on the hardware and because\n `x` is how every other cap is placed; change the WIDTH if the left edge must move.\n\n AND THE 0.25-UNIT GAP IS LEFT EMPTY, by the user's decision on 2026-10-23. The hardware key\n is an inverted L that fills it; drawn here, the overhang is a region that looks like a key\n and cannot be clicked -- the `{` needs the box to stay narrow. An oversized gap reads as\n deliberate where a dead half-key does not. See `ui.css` for the full note.\n */\n region: 'main', row: 1, x: 13.75, width: 1.25, height: 2,\n name: 'Newline',\n },\n\n // ROW 2: Caps (inert), ASDFGHJKL, three punctuation caps. The Return above covers 13.5-15.\n dead('\u21EA', 'main', 2, 0, 1.75, 'Caps Lock (not implemented: see the header)'),\n ...letters('ASDFGHJKL', 2, 1.75),\n ch(';', 2, 10.75, ':'),\n ch('\\'', 2, 11.75, '\"'),\n ch('{', 2, 12.75, '}'),\n\n /*\n ROW 3: a 2.25-UNIT SHIFT, ZXCVBNM, three punctuation caps, and a WIDE right Shift.\n\n BOTH SHIFT CAPS DRIVE THE SAME ONE-SHOT STATE -- neither is distinguishable to us, and they are\n matched by NAME in `canvas/src/keyboardUi.ts`. The left cap's printed legend is `>`/`<` on the\n hardware; it is drawn as `Shft` so the surface reads as a keyboard rather than a puzzle.\n */\n shift(3, 0, 2.25, 'left'),\n ...letters('ZXCVBNM', 3, 2.25),\n ch(',', 3, 9.25, '\\''),\n ch('.', 3, 10.25, ':'),\n ch('/', 3, 11.25, '?'),\n shift(3, 12.25, 2.75, 'right'),\n\n /*\n ROW 4: Reset, Alt, the spacebar, Alt, a blank, and `Enter`.\n\n `Reset` CARRIES `DvCntl` AS ITS SHIFTED LEGEND -- front-printed on the cap, read on this\n photograph and identified by the user as Device Control. We have no action for it, so it is\n recorded as a `shiftLabel` on a cap whose unshifted function we DO implement: pressing Shift\n re-letters it, and clicking it still sends `reset`. That is honest -- the shifted function is\n documented as existing and unimplemented, rather than silently absent.\n */\n {\n label: 'Reset',\n shiftLabel: 'DvCn',\n action: { kind: 'reset' },\n region: 'main', row: 4, x: 0, width: 1.5,\n name: 'Reset (shifted: Device Control, not implemented)',\n },\n dead('Alt', 'main', 4, 1.5, 1.25, 'Alt, left (not a 3270 key)'),\n {\n label: '', action: { kind: 'type', text: ' ' },\n region: 'main', row: 4, x: 2.75, width: 7, name: 'Space',\n },\n dead('Alt', 'main', 4, 9.75, 1.25, 'Alt, right (not a 3270 key)'),\n dead('', 'main', 4, 11, 1.25, 'blank, right of the spacebar'),\n key('Enter', { kind: 'enter' }, 'main', 4, 12.25, 2.75, 'Enter'),\n\n /*\n THE CURSOR CLUSTER.\n\n `Dup`+`PA1` and `Field Mark`+`PA2` are DOUBLE-LEGEND CAPS on the hardware -- the lower legend\n is the key's alternate function. We bind the UPPER one, because `Dup` and `Field Mark` are\n typed characters this client implements (`keymap.ts`: \"Dup and Field Mark are TYPED CHARACTERS,\n not AIDs\") while PA1/PA2 already have caps in the keypad and chords of their own. The lower\n legend is drawn in the label so the cap matches the photograph.\n\n THE THREE GLYPH KEYS ABOVE THE ARROWS COULD NOT BE READ at this resolution -- a left-pointing\n arrow, a house-like glyph and a third mark. They are INERT with descriptive names, per this\n file's header rule, and are the first thing to fix if a better photograph turns up.\n */\n /*\n THE CURSOR CLUSTER, AS A DIAMOND -- REWRITTEN 2026-10-22 FROM THE SHARP REFERENCE.\n\n The user prefers this to the modern inverted-T and the photograph shows why: the arrows sit in\n a 3x3 diamond with a CENTRE CAP between them, which is what the hardware actually has.\n\n Dup/PA1 | FieldMark/PA2 | PA3\n BackTab | Home | DelEOL\n | up |\n left | (centre) | right\n | down |\n\n ## WHAT THIS PHOTOGRAPH IDENTIFIED, ALL OF IT NEW\n\n - THE THIRD CAP OF ROW 1 is the `a`-with-a-squiggle glyph, which the user read as\n delete-to-end-of-line. We have no such action -- `delete` is a single character and\n `eraseEOF` erases to the end of the FIELD, not the line -- so it is inert with its real\n name. **AND THAT IS WHY `Xfer` MOVED OFF IT**: it is a real key, not a spare.\n - THE CENTRE CAP carries a DIAGONAL ARROW, which the user read as cursor-to-top-left. That is\n plausibly `Home` -- but `Home` already has its own cap two rows up, and a keyboard does not\n usually spend two caps on one function. Inert, with the ambiguity recorded rather than\n guessed away.\n - THE SHIFTED LEGENDS ON LEFT AND RIGHT ARE DOUBLE ARROWS, which the user read as\n beginning/end of line. No action exists for either, so they are `shiftLabel`s on live cursor\n keys: Shift re-letters the cap, and clicking still moves the cursor one position. The\n shifted function is documented as existing and unimplemented.\n */\n key('Dup\\nPA1', { kind: 'dup' }, 'cursor', 0, 0, 1, 'Dup'),\n key('Fld\\nMrk\\nPA2', { kind: 'fieldMark' }, 'cursor', 0, 1, 1, 'Field Mark'),\n key('PA3', { kind: 'pa', n: 3 }, 'cursor', 0, 2, 1, 'PA3'),\n\n /*\n REVERSE TAB: `U+21E4 LEFTWARDS ARROW TO BAR`, AND THE DIRECTION WAS MIRRORED BEFORE.\n\n I had `\u2190|` -- bar on the RIGHT, as if the arrow left a tab stop. The user caught it and the\n reference photograph is unambiguous: the BAR IS AT THE LEFT with the arrowhead terminating\n into it, because you are going back TO a tab stop, not back FROM one. The semantics were\n backwards, not just the drawing.\n */\n key('\u21E4', { kind: 'backTab' }, 'cursor', 1, 0, 1, 'Back Tab (cursor cluster)'),\n /*\n `U+21A5 UPWARDS ARROW FROM BAR` FOR INSERT -- AND THIS CAP WAS MISIDENTIFIED AS `Home`.\n\n The glyph is a lowercase `a` beneath a WIDE CARET, which the user reads as Insert; the caret is\n the classic insertion mark and the `a` is the character being inserted. I had drawn it as a\n house and called it Home, which was a guess from the shape alone.\n\n `toggleInsert` IS AN ACTION WE HAVE, so this becomes a working key rather than scenery -- the\n compact keypad carries it as `Ins`.\n\n THE GLYPH IS AN APPROXIMATION, NOT A TRANSCRIPTION: there is no Unicode point for an `a` under\n a caret, so this is the closest available stand-in, and an SVG of the real mark is the someday\n fix.\n\n `U+2041 CARET INSERTION POINT`, AND IT IS THE THIRD CHOICE -- the first two were rejected for\n different reasons, which is why all three are recorded.\n\n 1. `U+21A5 UPWARDS ARROW FROM BAR` was mine, and it is the wrong SHAPE: a thin solid arrow\n with a bar at its foot, where a 12x crop of the cap shows a wide HOLLOW caret.\n 2. `U+21E7 UPWARDS WHITE ARROW` was the user's suggestion and matches the shape exactly --\n but **U+21E7 IS THE SHIFT KEY**, universally, and this keyboard HAS two Shift caps. A\n glyph that means Shift on a key that means Insert is worse than one that merely looks\n wrong: it is actively misleading, and the shape being right makes the confusion more\n likely rather than less.\n 3. `U+2041 CARET INSERTION POINT` is the proofreader's insertion mark, so right shape and right\n meaning -- and it DOES NOT RENDER. Measured in the harness: 2.3px against 7.5px for a\n glyph known to be present, i.e. a near-blank fallback. General Punctuation is not what the\n symbol fonts in `ui.css` were chosen for.\n 4. `U+2303 UP ARROWHEAD` measures 7.5px, exactly matching the reference glyph, and it is a\n hollow caret that means \"control/up\" rather than Shift. Right shape, available, and\n unclaimed on this keyboard.\n\n THE LESSON IS THAT A GLYPH HAS THREE PROPERTIES and all of them matter: what it looks like,\n what it already means elsewhere, and whether the font actually has it. Choice 1 failed the\n first, choice 2 the second, choice 3 the third.\n\n STILL AN APPROXIMATION: the hardware draws an `a` tucked under the caret's apex, and no\n Unicode point does that. An SVG remains the someday fix.\n */\n key('\u2303', { kind: 'toggleInsert' }, 'cursor', 1, 1, 1, 'Insert (approximate glyph)'),\n /*\n `U+219B RIGHTWARDS ARROW WITH STROKE` FOR DELETE CHARACTER, SHIFTED TO TWO OF THEM.\n\n The cap is a lowercase `a` with a squiggle struck diagonally up-and-right, and its shifted\n legend is visibly LONGER -- the user reads it as the squiggle running through three `a`s, i.e.\n the same operation extended to the end of the line.\n\n I HAD `U+232B ERASE TO THE LEFT`, WHICH WAS WRONG IN MEANING: the user points out the stroke\n goes up and to the RIGHT, so this deletes forward, not backward. `delete` is exactly that\n action and we have it, so the unshifted key is LIVE; the shifted form is\n delete-to-end-of-line, which we do NOT have, and the doubled glyph records it the way the\n doubled arrows record start/end of line on the cursor keys.\n\n BOTH GLYPHS ARE APPROXIMATIONS, not transcriptions -- no Unicode point draws an `a` with a\n diagonal strike. Doubling for the shifted form follows the hardware's own idiom and this\n table's existing one.\n */\n {\n label: '\u219B', shiftLabel: '\u219B\u219B', action: { kind: 'delete' },\n region: 'cursor', row: 1, x: 2, width: 1,\n name: 'Delete character (shifted: delete to end of line, not implemented)',\n },\n\n // THE DIAMOND. `up` is centred on its own row; `left`, the centre cap and `right` share the\n // next; `down` is centred below. The row offsets come from the table and `keyboardUi.ts`\n // renders them -- see its note on `padding-left`.\n {\n label: '\u2191', action: { kind: 'up' },\n region: 'cursor', row: 2, x: 1, width: 1, name: 'Cursor up',\n },\n {\n label: '\u2190', shiftLabel: '\u219E', action: { kind: 'left' },\n region: 'cursor', row: 3, x: 0, width: 1,\n name: 'Cursor left (shifted: start of line, not implemented)',\n },\n dead('\u2196', 'cursor', 3, 1, 1, 'diagonal arrow, function unidentified'),\n {\n label: '\u2192', shiftLabel: '\u21A0', action: { kind: 'right' },\n region: 'cursor', row: 3, x: 2, width: 1,\n name: 'Cursor right (shifted: end of line, not implemented)',\n },\n {\n label: '\u2193', action: { kind: 'down' },\n region: 'cursor', row: 4, x: 1, width: 1, name: 'Cursor down',\n },\n\n /*\n THE NUMERIC KEYPAD.\n\n The digits are typed characters. The right-hand column could not be fully resolved: the top\n cap reads like `Space`, below it a tab-like glyph, then `-`, then a tall `Enter`. `Enter` is\n legible and is bound; the other three are INERT with descriptive names rather than guessed.\n */\n // `ch()` builds a `main` cap, so the region is overridden here rather than adding a parameter\n // for one caller. The digits are the same typed characters as the main block's.\n ...[...'789'].map((c, i) => ({ ...ch(c, 0, i), region: 'numpad' as const })),\n ...[...'456'].map((c, i) => ({ ...ch(c, 1, i), region: 'numpad' as const })),\n ...[...'123'].map((c, i) => ({ ...ch(c, 2, i), region: 'numpad' as const })),\n { ...ch('0', 3, 0), region: 'numpad' as const, width: 2, name: '0 (numeric pad)' },\n { ...ch('.', 3, 2), region: 'numpad' as const, name: '. (numeric pad)' },\n /*\n THE NUMERIC PAD'S RIGHT-HAND COLUMN -- READ AT LAST, 2026-10-22, FROM THE ORIGINAL PHOTOGRAPH.\n\n These three were inert with `unread` names because I could not resolve them. A 6x crop of the\n SAME image settles all three: `Space`, a FIELD TAB arrow, and `-`, with `Enter` below. **The\n pixels were always there; I gave up on the zoom too early**, which is worth recording next to\n the entries it cost.\n\n A SECOND KEYBOARD CONFIRMED NOTHING HERE, and that is the interesting part: the IBM 3270 PC\n (the layout this one otherwise matches exactly) has a PC NUMPAD in this position -- 7/Home,\n 8/up, 9/PgUp, with Esc/NumLk/ScrLk above. It is a PC with 3270 emulation, so its numpad is a\n PC's. Transplanting those legends would have been the worst kind of cross-check: a keyboard\n that agrees everywhere else, differing silently in exactly the place I needed.\n */\n key('Spc', { kind: 'type', text: ' ' }, 'numpad', 0, 3, 1, 'Space (numeric pad)'),\n // FIELD TAB, the same `\u2192|` glyph the main block's Tab carries.\n key('\u2192|', { kind: 'tab' }, 'numpad', 1, 3, 1, 'Tab (numeric pad)'),\n ch2('-', 'numpad', 2, 3, '- (numeric pad)'),\n key('Ent', { kind: 'enter' }, 'numpad', 3, 3, 1, 'Enter (numeric pad)'),\n]);\n", "/**\n * The `TransferFiles` implementation over `node:fs`.\n *\n * ITS OWN PACKAGE, and deliberately not part of `frontend`, for two reasons that both\n * matter later: the browser bundles (`gui`, `web`) must never acquire a `node:fs` import,\n * and stage 4's web gateway needs a DIFFERENT implementation of this same interface, whose\n * \"filesystem\" is an upload/download pair rather than a disk. Both front ends that do have\n * a disk (`cli`, `tui`) share this one, so a filesystem bug has exactly one home.\n *\n * Moved here from `cli/src/main.ts`, where it lived for the same reason `Replay(file)`'s\n * I/O does: `runner.ts` stays free of `node:fs` so every command's semantics are testable\n * without a temp directory. It was injected rather than special-cased because a transfer's\n * file access is interleaved with host round trips and cannot be lifted out of the caller\n * the way reading a replay file up front can.\n *\n * `Uint8Array`, never a string: these are file BYTES, and the whole point of the binary\n * default is that nothing in the path decodes them. `readFileSync` with no encoding returns\n * a Buffer, which IS a Uint8Array, but a fresh view is constructed so nothing downstream\n * can be surprised by Buffer's extra methods or by its pooled backing store.\n *\n * This package also holds `isMain` (packaging milestone, 2026-11-05), a second Node-only\n * helper with nothing to do with files. It lives here rather than in `frontend` or its own\n * package for the same reason `nodeTransferFiles` does: it needs `node:fs` (`realpathSync`),\n * so it must never reach a browser bundle, and `node-files` is already the one place `tui`\n * and (now) `web` share for exactly that kind of helper.\n */\n\nimport { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';\nimport type { TransferFiles } from '@tn3270/frontend';\n\nexport const nodeTransferFiles: TransferFiles = {\n exists: (path) => existsSync(path),\n read: (path) => new Uint8Array(readFileSync(path)),\n write: (path, bytes) => { writeFileSync(path, bytes); },\n append: (path, bytes) => { appendFileSync(path, bytes); },\n};\n\nexport { isMain } from './isMain.js';\n", "// TYPE-ONLY, AND IT MUST STAY THAT WAY: `dist/cg.js` therefore has NO imports at all, which is what\n// keeps this module usable from anywhere, the way `hittest.ts` is. `geometry.ts` is a leaf and\n// imports nothing, so this edge is not a cycle even as a type -- it used to come from\n// `drawlist.js`, which imports THIS module at runtime, and that was one. `module-cycles.test.ts`\n// pins both properties.\nimport type { AtlasGeometry } from './geometry.js';\n\n/**\n * EBCDIC code point to 3270 Character Generator (CG) index, and CG index to atlas column.\n *\n * BOTH LINKS OF THE GLYPH CHAIN LIVE HERE, in the order a character travels them: an EBCDIC byte\n * becomes a CG code (`ebcdicToCg`), and a CG code becomes the atlas column that holds its bitmap\n * (`column`). They are consecutive steps of one lookup and both fall back to `CG_BOXSOLID` below,\n * so splitting them across two modules bought nothing and cost an import cycle -- `column` used to\n * live in `drawlist.ts`, which `keypad.ts` then had to import at runtime while `drawlist.ts`\n * imported `keypad.ts` back.\n *\n * ## WHY THIS TABLE HAS TO EXIST, AGAINST WHAT THE SPEC ORIGINALLY CLAIMED\n *\n * The design said x3270's `3270.bdf` is indexed by EBCDIC, so the atlas would need \"no\n * translation step\". THAT IS WRONG, and it was caught by a test rather than by reading.\n * The font's own comment says *\"Page 0: EBCDIC US-International set, CG order\"* -- the\n * character SET is EBCDIC's, but the ORDER is the Character Generator's. Measured from the\n * vendored font: `ENCODING 16` is `space` where EBCDIC space is 0x40, and `ENCODING 160`\n * is `A` where EBCDIC `A` is 0xc1.\n *\n * The atlas is still the right choice -- APL really is free on page 1, and bitmaps at\n * integer scale really are deterministic -- but the no-translation argument was\n * overstated. This is what it costs: one 256-byte lookup.\n *\n * ## PROVENANCE\n *\n * Generated mechanically from `ebc2cg0[256]` in x3270 4.5's `x3270/xtables.c:76`, same\n * BSD-3-clause source as the font itself (see assets/LICENSE-3270-font.txt), so the table\n * and the glyphs cannot disagree about which index means what.\n *\n * It is CORROBORATED INDEPENDENTLY by the font's own glyph names: the table sends EBCDIC\n * 0x40 to 16, 0xc1 to 160 and 0x81 to 128, and the BDF names those encodings `space`, `A`\n * and `a`. Two sources, generated separately, agreeing -- which is a stronger footing than\n * the premise it replaces had.\n *\n * 55 of the 256 EBCDIC codes are unmapped and point at CG 0xdf, which the font names\n * `boxsolid`: x3270's visible \"unprintable\" indicator rather than a blank, so a host\n * sending junk shows something is wrong instead of nothing at all.\n */\nconst EBC2CG: readonly number[] = Object.freeze([\n /* 00 */ 0x00, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf,\n /* 08 */ 0xdf, 0xdf, 0xdf, 0xdf, 0x02, 0x03, 0x00, 0x00,\n /* 10 */ 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0x04, 0xdf, 0xdf,\n /* 18 */ 0xdf, 0x05, 0xdf, 0xdf, 0x9f, 0xdf, 0x9e, 0xdf,\n /* 20 */ 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf,\n /* 28 */ 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf,\n /* 30 */ 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf,\n /* 38 */ 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf, 0xdf,\n /* 40 */ 0x10, 0x01, 0x55, 0x50, 0x40, 0x5a, 0x45, 0x9c,\n /* 48 */ 0x4f, 0x5f, 0x1b, 0x32, 0x09, 0x0d, 0x35, 0x16,\n /* 50 */ 0x30, 0x4a, 0x56, 0x51, 0x41, 0x5c, 0x57, 0x52,\n /* 58 */ 0x42, 0x2a, 0x19, 0x1a, 0xbf, 0x0c, 0xbe, 0x36,\n /* 60 */ 0x31, 0x14, 0x75, 0x70, 0x60, 0x7a, 0x65, 0xbc,\n /* 68 */ 0xbd, 0x7f, 0x17, 0x33, 0x2e, 0x2f, 0x08, 0x18,\n /* 70 */ 0x9b, 0x7b, 0x76, 0x71, 0x61, 0x7c, 0x77, 0x72,\n /* 78 */ 0x62, 0x3d, 0x34, 0x2c, 0x2d, 0x12, 0x11, 0x13,\n /* 80 */ 0xbb, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86,\n /* 88 */ 0x87, 0x88, 0x6c, 0x6d, 0xf7, 0x49, 0xf9, 0xd6,\n /* 90 */ 0x38, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,\n /* 98 */ 0x90, 0x91, 0x6a, 0x6b, 0x9a, 0x3f, 0xba, 0x1f,\n /* a0 */ 0x54, 0x3b, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n /* a8 */ 0x98, 0x99, 0x6e, 0x6f, 0xd7, 0x48, 0xd9, 0xd1,\n /* b0 */ 0x3a, 0x1c, 0x1d, 0x39, 0xd0, 0x2b, 0x1e, 0x4b,\n /* b8 */ 0x4c, 0x4d, 0x0a, 0x0b, 0x37, 0x3c, 0x3e, 0x5b,\n /* c0 */ 0x0f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,\n /* c8 */ 0xa7, 0xa8, 0x07, 0x58, 0x53, 0x43, 0x5d, 0x46,\n /* d0 */ 0x0e, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,\n /* d8 */ 0xb0, 0xb1, 0x67, 0x59, 0x4e, 0x44, 0x5e, 0x47,\n /* e0 */ 0x15, 0x9d, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,\n /* e8 */ 0xb8, 0xb9, 0x68, 0x78, 0x73, 0x63, 0x7d, 0x66,\n /* f0 */ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,\n /* f8 */ 0x28, 0x29, 0x69, 0x79, 0x74, 0x64, 0x7e, 0x06,]);\n\n/** The CG index the font draws for an unmapped EBCDIC byte: a solid box. */\nexport const CG_BOXSOLID = 0xdf;\n\n/**\n * Translate one EBCDIC byte to its CG glyph index.\n *\n * Out-of-range input yields `CG_BOXSOLID` rather than throwing: this sits on the drawing\n * path, and a malformed byte from a host must show as a visible box, not crash a window.\n */\nexport function ebcdicToCg(ebcdic: number): number {\n return EBC2CG[ebcdic & 0xff] ?? CG_BOXSOLID;\n}\n\n/**\n * The atlas column for a CG code, falling back to the solid box.\n *\n * A MISS MUST NOT BECOME AN OUT-OF-RANGE COLUMN. Sampling past the end of the atlas draws\n * whichever glyph sits next along, which reads as corruption rather than as a missing\n * character -- so an unknown code gets x3270's visible unprintable marker instead.\n *\n * THE ONE COPY, SHARED BY `drawlist.ts` AND `keypad.ts`. `atlas.index` is a sparse map and\n * emphatically not the identity: 175 of its 431 entries differ from their CG code, because the\n * font's encodings run 0..543 with holes and the atlas is packed. Any second calculation --\n * `cg % atlas.cols` was the one proposed for the keypad -- both loses the fallback above and\n * returns the wrong column for every code past 256, which is a font bug only a golden could catch.\n */\nexport function column(atlas: AtlasGeometry, cg: number): number {\n return atlas.index[cg] ?? atlas.index[CG_BOXSOLID] ?? 0;\n}\n", "/**\n * Turn a screen snapshot plus its resolved attributes into per-cell draw instructions.\n *\n * THIS FILE RUNS IN THE MAIN PROCESS ONLY. `renderer.js` reaches it through `import type`\n * alone, which erases, so the value import above cannot reach the browser. Task 4 pins that.\n *\n * ## PURE, AND THAT IS THE POINT\n *\n * This is where reverse video, the cursor, color and the hidden-field rule are decided,\n * and it is testable with no canvas, no Xvfb and no Electron. `render.ts` in the TUI is\n * built the same way -- it returns a string and lets the caller write it -- and that is\n * what made the TUI's output diffable and its tests fast. Putting these decisions inside a\n * canvas call would put every one of them behind a screenshot.\n *\n * ## WHY IT TAKES BOTH THE SNAPSHOT AND THE RESOLVED CELLS\n *\n * They are parallel arrays and each has half of what a glyph needs. `resolve()` gives\n * color and the attribute flags but its `text` is a STRING, while the atlas is indexed by\n * the font's Character Generator codes, reached from the EBCDIC byte that only the raw\n * snapshot carries. Zipping them by index costs nothing and avoids a Unicode round trip\n * that would lose APL. The design assumed `resolve()` alone would do; it does not.\n *\n * ## THE HIDDEN-FIELD RULE IS A SECURITY REQUIREMENT, NOT A DETAIL\n *\n * core's comment on `ResolvedCell.hidden`: it \"is the ONLY thing standing between a\n * password field and the screen\", and neither `text` nor the snapshot's `ebcdic` is\n * pre-redacted. A renderer that draws the glyph without checking this puts the password on\n * screen -- and in this project's case into committed screenshot goldens. Blanked here,\n * matching the TUI at `render.ts:369`.\n *\n * ## `AtlasGeometry` AND `DrawCell` ARE NOT DECLARED HERE ANY MORE\n *\n * They are in `geometry.ts`, which imports no sibling. They were here, and `cg.ts`, `assets.ts`\n * and `blit.ts` all took them from here with `import type` -- while this module value-imports\n * `column` from `cg.ts`. That is an import cycle in the type position: harmless while the imports\n * erase, and a module-initialization ordering bug the day one of them becomes a value. See\n * `geometry.ts` and the guard in `test/module-cycles.test.ts`.\n *\n * `keypad.ts` WAS THE OTHER HALF OF THAT CYCLE and is gone (2026-10-06): neither front end draws\n * a keypad into the canvas any more, so `keypadRegion` had no consumer. `DrawList` named its\n * `KeypadRegion`, which was the stated reason the type could not live in a leaf module -- that\n * reason has now expired, and moving `DrawList` into `geometry.ts` is a tidy somebody could do.\n * Not done here: this change is already deleting a shipped feature's worth of code, and mixing a\n * type move into it would make the diff harder to read than the tidy is worth.\n */\n\nimport {\n cp037, Color, type ResolvedCell, type ScreenSnapshot,\n} from '@tn3270/core';\nimport { schemeRgb, type Scheme } from '@tn3270/frontend';\nimport { ebcdicToCg, column } from './cg.js';\nimport type { AtlasGeometry, DrawCell } from './geometry.js';\n\nexport interface DrawList {\n readonly cells: readonly DrawCell[];\n /**\n * Absent when no OIA text was supplied, and the height then omits its row.\n *\n * `cells` is the OIA rendered THROUGH THE SAME ATLAS as the screen, not text for the\n * canvas to typeset. That is not cosmetic consistency: `fillText` would pull in a system\n * font, and font rasterisation is exactly the machine-dependent thing that would stop\n * screenshot goldens being byte-reproducible. `text` is kept alongside for debugging and\n * for tests to assert against something readable.\n */\n readonly oia?: {\n readonly text: string;\n readonly y: number;\n readonly cells: readonly DrawCell[];\n };\n readonly width: number;\n readonly height: number;\n}\n\n/** EBCDIC space, which is what a hidden cell draws instead of its own character. */\nconst EBCDIC_SPACE = 0x40;\n\n// `scheme` comes BEFORE `oiaText`: an existing call passing OIA text positionally would\n// otherwise silently take it as the scheme, with no type error and no failing test.\n//\n// A SIXTH `showKeypad` PARAMETER WAS HERE AND IS GONE (2026-10-06). It was appended and defaulted\n// to off for exactly the reason above -- so existing callers kept their meaning -- and both front\n// ends now draw their keypad as real HTML controls instead, so nothing ever passed `true`.\n// Removing it rather than leaving a parameter no caller sets: a defaulted flag that cannot be\n// turned on is a reader trap, and `tsc` catches every stale `true` at the call site.\nexport function drawList(\n snapshot: ScreenSnapshot,\n resolved: readonly ResolvedCell[],\n atlas: AtlasGeometry,\n scheme: Scheme,\n oiaText?: string,\n): DrawList {\n const blank = column(atlas, ebcdicToCg(EBCDIC_SPACE));\n const cells: DrawCell[] = [];\n\n for (let i = 0; i < snapshot.cells.length; i++) {\n const raw = snapshot.cells[i]!;\n const r = resolved[i]!;\n const row = Math.floor(i / snapshot.cols);\n const col = i % snapshot.cols;\n\n // Reverse video swaps the pair rather than picking a fixed inverse: the cell's own\n // colors are what a 3279 inverts.\n const fg = schemeRgb(scheme, r.reverse ? r.bg : r.fg);\n const bg = schemeRgb(scheme, r.reverse ? r.fg : r.bg);\n\n cells.push({\n x: col * atlas.cellWidth,\n y: row * atlas.cellHeight,\n glyph: r.hidden ? blank : column(atlas, ebcdicToCg(raw.ebcdic ?? EBCDIC_SPACE)),\n fg,\n bg,\n cursor: i === snapshot.cursor,\n underline: r.underscore,\n blink: r.blink,\n intensify: r.intensify,\n });\n }\n\n const rows = snapshot.rows + (oiaText !== undefined ? 1 : 0);\n const oiaY = snapshot.rows * atlas.cellHeight;\n return {\n cells,\n ...(oiaText !== undefined\n ? {\n oia: {\n text: oiaText,\n y: oiaY,\n cells: oiaCells(oiaText, oiaY, snapshot.cols, atlas, scheme),\n },\n }\n : {}),\n width: snapshot.cols * atlas.cellWidth,\n // SCREEN PLUS THE OIA's ROW, AND NOTHING ELSE NOW. A `keypadY + keypad.height` branch was here\n // until 2026-10-06 and the keypad was the only thing that ever made the drawing taller than\n // its rows; `rows` already counts the OIA's row when there is one, so this one expression\n // covers both cases. `gui/src/main.ts`'s `fit` sizes the window from this, which is why the\n // keypad had to live in the draw list at all -- a renderer-owned one would have left main\n // unaware the drawing had grown and the Electron page is `overflow:hidden`, so it would have\n // been clipped. That whole argument is moot now: the keypad is a separate WINDOW in Electron\n // and an overlay in the browser, and neither changes this height.\n height: rows * atlas.cellHeight,\n };\n}\n\n/**\n * The OIA as atlas cells on the row below the screen buffer.\n *\n * Neutral white on black, which is a presentation choice rather than a protocol fact: the\n * OIA is OUR chrome, not one of the host's 1920 cells, so nothing in the data stream says\n * what color it should be. Truncated at the screen width rather than wrapped, because a\n * status line that reflowed would move the screen.\n */\nfunction oiaCells(\n text: string, y: number, cols: number, atlas: AtlasGeometry, scheme: Scheme,\n): readonly DrawCell[] {\n const fg = schemeRgb(scheme, Color.NEUTRAL_WHITE);\n const bg = schemeRgb(scheme, Color.NEUTRAL_BLACK);\n const out: DrawCell[] = [];\n const chars = [...text].slice(0, cols);\n for (let i = 0; i < chars.length; i++) {\n out.push({\n x: i * atlas.cellWidth,\n y,\n glyph: column(atlas, ebcdicToCg(cp037.fromUnicode(chars[i]!))),\n fg,\n bg,\n cursor: false,\n underline: false,\n blink: false,\n intensify: false,\n });\n }\n return out;\n}\n", "import type { Rgb } from '@tn3270/core';\nimport type { AtlasGeometry } from './geometry.js';\nimport type { DrawList } from './drawlist.js';\n\n/**\n * Draw a `DrawList` onto a 2D canvas context.\n *\n * ## THE ATLAS IS COVERAGE, SO EVERY COLOR NEEDS ITS OWN TINTED COPY\n *\n * `atlas.bin` is alpha only. Tinting per CELL would mean a composite operation 1920 times\n * a frame; tinting per COLOR means at most sixteen, because that is how many a 3279 has.\n * So each color gets one pre-tinted `ImageBitmap`-alike, built on demand and cached. The\n * cache is bounded by the palette, not by the screen, which is why it can be unbounded in\n * code without being unbounded in fact.\n *\n * ## SMOOTHING OFF, INTEGER SCALE ONLY\n *\n * `imageSmoothingEnabled = false` and an integer `scale`. This is what keeps a bitmap font\n * pixel-exact, and it is also what makes screenshot goldens byte-reproducible -- there is no\n * hinting and no subpixel antialiasing to vary between machines. A fractional scale would\n * smear the glyphs and make every golden machine-specific.\n *\n * Kept free of `document` and `window` so the arithmetic is testable: the caller supplies\n * the context and a factory for offscreen surfaces.\n */\nexport interface Surface {\n readonly width: number;\n readonly height: number;\n}\n\n/** The 2D context operations this needs, named so a test can supply a recorder. */\nexport interface Ctx2D {\n fillStyle: string;\n imageSmoothingEnabled: boolean;\n /**\n * Composition mode, for the SELECTION HIGHLIGHT only.\n *\n * A string and not a union of the 26 canvas modes: narrowing it would buy nothing here (the one\n * caller passes a literal) and would make this interface a second, drifting copy of a DOM type.\n * `blit` itself never touches it -- it is here so `renderer.ts` can set `difference` without a\n * cast, which is what inverts a selection for every palette scheme without a per-scheme table.\n */\n globalCompositeOperation: string;\n /**\n * Save and restore the drawing state, so the selection highlight's `difference` mode and fill\n * color cannot leak into the next frame's blit. Paired, always.\n */\n save(): void;\n restore(): void;\n fillRect(x: number, y: number, w: number, h: number): void;\n drawImage(\n image: unknown, sx: number, sy: number, sw: number, sh: number,\n dx: number, dy: number, dw: number, dh: number,\n ): void;\n}\n\nexport interface BlitOptions {\n readonly atlas: AtlasGeometry;\n /** Integer multiple. 1 draws one atlas pixel per canvas pixel. */\n readonly scale: number;\n /** Left offset in canvas pixels, for letterboxing. */\n readonly offsetX: number;\n readonly offsetY: number;\n /**\n * A pre-tinted atlas for one color, or `undefined` if it is not ready yet.\n *\n * Returning undefined SKIPS the glyph for this frame rather than drawing a placeholder.\n * An earlier version returned an empty `ImageData`, which `drawImage` rejects outright --\n * \"the provided value is not of type ... ImageBitmap\" -- so every cell threw and the\n * window drew backgrounds only. The caller repaints when the tint finishes building.\n */\n readonly tinted: (color: Rgb) => unknown | undefined;\n /**\n * Atlas columns with NO INK, whose `drawImage` can be skipped. From `blankColumns`.\n *\n * Necessary rather than an optimisation detail: an untouched 3270 cell holds EBCDIC\n * 0x00, which maps to CG 0 (`null`), NOT to CG 16 (`space`). An earlier version tested\n * only for space and therefore stamped all 1920 cells of a blank screen -- caught by a\n * test asserting an empty screen issues no draws.\n */\n readonly blank: ReadonlySet<number>;\n}\n\nexport const rgbCss = ([r, g, b]: Rgb): string => `rgb(${r},${g},${b})`;\n\n/** Cache key for a tinted atlas. Colors are triples, so a string key is the simplest. */\nexport const tintKey = ([r, g, b]: Rgb): string => `${r},${g},${b}`;\n\n/**\n * The largest integer scale whose letterboxed screen fits `within`, minimum 1.\n *\n * Takes a plain SIZE rather than a `DrawList`, because that is all it reads -- and main\n * needs it before a frame exists, to size the window to the screen the host just described.\n *\n * Stated as a rule rather than \"fit to the window\": a fractional fit would smear a bitmap\n * font, and refusing to draw at all when the window is small would be worse than drawing\n * small. 1 is the floor because there is no half-pixel glyph.\n */\nexport function bestScale(list: Surface, within: Surface): number {\n const byWidth = Math.floor(within.width / list.width);\n const byHeight = Math.floor(within.height / list.height);\n return Math.max(1, Math.min(byWidth, byHeight));\n}\n\n/** Where to put the screen so it sits centerd in `within` at `scale`. */\nexport function center(list: Surface, within: Surface, scale: number): { x: number; y: number } {\n return {\n x: Math.max(0, Math.floor((within.width - list.width * scale) / 2)),\n y: Math.max(0, Math.floor((within.height - list.height * scale) / 2)),\n };\n}\n\nexport function blit(ctx: Ctx2D, list: DrawList, opts: BlitOptions): void {\n const { atlas, scale, offsetX, offsetY } = opts;\n ctx.imageSmoothingEnabled = false;\n\n const w = atlas.cellWidth * scale;\n const h = atlas.cellHeight * scale;\n\n for (const cell of list.cells) {\n const dx = offsetX + cell.x * scale;\n const dy = offsetY + cell.y * scale;\n\n // Background first, always -- including for a blank cell, or a previous frame's glyph\n // shows through. The 3270 has no transparency.\n ctx.fillStyle = rgbCss(cell.bg);\n ctx.fillRect(dx, dy, w, h);\n\n if (!opts.blank.has(cell.glyph)) {\n const image = opts.tinted(cell.fg);\n if (image !== undefined) {\n ctx.drawImage(\n image,\n cell.glyph * atlas.cellWidth, 0, atlas.cellWidth, atlas.cellHeight,\n dx, dy, w, h,\n );\n }\n }\n\n if (cell.underline) {\n ctx.fillStyle = rgbCss(cell.fg);\n ctx.fillRect(dx, dy + h - scale, w, scale);\n }\n\n if (cell.cursor) {\n // A block cursor drawn as a reverse bar over the bottom third: visible over a glyph\n // without hiding it, which a full reverse block does.\n ctx.fillStyle = rgbCss(cell.fg);\n ctx.fillRect(dx, dy + h - Math.max(scale, Math.floor(h / 4)), w, Math.max(scale, Math.floor(h / 4)));\n }\n }\n}\n\n/**\n * Which atlas columns contain no ink at all.\n *\n * Computed from the coverage bitmap rather than from a list of \"blank-looking\" codes,\n * because there is more than one: EBCDIC 0x00 (an untouched cell) is CG 0 `null`, EBCDIC\n * 0x40 is CG 16 `space`, and the font has others. Guessing the set is what made the first\n * version of this file stamp every cell of an empty screen.\n *\n * Worth the single startup pass over ~54 KB: a 3270 screen is 1920 cells and a logon panel\n * is mostly empty, so this removes the large majority of per-frame composites.\n */\nexport function blankColumns(bytes: Uint8Array, atlas: AtlasGeometry): ReadonlySet<number> {\n const stride = atlas.cols * atlas.cellWidth;\n const blank = new Set<number>();\n for (let col = 0; col < atlas.cols; col++) {\n let inked = false;\n for (let y = 0; y < atlas.cellHeight && !inked; y++) {\n const base = y * stride + col * atlas.cellWidth;\n for (let x = 0; x < atlas.cellWidth; x++) {\n if (bytes[base + x] !== 0) { inked = true; break; }\n }\n }\n if (!inked) blank.add(col);\n }\n return blank;\n}\n", "import type { Action } from '@tn3270/frontend';\n\n/**\n * Chromium `KeyboardEvent` to a named 3270 action.\n *\n * The GUI's counterpart to the terminal keymap, and deliberately NOT derived from it: that\n * table is measured byte sequences and this one matches `key` names, so a shared\n * abstraction over both would be an untested third thing. What IS shared is the `Action`\n * vocabulary from `@tn3270/frontend`, and `BINDING_INTENT` there records which key is meant\n * to do what -- `keys.test.ts` checks this mapper against it, so a key added to one front\n * end is visibly missing from the other.\n *\n * ## RETURNS null RATHER THAN GUESSING\n *\n * An unknown key must do nothing. Falling through to `type` would put the literal string\n * \"AudioVolumeUp\" into a field, and a bare `Shift` would type \"Shift\" -- both worse than\n * ignoring the key. An unmapped Ctrl chord is likewise dropped rather than typing its\n * letter: Ctrl-Z has no 3270 meaning, and inserting a bare \"z\" is not a reasonable guess.\n *\n * ## Ctrl-C IS CLEAR, Ctrl-] QUITS\n *\n * Correct for a 3270 and surprising to everyone: Clear is an AID a user needs constantly to\n * dismiss VM's `MORE...` state, so the usual instinct for escaping cannot be the way out.\n * The window says so at startup, as the TUI's banner does.\n */\nexport interface KeyLike {\n readonly key: string;\n /**\n * The PHYSICAL key. Load-bearing for Alt chords: on macOS, Option-1 reports\n * `key === '\u00A1'`, so an `e.key` binding works on Linux and fails on a Mac.\n */\n readonly code: string;\n readonly ctrlKey: boolean;\n readonly altKey: boolean;\n readonly metaKey: boolean;\n readonly shiftKey: boolean;\n /**\n * `KeyboardEvent.getModifierState`. OPTIONAL, and the optionality is the point: a real\n * event always has it, but `Electron.sendInputEvent` synthesizes events that do not, and\n * `keyspec.ts` documents that an unrecognized spelling arrives as `key: ''` with no\n * methods at all. Code here must therefore treat its absence as \"no AltGraph\", never\n * call it unguarded.\n */\n readonly getModifierState?: (modifier: string) => boolean;\n}\n\n/**\n * Does this event carry a LAYOUT CHARACTER -- a character reachable only through a\n * layout-shift modifier?\n *\n * ## THE BUG THIS FIXES\n *\n * On a German, Spanish or Nordic layout the C programmer's characters live behind AltGr:\n * `{` is AltGr+7, `}` is AltGr+0, and `[`, `]`, `@`, `\\` and `~` are AltGr chords too. Those\n * arrive with `altKey` set, so the Alt branch below dropped every one of them and a whole\n * class of keyboard could not type a brace into a field at all.\n *\n * `getModifierState('AltGraph')` is the portable signal (MDN,\n * `KeyboardEvent.getModifierState`): true for **Windows AltGr**, for **macOS Option**, and\n * for the **GTK level-3/5 shift** on Linux. Plain `altKey` cannot stand in for it, because\n * Windows reports AltGr AS ctrlKey+altKey -- so a `ctrlKey && altKey` test would swallow\n * genuine Ctrl-Alt chords, and an `altKey` test would steal Alt+digit from the PA keys.\n *\n * ## WHY THIS IS A FALLBACK AND NOT A BRANCH TAKEN FIRST\n *\n * MEASURED, and the first version of this fix got it wrong: on **macOS, Option sets\n * AltGraph too**, so checking it before the bindings made Option-1 type `\u00A1` instead of\n * sending PA1 and Option-K type `\u02DA` instead of toggling the keypad. That is precisely the\n * defect the notes on `KeyLike.code` and the PA tests already warn about -- a change that\n * works on Linux and silently breaks a Mac -- and the existing tests passed through it\n * because none of them set `getModifierState`.\n *\n * So the order is: **every binding gets its chance first**, and only a keystroke that no\n * binding claimed is reconsidered as a layout character. Option-1 still reaches PA1,\n * AltGr+7 still types `{`, because `Digit7` is in no table.\n *\n * NOT DRIVEN BY THE Xvfb HARNESS, and `keys.mjs` cannot be extended to cover it:\n * `sendInputEvent`'s modifier list (`electron.d.ts:8961`) has no `altgr`, so there is no way\n * to synthesize one of these events from the harness. `keys.test.ts` is the whole of the\n * cover, which is why it states both platforms' shapes explicitly.\n */\nfunction layoutCharacter(e: KeyLike): Action | null {\n if (e.getModifierState?.('AltGraph') !== true) return null;\n // The same single-code-point rule the printable path uses, so a dead key (`key: 'Dead'`)\n // or an F-key with AltGr held still types nothing.\n if ([...e.key].length === 1) return { kind: 'type', text: e.key };\n return null;\n}\n\n/** Keys whose `key` value is a name rather than a character. */\nconst NAMED: Readonly<Record<string, Action>> = Object.freeze({\n Enter: { kind: 'enter' },\n ArrowLeft: { kind: 'left' },\n ArrowRight: { kind: 'right' },\n ArrowUp: { kind: 'up' },\n ArrowDown: { kind: 'down' },\n Home: { kind: 'home' },\n Backspace: { kind: 'backspace' },\n Delete: { kind: 'delete' },\n // vt220 Select/End bound to EraseEOF, following c3270 -- a CHOICE, not a derivation.\n End: { kind: 'eraseEOF' },\n // x3270's Toggle(insertMode), fb-x3270:210.\n Insert: { kind: 'toggleInsert' },\n});\n\n/**\n * Modifier keys arrive as key events in their own right and mean nothing alone.\n * Without this, holding Shift types \"Shift\" into the field.\n */\nconst MODIFIERS: ReadonlySet<string> = new Set([\n 'Shift', 'Control', 'Alt', 'Meta', 'CapsLock', 'AltGraph', 'NumLock', 'ScrollLock',\n]);\n\n/**\n * The Ctrl chords a 3270 uses. Anything else with Ctrl held is dropped.\n *\n * `d`, `f` and `k` are c3270's own, from the keymap a terminal build reads:\n * `Ctrl<Key>d: Dup()`, `Ctrl<Key>f: FieldMark()`, `Ctrl<Key>k: Keypad()`\n * (Common/fb-c3270:186-187, :191). Matching the terminal keymap here is the point -- an\n * operator moving between the TUI and a window should not have to learn two sets.\n */\nconst CTRL: Readonly<Record<string, Action>> = Object.freeze({\n c: { kind: 'clear' },\n r: { kind: 'reset' },\n u: { kind: 'eraseInput' },\n ']': { kind: 'quit' },\n a: { kind: 'attn' },\n d: { kind: 'dup' },\n f: { kind: 'fieldMark' },\n k: { kind: 'toggleKeypad' },\n // The full 122-key keyboard, 2026-10-22. Free in both mappers -- the taken chords are\n // C R U ] A D F K T -- and claiming it also suppresses Firefox's bookmarks sidebar, which is\n // correct: `renderer.ts` calls `preventDefault` only for keys we claimed, so claiming it is\n // what makes it ours. Intercepted CLIENT-SIDE in both DOM front ends, exactly as `toggleKeypad`\n // is, and the gateway returns before `applyAction` for it -- see `web/src/main.ts`.\n b: { kind: 'toggleKeyboard' },\n /**\n * Ctrl-T, the transfer form -- `BINDING_INTENT`'s own spelling, now that the GUI has a dialog.\n *\n * THE GUI HAD THIS CHORD DEAD WHILE THE WINDOW WORKED, which is how this was found: a real\n * Electron run with `TN3270_GUI_KEYS=Ctrl+T` printed `keys: sent Ctrl+T` and NO `action:` line\n * at all, where a click on the `Xfer` keypad button opened the window fine. The keypad route\n * went through `KEYPAD_KEYS` and never through this table, so the one that was missing was also\n * the one no harness drove.\n *\n * ACCEPTED BY THE WEB FRONT END TOO, since this mapper is shared, and that is safe rather than\n * merely tolerable: `protocol.ts:129-131` REJECTS the kind at decode with a per-client `error`\n * frame, and `web/src/main.ts:151-154` answers a decode failure on that one socket without\n * touching the session or the process. The browser could already produce this kind from the\n * `Xfer` button it draws today, so this adds a second route to an answer that already exists --\n * no new failure mode, and the refusal names its reason. When stage 4 gives the gateway a\n * transfer path that rejection becomes an interception, and this entry needs no change.\n */\n t: { kind: 'transferForm' },\n});\n\n/** Physical digit keys that carry the PA keys when Alt is held. Same shape as CTRL. */\nconst PA_CODES: Readonly<Record<string, Action>> = Object.freeze({\n Digit1: { kind: 'pa', n: 1 },\n Digit2: { kind: 'pa', n: 2 },\n Digit3: { kind: 'pa', n: 3 },\n});\n\nexport function actionForKey(e: KeyLike): Action | null {\n if (MODIFIERS.has(e.key)) return null;\n\n /**\n * COPY, on `Ctrl-Shift-C` (Linux, Windows) and `Cmd-Shift-C` (macOS). 2026-10-27.\n *\n * ## BEFORE THE `CTRL` LOOKUP, AND THAT ORDER IS THE WHOLE TRICK\n *\n * That lookup is case-folded, so `Ctrl-Shift-C` used to mean Clear just as `Ctrl-C` does --\n * DRIVEN, not assumed: both returned `{kind:'clear'}` before this branch existed. Taking the\n * shifted form for copy therefore requires getting in first; a `copy` entry in `CTRL` could not\n * work at all. Plain `Ctrl-C` is untouched and still Clear, which is the property the fold\n * exists to protect, and the comment on the lookup now says which of the two jobs it still has.\n *\n * ## WHY NOT THE NATIVE `copy` EVENT, WHICH IS HOW PASTE WORKS\n *\n * MEASURED, Electron 44.3.0 under Xvfb, in a real Chromium window with NO text selected in the\n * document -- our case, since the canvas selection is `renderer.ts`'s module-level\n * `anchor`/`focus` (`:88-89`) and not a DOM Range; there is no `getSelection` or `createRange`\n * CALL anywhere in `packages/canvas` or `packages/web` -- a claim about calls and not about\n * mentions, which matters because grepping for it finds only this sentence:\n *\n * Ctrl-C -> copy event FIRES, clipboardData present\n * Cmd-C -> no copy event\n * Ctrl-Shift-C -> no copy event\n *\n * So the native copy gesture on Linux is `Ctrl-C` -- the Clear AID -- and adopting it the way\n * paste adopted the native `paste` event would collide with the one binding this project has\n * decided must stay Clear on every platform. `menu.ts:56-72` records that decision AND records\n * rejecting the conditional form, \"Ctrl-C copies when a selection exists and Clears when it does\n * not\", because it makes a DESTRUCTIVE AID depend on invisible state. Paste got a free ride\n * because `keys.ts` never claimed `Ctrl-V`; copy gets none, so it needs an explicit chord.\n *\n * The same measurement is why macOS needs a binding rather than the browser default: `Cmd-C`\n * copies NOTHING from a canvas. A `null` from this mapper means \"we declined it\", never \"the\n * platform handles it\" -- the reading that made an earlier claim in this feature wrong.\n *\n * (2026-11-02: THAT `Cmd-C` ROW WAS MEASURED ON LINUX CHROMIUM, which binds no copy command to\n * Meta+C. On macOS the Edit menu's Copy command does raise the event, and since 2026-11-02\n * `forwardKeys.ts`'s `forwardCopy` listens for it. So `Cmd-C` now copies in the web gateway, but\n * through the event and not through this mapper. `Ctrl-C` still cannot reach it: the Clear claim\n * below cancels the keystroke, and that cancellation suppresses the copy event.)\n *\n * ## `requestCopy`, NOT `copy`\n *\n * `copy` carries a rect and this function is a pure function of one key event, so it cannot\n * see a selection -- the union member's own note in `frontend/src/keymap.ts` has the detail.\n * The renderer, which holds the rectangle, is what turns the intent into a real `copy`.\n *\n * ## `e.code`, AND NEITHER SPELLING OF `e.key` WOULD DO\n *\n * `e.key.toLowerCase()` is the fold itself: it is what makes `Ctrl-Shift-C` indistinguishable\n * from `Ctrl-C`, so the one chord whose discriminator IS Shift cannot be matched through it.\n * Raw `e.key === 'C'` fails differently -- `e.key` is the shifted, LAYOUT-DEPENDENT form, the\n * hazard `KeyLike.code`'s note records, and Shift inverts caps lock, so an operator with caps\n * lock on reports `key: 'c'` with `shiftKey` set. Matching the physical key is what `Alt-K`\n * above does, and for the same reason.\n *\n * ## THE MODIFIER GUARDS\n *\n * `!altKey` leaves `Ctrl-Alt-Shift-C` with the window manager, following this file's rule that\n * an Alt or Meta chord is the OS's. It is also what keeps the Windows AltGr path intact: Windows\n * reports AltGr as ctrlKey+altKey, so an AltGr chord over `KeyC` must fall through to\n * `layoutCharacter` below rather than be eaten here.\n *\n * `ctrlKey !== e.metaKey` IS AN EXCLUSIVE OR AND NOT A LOOSE `(ctrlKey || metaKey)`, which is\n * the form the plan for this task proposed. It demands EXACTLY ONE of the two, so it rejects\n * both edges a disjunction would admit: `Ctrl-Cmd-Shift-C`, a chord belonging to the OS by the\n * same rule the bail below states -- and NEITHER modifier, where a bare `Shift-C` must stay\n * typed text. The Alt-K branch spells the same exclusion `!ctrlKey && !metaKey`, because no\n * branch here may claim a chord another one half-matches.\n */\n if (e.shiftKey && !e.altKey && e.ctrlKey !== e.metaKey && e.code === 'KeyC') {\n return { kind: 'requestCopy' };\n }\n\n if (e.ctrlKey && !e.altKey && !e.metaKey) {\n // Case-folded so that Ctrl-Shift-R, Ctrl-Shift-U and every other entry here reach their\n // unshifted meaning -- caps lock, or a held shift, must not make a chord inert. `Ctrl-Shift-C`\n // is now the ONE exception and is claimed by the copy branch above, which is why that branch\n // has to run first; plain `Ctrl-C` is still Clear. `?? null` is what drops Ctrl-Z rather than\n // typing \"z\".\n return CTRL[e.key.toLowerCase()] ?? null;\n }\n // The PA keys, on Alt+digit as x3270 and c3270 both have them (Common/fb-c3270:43-45).\n // Matched on e.code and not e.key: see the note on KeyLike.code. Checked BEFORE the bail\n // below, which is what used to make every PA key unreachable in this front end. The\n // `!ctrlKey && !metaKey` guard leaves Ctrl-Alt-digit and Cmd-Alt-digit falling through to\n // that bail, where they are `null` unless AltGraph says the keystroke is a layout\n // character -- which is how Windows AltGr+7, reported as ctrlKey+altKey, reaches `{`.\n if (e.altKey && !e.ctrlKey && !e.metaKey) {\n // Alt-K is how c3270's _WIN32 keymap spells `Keypad()` (Common/fb-c3270:48-49, which binds\n // both `k` and `K`), so it is honored here alongside the Ctrl-K its terminal keymap uses\n // (:191). A terminal cannot have both -- Alt-K arrives there as `ESC k` and this project\n // keeps new bindings off the ESC path -- but a real KeyboardEvent carries no such ambiguity.\n //\n // `e.code`, NOT `e.key.toLowerCase()`, and for the reason the note on `KeyLike.code` gives:\n // Option-K on macOS reports `key === '\u02DA'`, exactly as Option-1 reports `'\u00A1'`. A `key`-based\n // test here would work on Linux, fail on a Mac and pass every test written on Linux --\n // the same defect that once made the PA keys unreachable. Matching the physical key also\n // makes Alt-Shift-K work, as c3270's pair of `k`/`K` bindings does.\n //\n // Checked BEFORE PA_CODES so a future Alt entry in that table cannot shadow it silently.\n if (e.code === 'KeyK') return { kind: 'toggleKeypad' };\n // Alt-M, the model/oversize chooser, 2026-11-04 -- `e.code` for the reason Alt-K gives just\n // above: Option-M on macOS reports `key === '\u00B5'`, so a `key` test would pass on Linux and fail\n // on a Mac. Shadows nothing: `KeyM` is in no other Alt entry, PA_CODES holds only digits, and\n // the Ctrl and Meta branches never reach here. The cost is that Option-M no longer types `\u00B5`,\n // the trade Option-K already makes. Not `Ctrl-Shift-M`, which both browsers keep for themselves.\n if (e.code === 'KeyM') return { kind: 'geometryMenu' };\n // `??` and not `return ... ?? null`: a key no PA entry claims may still be a layout\n // character. This is the macOS Option path -- Option-1 matched `Digit1` above and never\n // reaches here, while Option-8 on a German Mac has no binding and types `{`.\n return PA_CODES[e.code] ?? layoutCharacter(e);\n }\n\n // A Meta or Alt chord belongs to the window or the OS, never to the field. Cmd-digit is\n // deliberately NOT a PA: that is where menu accelerators live.\n //\n // THE WINDOWS AltGr PATH ENDS HERE, because Windows reports AltGr as ctrlKey+altKey and so\n // misses both branches above. A genuine Ctrl-Alt chord has no AltGraph state and still\n // returns null, which is what keeps Ctrl-Alt-K with the window manager.\n if (e.metaKey || e.altKey) return layoutCharacter(e);\n\n if (e.key === 'Tab') return e.shiftKey ? { kind: 'backTab' } : { kind: 'tab' };\n\n const fn = /^F(\\d{1,2})$/.exec(e.key);\n if (fn !== null) {\n const n = Number(fn[1]);\n if (n < 1 || n > 12) return null; // F13+ is not a 3270 key\n // Shifted Fn is PF(n+12), the c3270 convention the terminal keymap also follows.\n return { kind: 'pf', n: e.shiftKey ? n + 12 : n };\n }\n\n const named = NAMED[e.key];\n if (named !== undefined) return named;\n\n // Exactly one code point means a printable key; `key` is already the shifted form.\n if ([...e.key].length === 1) return { kind: 'type', text: e.key };\n return null;\n}\n", "import { existsSync, readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { basename, dirname, join } from 'node:path';\nimport type { AtlasGeometry } from './geometry.js';\n\n/**\n * Where this package's BUILT assets live, and how to read the atlas.\n *\n * WHY THIS EXISTS: two consumers now need `atlas.json`, `atlas.bin` and -- for the web front\n * end -- the three browser modules as static files. Both previously lived beside `gui`'s own\n * `dist`, so `main.ts` could say `join(here, 'atlas.json')`. A second consumer guessing a\n * relative path into a sibling package's `dist` is how that breaks silently when either\n * package moves, so the package that OWNS the assets answers where they are.\n *\n * `assetDir()` IS `dist`, NOT THE `assets/` DIRECTORY BESIDE IT. Two different things in this\n * package are called assets: `assets/3270.bdf` is the checked-in BUILD INPUT that\n * `scripts/build-atlas.mjs` reads, and `dist` is where it bakes the atlas that ships. Only\n * the latter is what a consumer wants; asking for the font at runtime would mean shipping a\n * BDF parser to every front end, which is the work the atlas exists to have already done.\n *\n * Node-side only. The browser never imports this file, and `renderer-imports.test.ts` is what\n * keeps that true.\n */\nconst here = dirname(fileURLToPath(import.meta.url));\n\n/** This package's built output directory: the atlas and the browser modules. */\nexport function assetDir(): string {\n return here;\n}\n\n/**\n * The modules a browser must be served: the entry point and every module its runtime graph reaches.\n *\n * A module the entry point imports and this list omits 404s, and the page is then a BLACK CANVAS\n * WITH NO ERROR in any console -- measured on `bridgecore.js`, which is why `httpstatic.test.ts`\n * walks the built import graph rather than trusting this list to be complete.\n *\n * `hittest.js` WAS A MEMBER because the renderer hit-tested a keypad click itself. It left on\n * 2026-10-06 with the canvas keypad, so this list went DOWN by one as `keypadUi.js` arrived --\n * worth noting because a list that only ever grows invites a count assertion, and this one does\n * not only grow.\n *\n * `selection.js` joined for the selection gesture, and it is the reason that walker earned its\n * keep a second time: the plan for copy/paste never mentioned this list, and the renderer's new\n * import would have 404'd the WEB gateway's page -- blank, silent -- while every Electron harness\n * stayed green, because Electron loads from the filesystem and never asks this server for anything.\n * It is safe to serve: `selection.ts`'s only workspace import is an `import type`, which erases.\n *\n * `keypadUi.js` joined for the keypad overlay, and it is the FIRST ENTRY HERE THE RENDERER DOES\n * NOT REACH -- `web/src/bridge.js` imports it directly, through an import-map entry naming this\n * exact module rather than the package barrel. So `renderer-imports.test.ts` cannot see it (it\n * walks the renderer's graph) and `canvas/test/keypadUiModule.test.ts` walks its one-import graph\n * instead. Note what that means for this list's own description: \"every module the ENTRY POINT's\n * graph reaches\" is now one sentence short of the truth, because the page has two entry points.\n *\n * `transferUi.js` joined 2026-10-06 for the web gateway's in-pane transfer form, and it is the\n * SECOND entry the renderer does not reach. NOTHING ON THE SERVED PAGE IMPORTS IT YET -- the\n * overlay that will is a later task in the same plan, and it is listed here first precisely so the\n * module cannot 404 on the day that lands. Its one consumer today is the Electron window, through\n * `gui/transfer.html`'s own map. It is the moved GUI view (`packages/gui` -> here, same day), and\n * `canvas/test/transferUiModule.test.ts`\n * walks its one-import graph the way `keypadUiModule.test.ts` does the keypad's. Its single runtime\n * import is `@tn3270/frontend/dist/transferForm.js`, a DEEP path rather than the bare package name,\n * because the web page's map already binds the bare `@tn3270/frontend` to `keypadView.js` for the\n * keypad and an import-map key without a trailing `/` matches EXACTLY -- so `httpstatic.ts` must\n * serve `transferForm.js` as well, from the `frontend` package rather than from this list.\n *\n * `forwardKeys.js` joined 2026-10-23 and is back INSIDE the renderer's graph, like `selection.js`\n * and unlike the three entries before it. Note which test that does and does not bring into play:\n * `renderer-imports.test.ts` walks the same graph but only asks whether the imports are relative,\n * so it passes either way; the case that fails on an omission is `httpstatic.test.ts`'s graph\n * closure, over in `web`. See the comment on the entry itself.\n */\nexport const BROWSER_MODULES: readonly string[] = Object.freeze([\n 'renderer.js', 'blit.js', 'keys.js', 'selection.js', 'keypadUi.js', 'transferUi.js',\n // The full 122-key keyboard's view, 2026-10-22. SERVED BECAUSE `bridge.js` IMPORTS IT: a module\n // missing from this list is a 404 that makes the IMPORTING module fail to evaluate, which the\n // browser reports only as `window.tn3270` never being assigned -- measured on the way in, as a\n // blank canvas with `Cannot read properties of undefined (reading 'onAtlas')` in the console.\n 'keyboardUi.js',\n // `forwardKeys.js` joins 2026-10-23, and unlike the three entries above it IS in the renderer's\n // own graph: `renderer.ts` stopped inlining the is-this-target-editable rule and imports the\n // shared `isEditableTarget` from here, so the served page's `renderer.js` now has a fourth\n // relative import. Safe to serve: `dist/forwardKeys.js` imports only `./keys.js`, already here.\n //\n // `httpstatic.test.ts`'s graph-closure case DOES catch its absence (`imported but not served:\n // /forwardKeys.js`) -- verified by removing this line, not assumed. What it cost anyway is a\n // lesson about SCOPE: the task that added the import ran only `vitest run packages/canvas`, where\n // the build was clean, 171/171 passed and `module-cycles` and `renderer-imports` BOTH passed,\n // because the guard that matters lives in another package. `browser-clicks.mjs` then went from\n // 9/9 plus 8/8 to ZERO keypad buttons and 1/8 transfer checks, since `renderer.js` 404'd on its\n // own import and never evaluated -- and every failing check named something else\n // (`showTransfer`, `onTransfer`, a deleted `oncancel`). A RENDERER IMPORT IS A WEB-GATEWAY\n // CHANGE: run the whole suite, not this package's.\n 'forwardKeys.js',\n // `copyCache.js` joins 2026-11-02, the renderer's fifth relative import and, like `forwardKeys.js`,\n // inside its graph: the prefetched selection text `renderer.ts` hands a `copy` event. Safe to\n // serve: its only import is `import type { CellRect }`, which erases. The same guard catches its\n // absence -- `httpstatic.test.ts`'s graph closure, in `web` -- verified by removing this line.\n 'copyCache.js',\n]);\n\n/**\n * The two files `readAtlas` reads, by name. EXPORTED so the GUI bundle (`gui/scripts/bundle.mjs`)\n * copies and verifies exactly these rather than a retyped list: a third atlas file added here\n * would otherwise be read by the app and missing from the bundle, which the bundle's self-check\n * then could not notice -- a reviewer reproduced a bundle without `atlas.bin` exiting 0 before\n * this constant existed. `build-atlas.mjs` writes them; it is a build-time script and does not\n * import this compiled module, so it keeps its own spelling.\n */\nexport const ATLAS_FILES = Object.freeze({ geometry: 'atlas.json', coverage: 'atlas.bin' } as const);\n\n/**\n * Read the baked atlas. Throws NAMING THE BUILD when the package was not built.\n *\n * ## A SCOPED BUILD OF A CONSUMER LEAVES THE ATLAS UNBUILT\n *\n * MEASURED 2026-09-15, from a clean tree: `npm run build -w @tn3270/gui` EXITS 0 and fills\n * `packages/canvas/dist` with compiled `.js` -- and no `atlas.json` or `atlas.bin`. Project\n * references chain TYPESCRIPT COMPILATION and nothing else: `tsc --build` follows\n * `gui`'s reference to this package and compiles it, but it has no idea this package's own\n * `build` script has a SECOND step (`node scripts/build-atlas.mjs`) that bakes the atlas.\n * Only `npm run build` at the root, or `-w @tn3270/canvas`, runs that step.\n *\n * `gui` used to carry `&& node scripts/build-atlas.mjs` in its own build script, so a scoped\n * gui build was self-sufficient; extracting this package moved that step behind a reference\n * that cannot pull it. Hence the message below names the command rather than letting a bare\n * `ENOENT ... dist/atlas.json` be the whole diagnosis -- that failure surfaced as an\n * unhandled rejection and a BLANK WINDOW that never quit, which is the shape this repo keeps\n * writing comments to prevent.\n *\n * The original error is kept as `cause` so a developer can still see which file was missing.\n *\n * ## `dir`, AND WHY THE DEFAULT IS NOT ENOUGH FOR A BUNDLE\n *\n * Defaults to `assetDir()`, which is right for every consumer that imports this package as a\n * package. The GUI's esbuild bundle (`packages/gui/app/`) is not one: there `assets.ts` is\n * inlined into `app/main.js`, so `here` becomes `app/` -- which COINCIDES with where the bundle\n * copies the atlas, and a coincidence is not a mechanism. The GUI passes its layout's atlas\n * directory explicitly (`gui/src/appRoot.ts` has the full reasoning).\n *\n * THE MESSAGE NAMES THE DIRECTORY IT READ, and suggests rebuilding canvas ONLY when that directory\n * is canvas's own `dist` IN A SOURCE TREE (`isCanvasSourceDist`). In a bundled or installed app\n * there is no canvas package to rebuild, so that hint would send the user after a command that\n * cannot exist on their machine; there the truthful message is \"this install is incomplete\".\n */\n/**\n * Whether `dir` is `@tn3270/canvas`'s own `dist` in a SOURCE CHECKOUT, the one place where\n * \"rebuild canvas\" is a command that exists: named `dist`, beside a `src/`, under a `package.json`\n * named `@tn3270/canvas`.\n *\n * DECIDED BY THE DIRECTORY, NOT BY `dir === here`, which is how this used to read and is wrong in\n * the bundle: esbuild inlines this file into `gui/app/main.js`, so `here` IS `app/`, the GUI passes\n * `app/` as `dir`, and an installed user with a missing atlas was told to rebuild a package they do\n * not have -- reproduced by review. The `src/` test is what tells a checkout from an INSTALLED\n * canvas (`node_modules/@tn3270/canvas/dist`, as the web gateway's install has), which ships `dist`\n * alone and has nothing to rebuild either.\n */\nexport function isCanvasSourceDist(dir: string): boolean {\n if (basename(dir) !== 'dist') return false;\n const pkg = dirname(dir);\n if (!existsSync(join(pkg, 'src'))) return false;\n try {\n return (JSON.parse(readFileSync(join(pkg, 'package.json'), 'utf8')) as { name?: unknown }).name\n === '@tn3270/canvas';\n } catch {\n return false;\n }\n}\n\nexport function readAtlas(dir: string = here): { geometry: AtlasGeometry; coverage: Uint8Array } {\n try {\n const geometry = JSON.parse(\n readFileSync(join(dir, ATLAS_FILES.geometry), 'utf8')) as AtlasGeometry;\n const coverage = new Uint8Array(readFileSync(join(dir, ATLAS_FILES.coverage)));\n return { geometry, coverage };\n } catch (err) {\n if ((err as { code?: string }).code === 'ENOENT') {\n throw new Error(isCanvasSourceDist(dir)\n ? '@tn3270/canvas has not been built: run npm run build -w @tn3270/canvas'\n : `the glyph atlas is missing from ${dir}: this install is incomplete`,\n { cause: err });\n }\n throw err;\n }\n}\n", "/**\n * Rectangular selection geometry, pure and renderer-agnostic.\n *\n * ## WHY RECTANGULAR AND NOT LINEAR\n *\n * A 3270 panel is COLUMNAR -- datasets, LRECLs and option lists sit in columns, and the common\n * want is one column of a list without the labels either side of it. Linear selection on a fixed\n * grid also has a question with no good answer: whether to include the trailing spaces at the end\n * of each row. x3270 selects rectangularly by default on a 3270 screen.\n *\n * ## THIS FILE IS REACHED BY THE RENDERER, SO IT MUST STAY VALUE-IMPORT-FREE OF WORKSPACE PACKAGES\n *\n * `renderer.ts` imports `normalizeRect` and `movedPastThreshold`, and a browser cannot resolve a bare\n * specifier like `@tn3270/core` -- the window goes BLANK WITH NO ERROR, which\n * `renderer-imports.test.ts` exists to catch. The `ResolvedCell` import below is therefore an\n * `import type`, which erases at compile time; `extractText` itself runs in MAIN, never in the\n * renderer. Do not add a value import to this file.\n */\n\nimport type { ResolvedCell } from '@tn3270/core';\n\n/** A cell address in screen coordinates. Row and column are 0-based. */\nexport interface CellAddr {\n readonly row: number;\n readonly col: number;\n}\n\n/** An inclusive rectangle of cells. */\nexport interface CellRect {\n readonly top: number;\n readonly left: number;\n readonly bottom: number;\n readonly right: number;\n}\n\n/**\n * Two cell addresses into an inclusive rectangle.\n *\n * Normalizing is what makes all four drag directions behave identically: an up-left drag selects\n * exactly what the equivalent down-right drag does.\n */\nexport function normalizeRect(anchor: CellAddr, focus: CellAddr): CellRect {\n return {\n top: Math.min(anchor.row, focus.row),\n left: Math.min(anchor.col, focus.col),\n bottom: Math.max(anchor.row, focus.row),\n right: Math.max(anchor.col, focus.col),\n };\n}\n\n/**\n * How far, in CSS pixels on EITHER axis, the pointer must move from the press before a press is a\n * drag. Chromium's own drag threshold off macOS (`kDragThresholdX/Y` in Blink's\n * `mouse_event_manager.cc`: 4, and 3 on macOS), so a press here turns into a selection at about the\n * distance a press on ordinary page text starts one.\n */\nexport const DRAG_THRESHOLD_PX = 4;\n\n/**\n * Has the pointer moved far enough from the press to make it a DRAG rather than a click?\n *\n * ## CLICK OR DRAG IS DECIDED BY MOVEMENT, NOT BY THE RECTANGLE'S SIZE\n *\n * The rule this replaced, `isEmptyRect` (2026-10-05 to 2026-11-03), called every 1x1 rectangle a\n * click, so that a stray click -- including the clicks that place the cursor -- would not arm a\n * one-character copy. It did that, and it ALSO made a single character impossible to select: the\n * minimum selection was two cells, found by the user in Firefox and Safari on 2026-11-03. Movement\n * keeps the first property and drops the second: a click with ordinary jitter stays under the\n * threshold and selects nothing, and a drag that starts and ends on one character selects it.\n *\n * PER AXIS AND `>=`, exactly as Blink's `DragThresholdExceeded` measures it, rather than a\n * Euclidean distance: the point is to agree with what the browser itself calls a drag.\n *\n * The two points must be in the SAME coordinate space; `renderer.ts` uses `clientX`/`clientY` for\n * both, and says why.\n */\nexport function movedPastThreshold(\n press: { readonly x: number; readonly y: number },\n now: { readonly x: number; readonly y: number },\n): boolean {\n return Math.abs(now.x - press.x) >= DRAG_THRESHOLD_PX\n || Math.abs(now.y - press.y) >= DRAG_THRESHOLD_PX;\n}\n\n/**\n * Clipboard text for a rectangle, from the RESOLVED cells.\n *\n * ## WHY THIS TAKES `ResolvedCell[]` AND NOT A `DrawList`\n *\n * A draw list has no characters. `DrawCell` is `{x, y, glyph, ...}` and `glyph` is a CG-order atlas\n * column (`drawlist.ts:111-121`), so text cannot be recovered from it -- reversing those indices\n * would be a second, drifting copy of `cg.ts`'s mapping. `ResolvedCell` is where `text` lives, and\n * `main.ts` already computes it every frame for the draw list.\n *\n * ## `hidden` IS HONORED HERE AND THAT IS NOT OPTIONAL\n *\n * `ResolvedCell.text` is still the real character when `hidden` is set -- core leaves redaction to\n * the consumer precisely because only the consumer knows what to substitute. A hidden cell yields a\n * SPACE, the same substitution `drawlist.ts:114` makes when drawing. Omitting this check puts a\n * password on the clipboard.\n *\n * Trailing whitespace is trimmed PER LINE, which is what makes a copied column paste usefully;\n * interior spaces are kept, because they carry a panel's alignment.\n */\nexport function extractText(\n cells: readonly ResolvedCell[], cols: number, rect: CellRect,\n): string {\n const rows = Math.ceil(cells.length / cols);\n // CLAMPED, so a rectangle running off the grid yields short lines rather than reading\n // `undefined` out of the array and stringifying it.\n const top = Math.max(0, rect.top);\n const bottom = Math.min(rows - 1, rect.bottom);\n const left = Math.max(0, rect.left);\n const right = Math.min(cols - 1, rect.right);\n\n const lines: string[] = [];\n for (let row = top; row <= bottom; row++) {\n let line = '';\n for (let col = left; col <= right; col++) {\n const cell = cells[row * cols + col];\n if (cell === undefined) continue;\n line += cell.hidden ? ' ' : cell.text;\n }\n lines.push(line.replace(/\\s+$/, ''));\n }\n return lines.join('\\n');\n}\n", "import {\n resolveGeometry, resolveTerminalType, resolveAlternateSize, OversizeError,\n type GeometrySetting, type Oversize, type Session, type Size,\n} from '@tn3270/core';\nimport {\n defaultSession, resolveHostSpec, takeTlsFlag, takeOversizeFlag, resolveTls, resolveScheme, SCHEME_NAMES,\n OVERSIZE_USAGE, TLS_USAGE, type TlsFlags, type TlsOptions, type Scheme,\n} from '@tn3270/frontend';\n\n/**\n * Parse the GUI's argument vector.\n *\n * THE FLAGS ARE THE TUI'S, AND NOT BY COPYING THEM. Every rule here comes from\n * `@tn3270/frontend` -- `takeTlsFlag`, `resolveTls`, `resolveHostSpec` -- which is the\n * whole reason that package exists. Re-implementing them would be the third copy of one\n * rule, and the two defects fixed on 2026-08-28 were both exactly that.\n *\n * Deliberately NOT the same as the TUI in one respect: there is no `--colors`. A canvas has\n * no terminfo depth to detect; it draws 24-bit RGB straight from the scheme. `-scheme` IS\n * shared, because which palette to draw is not a terminal question.\n */\nexport class UsageError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'UsageError';\n }\n}\n\n/**\n * The ONE usage error that means \"no host was given\": the GUI was started bare, e.g.\n * double-clicked, and main opens the connect dialog instead of failing. A SUBCLASS, so every\n * existing `instanceof UsageError` still catches it and its message is still the usage line --\n * and so main tells it from every other usage error by type, never by matching text that changes\n * whenever a flag is added.\n *\n * Thrown only AFTER the flag checks above it in `parseGuiArgs` (oversize, TLS): a bad flag with no\n * host is that flag's error, not this one, and fails as it always has.\n */\nexport class MissingHostError extends UsageError {\n constructor() {\n super(USAGE);\n this.name = 'MissingHostError';\n }\n}\n\nexport interface GuiArgs {\n model?: string;\n terminalType?: string;\n /** The bare hostname: prefixes and the LU list are stripped by `resolveHostSpec`. */\n host?: string;\n /** Resolved and range-checked. Absent only when no host was given at all. */\n port?: number;\n /** LU names from the host argument, in the order written. Empty if none. */\n lus?: readonly string[];\n /** How the socket is made. Absent only before `resolveTls` has run. */\n tls?: TlsOptions;\n /** Offer TN3270E. Absent means the default, which is on. */\n tn3270e?: boolean;\n /** `-scheme`. Absent means the readable default. */\n scheme?: Scheme;\n /** Request BIND-IMAGE. Absent means the default, which is on. */\n bindImage?: boolean;\n /**\n * Advertise Query Reply (DDM), which lets a host choose DFT file transfer\n * instead of CUT. Absent means the default, which is OFF -- unlike the other\n * capability flags here. See `SessionOptions.ddm`.\n */\n ddm?: boolean;\n /** Range-check a BIND's geometry. Absent means the default, which is on. */\n bindLimit?: boolean;\n /**\n * Device-name template for NEW-ENVIRON's DEVNAME uservar. Absent means we refuse\n * telnet option 39 entirely -- see `SessionOptions.devname`.\n */\n devname?: string;\n /**\n * `-oversize <cols>x<rows>|auto|none`. Absent means `none`: the model's own alternate size.\n * `auto` is allowed, as in the TUI: the display is measured at every connect (`measure.ts`).\n * A combination `resolveGeometry` would refuse at connect is refused here instead.\n */\n oversize?: Oversize;\n}\n\nexport const USAGE =\n `usage: tn3270-gui [-model M] [--terminal-type T] [-scheme S] [-tn3270e on|off] `\n + `[-bind-image on|off] [-bind-limit on|off] [-ddm on|off] [-devname NAME] `\n + `${OVERSIZE_USAGE} ${TLS_USAGE} [prefix:][LU,LU@]host[:port]`;\n\nexport function parseGuiArgs(argv: readonly string[]): GuiArgs {\n const args: GuiArgs = {};\n const tlsFlags: TlsFlags = {};\n\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]!;\n const value = argv[i + 1];\n const eaten = takeTlsFlag(tlsFlags, flag, value, (m) => new UsageError(m));\n if (eaten !== undefined) {\n i += eaten;\n continue;\n }\n const oversizeEaten = takeOversizeFlag(args, flag, value, (m) => new UsageError(m));\n if (oversizeEaten !== undefined) {\n i += oversizeEaten;\n continue;\n }\n switch (flag) {\n case '-tn3270e': {\n if (value === undefined) throw new UsageError('-tn3270e needs a value, on or off');\n if (value !== 'on' && value !== 'off') {\n throw new UsageError(`-tn3270e takes on or off, not ${JSON.stringify(value)}`);\n }\n args.tn3270e = value === 'on';\n i++;\n break;\n }\n case '-model':\n if (value === undefined) throw new UsageError('-model needs a value, e.g. -model 3278-2-E');\n args.model = value;\n i++;\n break;\n case '-scheme': {\n if (value === undefined) {\n throw new UsageError(`-scheme needs a value: ${SCHEME_NAMES.join(', ')}`);\n }\n // resolveScheme throws RangeError listing the valid names; rethrow as a UsageError\n // so the front end reports it the same way it reports every other bad flag.\n try {\n args.scheme = resolveScheme(value);\n } catch (err) {\n throw new UsageError(err instanceof Error ? err.message : String(err));\n }\n i++;\n break;\n }\n case '--terminal-type':\n if (value === undefined) {\n throw new UsageError('--terminal-type needs a value, e.g. --terminal-type IBM-DYNAMIC');\n }\n args.terminalType = value;\n i++;\n break;\n case '-bind-image': {\n if (value === undefined) throw new UsageError('-bind-image needs a value, on or off');\n if (value !== 'on' && value !== 'off') {\n throw new UsageError(`-bind-image takes on or off, not ${JSON.stringify(value)}`);\n }\n args.bindImage = value === 'on';\n i++;\n break;\n }\n case '-ddm': {\n if (value === undefined) throw new UsageError('-ddm needs a value, on or off');\n if (value !== 'on' && value !== 'off') {\n throw new UsageError(`-ddm takes on or off, not ${JSON.stringify(value)}`);\n }\n args.ddm = value === 'on';\n i++;\n break;\n }\n case '-bind-limit': {\n if (value === undefined) throw new UsageError('-bind-limit needs a value, on or off');\n if (value !== 'on' && value !== 'off') {\n throw new UsageError(`-bind-limit takes on or off, not ${JSON.stringify(value)}`);\n }\n args.bindLimit = value === 'on';\n i++;\n break;\n }\n case '-devname':\n if (value === undefined) throw new UsageError('-devname needs a value, e.g. -devname foo===');\n args.devname = value;\n i++;\n break;\n default:\n if (flag.startsWith('-')) {\n throw new UsageError(`unrecognized argument ${JSON.stringify(flag)}`);\n }\n if (args.host !== undefined) {\n throw new UsageError(\n `more than one host given: ${JSON.stringify(args.host)} and ${JSON.stringify(flag)}`);\n }\n args.host = flag;\n break;\n }\n }\n\n // Refuse at PARSE time whatever `resolveGeometry` would refuse at connect -- the TUI's and the\n // CLI's check, one call, because those are exactly the checks it makes. The measure is a\n // STAND-IN reporting no window, so `auto` passes every rule it can break and then takes the\n // \"no window size known\" fallback without the display being read: there is no atlas yet, and\n // the real measure belongs to connect (spec, \"Why auto is measured at connect\").\n if (args.oversize !== undefined) {\n try {\n resolveGeometry({\n ...(args.model !== undefined ? { model: args.model } : {}),\n ...(args.terminalType !== undefined ? { terminalType: args.terminalType } : {}),\n oversize: args.oversize,\n }, () => undefined);\n } catch (e) {\n if (e instanceof OversizeError) throw new UsageError(e.message);\n throw e;\n }\n }\n args.tls = resolveTls(tlsFlags, (m) => new UsageError(m));\n\n if (args.host === undefined) throw new MissingHostError();\n\n const raw = args.host;\n const spec = resolveHostSpec(raw, (m) => new UsageError(m));\n args.host = spec.host;\n args.port = spec.port;\n args.lus = spec.lus;\n\n // Both the host and the flags are in argv here, so these contradictions are caught before\n // a socket is opened -- the same checks the TUI makes, for the same reasons.\n if (spec.tlsRequested && args.tls.kind === 'plaintext') {\n throw new UsageError(`${raw} asks for TLS with the L: prefix, but -insecure disables TLS`);\n }\n if (spec.tn3270e === false) {\n if (args.tn3270e === true) {\n throw new UsageError(\n `${raw} has the N: prefix, which means no TN3270E, but -tn3270e on asks for it. `\n + 'Drop one of them rather than leaving it ambiguous.');\n }\n args.tn3270e = false;\n }\n return args;\n}\n\n/**\n * Builds the session main.ts connects, from a parsed `GuiArgs` -- the CLI's and the TUI's\n * `buildSession`, and HERE rather than in main.ts for the reason theirs were extracted: main.ts\n * imports `electron`, which vitest cannot load, so a session built there could only be tested by\n * copying its expression -- the copy the CLI's review caught passing a hardcoded-model mutant.\n * This file already imports `@tn3270/frontend`, and `defaultSession` touches no socket until\n * `connect()`.\n *\n * Geometry mode unless `--terminal-type` was given (a raw string `resolveGeometry` cannot\n * interpret as a model). `measure` is called by the session AT EVERY CONNECT, never here, and is\n * handed the setting the session holds at that moment -- so a model changed between connects is\n * the one measured against. It is required, not defaulted: a GUI session without the display's\n * measure would make `auto` fall back silently, which is exactly the bug it would hide.\n */\nexport function buildSession(\n args: GuiArgs, measure: (setting: GeometrySetting | undefined) => Size | undefined,\n): Session {\n if (args.terminalType !== undefined) {\n // Built once and passed to both resolvers, so the terminal-type string and the geometry\n // can never come from different readings of the same arguments.\n const typeOpts = {\n ...(args.model !== undefined ? { model: args.model } : {}),\n terminalType: args.terminalType,\n };\n return defaultSession(\n resolveTerminalType(typeOpts), args.tls, resolveAlternateSize(typeOpts), args.tn3270e,\n args.bindImage, args.bindLimit, args.devname, args.ddm,\n );\n }\n // `session` is read only when connect() calls the measure, by which time it is assigned.\n const session: Session = defaultSession(\n undefined, args.tls, undefined, args.tn3270e, args.bindImage, args.bindLimit,\n args.devname, args.ddm,\n {\n setting: {\n ...(args.model !== undefined ? { model: args.model } : {}),\n oversize: args.oversize ?? { kind: 'none' },\n },\n measure: () => measure(session.geometrySetting),\n },\n );\n return session;\n}\n\n/**\n * Every `lastGeometryNotice` -- an `auto` that fell back to the model -- on stderr, the first\n * connect's and every reconnect's, as the spec's \"Data flow on a connect\" item 2 asks. Written\n * AT ONCE, unlike the TUI's: the GUI's stderr is a terminal or a log, not the alternate screen\n * the TUI has to wait to leave. Nothing on the canvas: the GUI's only on-canvas painter is the\n * red error painter, which the next frame wipes.\n *\n * On the 'geometry' event, which `connect()` emits after resolving and before any socket I/O, so\n * one listener registered before the first connect covers that connect too.\n */\nexport function printGeometryNotices(session: Session, stderr: { write(text: string): unknown }): void {\n session.on('geometry', () => {\n const notice = session.lastGeometryNotice;\n if (notice !== undefined) stderr.write(`${notice}\\n`);\n });\n}\n", "/**\n * The connect dialog's form, turned into the argv the user would have typed.\n *\n * ONE PARSER: the result goes through `parseGuiArgs` exactly as a real command line does, so\n * the dialog cannot accept a host the command line refuses or refuse one it accepts. This\n * function validates NOTHING -- an empty host becomes no host argument, and `parseGuiArgs`\n * says what is wrong in the words it always uses. See\n * docs/superpowers/specs/2026-11-05-packaging-design.md section 2.\n */\nexport interface ConnectForm {\n readonly host: string;\n readonly model: string;\n readonly tls: boolean;\n}\n\n/**\n * Flags the form owns outright. A copy on the real command line is dropped so the form's own\n * choice -- the model dropdown, the TLS checkbox -- is the one `parseGuiArgs` sees, never a\n * stale value left over from how the GUI happened to be launched.\n *\n * `-insecure` is the ONLY TLS flag the form owns. `-cafile`, `-noverifycert` and `-verifycert`\n * are not: the dialog has no field for them, so a command line that set one is left alone, and\n * stays exactly as liable to the parser's own TLS contradictions (e.g. `-cafile` plus the form's\n * TLS checkbox unticked, which becomes `-insecure` alongside `-cafile` -- `resolveTls`'s\n * contradiction, not a second one invented here).\n */\nconst OWNED_WITH_VALUE = new Set(['-model']);\nconst OWNED_BARE = new Set(['-insecure']);\n\n/**\n * Drops the command line's copies of the flags the form owns.\n *\n * Walks `commandLine` left to right rather than searching for `-model` and splicing: a flag's\n * value is identified by POSITION (the token right after it), the same way `parseGuiArgs`\n * itself reads argv, so this can never mistake an unrelated token for one flag's value or vice\n * versa.\n *\n * A `-model` with no value cannot reach here: `parseGuiArgs` throws \"-model needs a value\" before\n * it looks for a host, so that command line fails at startup and the dialog never opens.\n */\nfunction dropOwned(commandLine: readonly string[]): string[] {\n const kept: string[] = [];\n for (let i = 0; i < commandLine.length; i++) {\n const flag = commandLine[i]!;\n if (OWNED_BARE.has(flag)) continue;\n if (OWNED_WITH_VALUE.has(flag)) {\n i++; // also drop the value that goes with it\n continue;\n }\n kept.push(flag);\n }\n return kept;\n}\n\nexport function dialogToArgv(form: ConnectForm, commandLine: readonly string[]): string[] {\n const kept = dropOwned(commandLine);\n const host = form.host.trim();\n return [\n ...kept,\n ...(form.tls ? [] : ['-insecure']),\n '-model', form.model,\n ...(host === '' ? [] : [host]),\n ];\n}\n", "import { KNOWN_MODELS, resolveTerminalType, TerminalTypeError } from '@tn3270/core';\nimport { MissingHostError, parseGuiArgs, UsageError, type GuiArgs } from './args.js';\nimport { dialogToArgv, type ConnectForm } from './connectForm.js';\n\n/**\n * The decidable half of the startup Connect dialog: when it opens, what a submit means, and what\n * its test seam says. Spec docs/superpowers/specs/2026-11-05-packaging-design.md section 2.\n *\n * HERE AND NOT IN main.ts for the reason `buildSession` is in args.ts: main.ts imports `electron`,\n * which vitest cannot load, so a decision left there could only be tested by copying it.\n */\n\n/** The model the dialog's dropdown starts on: the GUI's own flagless default. */\nexport const CONNECT_DEFAULT_MODEL = '3278-2-E';\n\n/** What the dialog's dropdown lists, sent to the renderer by main -- the page imports no core. */\nexport function connectModels(): { readonly models: readonly string[]; readonly default: string } {\n return { models: Object.keys(KNOWN_MODELS), default: CONNECT_DEFAULT_MODEL };\n}\n\n/** What startup does with the command line it was given. */\nexport type StartupPlan =\n | { readonly kind: 'session'; readonly args: GuiArgs }\n | { readonly kind: 'dialog' }\n | { readonly kind: 'fail'; readonly err: unknown };\n\n/**\n * Session, dialog or failure, from the command line and the seams that stand in for a host.\n *\n * THE DIALOG OPENS ONLY FOR `MissingHostError`, never for a usage error in general: a bad flag with\n * no host is that flag's error, as it always was, and `parseGuiArgs` checks oversize and TLS BEFORE\n * the host so those are bad-flag errors too.\n *\n * TWO SEAMS KEEP THE OLD FAILURE, because a dialog nobody can answer under a harness is a HANG,\n * not a failure:\n * - `TN3270_GUI_REPLAY`: a replay never dials, so a host it was not given is still the usage\n * error it always was.\n * - `TN3270_GUI_SHOT` WITHOUT `TN3270_GUI_CONNECT`: a87a3fd made every startup failure under the\n * shot seam a fast non-zero exit, and a bare launch under it is one of those. With the connect\n * seam set the dialog WILL be answered, so it opens.\n * `TN3270_GUI_URL` never gets here: main returns before argv is read in URL mode.\n *\n * `parse` is injectable only so a test can make it throw something that is not a usage error.\n */\nexport function planStartup(\n argv: readonly string[],\n seams: { readonly replay: string; readonly shot: string; readonly connect: string },\n parse: (argv: readonly string[]) => GuiArgs = parseGuiArgs,\n): StartupPlan {\n try {\n return { kind: 'session', args: parse(argv) };\n } catch (err) {\n const unanswerable = seams.replay !== '' || (seams.shot !== '' && seams.connect === '');\n if (err instanceof MissingHostError && !unanswerable) return { kind: 'dialog' };\n return { kind: 'fail', err };\n }\n}\n\n/** The answer to one press of Connect. */\nexport type SubmitResult =\n | { readonly ok: true; readonly args: GuiArgs }\n | { readonly ok: false; readonly error: string };\n\n/** The form's shape, checked: it arrived over IPC, so nothing about it is a type yet. */\nfunction isConnectForm(form: unknown): form is ConnectForm {\n if (typeof form !== 'object' || form === null) return false;\n const f = form as Record<string, unknown>;\n return typeof f['host'] === 'string' && typeof f['model'] === 'string' && typeof f['tls'] === 'boolean';\n}\n\n/**\n * One Connect press: the form, as the argv the user would have typed, through the real parser.\n *\n * A `UsageError` -- an empty host's `MissingHostError` included -- is an ANSWER, shown in the\n * dialog, which stays open. Anything else is rethrown: it is a bug, and main's handler turning it\n * into a rejected `invoke` is the visible failure it deserves.\n *\n * THE MODEL IS CHECKED HERE TOO, by core's own `resolveTerminalType`, because `parseGuiArgs` takes\n * any string for `-model` and leaves it to `buildSession` -- which runs AFTER the dialog has closed,\n * so a bad model would land in the terminal window with no dialog left to fix it in. The dropdown\n * offers only known models, so this is reached only by a page that sent something it never listed.\n */\nexport function submitConnect(\n form: unknown,\n commandLine: readonly string[],\n parse: (argv: readonly string[]) => GuiArgs = parseGuiArgs,\n): SubmitResult {\n if (!isConnectForm(form)) {\n return { ok: false, error: `malformed connect form ${JSON.stringify(form) ?? String(form)}` };\n }\n try {\n resolveTerminalType({ model: form.model });\n return { ok: true, args: parse(dialogToArgv(form, commandLine)) };\n } catch (err) {\n if (err instanceof UsageError || err instanceof TerminalTypeError) return { ok: false, error: err.message };\n throw err;\n }\n}\n\n/**\n * `TN3270_GUI_CONNECT`, the dialog's test seam: `{\"host\": ..., \"model\": ..., \"tls\": ...}`, which\n * main types into the dialog's own controls. EMPTY MEANS ABSENT, as for every seam in main.ts.\n *\n * Refused by name when malformed, rather than driving a half-filled form: a typo would otherwise\n * read as a dialog that ignores its input.\n */\nexport function parseConnectSeam(raw: string): ConnectForm | undefined {\n if (raw === '') return undefined;\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n value = undefined;\n }\n if (!isConnectForm(value)) {\n throw new Error(`TN3270_GUI_CONNECT must be JSON {\"host\": string, \"model\": string, \"tls\": boolean}, `\n + `not ${JSON.stringify(raw)}`);\n }\n return { host: value.host, model: value.model, tls: value.tls };\n}\n", "import { bestScale, type Surface } from '@tn3270/canvas';\nimport { resolveGeometry, type GeometrySetting, type Size } from '@tn3270/core';\n\n/**\n * The part of the display the window may use: 80% of the work area each way, rounded down.\n * `fit()` in main.ts and `measureDisplay` below both use THIS, so they cannot disagree.\n */\nexport function windowArea(area: { readonly width: number; readonly height: number }): Surface {\n return { width: Math.floor(area.width * 0.8), height: Math.floor(area.height * 0.8) };\n}\n\n/**\n * The scale the GUI draws `list` at on a display with this work area: the largest integer multiple\n * that fits `windowArea`, never below 1 (`bestScale`, canvas/src/blit.ts). THIS IS `fit()`'s RULE,\n * moved here from main.ts so it can be tested -- main.ts imports `electron`, which vitest cannot\n * load -- and so `measureDisplay` uses the rule itself rather than a copy of it.\n */\nexport function windowScale(list: Surface, area: { readonly width: number; readonly height: number }): number {\n return bestScale(list, windowArea(area));\n}\n\n/**\n * `-oversize auto` in the GUI: how many rows and columns the display can hold \"at the scale the\n * window would pick anyway\" (the spec's \"What each front end measures\", the user's choice over\n * scale 1 and over refusing `auto` in the GUI).\n *\n * PURE, AND OUT OF main.ts FOR THAT REASON: main.ts imports `electron`, which vitest (node\n * environment, no Electron) cannot load, so arithmetic left there would be untested. main.ts\n * supplies the two things only Electron knows -- the work area and the atlas cell -- at call time.\n *\n * \"The scale the window would pick\" is `windowScale`, the function `fit()` itself calls:\n * - of the MODEL's draw list -- `model.cols` cells wide and `model.rows + 1` high, the +1 being\n * the OIA row `drawList` adds whenever there is OIA text (canvas/src/drawlist.ts), which is\n * always: `Oia.toText()` returns at least the connection state, connected or not;\n * - the answer is every whole cell that fits `windowArea` at that scale, less the OIA's row.\n *\n * `fit()` sizes the window from the draw list it is about to send, which for an oversize screen is\n * the OVERSIZE list -- bigger than the model's. `windowScale` nonetheless gives it THE SAME scale,\n * so the screen is drawn at the scale the user chose: at scale `s` the oversize list fits\n * `windowArea` by construction (each dimension was cut to it), so the scale is at least `s`; and\n * when the result is at least the model in both dimensions (anything smaller falls back to the\n * model in `resolveGeometry`) the list is at least the model's, so it is at most `s`. The 14-bit\n * clamp cuts rows only, keeping the width, and the width alone still pins it to `s`.\n * `measure.test.ts` sweeps displays and models through `windowScale` to check it.\n */\nexport function measureDisplay(\n area: { readonly width: number; readonly height: number },\n cell: { readonly width: number; readonly height: number },\n model: Size,\n): Size | undefined {\n const within = windowArea(area);\n const scale = windowScale(\n { width: model.cols * cell.width, height: (model.rows + 1) * cell.height }, area);\n const cols = Math.floor(within.width / (cell.width * scale));\n const rows = Math.floor(within.height / (cell.height * scale)) - 1;\n // No room for a row plus the OIA, or a cell with no size: no measurement, which geometry.ts\n // turns into its \"no window size known\" fallback.\n if (!(rows > 0 && cols > 0 && Number.isFinite(rows) && Number.isFinite(cols))) return undefined;\n return { rows, cols };\n}\n\n/**\n * The session's `auto` measure, given what main.ts can read when `connect()` calls it.\n *\n * The MODEL is the setting's at call time -- `resolveGeometry` with the oversize dropped, which is\n * the model's own alternate -- so a model changed between connects (step 2's switching) is the one\n * measured against. Any input missing (no atlas read, no display, a legacy-mode session) is no\n * measurement rather than a throw: a throw here would fail the connect, and the spec's answer for\n * a window that cannot be measured is to fall back to the model and say so.\n */\nexport function measureAuto(\n setting: GeometrySetting | undefined,\n area: { readonly width: number; readonly height: number } | undefined,\n cell: { readonly width: number; readonly height: number } | undefined,\n): Size | undefined {\n if (setting === undefined || area === undefined || cell === undefined) return undefined;\n const model = resolveGeometry({ ...setting, oversize: { kind: 'none' } }).alternate;\n return measureDisplay(area, cell, model);\n}\n", "/**\n * One `TN3270_GUI_KEYS` spelling to an Electron `sendInputEvent` chord.\n *\n * ## WHY THIS REFUSES INSTEAD OF TRANSLATING\n *\n * MEASURED on Electron 44.3.0, 2026-09-15 (`package.json` asks for `^44.0.0`; 44.3.0 is what\n * this tree resolves to and what every run here used): `sendInputEvent` accepts Accelerator names\n * (`1`, `Up`, `F1`, `]`), and a DOM CODE NAME is not rejected -- it is delivered as an\n * EMPTY event, `key: ''` and `keyCode: 0`. `actionForKey` then returns null, no action is\n * sent, and the harness still reports that it sent the key. A guard written with the wrong\n * spelling therefore passes while proving nothing.\n *\n * That spelling is the natural mistake here, because the GUI's own keymap is written in\n * terms of `e.code` and `e.key`: `Digit1` is exactly what the PA table matches on. So this\n * throws and names the valid form. It does NOT silently map `Digit1` to `1`, because that\n * would teach a spelling only our harness accepts.\n *\n * Modifier names are the ones a person writes; Electron wants lowercase `control`, `alt`,\n * `shift`, `meta`.\n */\n\n/** Electron's modifier names, which are not the names anybody types. */\nexport type ChordModifier = 'control' | 'alt' | 'shift' | 'meta';\n\nexport interface KeySpec {\n /** Passed to `sendInputEvent` verbatim: an Electron Accelerator key name. */\n readonly keyCode: string;\n readonly modifiers: readonly ChordModifier[];\n}\n\n// A Map, not a plain object: a frozen plain object still inherits Object.prototype, so a\n// bare index lookup on a segment like 'constructor' or '__proto__' returns a function or\n// object instead of undefined -- truthy, not a ChordModifier, and invisible to tsc.\nconst MODIFIER_ALIASES: ReadonlyMap<string, ChordModifier> = new Map([\n ['ctrl', 'control'], ['control', 'control'],\n ['alt', 'alt'], ['option', 'alt'],\n ['shift', 'shift'],\n ['meta', 'meta'], ['cmd', 'meta'], ['command', 'meta'], ['super', 'meta'],\n]);\n\n/** The families of DOM code names, all of which arrive empty. */\nconst DOM_CODE = /^(Digit|Key|Numpad|Arrow)/;\n\n/** The valid spelling for the DOM names somebody is most likely to write. */\nfunction validSpelling(key: string): string | undefined {\n const arrow = /^Arrow(Up|Down|Left|Right)$/.exec(key);\n if (arrow !== null) return arrow[1];\n const digit = /^Digit(\\d)$/.exec(key);\n if (digit !== null) return digit[1];\n const letter = /^Key([A-Za-z])$/.exec(key);\n if (letter !== null) return letter[1]!.toLowerCase();\n return undefined;\n}\n\nexport function parseKeySpec(spec: string): KeySpec {\n const trimmed = spec.trim();\n if (trimmed === '') throw new Error('empty key spec');\n\n const segments = trimmed.split('+');\n const modifiers: ChordModifier[] = [];\n let i = 0;\n // Consumed GREEDILY FROM THE FRONT, with the remainder rejoined as the key, so that '+'\n // and 'Ctrl++' still name a literal plus. A `pop()` would turn both into an empty key,\n // and an empty key is an empty event.\n while (i < segments.length - 1) {\n const alias = MODIFIER_ALIASES.get(segments[i]!.toLowerCase());\n if (alias === undefined) break;\n if (!modifiers.includes(alias)) modifiers.push(alias);\n i += 1;\n }\n\n const rest = segments.slice(i);\n const key = rest.join('+');\n if (key === '') {\n throw new Error(`key spec '${spec}' names modifiers but no key`);\n }\n // More than one leftover segment is only legitimate when the key IS a plus, which arrives\n // as empty segments. Anything else -- 'Ctl+1' -- is a misspelled modifier, and passing it\n // through verbatim is how it would reach Chromium as an empty event.\n if (rest.length > 1 && !rest.every((s) => s === '')) {\n throw new Error(\n `unknown modifier in key spec '${spec}': use Ctrl, Alt, Shift or Meta`,\n );\n }\n\n if (DOM_CODE.test(key)) {\n const valid = validSpelling(key);\n throw new Error(\n `key spec '${spec}' uses the DOM code name '${key}', which sendInputEvent delivers ` +\n `as an empty event rather than refusing` +\n (valid !== undefined ? `; use '${valid}'` : ''),\n );\n }\n\n return { keyCode: key, modifiers };\n}\n", "/**\n * The application menu. THE FIRST ONE THIS APP HAS HAD -- `setApplicationMenu` was never called\n * before copy/paste needed somewhere to live.\n *\n * ## DELIBERATELY NOT THE FINAL MENU STRUCTURE\n *\n * Edit, View and (geometry sessions only) Model. A connect dialog is roadmapped and will\n * want menus too; guessing their shape now would be a second copy of a decision nobody has taken.\n *\n * ## THE TEMPLATE IS A PURE FUNCTION OF THE PLATFORM, WHICH IS WHY IT IS TESTABLE\n *\n * `process.platform` is read by the CALLER and passed in. A module that read it directly could only\n * be tested on the machine it runs on, and the whole point of the split below is that both halves\n * are asserted on every machine.\n *\n * ## IT IMPORTS NOTHING FROM `electron`, ALSO DELIBERATELY\n *\n * A plain data template, so vitest can call it with no Electron runtime at all. `main.ts` passes\n * the result to `Menu.buildFromTemplate`. That is the same split the transfer window used to stay\n * testable. `@tn3270/core` IS imported, by the Model menu below: it is plain data and pure\n * functions, and the model list and the size wording must come from the one place a connect\n * reads them, not from a copy here. `@tn3270/frontend` only for a type.\n */\n\nimport {\n KNOWN_MODELS, describeSize, resolveTerminalType,\n type GeometrySetting, type Oversize, type Size,\n} from '@tn3270/core';\nimport type { GeometryChoice } from '@tn3270/frontend';\n\n/** The platform strings this file distinguishes. `process.platform`'s type. */\nexport type Platform = 'darwin' | 'linux' | 'win32' | string;\n\n/**\n * The menu roles this file uses.\n *\n * A LITERAL UNION AND NOT `string`, which is what lets `main.ts` pass the template to\n * `Menu.buildFromTemplate` with no cast at all: Electron's own `role` is a union of its 40-odd\n * role names, and `string` does not overlap with it -- TypeScript rejects even an `as` between\n * them (\"neither type sufficiently overlaps\"), which would have left a `as unknown as` double\n * cast in main. Naming only the role we actually use keeps that honest AND documents the\n * dependency; adding a role here is a one-word change.\n */\nexport type MenuRole = 'appMenu';\n\nexport interface MenuItemTemplate {\n readonly label?: string;\n readonly role?: MenuRole;\n readonly accelerator?: string;\n readonly id?: string;\n /** A literal union, for the reason `MenuRole` gives. Absent is Electron's `normal`. */\n readonly type?: 'radio' | 'separator';\n readonly checked?: boolean;\n readonly enabled?: boolean;\n readonly click?: () => void;\n /**\n * MUTABLE, UNLIKE EVERY FIELD BESIDE IT, and that asymmetry is deliberate rather than an\n * oversight to tidy. Electron's `MenuItemConstructorOptions.submenu` is a MUTABLE array, and a\n * `readonly` one is not assignable to it -- so a `readonly` here would force a double cast in\n * `main.ts` and switch off checking on the whole template to paper over one nested field.\n * Electron is entitled to sort or splice what it is given; `buildMenuTemplate` hands out a fresh\n * array on every call, so there is nothing shared for a caller to corrupt.\n */\n readonly submenu?: MenuItemTemplate[];\n}\n\n/**\n * Copy and paste accelerators.\n *\n * ## `Ctrl-C` STAYS CLEAR ON EVERY PLATFORM\n *\n * `canvas/src/keys.ts:72` binds it to `{kind:'clear'}` and the window says so at startup. Clear is\n * an AID needed constantly to dismiss VM's `MORE...` state. On macOS `Cmd` is free, so the native\n * key is available with no conflict; elsewhere `Ctrl-Shift-C` is the terminal-emulator convention,\n * adopted by gnome-terminal and VS Code's terminal for this very collision.\n *\n * REJECTED, and recorded so it is not revisited: \"Ctrl-C copies when a selection exists, Clear when\n * it does not.\" That makes a DESTRUCTIVE AID conditional on invisible state -- the\n * one-path-does-X-and-another-doesn't shape this project has been bitten by at least five times --\n * and its failure mode is a missed Clear on a locked `MORE...` screen.\n *\n * ## THIS ACCELERATOR NOW SHARES ITS CHORD WITH A PAGE BINDING, AND THE OVERLAP IS UNMEASURED\n *\n * 2026-10-27, with the web copy gesture: `canvas/src/keys.ts` claims `Ctrl-Shift-C` in the keydown\n * listener, and on Linux/Windows that is this accelerator's chord. Both routes end at\n * `__tn3270Copy`, so a double fire would copy the same rect to the same clipboard -- idempotent,\n * but it would print `copy: N chars` TWICE, and `select.mjs` asserts on that line.\n *\n * **WHETHER THEY BOTH FIRE IS NOT KNOWN, AND THE HARNESSES HERE CANNOT ANSWER IT.** Measured\n * 2026-10-27: `webContents.sendInputEvent` -- the only input route any harness in this repo has,\n * all 20 call sites in `main.ts` -- **does not trigger menu accelerators at all.** The control run\n * that proves it: with a real `Ctrl+Shift+C` accelerator installed and the page's own keydown\n * listener disabled, the click handler still never fired, so nothing reached the menu. A probe\n * built on that seam therefore reports \"no double fire\" on every build, whether one exists or not,\n * which is worse than no probe. `xdotool`/`xte` are not on this box, so there is no real-key\n * injector to settle it.\n *\n * The analogous `Ctrl+Shift+V` case WAS measured to fire both -- `forwardPaste`'s docstring in\n * `canvas/src/forwardKeys.ts` is the single home for that measurement, cited here rather than\n * restated for the reason `pasteAccelerator` below spells out. That run predates this finding\n * about `sendInputEvent`, so treat a double fire as the LIKELY case rather than the disproven one. **It needs a human on a real desktop**, which the feature's\n * live pass already owes. If it doubles, the fix is to gate the page branch off `file:` the way\n * `forwardPaste` gates its listener, leaving this accelerator to own the chord in Electron.\n */\nexport function copyAccelerator(platform: Platform): string {\n return platform === 'darwin' ? 'Cmd+C' : 'Ctrl+Shift+C';\n}\n\n/**\n * ## THIS ACCELERATOR IS THE GUI'S ONLY PASTE, AND THAT IS NOW LOAD-BEARING. 2026-10-26.\n *\n * `canvas/src/forwardKeys.ts`'s `forwardPaste` raises `{kind:'paste'}` from a real `paste` EVENT,\n * and `renderer.ts` installs it -- but it refuses to install on `file:`, which is what this app's\n * own window is. So the menu item below is the whole of this front end's paste.\n *\n * SO DO NOT \"FIX\" THE GUI'S MISSING `Ctrl+V` BY ADDING A `paste` ARM AND WIDENING THAT GATE. This\n * accelerator and the page's `paste` event OVERLAP rather than compose, so an arm plus a widened\n * gate is a DOUBLE PASTE into a 3270 field -- where a silently doubled dataset name can be a\n * valid different one, the failure `frontend/src/pasteAction.ts` exists to prevent.\n *\n * **`forwardPaste`'S DOCSTRING IS THE SINGLE HOME FOR THE MEASUREMENT** behind both of those\n * claims, and the Electron version it was taken on. Cited rather than restated ON PURPOSE: the\n * numbers were briefly copied into this comment and two others, which put one measurement in five\n * files and would have let an Electron upgrade falsify three of them at once -- the shape\n * `d4fce66` exists to stop. What belongs HERE is the instruction, not the evidence.\n *\n * A native `Ctrl+V` here is a real improvement and is deliberately NOT taken: it needs the\n * accelerator and the event reconciled (one of them has to stop firing), which is a decision\n * about this menu rather than a handler somebody can add.\n */\nexport function pasteAccelerator(platform: Platform): string {\n return platform === 'darwin' ? 'Cmd+V' : 'Ctrl+Shift+V';\n}\n\nexport interface MenuHandlers {\n readonly onCopy: () => void;\n readonly onPaste: () => void;\n /** Open the keypad window. OPEN, not toggle -- see the View menu below. */\n readonly onKeypad: () => void;\n /** Open the full-keyboard window. OPEN, not toggle, for the same reason. */\n readonly onKeyboard: () => void;\n /**\n * One Model-menu choice. Optional, unlike the four above, because only a geometry-mode session\n * has a Model menu; the other handlers predate it and a caller without it is still complete.\n */\n readonly onGeometry?: (choice: GeometryChoice) => void;\n /**\n * The Model menu's Disconnect item, 2026-11-05: `session.disconnect()`. Optional, as\n * `onGeometry` is, because only a geometry-mode session has a Model menu -- so a\n * `--terminal-type` session has no Disconnect item either, which is accepted: it has no choices\n * that a disconnect would free.\n */\n readonly onDisconnect?: () => void;\n}\n\n/**\n * What one Model-menu item asks for. The type and the rule that applies it\n * (`applyGeometryChoice`) live in `@tn3270/frontend`, so the web gateway's list uses the same one.\n */\nexport type { GeometryChoice };\n\n/**\n * What the Model menu is built from. `main.ts` reads it off the session each time.\n *\n * `startupFixed` is the `-oversize <cols>x<rows>` given at startup, if any: the spec's choices are\n * PICKED FROM LISTS, NOTHING TYPED (the user's decision, 2026-11-04), so the only fixed size the\n * menu can offer is the one already typed on the command line. A new size is `-oversize` or the\n * CLI's `Set(oversize,...)`.\n */\nexport interface GeometryMenu {\n readonly models: readonly string[];\n readonly current: GeometrySetting;\n readonly startupFixed?: Size;\n /**\n * Every choice is disabled while true. CONNECTED OR CONNECTING: `setGeometry` refuses both, and\n * an attempt in flight has already resolved the setting it will advertise.\n */\n readonly connected: boolean;\n /**\n * CONNECTING, NOT YET CONNECTED -- `isConnecting() && !isConnected()`, the web gateway's rule\n * (`web/src/geometryChoices.ts` says why the `!isConnected()` is needed on 'connect' itself).\n * Read only by the Disconnect item: `Session.disconnect()` does nothing to an attempt in\n * flight (a known core limitation), so the item is disabled then. Absent is false.\n */\n readonly connecting?: boolean;\n}\n\n/** The Disconnect item's id: NOT `model:...`, which names a model radio. */\nexport const DISCONNECT_ID = 'model-disconnect';\n\n/** One choice item's mutable state. `main.ts` applies these by id; see `modelMenuStates`. */\nexport interface ChoiceState {\n readonly id: string;\n /** ABSENT on the Disconnect item, a normal item with nothing to check -- see `applyChoiceStates`. */\n readonly checked?: boolean;\n readonly enabled: boolean;\n}\n\n/** The model a setting resolves to, as a `KNOWN_MODELS` key: `IBM-` stripped, core's casing. */\nfunction currentModel(setting: GeometrySetting): string | undefined {\n try {\n // Core's own resolution, so an absent model is the product default (TERMINAL_TYPE, today\n // 3278-2-E) and `ibm-3278-5-e` is 3278-5-E -- exactly what a connect would advertise.\n return resolveTerminalType({ ...(setting.model !== undefined ? { model: setting.model } : {}) })\n .replace(/^IBM-/, '');\n } catch {\n return undefined; // a model core refuses checks nothing rather than guessing one\n }\n}\n\n/** Whether a fixed oversize is the startup one: only then is that item honestly checked. */\nfunction isStartupFixed(o: Oversize, startup: Size | undefined): boolean {\n return o.kind === 'fixed' && startup !== undefined\n && o.rows === startup.rows && o.cols === startup.cols;\n}\n\n/**\n * Every choice item's `checked` and `enabled`, models first, then the oversize items, in menu\n * order.\n *\n * THE ONE SOURCE OF BOTH THE TEMPLATE AND `main.ts`'S UPDATES. After a click or a connect,\n * `main.ts` MUTATES the built items by id rather than rebuilding the application menu (a rebuild\n * re-creates the macOS app menu and every accelerator to change two booleans) -- and it must,\n * after EVERY click, refused or not: Electron checks a clicked radio item itself, before the\n * handler runs, so a refused choice would otherwise be left showing as chosen.\n */\nexport function modelMenuStates(g: GeometryMenu): ChoiceState[] {\n const enabled = !g.connected;\n const model = currentModel(g.current);\n const o = g.current.oversize;\n return [\n // FIRST, as in the menu. Enabled only while ACTUALLY connected -- see `GeometryMenu.connecting`.\n { id: DISCONNECT_ID, enabled: g.connected && g.connecting !== true },\n ...g.models.map((m) => ({ id: `model:${m}`, checked: m === model, enabled })),\n { id: 'oversize:none', checked: o.kind === 'none', enabled },\n { id: 'oversize:auto', checked: o.kind === 'auto', enabled },\n ...(g.startupFixed !== undefined\n ? [{ id: 'oversize:fixed', checked: isStartupFixed(o, g.startupFixed), enabled }]\n : []),\n ];\n}\n\n/** The two writable fields of a built Electron `MenuItem` this file touches. */\nexport interface MutableChoice { checked: boolean; enabled: boolean }\n\n/**\n * Write `states` onto the BUILT menu, looking each item up with `get` (main.ts passes\n * `menu.getMenuItemById`). Pure in the same sense as the template: no Electron import, so the\n * radio rule below is pinned by a test.\n *\n * ONLY `checked = true` IS EVER WRITTEN, BECAUSE WRITING `false` CHECKS THE ITEM. MEASURED on\n * Electron 44.3.0 under Xvfb: in a radio group a/b/c with `a` checked, `c.checked = false` left\n * `c` CHECKED and `a` cleared, and `a.checked = false` on the checked `a` changed nothing -- any\n * assignment to a radio item's `checked` selects it. The first version of this loop wrote every\n * item's boolean and so left the LAST item of each group checked after every refresh, whatever\n * the session held. Selecting the one true item clears the rest of its group, which is all that is\n * needed. (A group with no true item does not arise: `setGeometry` refuses an unknown model, and\n * the only fixed size the menu can set is the startup one, which it offers.)\n */\nexport function applyChoiceStates(\n states: readonly ChoiceState[], get: (id: string) => MutableChoice | null,\n): void {\n for (const st of states) {\n const item = get(st.id);\n if (item === null) continue;\n if (st.checked === true) item.checked = true;\n item.enabled = st.enabled;\n }\n}\n\n/**\n * The Model menu: Disconnect and a separator, the models as one radio group, a separator, and an\n * Oversize submenu.\n *\n * EACH CHOICE IS DISABLED, NOT THE MENU. The spec says \"the whole menu disabled while connected\";\n * disabling every item gets that everywhere, where disabling the top-level item is a per-platform\n * question (on macOS a disabled top-level menu cannot even be opened to see what is current), and\n * `Alt-M` pops the SUBMENU up directly, past any parent item, so only per-item state reaches it.\n */\nfunction modelMenu(\n g: GeometryMenu, onGeometry: ((c: GeometryChoice) => void) | undefined,\n onDisconnect: (() => void) | undefined,\n): MenuItemTemplate {\n const states = new Map(modelMenuStates(g).map((s) => [s.id, s]));\n const item = (id: string, label: string, choice: GeometryChoice): MenuItemTemplate => {\n const s = states.get(id)!;\n return {\n label, id, type: 'radio', checked: s.checked === true, enabled: s.enabled,\n ...(onGeometry !== undefined ? { click: () => { onGeometry(choice); } } : {}),\n };\n };\n const fixed = g.startupFixed;\n return {\n label: 'Model',\n id: 'model',\n submenu: [\n /*\n DISCONNECT FIRST, 2026-11-05: before it this menu could be used only after the HOST dropped\n the line. A NORMAL ITEM, NOT A RADIO, so Electron 44's radio quirk (`applyChoiceStates`)\n cannot touch it: its state carries no `checked`. The four-event refresh in `main.ts`\n enables it on 'connect' and disables it on 'geometry' (an attempt starting) and 'disconnect'.\n */\n {\n label: 'Disconnect', id: DISCONNECT_ID, enabled: states.get(DISCONNECT_ID)!.enabled,\n ...(onDisconnect !== undefined ? { click: onDisconnect } : {}),\n },\n { type: 'separator' },\n // The size in words beside each name, the spec's rule for every new message naming a size:\n // the names alone do not say that 3278-4 is the tall one.\n ...g.models.map((m) => {\n const alt = KNOWN_MODELS[m]?.alternate;\n return item(`model:${m}`, alt !== undefined ? `${m} (${describeSize(alt)})` : m, { model: m });\n }),\n { type: 'separator' },\n {\n label: 'Oversize',\n id: 'oversize',\n submenu: [\n item('oversize:none', 'None', { oversize: { kind: 'none' } }),\n item('oversize:auto', 'Auto', { oversize: { kind: 'auto' } }),\n // `<cols>x<rows>` as typed on `-oversize` (x3270's order), then the size in words.\n ...(fixed !== undefined\n ? [item('oversize:fixed', `${fixed.cols}x${fixed.rows} (${describeSize(fixed)})`,\n { oversize: { kind: 'fixed', rows: fixed.rows, cols: fixed.cols } })]\n : []),\n ],\n },\n ],\n };\n}\n\n/** The menu template for a platform. Handlers are optional so tests can inspect the shape. */\nexport function buildMenuTemplate(\n platform: Platform, handlers?: MenuHandlers, geometry?: GeometryMenu,\n): readonly MenuItemTemplate[] {\n const edit: MenuItemTemplate = {\n label: 'Edit',\n submenu: [\n {\n label: 'Copy',\n id: 'copy',\n accelerator: copyAccelerator(platform),\n // SPREAD AND NOT `click: handlers?.onCopy`, because `exactOptionalPropertyTypes` is on:\n // an absent handler must leave the key ABSENT rather than present-and-undefined, which is\n // a distinction Electron itself acts on.\n ...(handlers !== undefined ? { click: handlers.onCopy } : {}),\n },\n {\n label: 'Paste',\n id: 'paste',\n accelerator: pasteAccelerator(platform),\n ...(handlers !== undefined ? { click: handlers.onPaste } : {}),\n },\n ],\n };\n /**\n * The keypad's MENU route, which is what makes it discoverable.\n *\n * `Ctrl-K` alone is undiscoverable: nothing on screen says the keypad exists, and an operator\n * who has not read the README has no way to find 48 keys behind an unannounced chord. x3270\n * solves this with a keyboard ICON in its own toolbar (`x3270/keypad.bm`, placed by\n * `menubar.c:604`, shipped at three sizes beside a TLS padlock); a menu item is the\n * Electron-native spelling of the same affordance, and this app now has a menu bar to put it in\n * -- which the copy/paste work added, and which was one of the stated reasons for doing that\n * feature first.\n *\n * THE ACCELERATOR IS NOT PLATFORM-SPLIT, unlike Copy and Paste. `Ctrl+K` collides with nothing\n * in the 3270 keyboard -- the collision that forced the Copy/Paste split was `Ctrl-C` being the\n * Clear AID -- and `Ctrl-K` is already c3270's own binding (`Common/fb-c3270:191`) plus the\n * TUI's, so an operator moving between front ends does not learn two spellings. `Alt-K` is\n * honored too, in `canvas/src/keys.ts`, because that is how x3270's Windows keymap spells the\n * same command; only one of them can be shown here, and `Ctrl-K` is the documented one.\n *\n * \"KEYPAD\" AND NOT \"SHOW KEYPAD\" OR A CHECKBOX, because this OPENS and does not toggle: the\n * window's own close button closes it. A checked menu item would promise a toggle and then\n * misreport the state the moment the operator closed the window instead.\n */\n /*\n THE FULL KEYBOARD IS THE SECOND VIEW ITEM, 2026-10-23, and everything the note above says\n about discoverability applies to it MORE strongly: the keypad is 48 keys an operator might\n guess at, and this is the entire 122-key 3279 layout behind a chord nothing announces.\n\n `Ctrl+B` IS NOT PLATFORM-SPLIT, for the reason given above: the Copy/Paste split exists ONLY\n because `Ctrl-C` is the Clear AID. `Ctrl-B` collides with nothing in the 3270 keyboard --\n `canvas/src/keys.ts:85` is its only binding -- so `Cmd+B` on macOS would diverge from the\n documented chord for no collision at all. `menu.test.ts` pins both halves of that.\n\n \"KEYBOARD\" BESIDE \"KEYPAD\", AND THE TWO WORDS ARE CLOSE ENOUGH TO BE A HAZARD. They differ by\n two letters and sit adjacent. That is tolerated rather than fixed with a longer label, because\n these are the names the whole project already uses -- the README, both chords' documentation,\n `KEYPAD_BLOCKS` and `KEYBOARD_3279` -- and inventing a third name for one surface in one menu\n would be worse than the adjacency. The accelerators differ, the ids differ, and a test asserts\n all three are distinct so a copy-paste that wires one to the other's handler fails.\n */\n const view: MenuItemTemplate = {\n label: 'View',\n submenu: [\n {\n label: 'Keypad',\n id: 'keypad',\n accelerator: 'Ctrl+K',\n ...(handlers !== undefined ? { click: handlers.onKeypad } : {}),\n },\n {\n label: 'Keyboard',\n id: 'keyboard',\n accelerator: 'Ctrl+B',\n ...(handlers !== undefined ? { click: handlers.onKeyboard } : {}),\n },\n ],\n };\n // AN APP MENU IS MANDATORY ON macOS: without one the window gets NO menu bar at all, which is a\n // behavioral difference rather than a cosmetic one.\n //\n // THE MODEL MENU ONLY WITH A GEOMETRY, so a caller that passes none gets exactly the old\n // template. `main.ts` passes none for a `--terminal-type` session, which has no model to switch.\n const menus = geometry !== undefined ? [edit, view, modelMenu(geometry, handlers?.onGeometry, handlers?.onDisconnect)] : [edit, view];\n return platform === 'darwin' ? [{ role: 'appMenu' }, ...menus] : menus;\n}\n", "import type { StartTransferOptions, TransferRequest, TransferRun } from '@tn3270/frontend';\n\n/**\n * The transfer window's main-side logic, with Electron INJECTED.\n *\n * `BrowserWindow`, `dialog` and `ipcMain` cannot be constructed outside an Electron process, and\n * none of the decisions here are Electron's: which dialog a direction wants, whether a close may\n * proceed, and -- the one that earns this file -- that `cancel` is reached on EVERY path that\n * ends a transfer.\n *\n * ## THE TEARDOWN RULE, WRITTEN DOWN BECAUSE IT HAS BITTEN THIS PROJECT THREE TIMES\n *\n * `packages/core/src/session.ts` has been fixed three times for one shape: *one teardown path\n * clears the state and another does not* (`Session.e` cleared only on the REJECT path, then\n * `IAC DONT TN3270E` taking a generic arm, then `handleClose` not clearing `dft`). A window adds\n * new ways for a transfer to end that a TUI overlay never had -- the red button, Cmd-W, Cmd-Q,\n * the session closing underneath. Every one of them must reach `cancel`, because ABANDONING a\n * transfer leaves the host's program waiting for a frame that never comes, where ABORTING tells\n * it to stop.\n *\n * `TransferRun.cancel` already knows the hard parts and needs no help: it defers to the engine\n * for a DFT transfer that has started, discards one that lost the protocol race without sending\n * anything, and refuses a CUT abort at a geometry with no frame layout to write into. So this\n * file adds no cancellation logic -- only the guarantee that the existing one is called.\n *\n * ## ONE STATE WORD, NOT TWO, AND THAT IS THE FOURTH INSTANCE PRE-EMPTED\n *\n * The run and the running flag are the SAME FACT, so they are one variable. The plan's draft kept\n * `run` and `isRunning` separately and got them out of step on its very first path -- see\n * `startRun` below for the measurement. Two words for one fact is precisely the `session.ts`\n * shape quoted above; collapsing them is what makes \"every teardown path clears the state\"\n * structurally true rather than a thing each path has to remember.\n */\n\nexport interface TransferDeps {\n /** Send to the transfer window's renderer. */\n send(channel: 'transfer:progress' | 'transfer:done', payload: unknown): void;\n /**\n * Bring the window forward, so the operator SEES the form and its enabled Cancel button.\n *\n * NAMED FOR WHAT IT DOES. The plan called this `focusCancel` and documented it as focusing the\n * Cancel button, but its only implementation was `webContents.focus()` -- the web contents as a\n * whole, which does not move focus to any particular control. Focusing the button itself would\n * need a sixth bridge function and a renderer-side handler, i.e. a new IPC channel and a new\n * decision in `transferBoot.ts` (the file whose whole discipline is that it contains no\n * decisions), to replace a thing the operator can already see: `Cancel` is the one enabled\n * button on the form while a transfer runs, because `setRunning(true)` disables everything else\n * (`transferUi.ts`'s `setRunning` and `transferBoot.ts:239-246`). So the window comes forward\n * and the form says the rest. A comment claiming a focus that does not happen is worse than\n * either.\n */\n focusWindow(): void;\n /** Native Open dialog: resolves to a path, or undefined if canceled. */\n openDialog(): Promise<string | undefined>;\n /** Native Save dialog: resolves to a path, or undefined if canceled. */\n saveDialog(): Promise<string | undefined>;\n /**\n * `startTransfer` from `@tn3270/frontend`, ALREADY BOUND to this process's session and files.\n *\n * NARROWER THAN `StartTransferOptions` ON PURPOSE, and the plan got this wrong once: that\n * interface REQUIRES `session` and `files` (`transferRun.ts:65-75`), which this controller\n * does not have and must not have -- it is the Electron-free half. Taking the full type here\n * would force a lying `as StartTransferOptions` cast at the call site. `main.ts` owns the\n * session and closes over both, so the two omitted fields are supplied there.\n *\n * **MAY CALL `onDone` BEFORE IT RETURNS.** See `startRun`; everything about the ordering below\n * follows from it.\n */\n startTransfer(opts: Omit<StartTransferOptions, 'session' | 'files'>): TransferRun;\n /** `transferCommand` from `@tn3270/frontend`. THROWS on invalid keywords. */\n buildCommand(keywords: readonly string[]): { request: TransferRequest; command: string };\n}\n\nexport interface TransferController {\n /**\n * Whether a transfer is in flight. **NO PRODUCTION CALLER -- tests only** (confirmed by sweeping\n * `packages/gui/src`: `main.ts` reads `shouldPreventClose()`, never this).\n *\n * KEPT ANYWAY, which is a departure from the rule that removed `finish`, `showWindow` and\n * `closeWindow` from this interface, so here is the distinction. Those three were SECOND TEARDOWN\n * ROUTES -- extra ways to end a transfer, with no caller, which is precisely the shape this\n * module's docstring is about and the reason they had to go. This is a read-only accessor; it\n * cannot create a path, cannot get out of step with `run` (it IS `run !== undefined`), and removing\n * it would cost real test clarity: a dozen assertions in `transferWindow.test.ts` and\n * `transferSeamIpc.test.ts` say \"is a transfer running\", and the only survivor to say it with would\n * be `shouldPreventClose()` -- an assertion about the CLOSE GUARD, which happens to be implemented\n * the same way today. Writing the close guard's name to ask about the run is how two facts become\n * one by accident, and this branch's own history is one teardown bug after another from exactly\n * that.\n */\n running(): boolean;\n browse(direction: string): Promise<string | undefined>;\n submit(keywords: readonly string[]): Promise<{ ok: boolean; error?: string }>;\n requestCancel(): void;\n /** True while a close must be refused. */\n shouldPreventClose(): boolean;\n onCloseAttempt(): void;\n /**\n * The transfer is over because something outside it ended: cancel rather than block.\n *\n * THE CALLER NAMES WHAT HAPPENED AND THIS MODULE DECIDES WHAT THAT MEANS, which is the division\n * the single no-argument `shutdown` got wrong -- see the implementation. The argument is REQUIRED\n * and has no default: a new teardown path must state which of these it is, rather than inheriting\n * whichever behavior happened to be the default and being silently wrong about a window that is\n * still on screen.\n */\n shutdown(why: ShutdownReason): void;\n}\n\n/**\n * WHY a transfer is being torn down from outside, which decides whether anyone is told.\n *\n * `'quit'` and `'windowGone'` are distinct VALUES with identical handling, deliberately: they are\n * different events (`app.on('before-quit')` and `tw.on('closed')`) and a reader at either call site\n * should see its own name rather than having to know which bucket it falls in. Collapsing them into\n * one word would also be a claim that they can never need to differ, which nothing here establishes.\n */\nexport type ShutdownReason =\n /** `app.on('before-quit')`: every renderer in the process is about to be torn down. */\n | 'quit'\n /** `tw.on('closed')`: the window has ALREADY been destroyed, form and all. */\n | 'windowGone'\n /** `session.on('disconnect')`: the socket dropped and THE FORM IS STILL ON SCREEN. */\n | 'sessionLost';\n\nexport function createTransferController(deps: TransferDeps): TransferController {\n /**\n * The run in flight, or `undefined` when nothing is running. ONE WORD FOR ONE FACT.\n *\n * `running()` is `run !== undefined`, so there is no second flag that can disagree with it --\n * see the module docstring. A run that has ENDED is cleared here, which is why `cancel` can\n * never be called on a dead one.\n */\n let run: TransferRun | undefined;\n\n /**\n * How many transfers have been STARTED, used as the identity of the current one.\n *\n * THE GENERATION IS WHAT MAKES A LATE `onDone` HARMLESS. `TransferRun.cancel`'s DFT arm\n * DEFERS -- `DftTransfer.cancel` sets a flag and acts on the next inbound frame\n * (`transferRun.ts:399-406`, citing `dft.ts:186-188`) -- so an ending can arrive after\n * `shutdown` has already cleared the state, and in principle after a LATER transfer has begun.\n * Comparing the generation means such an ending reports itself and stops, instead of clearing a\n * run it is not about. (`transferRun.ts`'s own `ended` guard, at `:180-181`, promises `onDone`\n * fires at most once PER RUN; it says nothing about two runs, which is this counter's job.)\n */\n let generation = 0;\n\n /**\n * End the run this ending BELONGS TO, and tell the window only if it is the current one.\n *\n * NOT PART OF THE PUBLIC INTERFACE. The plan exported a `finish` member; nothing in `main.ts`\n * called it, and the only ending there is `onDone`'s -- so a public `finish` was a second\n * teardown route with no caller, which is the exact shape the module docstring is about.\n * Removed rather than left as an invitation.\n *\n * ## THE GENERATION GOVERNS THE MESSAGE, NOT ONLY THE STATE\n *\n * The first version of this guarded `run` and then sent `transfer:done` UNCONDITIONALLY, which\n * left the bug it exists to fix half-fixed. The reachable sequence is the one\n * `transferWindow.test.ts` already constructs: a cancel that throws leaves the driver's timers\n * armed, run 2 starts, and run 1's 30-second deadline then fires. Main-side state stayed\n * correct -- no cancel was lost -- but run 1's ending still reached the renderer, and\n * `transferUi.ts`'s `finished()` has NO generation concept: it sets `isRunning = false` and\n * `setRunning(false)`, re-enabling the whole form and the Start button MID-TRANSFER, and paints\n * run 1's error over run 2's live progress line. Measured against the committed code:\n * `transfer:done {\"ok\":false,\"error\":\"stale timeout from run 1\"}` delivered while run 2 ran.\n *\n * So a mismatch returns EARLY and sends nothing. The renderer is told about one transfer at a\n * time, which is the same one-transfer rule `submit` enforces at the other end.\n */\n const finish = (\n forGeneration: number,\n result: { ok: boolean; error?: string; bytes?: number },\n ): void => {\n // A STALE ENDING IS DROPPED ENTIRELY. It cannot clear the live run's state and it cannot\n // speak to the window about it -- the run it describes is one nobody is watching any more.\n if (forGeneration !== generation) return;\n run = undefined;\n deps.send('transfer:done', result);\n };\n\n /**\n * Hand the driver its work and record the run -- ORDERING FIRST, CALL SECOND.\n *\n * ## THE BUG THIS SHAPE EXISTS TO PREVENT, MEASURED IN THE SOURCE\n *\n * `startTransfer` CAN CALL `onDone` BEFORE IT RETURNS. A whole DFT transfer can begin and\n * finish inside `session.sendAID(AID.ENTER)`'s record handling; the listeners are registered\n * BEFORE that send precisely so it works (`transferRun.ts:358-361` says so in as many words),\n * and the next statement is `if (ended) return { ok: true };` (`transferRun.ts:377`) -- a run\n * with NO `cancel` on it, because there is nothing left to cancel.\n * `tui/test/transferRun.test.ts:689-718` drives that case against a real `Session` and asserts\n * the completion lands with no `await` in between.\n *\n * The plan's draft assigned `run`/`isRunning` AFTER the call, so a synchronous `onDone` ran\n * `finish()` first and the assignment then armed the flag on a transfer that had ALREADY ENDED.\n * `shouldPreventClose()` would be true forever: the close guard refuses every Cmd-W and red\n * button, Start is refused for the rest of the window's life, and the app has to be killed.\n *\n * The fix is not an ordering convention anybody has to remember. `generation` is bumped and the\n * placeholder stored BEFORE the call, so a synchronous `onDone` matches the current generation\n * and clears it; the assignment afterwards is then CONDITIONAL on the run still being the one\n * this call started. A run that ended inside the call is left cleared.\n */\n const startRun = (\n /**\n * Builds the driver's options from the generation this run will have.\n *\n * A CALLBACK AND NOT A PLAIN OPTIONS OBJECT, because the generation has to reach `onDone` and\n * the only honest way to hand it over is to let this function allocate it. The first draft of\n * this file had `submit` write `generation + 1`, which is the same number arrived at by\n * guessing what `startRun` would do next -- two places agreeing by arithmetic about one fact,\n * which is the drift this module's docstring is about.\n */\n build: (forGeneration: number) => Omit<StartTransferOptions, 'session' | 'files'>,\n ): TransferRun => {\n generation += 1;\n const mine = generation;\n // A PLACEHOLDER, so `running()` is true for the duration of the call: that is what makes a\n // synchronous `onDone` find a run to clear, and what keeps a reentrant `submit` from the\n // renderer out. It carries no `cancel`, which is honest -- there is nothing to cancel until\n // the driver hands one back.\n run = { ok: true };\n /**\n * THE PLACEHOLDER IS GIVEN BACK IF THE CALL THROWS -- DEFENSE IN DEPTH, WITH NO DEMONSTRATED\n * TRIGGER.\n *\n * Read that second clause before deleting this. `run` is armed above and cleared on exactly two\n * paths: `onDone` firing (through `finish`) and `submit`'s `!started.ok` arm. AN EXCEPTION OUT\n * OF `deps.startTransfer` SKIPS BOTH, leaving the placeholder set for the window's life --\n * `shouldPreventClose()` permanently true, so the window refuses every Cmd-W and red button and\n * the app has to be killed. That is the same user-visible failure this function's main docstring\n * is about, reached by a different door.\n *\n * NOBODY HAS DEMONSTRATED A THROW THAT GETS HERE, and this comment does not claim one.\n * `startTransfer` was audited: every local refusal is a RETURN rather than a throw, `files.read`\n * is already wrapped there, `files.exists` is `existsSync` (which answers false rather than\n * throwing), and the one reachable `session.sendAID` throw -- `not connected` -- is pre-empted\n * by `startTransfer`'s own first guard, since `is3270Mode()` reads through the same\n * `this.telnet` whose absence is what makes `sendAID` throw.\n *\n * SO WHY KEEP IT: the argument above is an audit of ANOTHER PACKAGE, and it has to be redone\n * from scratch every time `transferRun.ts` changes. This makes the guarantee STRUCTURAL instead\n * -- the placeholder cannot outlive the call that armed it, whatever that call does. The cost is\n * three lines; the failure it bounds is an application that must be killed.\n *\n * AND IT IS NOT A LICENCE TO DELETE ON A GREEN SUITE. Failing to find a trigger is not a proof\n * that none exists, which is the whole reason this is written as defense in depth rather than as\n * a fix for a known bug: no test can redden this, by construction, because nothing reachable\n * throws here today.\n *\n * RETHROWN, not swallowed. The caller is `ipcMain.handle`, which turns a rejection into a\n * rejected `invoke()` the form reports (`transferUi.ts`'s `start` catches exactly this); eating\n * it here would leave the operator a form that went idle for no stated reason.\n */\n let started: TransferRun;\n try {\n started = deps.startTransfer(build(mine));\n } catch (err) {\n // CONDITIONAL, for the same reason the success assignment below is: if `onDone` already fired\n // and cleared the run before the throw, it must STAY cleared.\n if (mine === generation) run = undefined;\n throw err;\n }\n // CONDITIONAL. If `onDone` already fired, `run` is `undefined` and must STAY undefined --\n // overwriting it here is the plan's bug restated.\n if (run !== undefined && mine === generation) run = started;\n return started;\n };\n\n /**\n * Cancel the run and stop holding it, swallowing a throw from the driver.\n *\n * `cancel` CAN THROW, and that is not hypothetical: its CUT arm calls `session.sendAID`\n * (`transferRun.ts:437-438`) and `Session.sendAID` throws `Error('not connected')` once the\n * socket is gone (`core/src/session.ts:1513`). So a session that dropped mid-transfer plus a\n * Cmd-Q is a throw out of `app.on('before-quit')` -- which would make the application\n * unquittable, the one outcome this module's own justification forbids. The run is dropped\n * either way, because a cancel that could not be delivered has still ended our interest in it.\n *\n * Returns the failure text, if there was one, for the caller to report or discard.\n */\n const cancelRun = (): string | undefined => {\n const r = run;\n run = undefined;\n if (r?.cancel === undefined) return undefined;\n try {\n r.cancel();\n return undefined;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n };\n\n return {\n running: () => run !== undefined,\n\n async browse(direction) {\n // A SEND must choose a file that exists; a RECEIVE names a destination that need not.\n // The dialog returns a PATH ONLY and never sets `Exist`: that is a three-way choice\n // (`keep`/`replace`/`append`) whose `append` has no dialog equivalent at all, so letting\n // the chooser decide would make a legal transfer unexpressible. See the spec.\n //\n // NO `running` CHECK, deliberately: `transferUi.ts`'s `browseLocal` guards on BOTH sides of\n // its own `await`, and the second of those checks is the measured fix for a non-modal\n // dialog resolving mid-transfer. Refusing here as well would hide that guard from its own\n // tests without covering anything the form does not already cover.\n return direction === 'send' ? deps.openDialog() : deps.saveDialog();\n },\n\n async submit(keywords) {\n // ONE SESSION, ONE TRANSFER. Two would interleave two machines' frames on one screen and\n // the host is answering only one of them.\n if (run !== undefined) return { ok: false, error: 'a transfer is already running' };\n\n // THE VALIDATOR IS THE AUTHORITY, and it throws. `transferForm.ts` states the rule: the\n // form collects strings and `parseTransferKeywords` decides. Catching here is what lets\n // the form show the validator's own message rather than a guess of ours.\n let built;\n try {\n built = deps.buildCommand(keywords);\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n\n // NO CAST. `TransferDeps.startTransfer` is declared as\n // `Omit<StartTransferOptions, 'session' | 'files'>` precisely so these four fields are the\n // whole of what this Electron-free half supplies; `main.ts` adds the session and the files.\n //\n // `mine` IS CAPTURED FOR THE CALLBACKS and not read from the closure: by the time a late\n // `onDone` arrives, `generation` may belong to a different transfer. See `startRun`, which\n // hands the number over rather than letting this call site compute it.\n const started = startRun((mine) => ({\n request: built.request,\n command: built.command,\n onProgress: (text) => { deps.send('transfer:progress', text); },\n onDone: (result) => { finish(mine, result); },\n }));\n\n // A LOCAL REFUSAL MEANS THE HOST WAS TOLD NOTHING, and `onDone` will never fire for it --\n // `startTransfer` returns `{ ok: false, error }` for everything checkable locally (3270\n // mode, the keyboard lock, the input field, the source file, the destination). So the\n // answer goes back through this return value and the form stays open and populated. NO\n // `transfer:done` is sent: inventing an ending for a transfer that never began would make\n // the renderer's `finished()` overwrite the refusal with 'transfer failed'.\n if (!started.ok) {\n // CLEARED, because `startRun` armed the placeholder before the call and this run never\n // happened. Without this the window would refuse to close over a refusal.\n run = undefined;\n return { ok: false, error: started.error ?? 'transfer refused' };\n }\n\n return { ok: true };\n },\n\n requestCancel() {\n // `cancel` is absent when the run never started; calling it is the only route out of a\n // running transfer, since the window refuses to close.\n if (run === undefined) return;\n const failed = cancelRun();\n // REPORTED, not swallowed. The form is showing 'transferring' with every other control\n // disabled, so a cancel that silently failed would leave the operator nothing to press --\n // the same dead end as a window that will not close. `onDone` cannot rescue this: the\n // throw happened on the way to it.\n if (failed !== undefined) {\n deps.send('transfer:done', { ok: false, error: `could not cancel the transfer: ${failed}` });\n }\n },\n\n shouldPreventClose: () => run !== undefined,\n\n onCloseAttempt() {\n // REFUSED, NOT QUEUED. An accidental Cmd-W or red button must not abandon a transfer, so\n // the close does nothing except bring forward the form, whose only enabled control is the\n // Cancel button that ends it properly.\n deps.focusWindow();\n },\n\n /**\n * Cancel rather than block, and TELL THE FORM IF THERE IS STILL A FORM TO TELL.\n *\n * ## THE THREE CALLERS DO NOT AGREE ABOUT WHO IS LOOKING, WHICH IS WHY THEY NAME THEMSELVES\n *\n * This method used to take no argument and send nothing, justified as \"there is nobody left to\n * tell\". That is TRUE for a quit (`app.on('before-quit')`, after which every renderer in the\n * process is torn down) and for a destroyed window (`tw.on('closed')`, where the form is\n * already gone -- and `main.ts`'s `send` would no-op on `tw.isDestroyed()` regardless). It is\n * FALSE for a dropped session: the window is still on screen and the operator is still looking\n * at it.\n *\n * ## WHAT THE DISCONNECT CASE ACTUALLY DID, MEASURED AGAINST REAL OBJECTS\n *\n * The chain: socket drops -> `shutdown()` -> `cancelRun()` -> `TransferRun.cancel` reaches\n * `session.sendAID(step.ack)` (`transferRun.ts:438`) -> `Session.sendAID` throws\n * `not connected` (`core/src/session.ts:1513`) -> `cancelRun` returns the message -> the old\n * `shutdown` DISCARDED it. So the driver's own `finish()` never ran, `onDone` never fired, and\n * the renderer was never told anything at all.\n *\n * It is GEOMETRY-DEPENDENT, and the common case is the broken one, because\n * `transferRun.ts:429-432` returns `finish(...)` before that throwing `sendAID` at any\n * non-CUT geometry. Driven with a real `Session`, real `startTransfer`, real controller and\n * real UI:\n *\n * 24x80 (screen.size=1920): ui.running()=true status=\"transferring\" <-- STUCK\n * 43x80 (screen.size=3440): ui.running()=false status=\"transfer canceled by user\"\n *\n * and at 24x80 both remaining controls were dead: Cancel because main answered\n * `if (run === undefined) return;`, Start because the form's own `isRunning` was still armed.\n *\n * It eventually self-healed by an undocumented route, which is the part that made it WORSE\n * than a stuck form: the old `shutdown` did not bump `generation`, so the driver's still-armed\n * 30-second frame timer fired and `finish` forwarded it -- showing `press Attn or Clear: host\n * may still be transferring. stalled: no CUT frame...`. That message is FALSE. The session is\n * gone; there is no host to press Attn at.\n *\n * ## SO: AN HONEST ENDING, AND THE GENERATION BUMPED SO NOTHING CAN CONTRADICT IT\n *\n * `generation += 1` is what makes the stale timeout harmless, and it is the same mechanism\n * `finish`'s own docstring is about rather than a new one. The timer is still armed -- nothing\n * here can reach into the driver and disarm it, since `cancel` threw on its way to `finish` and\n * `ended` therefore stayed false -- so run N's `onDone` WILL arrive about 30 seconds later.\n * With the generation moved on, `finish` drops it entirely: it neither clears state nor speaks\n * to the window. Without the bump it would arrive and overwrite the true message with the false\n * one, which is the contradiction this fix exists to prevent.\n *\n * The bump is correct for the quit and window-gone cases too, for the same reason and at no\n * cost: a late ending there has nothing to speak to anyway.\n */\n shutdown(why) {\n // CANCEL FIRST, WHATEVER THE REASON. The window may refuse a close; it may not make the\n // application unquittable, and a session that dropped must not leave the host's program\n // waiting for a frame -- the abort/abandon distinction this module's docstring turns on. A\n // failed cancel is never thrown: on a dropped socket it is the EXPECTED outcome.\n const running = run !== undefined;\n cancelRun();\n // MOVED ON BEFORE ANYTHING IS SENT, so the ending below is the last word about this run. See\n // the docstring: the driver's 30-second frame timer survives a `cancel` that threw, and\n // `finish` compares against this number to decide whether an ending still belongs to anyone.\n generation += 1;\n // NOTHING TO REPORT, AND NOBODY TO REPORT IT TO. A quit tears down every renderer; a\n // destroyed window has no form left. Only a dropped session leaves a form on screen -- and\n // only if something was actually running, since a `transfer:done` for a transfer that never\n // started would make the renderer's `finished()` paint 'transfer failed' over the help text.\n if (why !== 'sessionLost' || !running) return;\n // THE HONEST MESSAGE: the session dropped. NOT the driver's timeout text, which names a host\n // that is no longer there, and not 'transfer canceled by user' -- the operator canceled\n // nothing. `requestCancel`'s report is the model for saying so rather than leaving the form\n // showing 'transferring' with nothing to press.\n deps.send('transfer:done', {\n ok: false,\n error: 'transfer ended: the session disconnected',\n });\n },\n };\n}\n", "import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/**\n * Where the GUI's files are, given the directory `main.js` runs from.\n *\n * ## TWO LAYOUTS, ONE RULE\n *\n * UNBUNDLED (`tsc --build`, what every harness and the monorepo run): `main.js` is\n * `packages/gui/dist/main.js`, the preloads compile beside it as `dist/*.cjs`, the HTML documents\n * sit one level up in `packages/gui/`, and the atlas belongs to `@tn3270/canvas`, whose own\n * `assetDir()` knows where it is. The documents reach into `../canvas/dist` and `../frontend/dist`\n * through import maps, which works ONLY because the workspace keeps those packages as siblings.\n *\n * BUNDLED (`scripts/bundle.mjs` -> `packages/gui/app/`, the single input to both the npm package\n * and electron-builder): everything is FLAT beside `main.js` -- documents, preloads, page modules\n * and `atlas.{json,bin}` -- because there is no `node_modules` and no sibling package to reach.\n *\n * The bundle writes a `BUNDLED` marker file, and this function asks for it. That makes the answer\n * a FACT ON DISK rather than a guess from a directory name: a packaged app runs from\n * `.../resources/app.asar`, a name nobody here chose, and a check like `basename(here) === 'app'`\n * would get it wrong the day electron-builder (or a user's install prefix) names it differently.\n * `existsSync` works inside asar because Electron patches `fs` in the main process.\n *\n * The marker is looked for BESIDE `main.js` ONLY, never in a parent: the unbundled `dist/` must\n * stay unbundled even if someone copies an `app/` over `packages/gui/`. `appRoot.test.ts` pins it.\n *\n * ## WHY THE ATLAS IS EXPLICIT IN THE BUNDLE, NOT LEFT TO `assetDir()`\n *\n * esbuild inlines `@tn3270/canvas`'s `assets.ts` into `app/main.js`, and its module-level\n * `dirname(fileURLToPath(import.meta.url))` then evaluates to `app/` -- which HAPPENS to be where\n * the bundle copies the atlas. Relying on that would make a correct-looking app depend on two\n * unrelated decisions agreeing by accident (where esbuild puts the output, and where the copy\n * step puts the atlas); a bundler change to `import.meta` handling would break it with nothing\n * naming the cause. So the bundled layout states its atlas directory, and `main.ts` passes it.\n */\nexport interface Layout {\n /** True when `BUNDLED` sits beside `main.js`. */\n readonly bundled: boolean;\n /** An HTML document (`index.html`, `keypad.html`, ...). */\n document(name: string): string;\n /** A compiled preload (`preload.cjs`, `keypadPreload.cjs`, ...). */\n preload(name: string): string;\n /** The atlas directory, or undefined to use `@tn3270/canvas`'s own `assetDir()`. */\n readonly atlasDir: string | undefined;\n /**\n * The window icon PNG. Unbundled it is the tracked `resources/icon.png`, which is also what\n * electron-builder converts into the `.icns`/`.ico`/Linux icon set; the bundle copies it flat.\n */\n readonly icon: string;\n}\n\n/** The marker `scripts/bundle.mjs` writes into `app/`. Its contents are irrelevant. */\nexport const BUNDLE_MARKER = 'BUNDLED';\n\n/** The icon's file name, in `resources/` unbundled and flat in `app/` bundled. */\nexport const ICON_FILE = 'icon.png';\n\nexport function layoutFor(mainDir: string): Layout {\n if (existsSync(join(mainDir, BUNDLE_MARKER))) {\n return {\n bundled: true,\n document: (name) => join(mainDir, name),\n preload: (name) => join(mainDir, name),\n atlasDir: mainDir,\n icon: join(mainDir, ICON_FILE),\n };\n }\n return {\n bundled: false,\n document: (name) => join(mainDir, '..', name),\n preload: (name) => join(mainDir, name),\n atlasDir: undefined,\n icon: join(mainDir, '..', 'resources', ICON_FILE),\n };\n}\n", "/**\n * Chromium switches this app is launched with, stripped before OUR parser sees argv.\n *\n * HERE AND NOT IN main.ts for the reason `planStartup` is in connectDialog.ts: main.ts imports\n * `electron`, which vitest cannot load.\n *\n * Listed explicitly rather than filtered by a `--` prefix rule, because our own\n * `--terminal-type` takes a VALUE: a prefix rule either eats that value or needs to know\n * about it, and both readings were wrong in the first draft of this file. Anything not\n * named here still reaches `parseGuiArgs`, so an unrecognized flag is still an error --\n * silently swallowing one is what produces a session that negotiates something nobody\n * asked for.\n */\nexport const ELECTRON_SWITCHES: ReadonlySet<string> = new Set([\n '--no-sandbox', '--disable-gpu', '--disable-software-rasterizer',\n '--disable-dev-shm-usage', '--enable-logging', '--in-process-gpu',\n]);\n\n/**\n * Switches that carry their value after `=` in the SAME argument, matched by this prefix. Only\n * the `=` form: a separate-argument value (`--user-data-dir /x`) would leave `/x` behind for\n * `parseGuiArgs` to read as the host. Chromium itself parses switches only in the `=` form.\n *\n * `--user-data-dir=<dir>`: Electron's userData (and Chromium's profile) directory -- the smoke\n * scripts give every launch a fresh one so a test run never writes the user's real app data.\n */\n/**\n * Chromium switches that take an `=value`, stripped only in that form with a non-empty value.\n *\n * `--force-device-scale-factor=` and `--force-color-profile=` make a screenshot reproducible across\n * displays: CI run 3 (2026-11-06) captured the golden on macos-26-intel at 1440x700 (scale 2) with\n * every green shifted by the display profile, while macOS arm64, Windows and Linux matched it\n * byte for byte. The smokes pass `=1` and `=srgb`, which is what the golden was recorded under.\n */\nexport const ELECTRON_VALUE_SWITCHES: readonly string[] = [\n '--user-data-dir=', '--force-device-scale-factor=', '--force-color-profile=',\n];\n\n/** `argv` without the Electron/Chromium switches above; an empty `=` value is NOT stripped. */\nexport function stripElectronSwitches(argv: readonly string[]): string[] {\n return argv.filter((a) => !ELECTRON_SWITCHES.has(a)\n && !ELECTRON_VALUE_SWITCHES.some((p) => a.startsWith(p) && a.length > p.length));\n}\n"],
|
|
5
|
+
"mappings": "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;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;AAIrD,IAAM,UAA6B;;EACxB;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;EAAM;;EAC1C;EAAM;EAAM;EAAM;EAAM;EA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",
|
|
6
|
+
"names": ["fileURLToPath", "writeFileSync", "readFileSync", "dirname", "ch", "St", "ch", "alt", "alt", "tokens", "screen", "modelName", "screen", "KeyboardState", "screen", "ch", "alt", "screen", "screen", "screen", "u16", "reply", "u16", "host", "dialog", "resolve", "alt", "key", "key", "screen", "ch", "key", "positiveInt", "pfRow", "readFileSync", "existsSync", "readFileSync", "existsSync", "readFileSync", "key", "alt", "existsSync", "join", "here", "dirname", "fileURLToPath", "key", "readFileSync", "ch", "resolve", "writeFileSync"]
|
|
7
|
+
}
|