@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.
@@ -0,0 +1,501 @@
1
+ // packages/frontend/dist/keypad.js
2
+ var pfRow = (from) => Array.from({ length: 12 }, (_, i) => ({
3
+ label: `PF${from + i}`,
4
+ action: { kind: "pf", n: from + i },
5
+ name: `PF${from + i}`
6
+ }));
7
+ var KEYPAD_KEYS = Object.freeze([
8
+ ...pfRow(13),
9
+ ...pfRow(1),
10
+ { label: "PA1", action: { kind: "pa", n: 1 }, name: "PA1" },
11
+ { label: "PA2", action: { kind: "pa", n: 2 }, name: "PA2" },
12
+ { label: "PA3", action: { kind: "pa", n: 3 }, name: "PA3" },
13
+ { label: "Home", action: { kind: "home" }, name: "Home" },
14
+ { label: "^", action: { kind: "up" }, name: "Cursor up" },
15
+ { label: "Ins", action: { kind: "toggleInsert" }, name: "Insert mode" },
16
+ { label: "Dup", action: { kind: "dup" }, name: "Dup" },
17
+ { label: "Reset", action: { kind: "reset" }, name: "Reset" },
18
+ { label: "Attn", action: { kind: "attn" }, name: "Attention" },
19
+ { label: "SysRq", action: { kind: "sysreq" }, name: "System Request" },
20
+ { label: "Clear", action: { kind: "clear" }, name: "Clear" },
21
+ { label: "<", action: { kind: "left" }, name: "Cursor left" },
22
+ { label: "v", action: { kind: "down" }, name: "Cursor down" },
23
+ { label: ">", action: { kind: "right" }, name: "Cursor right" },
24
+ { label: "FldMk", action: { kind: "fieldMark" }, name: "Field Mark" },
25
+ { label: "Enter", action: { kind: "enter" }, name: "Enter" },
26
+ { label: "ErEOF", action: { kind: "eraseEOF" }, name: "Erase EOF" },
27
+ { label: "ErInp", action: { kind: "eraseInput" }, name: "Erase Input" },
28
+ { label: "Tab", action: { kind: "tab" }, name: "Tab" },
29
+ { label: "BkTab", action: { kind: "backTab" }, name: "Back Tab" },
30
+ { label: "Del", action: { kind: "delete" }, name: "Delete" },
31
+ { label: "BkSp", action: { kind: "backspace" }, name: "Backspace" },
32
+ // The 47th key, and the only one on this row past `BkSp`: row 4 ended at column 36, so this
33
+ // takes 42 for a right edge of 48 -- inside 72, and NO NEW ROW, which is what keeps the Electron
34
+ // window the same size and leaves the two non-keypad goldens byte-identical.
35
+ { label: "NewLn", action: { kind: "newline" }, name: "Newline" },
36
+ // The 48th key, and the FIRST whose action `applyAction` throws on -- see the note on
37
+ // `KeypadKey.action`, which this key made it necessary to rewrite. A transfer needs arguments,
38
+ // so the button opens a form; the front end intercepts it exactly as it intercepts Ctrl-T.
39
+ //
40
+ // `NewLn` ended at 48, so this takes 48 for a right edge of 54 -- still inside 72 and still NO
41
+ // NEW ROW, so the Electron window keeps its size and only the keypad golden moves. The gap at
42
+ // column 18 is NOT free space: it separates the Tab/BkTab clusters deliberately.
43
+ { label: "Xfer", action: { kind: "transferForm" }, name: "File Transfer" }
44
+ ]);
45
+
46
+ // packages/frontend/dist/bindings.js
47
+ var BINDING_INTENT = Object.freeze([
48
+ {
49
+ key: "Enter",
50
+ action: { kind: "enter" },
51
+ terminal: "\r",
52
+ note: 'the Enter AID -- EXCEPT while disconnected, when it RECONNECTS to the same host instead of sending anything. VM prints "Press Enter or Clear to continue" as a LOGOFF drops the line, and both keys used to do nothing at all. A deliberate divergence: x3270 binds no key to `Reconnect()`. See `applyAction`, and the OIA, which says so'
53
+ },
54
+ {
55
+ key: "Ctrl-C",
56
+ action: { kind: "clear" },
57
+ terminal: "",
58
+ note: "the Clear AID, not an interrupt: it dismisses VM's MORE... state. Also RECONNECTS while disconnected, exactly as Enter does -- VM names both keys"
59
+ },
60
+ {
61
+ key: "Ctrl-]",
62
+ action: { kind: "quit" },
63
+ terminal: "",
64
+ note: "the way out, because Ctrl-C is Clear. Say so on startup."
65
+ },
66
+ { key: "Ctrl-R", action: { kind: "reset" }, terminal: "" },
67
+ { key: "Ctrl-U", action: { kind: "eraseInput" }, terminal: "" },
68
+ { key: "Tab", action: { kind: "tab" }, terminal: " " },
69
+ { key: "Shift-Tab", action: { kind: "backTab" }, terminal: "\x1B[Z" },
70
+ { key: "Backspace", action: { kind: "backspace" }, terminal: "\x7F" },
71
+ { key: "Delete", action: { kind: "delete" }, terminal: "\x1B[3~" },
72
+ {
73
+ key: "End",
74
+ action: { kind: "eraseEOF" },
75
+ terminal: "\x1B[4~",
76
+ note: "vt220 Select/End, bound to EraseEOF as a CHOICE following c3270 -- not a terminfo-derived mapping"
77
+ },
78
+ { key: "F1", action: { kind: "pf", n: 1 }, terminal: "\x1BOP" },
79
+ { key: "F3", action: { kind: "pf", n: 3 }, terminal: "\x1BOR" },
80
+ {
81
+ key: "F12",
82
+ action: { kind: "pf", n: 12 },
83
+ terminal: "\x1B[24~",
84
+ note: "the function keys are irregular: F7-F11 are 18/19/20/21/23~, with no 22~"
85
+ },
86
+ {
87
+ key: "Shift-F1",
88
+ action: { kind: "pf", n: 13 },
89
+ terminal: "\x1B[1;2P",
90
+ note: "Shift+F(n) is PF(n+12), the c3270 convention"
91
+ },
92
+ {
93
+ key: "Alt-1",
94
+ action: { kind: "pa", n: 1 },
95
+ terminal: "\x1B1",
96
+ note: 'the PA keys have no terminal equivalent, so ESC-digit, as c3270 does. The GUI matches e.code (Digit1), NOT e.key: on macOS Option-1 reports key "\xA1".'
97
+ },
98
+ { key: "Alt-2", action: { kind: "pa", n: 2 }, terminal: "\x1B2" },
99
+ { key: "Alt-3", action: { kind: "pa", n: 3 }, terminal: "\x1B3" },
100
+ {
101
+ key: "Ctrl-A",
102
+ action: { kind: "attn" },
103
+ terminal: "",
104
+ note: "c3270's own default (Common/fb-c3270:83). A Telnet BREAK, not an AID."
105
+ },
106
+ {
107
+ key: "Insert",
108
+ action: { kind: "toggleInsert" },
109
+ terminal: "\x1B[2~",
110
+ note: "x3270's Toggle(insertMode) (fb-x3270:210); `tput kich1` measured \\x1b[2~"
111
+ },
112
+ {
113
+ key: "Ctrl-D",
114
+ action: { kind: "dup" },
115
+ terminal: "",
116
+ note: "c3270's own binding (Common/fb-c3270:186, and :88 in its _WIN32 keymap). Writes EBCDIC 0x1c and then TABS to the next unprotected field: kybd.c:1435 suppresses `key_Character`'s auto-skip for a keyboard Dup, but `Dup_action` moves the cursor itself at kybd.c:2790, so the tab happens exactly ONCE. The manual says so outright (p. 7-12, \"a Tab key operation to be performed\")"
117
+ },
118
+ {
119
+ key: "Ctrl-F",
120
+ action: { kind: "fieldMark" },
121
+ terminal: "",
122
+ note: "c3270's own binding (Common/fb-c3270:187, and :93 in its _WIN32 keymap). Writes EBCDIC 0x1e and auto-skips normally, as any typed character does -- the Dup suppression above is specific to EBC_dup"
123
+ },
124
+ {
125
+ key: "Ctrl-K",
126
+ action: { kind: "toggleKeypad" },
127
+ terminal: "\v",
128
+ note: "shows the keypad (GUI, browser) or the special-keys overlay (TUI). ALSO c3270's own: `Ctrl<Key>k: Keypad()` at Common/fb-c3270:191. Its _WIN32 keymap spells the same command Alt-K (:48), which would be ESC k in a terminal and so is not bound there; the canvas front ends accept both. NOT a 3270 key -- `applyAction` throws on it and each front end owns its own display"
129
+ },
130
+ {
131
+ key: "Ctrl-B",
132
+ action: { kind: "toggleKeyboard" },
133
+ note: "opens the FULL 122-key 3279 keyboard -- THE CANVAS FRONT ENDS ONLY, which is why this row gives NO `terminal` sequence. The TUI renders the keypad as a LIST because it cannot draw a keyboard inside 24x80, so `buildTable` deliberately leaves 0x02 unmapped and this entry documents an intent two of the four front ends honour. Stating a sequence here would be a lie the table's own test catches, since that test demands every `terminal` resolve in the terminal keymap. NOT c3270's either: it binds no such key in either keymap, so the spelling is ours. Invisible to the TUI overlay, which looks chords up BY ACTION from KEYPAD_KEYS -- a table this kind may never appear in, since a button for it would toggle the surface it is drawn on"
134
+ },
135
+ {
136
+ key: "Alt-M",
137
+ action: { kind: "geometryMenu" },
138
+ note: "opens the MODEL/OVERSIZE chooser -- the GUI's Model menu, the browser's picker overlay -- whose choice applies at the next connect. THE CANVAS FRONT ENDS ONLY, which is why this row gives NO `terminal` sequence, as Ctrl-B's does not: the TUI changes size by being RESIZED and model only by flag, so there is no chooser for a sequence to open, and Alt-M would be ESC m there besides. Matched on e.code (KeyM), NOT e.key: on macOS Option-M reports key \"\xB5\", the Alt-K trap again. NOT c3270's: it binds no such key. NOT a 3270 key -- `applyAction` throws on it and each front end owns its own display"
139
+ },
140
+ {
141
+ key: "Ctrl-T",
142
+ action: { kind: "transferForm" },
143
+ terminal: "",
144
+ note: "opens the IND$FILE transfer form (TUI). NOT c3270's -- it binds NO transfer key at all, in either half of Common/fb-c3270, so there is no reference spelling to match and 0x14 is free in both. NOT a 3270 key -- `applyAction` throws on it, because a transfer needs arguments and each front end owns its own dialog"
145
+ },
146
+ {
147
+ key: "Ctrl-Shift-C",
148
+ action: { kind: "requestCopy" },
149
+ note: "copies the selected cells. NO `terminal` sequence, and for a stronger reason than Ctrl-B's: the TUI has neither a cell selection nor a clipboard of any kind -- no OSC 52, no selection state over the screen -- so there is nothing for a sequence to do, where Ctrl-B at least names a surface the TUI draws differently. A CANVAS-FRONT-END binding, and Cmd-Shift-C is its macOS spelling: this table names ONE key per action, so the pair is asserted directly in canvas/test/keys.test.ts. THE SHIFTED FORM IS DELIBERATE -- plain Ctrl-C is the Clear AID and stays Clear on every platform, which is why canvas/src/keys.ts matches this chord BEFORE its case-folded Ctrl table. NOT a 3270 key -- `applyAction` throws on it, because only the renderer knows which cells were dragged"
150
+ },
151
+ // Sequence-less on purpose: two encodings each. See the header note on DECCKM.
152
+ { key: "Up", action: { kind: "up" } },
153
+ { key: "Down", action: { kind: "down" } },
154
+ { key: "Left", action: { kind: "left" } },
155
+ { key: "Right", action: { kind: "right" } },
156
+ { key: "Home", action: { kind: "home" } }
157
+ ]);
158
+
159
+ // packages/frontend/dist/keypadView.js
160
+ var byLabel = (label) => {
161
+ const key = KEYPAD_KEYS.find((k) => k.label === label);
162
+ if (key === void 0)
163
+ throw new Error(`no keypad key labelled ${label}`);
164
+ return key;
165
+ };
166
+ var pfRange = (from) => Array.from({ length: 12 }, (_, i) => byLabel(`PF${from + i}`));
167
+ var KEYPAD_BLOCKS = Object.freeze([
168
+ // PF13-24 ABOVE PF1-12. That is `KEYPAD_KEYS`' own order -- it opens `pfRow(0, 13)` then
169
+ // `pfRow(1, 1)` -- and its header cites c3270 for the same arrangement
170
+ // (`Common/c3270/keypad.labels:2` and `:4`). Reversing it here would be a silent relearn for
171
+ // anyone used to the shipped keypad, which is why the test pins the first label of each row
172
+ // rather than just the count.
173
+ { id: "pf-high", title: "PF13-24", keys: pfRange(13) },
174
+ { id: "pf-low", title: "PF1-12", keys: pfRange(1) },
175
+ {
176
+ // The INTERRUPT keys: each one reaches the host OUTSIDE the ordinary read-modified exchange.
177
+ // PA1-3 and Clear go as short-read AIDs; `SysRq` is Telnet IAC AO on a TN3270E session that
178
+ // agreed the SYSREQ function and a TEST REQUEST READ on a classic one (`actions.ts:109-120`
179
+ // records both forms); and `Attn` is TELNET BREAK.
180
+ //
181
+ // `Attn` IS `IAC BREAK` AND NOT `IAC IP`. Stated this loudly because the first version of
182
+ // this comment said IP, and because the two are a real pair of Telnet commands that a reader
183
+ // can plausibly swap. MEASURED: `Telnet.sendAttn()` is `Uint8Array.of(T.IAC, T.BREAK)` with
184
+ // the trace label `'Attn (IAC BREAK)'` (`core/src/telnet.ts:225-230`), both it and
185
+ // `Session.sendAttn()` (`core/src/session.ts:1616`) carry the docstring "The 3270 Attn key is
186
+ // Telnet BREAK (RFC 1576 §8), not an AID", three tests assert the exact two bytes
187
+ // (`core/test/telnet.test.ts:448`, `core/test/session.test.ts:493`,
188
+ // `cli/test/runner.test.ts:373`) and a fourth pins that it never reaches `sendAID`
189
+ // (`frontend/test/actions.test.ts:120`, with the `sendAID` assertion at `:128`), and `T.IP` is
190
+ // referenced NOWHERE in this repository (`grep -rn 'T\.IP\b' packages/*/src` -> no hits). IP
191
+ // is not a loose synonym for BREAK: they are two distinct Telnet commands, and a reader who
192
+ // swaps them has the wrong byte on the wire.
193
+ //
194
+ // BUT THAT IS OURS, NOT x3270's, AND WE DIVERGE FROM IT. An earlier draft of this comment
195
+ // claimed x3270 "draws the same distinction", with `net_break` for Attn and `IAC IP` behind a
196
+ // separate `Interrupt()` action. x3270 does close to the OPPOSITE, read from
197
+ // `Common/kybd.c:978-1002`: `Attn_action`'s docstring is "ATTN key, per RFC 2355. Sends IP,
198
+ // regardless." and its FIRST branch is `if (IN_E) { if (net_bound()) net_interrupt(0); ... }`.
199
+ // `net_break` is only the `IN_3270` fallthrough for a CLASSIC session. And `Interrupt_action`
200
+ // is not a separate key at all any more: its docstring ends "This is now the same as the Attn
201
+ // action" (`:1008`), so x3270 says the two have CONVERGED.
202
+ //
203
+ // A PA1-BEFORE-BREAK CLAIM WAS ALSO DRAFTED HERE AND IS REMOVED, BECAUSE IT IS NOT TRUE OF
204
+ // THE VERSION THIS PROJECT CITES. A newer upstream `Attn_action` does send
205
+ // `ctlr_read_modified(AID_PA1, false)` before `net_break(0)`, commented "This is what PCOMM
206
+ // does in plain TN3270 mode" -- but that line does not exist in SUITE3270 4.5, which is the
207
+ // tree in `~/src/suite3270-4.5` that every other citation in this repo is measured against
208
+ // (`grep -rn 'ctlr_read_modified(AID_PA1' Common/*.c` -> zero hits there; the `IN_3270` arm is
209
+ // a bare `net_break(0)`). Mixing versions is how a wrong wire constant gets in: cite the tree
210
+ // we build and test against, and if a newer one diverges say WHICH version did what.
211
+ //
212
+ // ## FINDING: OUR ATTN DIVERGES FROM x3270 IN TN3270E MODE, AND IS UNTESTED THERE
213
+ //
214
+ // `Session.sendAttn()` (`core/src/session.ts:1617-1620`) is three lines with NO branch on
215
+ // `inTn3270e()`: it calls `Telnet.sendAttn()`, which sends `{IAC, BREAK}` unconditionally. So
216
+ // on a TN3270E session x3270 sends IP and we send BREAK. No test covers Attn in E mode --
217
+ // the only three are classic (cited above) -- and nothing else in the repo records this.
218
+ //
219
+ // THE ASYMMETRY WITH `sysreq()` IS THE REASON TO THINK THIS IS AN OVERSIGHT RATHER THAN A
220
+ // DECISION: `sysreq()` sits IMMEDIATELY ABOVE `sendAttn()` in the same file (`:1596`), it
221
+ // branches on `inTn3270e()` as its first act, and its docstring opens "TWO COMPLETELY
222
+ // DIFFERENT THINGS ON THE WIRE, chosen by whether the session is TN3270E" and cites x3270's
223
+ // `IN_E` split for exactly that. The neighbouring key got the branch; this one did not, and
224
+ // no comment anywhere says it was considered.
225
+ //
226
+ // DELIBERATELY NOT FIXED HERE. Changing what `Attn` puts on the wire is protocol work, it is
227
+ // outside a keypad-presentation feature, and it wants a live TN3270E witness this project
228
+ // does not have -- both of its live hosts answer `IAC WILL TN3270E` with DONT, measured three
229
+ // times (`session.ts`, on `sysreq`). Recorded rather than silently repaired or silently
230
+ // ignored. `actions.ts:108` is a bare `case 'attn': session.sendAttn(); break;` with no wire
231
+ // note, which is why the citation for this half points at core and not at the dispatch.
232
+ //
233
+ // None of the six is the ordinary "submit this screen" that `Enter` is, which is why `Enter`
234
+ // is not in this block despite also being an AID. `Reset` is the odd one, and it is here
235
+ // deliberately: `Keyboard.reset()` (`core/src/keyboard.ts:434`) only calls `oia.reset()`, and
236
+ // the input-inhibit state it clears is the state a host error and the AID keys leave you in.
237
+ //
238
+ // THAT IS AN AFFINITY, NOT A RULE, and the stronger version of it is false: `Enter` and all
239
+ // 24 PF keys raise `SystemWait` through the same `sendAID` path from OTHER blocks
240
+ // (`session.ts:1553`), the three operator-error inhibits come from typing into a protected or
241
+ // numeric field rather than from any key here, and `Attn` -- which IS in this block -- does
242
+ // not touch the OIA at all (`session.ts:1617-1620` goes straight to the Telnet layer). Reset
243
+ // is grouped with the keys whose kind of trouble it answers; it is not claimed that this
244
+ // block is the only way to need it.
245
+ id: "attention",
246
+ title: "Attention",
247
+ keys: ["PA1", "PA2", "PA3", "Attn", "SysRq", "Clear", "Reset"].map(byLabel)
248
+ },
249
+ {
250
+ // Everything that MOVES THE CURSOR without altering the buffer. `NewLn` is a cursor move in
251
+ // 3270 terms and nothing else: `Keyboard.newline()` (`core/src/keyboard.ts:348`) is documented
252
+ // "first unprotected cell at or after the start of the next line" and its whole body assigns
253
+ // `s.cursor`. It belongs here, not beside `Enter`, which its name invites a reader to assume.
254
+ //
255
+ // The four arrows are `^ v < >` because that is what `KEYPAD_KEYS` labels them, and the
256
+ // labels are the button text. `keypad.ts` records why they exist at all: c3270's keypad has
257
+ // no cursor callbacks, but a keypad drivable by MOUSE ALONE needs them or a mouse user could
258
+ // never move the cursor -- which is exactly the DOM keypad's case too.
259
+ id: "cursor",
260
+ title: "Cursor",
261
+ keys: ["Home", "^", "v", "<", ">", "Tab", "BkTab", "NewLn"].map(byLabel)
262
+ },
263
+ {
264
+ // Everything that CHANGES THE BUFFER locally, with no transmission to the host. `Dup` and
265
+ // `FldMk` are here because they are TYPED CHARACTERS and not AIDs -- see the note on the
266
+ // `Action` union in `keymap.ts` -- so a user hunting them under "Attention" beside PA1 would
267
+ // be looking in the wrong place.
268
+ //
269
+ // `Ins` toggles a mode rather than editing, and it is the mode that decides what `Dup` and
270
+ // `FldMk` write into -- the only two keys here that go through `writeControl`
271
+ // (`core/src/keyboard.ts:127` and `:165`). NOT "what every other key in this block does",
272
+ // which an earlier draft of this comment claimed: `insertMode` is read in exactly two places,
273
+ // `type()` (`keyboard.ts:74`) and `writeControl()` (`:223`), and `Del` (`:413`), `BkSp`
274
+ // (`:398`), `ErEOF` (`:369`) and `ErInp` (`:393`) never read it. Four of the six are
275
+ // unaffected by the toggle, so `Ins` earns its place here by editing the buffer like the
276
+ // rest and not by governing them.
277
+ id: "editing",
278
+ title: "Editing",
279
+ keys: ["Ins", "Del", "BkSp", "ErEOF", "ErInp", "Dup", "FldMk"].map(byLabel)
280
+ },
281
+ {
282
+ // The two keys that SUBMIT OR START SOMETHING: `Enter` transmits the modified fields, `Xfer`
283
+ // starts a file transfer. Both reach the host, neither is an interrupt, which is what keeps
284
+ // them out of the `attention` block.
285
+ //
286
+ // `Xfer` IS THE ONE KEY IN THIS TABLE WHOSE ACTION `applyAction` REFUSES -- `actions.ts:57`,
287
+ // "applyAction does not handle transferForm: the front end owns its own dialog" -- so a
288
+ // button wired straight through to `applyAction` is a button that only throws. Whoever
289
+ // renders this block must give it the same treatment that front end already gives Ctrl-T,
290
+ // and THAT DIFFERS BY FRONT END rather than being one rule: the Electron GUI opens a
291
+ // window (`gui/src/main.ts:924`) and the TUI opens an overlay (`tui/src/app.ts:672`), but
292
+ // the web gateway deliberately REJECTS it with an error frame naming the kind
293
+ // (`web/src/protocol.ts:146`), because a browser-initiated transfer would write to the
294
+ // gateway's filesystem and not the operator's. The web keypad therefore still shows the
295
+ // button -- the rejection is the feedback, and `protocol.ts` says so in as many words.
296
+ id: "send",
297
+ title: "Send",
298
+ keys: ["Enter", "Xfer"].map(byLabel)
299
+ }
300
+ ]);
301
+ function tooltipFor(key) {
302
+ const wanted = JSON.stringify(key.action);
303
+ const note = BINDING_INTENT.find((b) => JSON.stringify(b.action) === wanted)?.note;
304
+ if (note === void 0)
305
+ return key.name;
306
+ if (note.startsWith(key.name))
307
+ return note;
308
+ return `${key.name} -- ${note}`;
309
+ }
310
+
311
+ // packages/canvas/dist/keypadUi.js
312
+ function createKeypadUi(deps) {
313
+ for (const block of KEYPAD_BLOCKS) {
314
+ const section = deps.create("div");
315
+ deps.setAttr(section, "class", "keypad-block");
316
+ deps.setAttr(section, "id", `keypad-${block.id}`);
317
+ const heading = deps.create("h2");
318
+ deps.setText(heading, block.title);
319
+ deps.append(section, heading);
320
+ const keys = deps.create("div");
321
+ deps.setAttr(keys, "class", "keypad-keys");
322
+ for (const key of block.keys) {
323
+ const button = deps.create("button");
324
+ deps.setAttr(button, "type", "button");
325
+ deps.setAttr(button, "data-label", key.label);
326
+ deps.setText(button, key.label);
327
+ deps.setTitle(button, tooltipFor(key));
328
+ deps.onClick(button, () => {
329
+ deps.sendAction(key.action);
330
+ });
331
+ deps.append(keys, button);
332
+ }
333
+ deps.append(section, keys);
334
+ deps.append(deps.root, section);
335
+ }
336
+ }
337
+
338
+ // packages/canvas/dist/keys.js
339
+ function layoutCharacter(e) {
340
+ if (e.getModifierState?.("AltGraph") !== true)
341
+ return null;
342
+ if ([...e.key].length === 1)
343
+ return { kind: "type", text: e.key };
344
+ return null;
345
+ }
346
+ var NAMED = Object.freeze({
347
+ Enter: { kind: "enter" },
348
+ ArrowLeft: { kind: "left" },
349
+ ArrowRight: { kind: "right" },
350
+ ArrowUp: { kind: "up" },
351
+ ArrowDown: { kind: "down" },
352
+ Home: { kind: "home" },
353
+ Backspace: { kind: "backspace" },
354
+ Delete: { kind: "delete" },
355
+ // vt220 Select/End bound to EraseEOF, following c3270 -- a CHOICE, not a derivation.
356
+ End: { kind: "eraseEOF" },
357
+ // x3270's Toggle(insertMode), fb-x3270:210.
358
+ Insert: { kind: "toggleInsert" }
359
+ });
360
+ var MODIFIERS = /* @__PURE__ */ new Set([
361
+ "Shift",
362
+ "Control",
363
+ "Alt",
364
+ "Meta",
365
+ "CapsLock",
366
+ "AltGraph",
367
+ "NumLock",
368
+ "ScrollLock"
369
+ ]);
370
+ var CTRL = Object.freeze({
371
+ c: { kind: "clear" },
372
+ r: { kind: "reset" },
373
+ u: { kind: "eraseInput" },
374
+ "]": { kind: "quit" },
375
+ a: { kind: "attn" },
376
+ d: { kind: "dup" },
377
+ f: { kind: "fieldMark" },
378
+ k: { kind: "toggleKeypad" },
379
+ // The full 122-key keyboard, 2026-10-22. Free in both mappers -- the taken chords are
380
+ // C R U ] A D F K T -- and claiming it also suppresses Firefox's bookmarks sidebar, which is
381
+ // correct: `renderer.ts` calls `preventDefault` only for keys we claimed, so claiming it is
382
+ // what makes it ours. Intercepted CLIENT-SIDE in both DOM front ends, exactly as `toggleKeypad`
383
+ // is, and the gateway returns before `applyAction` for it -- see `web/src/main.ts`.
384
+ b: { kind: "toggleKeyboard" },
385
+ /**
386
+ * Ctrl-T, the transfer form -- `BINDING_INTENT`'s own spelling, now that the GUI has a dialog.
387
+ *
388
+ * THE GUI HAD THIS CHORD DEAD WHILE THE WINDOW WORKED, which is how this was found: a real
389
+ * Electron run with `TN3270_GUI_KEYS=Ctrl+T` printed `keys: sent Ctrl+T` and NO `action:` line
390
+ * at all, where a click on the `Xfer` keypad button opened the window fine. The keypad route
391
+ * went through `KEYPAD_KEYS` and never through this table, so the one that was missing was also
392
+ * the one no harness drove.
393
+ *
394
+ * ACCEPTED BY THE WEB FRONT END TOO, since this mapper is shared, and that is safe rather than
395
+ * merely tolerable: `protocol.ts:129-131` REJECTS the kind at decode with a per-client `error`
396
+ * frame, and `web/src/main.ts:151-154` answers a decode failure on that one socket without
397
+ * touching the session or the process. The browser could already produce this kind from the
398
+ * `Xfer` button it draws today, so this adds a second route to an answer that already exists --
399
+ * no new failure mode, and the refusal names its reason. When stage 4 gives the gateway a
400
+ * transfer path that rejection becomes an interception, and this entry needs no change.
401
+ */
402
+ t: { kind: "transferForm" }
403
+ });
404
+ var PA_CODES = Object.freeze({
405
+ Digit1: { kind: "pa", n: 1 },
406
+ Digit2: { kind: "pa", n: 2 },
407
+ Digit3: { kind: "pa", n: 3 }
408
+ });
409
+ function actionForKey(e) {
410
+ if (MODIFIERS.has(e.key))
411
+ return null;
412
+ if (e.shiftKey && !e.altKey && e.ctrlKey !== e.metaKey && e.code === "KeyC") {
413
+ return { kind: "requestCopy" };
414
+ }
415
+ if (e.ctrlKey && !e.altKey && !e.metaKey) {
416
+ return CTRL[e.key.toLowerCase()] ?? null;
417
+ }
418
+ if (e.altKey && !e.ctrlKey && !e.metaKey) {
419
+ if (e.code === "KeyK")
420
+ return { kind: "toggleKeypad" };
421
+ if (e.code === "KeyM")
422
+ return { kind: "geometryMenu" };
423
+ return PA_CODES[e.code] ?? layoutCharacter(e);
424
+ }
425
+ if (e.metaKey || e.altKey)
426
+ return layoutCharacter(e);
427
+ if (e.key === "Tab")
428
+ return e.shiftKey ? { kind: "backTab" } : { kind: "tab" };
429
+ const fn = /^F(\d{1,2})$/.exec(e.key);
430
+ if (fn !== null) {
431
+ const n = Number(fn[1]);
432
+ if (n < 1 || n > 12)
433
+ return null;
434
+ return { kind: "pf", n: e.shiftKey ? n + 12 : n };
435
+ }
436
+ const named = NAMED[e.key];
437
+ if (named !== void 0)
438
+ return named;
439
+ if ([...e.key].length === 1)
440
+ return { kind: "type", text: e.key };
441
+ return null;
442
+ }
443
+
444
+ // packages/canvas/dist/forwardKeys.js
445
+ function isEditableTarget(target) {
446
+ const tag = typeof target?.tagName === "string" ? target.tagName : "";
447
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target?.isContentEditable === true;
448
+ }
449
+ function forwardKeys(deps) {
450
+ deps.on("keydown", (raw) => {
451
+ const e = raw;
452
+ if (isEditableTarget(e.target))
453
+ return;
454
+ const action = actionForKey(e);
455
+ if (action === null)
456
+ return;
457
+ e.preventDefault();
458
+ deps.sendAction(action);
459
+ });
460
+ }
461
+
462
+ // packages/gui/dist/keypadBoot.js
463
+ var root = document.getElementById("keys");
464
+ if (root === null) {
465
+ throw new Error("keypad window: #keys is missing from keypad.html");
466
+ }
467
+ var bridge = window.tn3270keypad;
468
+ if (bridge === void 0) {
469
+ throw new Error("keypad window: the tn3270keypad bridge is absent (preload failed to load?)");
470
+ }
471
+ createKeypadUi({
472
+ root,
473
+ create: (tag) => document.createElement(tag),
474
+ append: (parent, child) => {
475
+ parent.appendChild(child);
476
+ },
477
+ setText: (el, text) => {
478
+ el.textContent = text;
479
+ },
480
+ setTitle: (el, title) => {
481
+ el.title = title;
482
+ },
483
+ setAttr: (el, name, value) => {
484
+ el.setAttribute(name, value);
485
+ },
486
+ onClick: (el, fn) => {
487
+ el.addEventListener("click", fn);
488
+ },
489
+ sendAction: (action) => {
490
+ bridge.sendAction(action);
491
+ }
492
+ });
493
+ forwardKeys({
494
+ on: (type, handler) => {
495
+ window.addEventListener(type, handler);
496
+ },
497
+ sendAction: (action) => {
498
+ bridge.sendAction(action);
499
+ }
500
+ });
501
+ //# sourceMappingURL=keypadBoot.js.map