@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,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
|