@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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../frontend/src/keypad.ts", "../../frontend/src/bindings.ts", "../../frontend/src/keypadView.ts", "../../canvas/src/keypadUi.ts", "../../canvas/src/keys.ts", "../../canvas/src/forwardKeys.ts", "../src/keypadBoot.ts"],
|
|
4
|
+
"sourcesContent": ["/**\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 { KEYPAD_BLOCKS, tooltipFor, type Action } from '@tn3270/frontend';\n\n/**\n * The keypad's browser-side view, with the DOM INJECTED.\n *\n * ## IT LIVES IN `canvas` BECAUSE BOTH FRONT ENDS NEED IT, AND THAT WAS A CORRECTION\n *\n * It was written in `packages/gui` first, which was wrong and would not have compiled for the web\n * gateway: `packages/web` does not depend on `@tn3270/gui` and must not -- that package is an\n * Electron app, with `electron` imports and a `.cts` preload. `canvas` is the package whose own\n * docstring says it exists for exactly this, \"the canvas presentation layer, shared by the\n * Electron GUI and the web gateway\", and it already depends on `frontend` where `KEYPAD_BLOCKS`\n * lives. Found when wiring the web overlay, i.e. one task later than it should have been.\n *\n * NOTE THE PACKAGE NAME IS NOW SLIGHTLY WRONG FOR THIS FILE and that is accepted: nothing here\n * touches a canvas. `canvas` is the shared-presentation package, and a `frontend`-level home\n * would be worse -- that package's docstring explicitly excludes \"anything a front end owns\n * because of HOW it presents\", and a DOM is precisely that.\n *\n * ## WHY THE DOM IS INJECTED AND NOT REACHED FOR\n *\n * `vitest.config.ts` sets `environment: 'node'`, so there is no `document` in any test in this\n * repo and jsdom is not a dependency. The logic worth testing -- which buttons exist, what each\n * one sends, what tooltip it carries -- needs no real element, and injecting the few operations\n * that do keeps every line of it reachable from a unit test.\n *\n * ## PRIMITIVES, NOT A `render(fields)` CALLBACK, AND THE DIFFERENCE IS MEASURABLE\n *\n * `transferUi.ts` made the other choice: it hands its boot file a `render(fields)` callback and\n * lets THAT file build elements. The cost shows up in the line counts -- `transferBoot.ts` runs to\n * several hundred lines no test can execute, comparable in size to the `transferUi.ts` that every\n * test CAN -- and the boot file is exactly where this project's one shipped blank-window bug lived\n * (a TDZ read on an uninitialised `const`, which optional chaining does not guard).\n *\n * (Exact counts were cited here until 2026-10-06 and went stale the first time either file gained\n * a comment. The ratio is the argument; the integers were never the argument.)\n *\n * Injecting `create`/`append`/`setText`/`setAttr`/`onClick` instead moves the element building\n * into this file, where `keypadUi.test.ts` asserts all 48 buttons and all 48 actions against a\n * fake DOM, and leaves `keypadBoot.ts` as a handful of one-line adapters. The tradeoff is a wider\n * deps interface; the gain is that the one thing that must not be wrong -- which action each\n * button sends -- is pinned by a test rather than by a human looking at a window.\n *\n * ## A BROWSER LOADS THIS, AND AN IMPORT MAP IS WHAT MAKES THAT LEGAL\n *\n * The import above is a BARE SPECIFIER and stays one in the emitted JS -- `tsc` rewrites nothing.\n * `keypad.html` carries an import map resolving it to `../frontend/dist/keypadView.js`, whose\n * runtime graph closes at `keypad.js` and `bindings.js` (each with zero imports of their own,\n * measured 2026-10-06 and pinned by `frontend/test/keypadModule.test.ts`).\n *\n * SO DO NOT ADD A SECOND WORKSPACE IMPORT TO THIS FILE, and never import the package BARREL: it\n * reaches `tls.js`, whose `node:net`/`node:tls`/`node:fs` no browser can resolve, and the failure\n * arrives as a BLANK WINDOW WITH NO ERROR in any console. This repo has met that shape five ways.\n */\nexport interface KeypadDeps {\n /** The element the blocks are appended to. */\n readonly root: HTMLElement;\n readonly create: (tag: string) => HTMLElement;\n readonly append: (parent: HTMLElement, child: HTMLElement) => void;\n readonly setText: (el: HTMLElement, text: string) => void;\n readonly setTitle: (el: HTMLElement, title: string) => void;\n readonly setAttr: (el: HTMLElement, name: string, value: string) => void;\n readonly onClick: (el: HTMLElement, fn: () => void) => void;\n /** Fire a keypad action at the session. */\n readonly sendAction: (action: Action) => void;\n}\n\n/**\n * Build the keypad into `deps.root`.\n *\n * ONE BUTTON PER KEY, DRIVEN BY `KEYPAD_BLOCKS`, so `packages/frontend` stays the single\n * authority on which keys exist and what each does. This file decides nothing about that -- which\n * is why a key added to the table appears here with no edit, and why a key REMOVED from it\n * disappears from both front ends at once.\n *\n * NOT IDEMPOTENT, and it does not need to be: both callers build once. The GUI window builds at\n * load; the web overlay builds lazily on its first toggle and then never again (rebuilding 48\n * buttons per keystroke would be invisible work that also discards focus).\n */\nexport function createKeypadUi(deps: KeypadDeps): void {\n for (const block of KEYPAD_BLOCKS) {\n const section = deps.create('div');\n deps.setAttr(section, 'class', 'keypad-block');\n // PREFIXED, because these ids share a document with the transfer form's in the web overlay's\n // case and with nothing today in the window's -- but `keypad-cursor` cannot collide where\n // `cursor` might.\n deps.setAttr(section, 'id', `keypad-${block.id}`);\n\n const heading = deps.create('h2');\n deps.setText(heading, block.title);\n deps.append(section, heading);\n\n const keys = deps.create('div');\n deps.setAttr(keys, 'class', 'keypad-keys');\n for (const key of block.keys) {\n const button = deps.create('button');\n // `type=button` EXPLICITLY: a bare <button> inside a form defaults to type=submit. There is\n // no form in the keypad window today, and this is what stops a later one turning a PF key\n // into a page reload.\n deps.setAttr(button, 'type', 'button');\n // THE HANDLE THE HARNESS USES. `clicks.mjs` queries by label, deliberately -- a coordinate\n // list would be a second copy of the layout and would pass while the layout was wrong. Held\n // in an attribute SEPARATE from the visible text so that giving the arrows real icons later\n // cannot break the query.\n deps.setAttr(button, 'data-label', key.label);\n deps.setText(button, key.label);\n // `name` always, plus `BINDING_INTENT`'s note where one exists -- see `tooltipFor`. 22 of\n // the 48 keys have no entry and 15 more have an entry with no note, so a tooltip that\n // demanded prose would leave most of the window bare.\n deps.setTitle(button, tooltipFor(key));\n // THE CLOSURE CAPTURES `key`, which is the whole correctness question in this file: a loop\n // that hoisted the handler would wire all 48 buttons to the last key. `for...of` gives a\n // fresh binding per iteration, and `keypadUi.test.ts` presses all 48 and compares the whole\n // action sequence rather than spot-checking.\n deps.onClick(button, () => { deps.sendAction(key.action); });\n deps.append(keys, button);\n }\n deps.append(section, keys);\n deps.append(deps.root, section);\n }\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 { actionForKey, type KeyLike } from './keys.js';\nimport type { Action } from '@tn3270/frontend';\n\n/**\n * Forward a window's real keystrokes -- and, in the browser, its real pastes -- to the session.\n *\n * TWO LISTENERS WITH DIFFERENT AUDIENCES, which is worth knowing before reading either: the\n * `keydown` half (`forwardKeys`) exists for the GUI's CHILD WINDOWS and runs in every front end,\n * while the `paste` half (`forwardPaste`) is for the WEB GATEWAY and refuses to install anywhere\n * else. They share `isEditableTarget` and nothing else. The paragraph below is the first half's\n * history; `forwardPaste`'s own docstring carries the second's, including the measurement that\n * makes its gate load-bearing.\n *\n * ## WHY THIS EXISTS: THE GUI'S CHILD WINDOWS WERE DEAF\n *\n * `renderer.ts` has the product's only other `keydown` listener and it lives in the CANVAS\n * window. The GUI's keypad and keyboard are separate `BrowserWindow`s, so with either focused the\n * physical keyboard did nothing at all -- not a dropped chord, silence. The user met it the day\n * the keyboard window shipped: a window full of keys is an invitation to type.\n *\n * **The web gateway never had THIS KEYDOWN problem and needs `forwardKeys` itself for nothing.**\n * Its surfaces are `<div>` overlays in the canvas page, so one `window` listener already covers\n * them. That asymmetry is Electron's, and it is the GUI's alone.\n *\n * (SCOPED TO `forwardKeys`, 2026-10-26, because this line said the gateway \"needs nothing from\n * this module\" and that became false while the diff that falsified it never touched it: the\n * gateway already imported `isEditableTarget` from here, and now takes `forwardPaste` too. This\n * is the summary-line-outlives-its-correction shape -- the heading five lines above was written\n * in the same edit and contradicted this one.)\n *\n * ## INJECTED DEPS, LIKE `keypadUi.ts` AND `keyboardUi.ts`\n *\n * Nothing here touches a real `window`: the caller supplies `on` and `sendAction`. That is what\n * lets this be unit-tested in `environment: 'node'` with a hand-rolled fake, and jsdom is\n * deliberately not a dependency of this repo.\n *\n * ## IT IMPORTS ONLY `keys.js`, WHICH IS THE WHOLE REASON IT IS A NEW FILE\n *\n * The obvious alternative was to export the listener from `renderer.ts`. That file value-imports\n * `blit.js` and `selection.js`, so a child window importing it would drag the whole rasteriser\n * into a document that has no canvas -- and every new specifier needs its own entry in two import\n * maps. `keys.js` has ZERO runtime imports, so each window's graph grows by exactly one file.\n */\nexport interface ForwardKeysDeps {\n /** Register the listener. The real caller passes `window.addEventListener`. */\n readonly on: (type: string, handler: (e: never) => void) => void;\n /** Deliver an action. The real caller passes its window's bridge. */\n readonly sendAction: (action: Action) => void;\n}\n\n/**\n * Is this event's target a thing the operator is typing INTO?\n *\n * ## EXTRACTED FROM `renderer.ts` RATHER THAN COPIED, 2026-10-23\n *\n * That file had this inline and now imports it. Two copies of a rule that must agree is how they\n * diverge, and this one is load-bearing: MEASURED 2026-10-07, without it typing into the web\n * gateway's transfer form sent every character TO THE MAINFRAME and left the field empty, because\n * `actionForKey` claims any single-code-point key as `{kind:'type'}` without knowing what was\n * focused, and the `preventDefault` that follows suppressed the character locally.\n *\n * ## CHECKED HERE AND NOT INSIDE `actionForKey`\n *\n * `keys.ts` is deliberately DOM-free -- `KeyLike` carries six scalars and no target, which is what\n * lets its cases run without jsdom. The target is a property of the EVENT, so the code that owns\n * real events owns this question.\n *\n * ## `BUTTON` IS DELIBERATELY NOT IN THE LIST\n *\n * Every cap on both surfaces is a `<button>`, and one of them holds focus after every click. A\n * predicate widened to \"any form control\" would make the keyboard window deaf again the moment an\n * operator clicked a key -- which is the exact bug this module exists to fix. The list is the\n * three TEXT-ENTRY tags plus `contentEditable`, and nothing else.\n */\nexport function isEditableTarget(\n target: { tagName?: unknown; isContentEditable?: unknown } | null,\n): boolean {\n const tag = typeof target?.tagName === 'string' ? target.tagName : '';\n return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'\n || target?.isContentEditable === true;\n}\n\n/** The shape this module reads off a real `KeyboardEvent`. */\ntype KeyEventLike = KeyLike & {\n readonly target: { tagName?: unknown; isContentEditable?: unknown } | null;\n readonly preventDefault: () => void;\n};\n\n/**\n * Install the listener. Called once per window, at load.\n *\n * ALL CHORDS WORK EVERYWHERE, which is the user's decision of 2026-10-23 and not an oversight:\n * `Ctrl-B` pressed in the keyboard window raises `toggleKeyboard`, which `main.ts` answers by\n * showing and focusing the window that is already open -- a harmless self-focus.\n * `openKeyboardWindow` is idempotent precisely so a repeated chord cannot stack a second window.\n * Special-casing the two toggle chords would add a second place for the rule to be wrong.\n */\nexport function forwardKeys(deps: ForwardKeysDeps): void {\n deps.on('keydown', (raw: never) => {\n const e = raw as unknown as KeyEventLike;\n if (isEditableTarget(e.target)) return;\n const action = actionForKey(e);\n if (action === null) return;\n // `preventDefault` ONLY for keys we CLAIMED, so shortcuts we do not use keep working and Tab\n // does not move focus out of the window. `renderer.ts` makes the same call for the same reason.\n e.preventDefault();\n deps.sendAction(action);\n });\n}\n\n/** The shape this module reads off a real `ClipboardEvent`. */\ntype PasteEventLike = {\n readonly target: { tagName?: unknown; isContentEditable?: unknown } | null;\n readonly clipboardData?: { getData: (format: string) => string } | null;\n readonly preventDefault: () => void;\n};\n\nexport interface ForwardPasteDeps {\n /**\n * Register the listener. The real caller passes `window.addEventListener`.\n *\n * THE HANDLER IS TYPED `PasteEventLike` AND NOT `never`, WHICH DIVERGES FROM `ForwardKeysDeps`\n * ABOVE ON PURPOSE. `never` costs the listener body its type checking: every field read has to\n * go through a `raw as unknown as ...` cast, and after that cast the compiler cannot tell\n * `clipboardData` from a typo. Naming the shape instead means `tsc` checks the body, and it\n * costs nothing elsewhere -- `PasteEventLike` is a local type that emits fine into the `.d.ts`,\n * and this file still pulls in no DOM lib types. The renderer's own\n * `handler as (e: Event) => void` is unavoidable either way (`TS2769` with `never`, `TS2345`\n * with this), so the count of casts goes from two to one rather than to zero.\n */\n readonly on: (type: string, handler: (e: PasteEventLike) => void) => void;\n /** Deliver the action. The real caller passes its window's bridge. */\n readonly sendAction: (action: Action) => void;\n /**\n * `location.protocol` of the document installing this. THE WHOLE SAFETY GATE; see below.\n *\n * PASSED IN RATHER THAN READ HERE for the reason the module header gives about every other\n * capability: these tests run under `environment: 'node'`, where there is no `location` at all,\n * and a gate nothing can drive is a gate nobody has checked.\n */\n readonly protocol: string;\n /**\n * Forget the renderer's selection, called ONLY once a paste has been claimed.\n *\n * A HOOK AND NOT AN IMPORT, because the selection is `renderer.ts`-local state by that file's\n * own deliberate design -- it is not in the `DrawList` and main cannot see it. This module\n * stays DOM-free and state-free, which is what keeps it testable.\n */\n readonly clearSelection: () => void;\n}\n\n/**\n * THE HOSTS WHOSE `paste` ACTION SOMEBODY ANSWERS. Nothing else may raise one.\n *\n * `http:`/`https:` is the WEB GATEWAY and only the web gateway: `web/src/main.ts` intercepts\n * `{kind:'paste'}` before `applyAction` and calls `applyPaste`. Electron's own page is `file:`\n * (MEASURED 2026-10-26: a `loadFile` window reports `location.protocol === 'file:'`), and the\n * gateway can never be `file:` -- `socketUrl` builds a `ws:`/`wss:` URL from the document's own\n * href, so a `file:`-loaded page has no socket to send on in the first place.\n */\nconst PASTE_HOSTS: ReadonlySet<string> = new Set(['http:', 'https:']);\n\n/**\n * A PASTE, from the browser's own `Ctrl-V`. 2026-10-26.\n *\n * ## WHY AN EVENT AND NOT A CHORD OR A BUTTON\n *\n * `navigator.clipboard.readText()` would need a permission prompt in Chromium and does not exist\n * in Firefox. The `paste` event hands over the text with NO prompt in every browser, and the\n * native `Ctrl-V` is already the gesture an operator reaches for -- so this adds no chord to\n * `keys.ts` and needs no menu bar, which a browser tab does not have.\n *\n * ## IT IS INSTALLED ONLY ON THE WEB GATEWAY, AND THAT GATE IS NOT CAUTION -- IT IS A FIX\n *\n * `renderer.ts` IS LOADED BY TWO FRONT ENDS: `web/static/index.html` serves it to a browser, and\n * `gui/index.html`'s `<script type=\"module\" src=\"../canvas/dist/renderer.js\">` pulls the same\n * file into the ELECTRON GUI. (BY TAG AND NOT BY LINE: that document is five lines long, so any\n * comment added to it moves the line -- and `d4fce66` retired this feature's line citations for\n * exactly that.) A `paste` action raised\n * in the GUI reaches `gui/src/main.ts`'s `ipcMain.on('action')`, which intercepts `quit`,\n * `toggleKeypad`, `toggleKeyboard`, `geometryMenu`, `transferForm`, `copy` and `requestCopy` --\n * and NOT `paste` -- then falls\n * through to `applyAction`, which THROWS on it by design (`frontend/src/actions.ts`: the front\n * end owns its own clipboard).\n *\n * MEASURED 2026-10-26, Electron 44.3.0 under Xvfb, because the cost of guessing here is the\n * operator's whole session:\n * 1. A `paste` EVENT really does reach the page in an Electron window, by FIVE routes, each\n * `isTrusted: true` and carrying the real clipboard text: `Ctrl+V`, `Ctrl+Shift+V`,\n * `Shift+Insert`, a middle-click, and `webContents.paste()`. The menu accelerator does NOT\n * swallow the keystroke -- a run with no application menu at all saw the same events.\n * 2. An uncaught throw inside an `ipcMain.on` listener does not merely log. It FREEZES THE MAIN\n * PROCESS EVENT LOOP: a 400ms heartbeat timer logged nothing at all afterwards and the\n * process had to be killed, where the identical run with the throw removed logged its eight\n * beats and exited 0. There is no `uncaughtException` handler in `gui/src/main.ts`.\n * 3. AND THE TWO HALVES REALLY DO JOIN UP IN THE SHIPPING CLIENT, which is the measurement\n * that matters and the only one taken against `packages/gui` itself. With the gate removed\n * and a live X11 clipboard owner, the real GUI under `TN3270_GUI_KEYS='Ctrl+v,Ctrl+a'`\n * NEVER EXITED: it ignored its harness's 60s timeout and the SIGTERM that followed, and had\n * to be `kill -9`ed -- no `action:` line, no `renderer[3]`, just a frozen client. WITH the\n * gate it exits 0 on the same keys, raises no paste, and the trailing `Ctrl+A` still logs\n * `{\"kind\":\"attn\"}` -- which is the liveness probe, since an absence alone would also be\n * what a dead process produced.\n * So an UNGATED listener here would have handed every Electron operator a frozen app on `Ctrl+V`.\n * (An X11 selection dies with the process that owns it, so a counterfactual run needs a second\n * Electron alive holding the clipboard -- a first attempt measured an EMPTY clipboard instead and\n * saw no paste at all, which would have read as the hazard being unreachable.)\n *\n * ## WHY THE GUI DOES NOT INSTEAD GET A `paste` ARM, WHICH LOOKS LIKE THE OBVIOUS FIX\n *\n * It would DOUBLE-PASTE on Linux and Windows, and that is measured too. `gui/src/menu.ts` already\n * binds Paste to `Ctrl+Shift+V` off `darwin`, wired to `onPaste` -> `applyPaste`. In the run\n * above, `Ctrl+Shift+V` fired the page's `paste` event AND the menu's `click` -- and\n * `preventDefault()` on the event did NOT suppress the accelerator (`defaultPrevented: true` in\n * the page, `MENU_PASTE_CLICK` immediately after). An arm answering the action would therefore\n * apply the same clipboard twice for one keystroke, into a 3270 field, where a silently\n * half-doubled dataset name may be a VALID DIFFERENT NAME -- the exact failure `pasteAction.ts`\n * is written to avoid.\n *\n * The GUI has a WORKING paste already (that menu item), so the gate costs it nothing today. What\n * it does cost is the GUI's native `Ctrl+V`, which stays unbound there -- deliberately left for\n * the user to decide on, since fixing it means reconciling the accelerator with the event rather\n * than adding a handler.\n *\n * **THE INVARIANTS THE NEXT PERSON MUST NOT BREAK.** The gate is the only thing standing between\n * a stray `paste` event and a frozen main process, and THREE separate things keep it sufficient:\n * 1. `applyAction` still THROWS on `paste`. If that goes, re-measure before widening\n * `PASTE_HOSTS` -- the throw is what makes a missing arm loud instead of silent.\n * 2. `gui/src/main.ts` still has no `paste` arm. If one is added, the double-paste measured\n * above applies and the accelerator has to be reconciled first.\n * 3. **THE ELECTRON GUI'S `TN3270_GUI_URL` MODE STAYS A SEPARATE, EARLIER PATH.** This is the\n * one the gate does NOT cover and the easiest to break by accident, so it is named rather\n * than left to be rediscovered. That mode is an Electron window loading the gateway over\n * `http:` -- so it PASSES `PASTE_HOSTS` and does install this listener. VERIFIED 2026-10-26\n * that it is nonetheless safe, and safe for a reason that is nothing to do with the\n * protocol: the URL branch `return`s out of `app.whenReady()` BEFORE either\n * `Menu.setApplicationMenu` or `ipcMain.on('action')` is reached, so that window has no\n * action listener and no Paste accelerator at all, and its `window.tn3270` is the GATEWAY's\n * own socket bridge (the Electron preload is deliberately omitted in URL mode), whose\n * `sendAction` reaches the gateway's real `paste` arm. Correct -- but by STATEMENT ORDER in\n * another package.\n * SO: MOVING THE MENU OR THE `ipcMain.on('action')` REGISTRATION ABOVE THAT EARLY RETURN --\n * an ordinary-looking tidy-up, since both look like setup that belongs at the top -- would\n * re-arm the freeze for URL mode with this gate untouched and nothing red anywhere. If that\n * ordering changes, this mode needs its own answer, not a wider allowlist.\n *\n * ## IT MUST SKIP FORM CONTROLS, AND THAT IS MEASURED RATHER THAN CAUTIOUS\n *\n * This listener goes on `window`, and the gateway's transfer form has real text inputs. The\n * 2026-10-07 bug in the other direction is recorded on `renderer.ts`'s `keydown` listener: typing\n * into that form sent every character TO THE MAINFRAME and left the field empty. A paste into the\n * `Local file` field would do exactly the same, so `isEditableTarget` guards both.\n *\n * ## IT SENDS `paste`, NOT `type`\n *\n * `keymap.ts` records the whole reason: `type` reaches `Keyboard.typeString`, which would send\n * every `\\n` and `\\f` into the field AS A CHARACTER (EBCDIC 0x25 and 0x0c, MDT set), so the next\n * AID carries the corruption to the host as field data. `pasteString`'s x3270 semantics are what\n * the `paste` kind buys. (An earlier draft of this feature's plan said `\\n` became ENTER and\n * submitted the panel. That is x3270's `kybd.c`, not this codebase -- `core/src/keyboard.ts` has\n * no AID path at all. Corruption, not submission.)\n *\n * ## A HUGE PASTE DROPS THE CONNECTION, AND IT IS WORTH KNOWING WHERE THAT COMES FROM\n *\n * There is NO LENGTH CHECK HERE and that is deliberate, but the consequence is not obvious from\n * this file. MEASURED 2026-10-26: the JSON envelope around the text is 53 bytes, and\n * `wsserver.ts`'s `MAX_MESSAGE_BYTES` is 8192 -- so a paste of up to 8139 ASCII characters is\n * delivered, 8140 is 8193 bytes, and `parseFrame` throws on the cap. `Connection` answers that by\n * CLOSING THE CLIENT rather than throwing (deliberately: a throw out of a `data` handler would\n * end the gateway process and disconnect every other operator's session), so what the operator\n * sees is \"The connection to the gateway closed. Reload to reconnect.\" -- with nothing said about\n * length, and no `error` frame, because the socket is already gone.\n *\n * SO A MYSTERY DISCONNECT AFTER A BIG PASTE IS THIS, NOT A GATEWAY BUG. The 3270 session itself\n * survives -- the registry's grace window holds it and a reload reattaches -- so the outcome is\n * handled, just by dropping the connection instead of refusing the paste.\n *\n * NO CLIENT-SIDE LENGTH CHECK IS ADDED, and the reason is that it would be a second copy of a\n * limit this file cannot see: `MAX_MESSAGE_BYTES` lives in `packages/web`, the renderer has no\n * import from there (by hard constraint -- see this file's header), and the real budget depends on\n * UTF-8 byte length rather than character count. A threshold hardcoded here would drift from the\n * server's the first time either moved, and would reject pastes the gateway would have accepted.\n * If this is worth fixing it belongs at the GATEWAY, which knows its own cap and can answer with\n * the `error` frame the protocol already has.\n */\nexport function forwardPaste(deps: ForwardPasteDeps): void {\n // BEFORE `on`, so a host we do not serve gets NO LISTENER AT ALL rather than one that returns.\n // A registered-but-inert listener would be one edit away from being the hazard again, and this\n // way the absence is what the test asserts.\n if (!PASTE_HOSTS.has(deps.protocol)) return;\n deps.on('paste', (e) => {\n if (isEditableTarget(e.target)) return;\n const text = e.clipboardData?.getData('text/plain') ?? '';\n /*\n AN EMPTY CLIPBOARD IS NOT A PASTE: it would type nothing and warn about nothing, so claiming\n it would take the operator's selection away for no visible reason.\n\n THE PREDICATE IS `text === ''` AND NOT \"TEXT THAT WOULD TYPE NOTHING\", which is a NEAR MISS\n deliberately not chased. MEASURED 2026-10-26 against the real `pasteString`: a `'\\r'`-only\n or `'\\r\\r\\r'` clipboard returns `{typed: 0, dropped: 0}` with no warning and the cursor\n unmoved -- indistinguishable from empty, so it DOES cost the selection for no visible\n effect, which is the thing the paragraph above says must not happen.\n\n It is left alone because the obvious tightening is wrong: `'\\n'` ALSO types nothing\n (`{typed: 0, dropped: 0}`) and yet is a REAL paste -- measured, it moves the cursor to the\n next unprotected field (0 -> 80 on a fresh 3278-2). So \"would type nothing\" does not\n separate the two, and any trim that did would be a second copy of `pasteString`'s drop set\n living in a DOM listener, free to drift from the one in `frontend/src/paste.ts`. A\n CR-only clipboard costing a highlight is the cheaper defect.\n */\n if (text === '') return;\n // `preventDefault` ONLY once we have claimed it, matching the `keydown` listeners' rule: a\n // paste we are not handling must keep its default behavior.\n e.preventDefault();\n // ANY ACTION THAT DOES SOMETHING INVALIDATES THE SELECTION, for the reason `renderer.ts`'s\n // `keydown` listener gives at length: the screen is about to change under it, and a highlight\n // over changed text would offer a copy of something never seen. AFTER the two returns above,\n // so a paste we declined leaves the selection alone -- the same ordering, for the same reason.\n deps.clearSelection();\n deps.sendAction({ kind: 'paste', text });\n });\n}\n\n/** The shape this module reads off a real `copy` `ClipboardEvent`. */\ntype CopyEventLike = {\n readonly target: { tagName?: unknown; isContentEditable?: unknown } | null;\n readonly clipboardData?: { setData: (format: string, data: string) => void } | null;\n readonly preventDefault: () => void;\n};\n\nexport interface ForwardCopyDeps {\n /** Register the listener. The real caller passes `window.addEventListener`. */\n readonly on: (type: string, handler: (e: CopyEventLike) => void) => void;\n /** `location.protocol`, gated on exactly as `ForwardPasteDeps.protocol` is. */\n readonly protocol: string;\n /** The prefetched text for the CURRENT selection, or `undefined`. See `copyCache.ts`. */\n readonly cached: () => string | undefined;\n /** The renderer's own `copySelection`: the async fallback, which `Ctrl-Shift-C` also reaches. */\n readonly copySelection: () => boolean;\n /**\n * Called once for every copy this listener took responsibility for, by either path. The\n * `Ctrl-Shift-C` arm reads it to know whether its `execCommand('copy')` reached here, and falls\n * back itself if not -- a browser that silently ignores `execCommand` still copies.\n */\n readonly onHandled: () => void;\n /**\n * Called after the CACHED path set its text and cancelled the event -- the one route on which\n * this listener knows the copy succeeded. OPTIONAL. 2026-11-03: `renderer.ts` uses it to take\n * down a \"copy failed\" notice, so the operator sees the retry worked. Not `onHandled`, which\n * fires on the fallback too, where nothing has been copied yet.\n */\n readonly onCopied?: () => void;\n}\n\n/**\n * THE BROWSER'S OWN COPY EVENT -- `Cmd-C` on macOS, Edit > Copy anywhere, and since 2026-11-02 the\n * `execCommand('copy')` that `renderer.ts`'s `Ctrl-Shift-C` arm raises. 2026-11-02.\n *\n * TWO PATHS, CHOSEN BY WHETHER THE TEXT IS ALREADY HERE. The text lives at the gateway, because\n * only the gateway can extract it, so the renderer PREFETCHES it when a drag ends (`copyCache.ts`).\n * - CACHED: the text is set on the event's own `clipboardData`, synchronously, inside the event.\n * That needs no secure context and no activation window, which is what makes copy work at a\n * plain-`http` LAN address, where `navigator.clipboard` does not exist.\n * - NOT CACHED -- the prefetch has not answered yet, or failed; the selection changed or was\n * cleared before its answer landed; or the event carries no `clipboardData` -- a second\n * trigger for `copySelection`, the async path. The text goes to the gateway as\n * `{kind:'copy', rect}` and comes back as `copyText`, exactly as before.\n * MEASURED 2026-10-27 in Chromium: the `copy` event fires `isTrusted` with no DOM selection, so it\n * reaches the page even though nothing is selected in the document.\n *\n * ## `Ctrl-C` CANNOT REACH THIS, AND THAT IS WHAT KEEPS CLEAR WORKING\n *\n * `keys.ts` claims `Ctrl-C` as Clear and `renderer.ts` calls `preventDefault` on it. MEASURED\n * 2026-10-27: that suppresses the `copy` event entirely, so on Linux and Windows the native copy key\n * stays the Clear AID: only the menu route and `Ctrl-Shift-C`'s own `execCommand('copy')` (raised\n * by `renderer.ts`'s `copyByChord`) arrive here. `Cmd-C` is unclaimed, so it does arrive.\n *\n * ## `preventDefault` ONLY WHEN TEXT WAS SET\n *\n * Under the Clipboard API spec, cancelling a `copy` tells the browser to write the event's own\n * `clipboardData`. On the cached path that is the point: the data was just set, and without the\n * cancel the browser would ignore it and copy the (empty) DOM selection instead. On the fallback\n * path it must NOT happen, and the paste listener's shape must not be copied there: `clipboardData`\n * is empty because the text is still at the gateway, and on a LAN page there is no clipboard API to\n * overwrite it afterwards. MEASURED 2026-11-02 in Electron's Chromium: a cancelled, empty copy did\n * NOT blank the clipboard. Firefox and Safari are UNMEASURED, so the fallback rule is a precaution\n * and not a fix for a known failure. The unit test pins it; the browser harness cannot, since\n * Chromium behaves the same either way. Left alone, the default copies an empty DOM selection,\n * which does nothing.\n *\n * ## THE GATE IS `PASTE_HOSTS`, FOR A WEAKER REASON THAN PASTE'S\n *\n * A stray `copy` action in the Electron GUI would not freeze main (`gui/src/main.ts` has a `copy`\n * arm), but the GUI already copies through its menu accelerator, `Cmd+C` on macOS, so a listener\n * there could only make one keystroke copy twice. The web gateway is the only host that needs it.\n */\nexport function forwardCopy(deps: ForwardCopyDeps): void {\n if (!PASTE_HOSTS.has(deps.protocol)) return;\n deps.on('copy', (e) => {\n // The transfer form's text fields must copy their own selection, not the screen's.\n if (isEditableTarget(e.target)) return;\n deps.onHandled();\n const text = deps.cached();\n // `''` FALLS BACK TOO: set and cancelled, it would write an EMPTY clipboard. `copyCache` never\n // holds one, but this module does not lean on that.\n if (text !== undefined && text !== '' && e.clipboardData) {\n // THE SYNCHRONOUS PATH: no secure context needed, no activation window. `preventDefault` is\n // what makes the browser write the data just set -- correct here, and only here.\n e.clipboardData.setData('text/plain', text);\n e.preventDefault();\n deps.onCopied?.();\n return;\n }\n // Nothing cached for this selection, or no `clipboardData` to set it on: the async path. NO\n // `preventDefault`, for the reason in the docstring. The return value is discarded, as\n // `copyByChord` discards it: no selection means nothing to copy, not worth a message.\n deps.copySelection();\n });\n}\n", "import { createKeypadUi } from '@tn3270/canvas';\nimport { forwardKeys } from '@tn3270/canvas/dist/forwardKeys.js';\n\n/**\n * The keypad window's entry point: the real DOM, handed to the testable view.\n *\n * ## EVERY LINE HERE IS UNTESTABLE, AND THAT IS WHY THERE ARE SO FEW OF THEM\n *\n * `vitest.config.ts` sets `environment: 'node'`, so nothing in this repo can execute a line of\n * this file -- it reads `document` in its module body. `keypadUi.ts` holds every decision; this\n * only adapts primitives.\n *\n * THAT SPLIT IS THE LESSON FROM `transferBoot.ts`, which took the other side of the same choice\n * and runs to several hundred untestable lines as a result (an exact count stood here until\n * 2026-10-06, and rotted the next time that file gained a comment) -- and which shipped this project's one BLANK WINDOW\n * bug, a TDZ read on an uninitialised `const`. The recorded finding from that is exact:\n * **optional chaining does not guard a TDZ read** (`ui?.x` still throws), so there is no clever\n * guard in this file -- just two hard failures with messages, and a `const` declared after\n * everything that reads it.\n *\n * The other half of that lesson: an untestable boot file must be LOADED to be believed, never\n * reasoned about. Task 8's Xvfb harness is what loads this one.\n *\n * ## THE THROWS REACH A HUMAN, WHICH IS WHY THEY ARE THROWS\n *\n * `main.ts` forwards this window's `console-message` to stdout as `keypad[level]`, so an\n * uncaught throw here prints with a file and line rather than vanishing. A silent `return` would\n * leave a window with a title and no buttons -- indistinguishable from a styling bug, and the\n * exact shape this repo keeps meeting.\n */\nconst root = document.getElementById('keys');\nif (root === null) {\n throw new Error('keypad window: #keys is missing from keypad.html');\n}\n\n/**\n * The preload's bridge, read ONCE into a local.\n *\n * NOT RE-READ PER CLICK, deliberately: `contextBridge.exposeInMainWorld` defines a non-writable\n * property, so it cannot change under us, and reading it here means a missing preload fails at\n * LOAD time with the message below rather than on the operator's first button press.\n */\nconst bridge = (window as unknown as {\n tn3270keypad?: { sendAction: (action: unknown) => void };\n}).tn3270keypad;\nif (bridge === undefined) {\n // A MISSING BRIDGE IS A LOUD FAILURE AND NOT A DEAD KEYPAD. The preload is a `.cjs` loaded by\n // absolute path from main; if it failed to load, every one of the 48 buttons would silently do\n // nothing -- which looks like a protocol problem and is not one.\n throw new Error('keypad window: the tn3270keypad bridge is absent (preload failed to load?)');\n}\n\ncreateKeypadUi({\n root,\n create: (tag) => document.createElement(tag),\n append: (parent, child) => { parent.appendChild(child); },\n setText: (el, text) => { el.textContent = text; },\n setTitle: (el, title) => { el.title = title; },\n setAttr: (el, name, value) => { el.setAttribute(name, value); },\n onClick: (el, fn) => { el.addEventListener('click', fn); },\n sendAction: (action) => { bridge.sendAction(action); },\n});\n\n/**\n * AND THE PHYSICAL KEYBOARD, for the reason `keyboardBoot.ts` gives at length.\n *\n * THIS WINDOW HAD THE IDENTICAL GAP and the user only named the keyboard one. Fixing a single\n * window would leave the other broken the same way, with the same silent symptom, and the fix is\n * the same two lines.\n */\nforwardKeys({\n on: (type, handler) => { window.addEventListener(type, handler as () => void); },\n sendAction: (action) => { bridge.sendAction(action); },\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// packages/gui/dist/keypadPreload.cjs
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
var electron_1 = require("electron");
|
|
6
|
+
electron_1.contextBridge.exposeInMainWorld("tn3270keypad", {
|
|
7
|
+
/**
|
|
8
|
+
* Fire a keypad action at the session.
|
|
9
|
+
*
|
|
10
|
+
* `send`, NOT `invoke`: there is nothing to answer. The screen repaints by the frame that main
|
|
11
|
+
* pushes to the CANVAS window afterwards, which is a different window entirely -- so a reply
|
|
12
|
+
* here would be a promise resolving to nothing, and awaiting it would only hide that.
|
|
13
|
+
*/
|
|
14
|
+
sendAction: (action) => {
|
|
15
|
+
electron_1.ipcRenderer.send("action", action);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=keypadPreload.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/keypadPreload.cts"],
|
|
4
|
+
"sourcesContent": ["import { contextBridge, ipcRenderer } from 'electron';\n\n/**\n * The keypad window's bridge. A `.cts` FILE ON PURPOSE -- an ESM preload fails with \"Cannot use\n * import statement outside a module\" and the bridge then SILENTLY NEVER APPEARS, leaving a window\n * whose buttons all do nothing and no error anywhere. See `preload.cts` and\n * `transferPreload.cts`, which carry the same note for the same measured reason.\n *\n * `tsconfig.json` already includes `src/**\\/*.cts`, so `tsc` emits `dist/keypadPreload.cjs` with\n * no build-script step -- the same path `transferPreload.cjs` takes.\n *\n * ## ONE FUNCTION, WHICH IS THE WHOLE SURFACE\n *\n * A keypad press is exactly the `sendAction` the canvas window already has, so this window needs\n * nothing else: no `invoke`, no reply, no events. It reuses main's EXISTING `ipcMain.on('action')`\n * handler, which is why this feature adds no new IPC channel anywhere -- and why a button here\n * reaches `applyAction` by precisely the route a canvas keypad click used to.\n *\n * ## FIVE FUNCTIONS IN THE TRANSFER BRIDGE DID NOT BREAK THE FOUR-FUNCTION RULE, AND NOR DOES ONE HERE\n *\n * That rule (`web/src/bridgecore.ts`) is about the CANVAS bridge, whose width is what lets\n * `renderer.ts` be reused UNMODIFIED by the web gateway. This is a different window with a\n * different surface. Widening the canvas bridge to carry keypad presses instead would be the\n * actual mistake: the browser front end would have to stub a window it cannot open.\n *\n * ## THE EXPOSED NAME IS UNIQUE, AND THAT IS MEASURED RATHER THAN STYLISTIC\n *\n * `exposeInMainWorld` defines a NON-WRITABLE property, and `main.ts:161-164` records the measured\n * failure from a collision: \"Cannot assign to read only property 'tn3270' of object '#<Window>'\".\n * The canvas window is `tn3270` and the transfer window `tn3270transfer`, so this is\n * `tn3270keypad`.\n *\n * `contextIsolation` stays on and `nodeIntegration` off. This window sits in front of a logged-on\n * host session and sends it AIDs, so it is the last place to grant Node access.\n */\ncontextBridge.exposeInMainWorld('tn3270keypad', {\n /**\n * Fire a keypad action at the session.\n *\n * `send`, NOT `invoke`: there is nothing to answer. The screen repaints by the frame that main\n * pushes to the CANVAS window afterwards, which is a different window entirely -- so a reply\n * here would be a promise resolving to nothing, and awaiting it would only hide that.\n */\n sendAction: (action: unknown): void => { ipcRenderer.send('action', action); },\n});\n"],
|
|
5
|
+
"mappings": ";;;;AAAA,IAAA,aAAA,QAAA,UAAA;AAmCA,WAAA,cAAc,kBAAkB,gBAAgB;;;;;;;;EAQ9C,YAAY,CAAC,WAAyB;AAAG,eAAA,YAAY,KAAK,UAAU,MAAM;EAAG;CAC9E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|