@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,799 @@
|
|
|
1
|
+
// packages/frontend/dist/keyboard3279.js
|
|
2
|
+
var ch = (label, row, x, shiftLabel) => ({
|
|
3
|
+
label,
|
|
4
|
+
...shiftLabel === void 0 ? {} : { shiftLabel },
|
|
5
|
+
action: { kind: "type", text: label },
|
|
6
|
+
region: "main",
|
|
7
|
+
row,
|
|
8
|
+
x,
|
|
9
|
+
width: 1,
|
|
10
|
+
name: shiftLabel === void 0 ? label : `${shiftLabel} ${label}`
|
|
11
|
+
});
|
|
12
|
+
var ch2 = (label, region, row, x, name) => ({
|
|
13
|
+
label,
|
|
14
|
+
action: { kind: "type", text: label },
|
|
15
|
+
region,
|
|
16
|
+
row,
|
|
17
|
+
x,
|
|
18
|
+
width: 1,
|
|
19
|
+
name
|
|
20
|
+
});
|
|
21
|
+
var dead = (label, region, row, x, width, name) => ({ label, action: void 0, region, row, x, width, name });
|
|
22
|
+
var SHIFT_NAME = "Shift";
|
|
23
|
+
var shift = (row, x, width, side) => ({
|
|
24
|
+
// `Shft`, NOT `Shift`: the LEFT cap is 1.25 units, which is 31px usable, and five capitals at
|
|
25
|
+
// the main block's 10px type need ~38px. The real keyboard prints `Shift` in full because its
|
|
26
|
+
// cap is physically wider than one unit of this layout's pitch; four characters is the honest
|
|
27
|
+
// abbreviation at this size. The NAME stays `Shift (...)`, which is what the view's `data-key`
|
|
28
|
+
// and both harnesses address -- a label is presentation, a name is the identifier.
|
|
29
|
+
label: "Shft",
|
|
30
|
+
action: void 0,
|
|
31
|
+
region: "main",
|
|
32
|
+
row,
|
|
33
|
+
x,
|
|
34
|
+
width,
|
|
35
|
+
// The side is in the name for the tooltip and for a screen reader, but `keyboardUi.ts` matches
|
|
36
|
+
// on the PREFIX, so both caps drive one state.
|
|
37
|
+
name: `${SHIFT_NAME} (${side}, one-shot)`
|
|
38
|
+
});
|
|
39
|
+
var key = (label, action, region, row, x, width, name) => ({ label, action, region, row, x, width, name });
|
|
40
|
+
var pfRow = (row, first) => Array.from({ length: 12 }, (_, i) => key(`PF${first + i}`, { kind: "pf", n: first + i }, "pf", row, i, 1, `PF${first + i}`));
|
|
41
|
+
var letters = (text, row, x) => [...text].map((c, i) => ch(c, row, x + i));
|
|
42
|
+
var KEYBOARD_3279 = Object.freeze([
|
|
43
|
+
/*
|
|
44
|
+
THE PF STRIP: TWO ROWS ABOVE THE MAIN KEYBOARD, PF13-24 ON TOP OF PF1-12.
|
|
45
|
+
|
|
46
|
+
## THE POSITION IS A DECISION, NOT THE ONLY OPTION -- CONFIRMED BY THE USER 2026-10-22
|
|
47
|
+
|
|
48
|
+
**"There's also a common layout that puts PF13-24 in a block to the right of the numeric
|
|
49
|
+
keypad but I prefer the two rows above the main keyboard."** Both arrangements shipped on real
|
|
50
|
+
3270-family hardware, and this one is what the reference 5271 has -- so the two-band layout in
|
|
51
|
+
`ui.css` (PF strip across the top, the other four regions in a row beneath) is matching the
|
|
52
|
+
photographed unit AND the user's stated preference, which happen to agree.
|
|
53
|
+
|
|
54
|
+
**RECORDED BECAUSE IT WOULD OTHERWISE LOOK LIKE AN UNEXAMINED DEFAULT.** Someone who knows the
|
|
55
|
+
right-hand-block variant could "fix" this toward it in good faith; they should know it was
|
|
56
|
+
considered and declined. If it is ever wanted as an OPTION, the change is in the view's band
|
|
57
|
+
assignment (`keyboardUi.ts` decides which band a region joins, from its name) and not in this
|
|
58
|
+
table -- the geometry here is per-region, so a PF block beside the numpad needs no new
|
|
59
|
+
coordinates, only a different container.
|
|
60
|
+
|
|
61
|
+
## THE ROW ORDER WITHIN THE STRIP
|
|
62
|
+
|
|
63
|
+
PF13-24 above PF1-12 is the hardware's order in the photograph, and it matches both
|
|
64
|
+
`KEYPAD_KEYS` (which opens `pfRow(0, 13)` then `pfRow(1, 1)`) and c3270
|
|
65
|
+
(`Common/c3270/keypad.labels:2` and `:4`). Reversing it would be a silent relearn for anyone
|
|
66
|
+
used to either. The table's test pins the first label of each row rather than only the count,
|
|
67
|
+
which is what catches that.
|
|
68
|
+
*/
|
|
69
|
+
...pfRow(0, 13),
|
|
70
|
+
...pfRow(1, 1),
|
|
71
|
+
/*
|
|
72
|
+
THE LEFT SPECIAL BAR: two columns, five rows.
|
|
73
|
+
|
|
74
|
+
`Attn` and `Clear` are real 3270 keys and work. `Cursr Sel` is a real 3270 key that does NOT:
|
|
75
|
+
`AID.SELECT` exists in core, but `keypad.ts` records the key as dropped by decision because it
|
|
76
|
+
"needs the field-intensity parse and belongs with the light-pen work". It is drawn inert, and
|
|
77
|
+
its tooltip is the place that says it is deferred rather than impossible.
|
|
78
|
+
|
|
79
|
+
`Esc`, `Print Screen`, `Scroll Lock` and `Pause` are PC keys present on this 122-key unit and
|
|
80
|
+
are not 3270 functions. They get no action because they SHOULD not have one.
|
|
81
|
+
|
|
82
|
+
THE TWO BLANK CAPS AT THE BOTTOM carry `Xfer` and nothing. The user spotted them -- "since
|
|
83
|
+
there are two unlabelled keys on the left hand special-key bar, one of those could get the
|
|
84
|
+
`Xfer` label" -- and there is precedent: `KEYPAD_KEYS` already carries `Xfer` as a deliberate
|
|
85
|
+
non-3270 addition, and `keypad.ts` argues a transfer button "is a perfectly sensible thing to
|
|
86
|
+
click" even though `applyAction` throws on the kind, because the front end intercepts it
|
|
87
|
+
exactly as it intercepts the chord.
|
|
88
|
+
*/
|
|
89
|
+
/*
|
|
90
|
+
THE LEFT SPECIAL BAR -- REWRITTEN 2026-10-22, AND FOUR OF ITS CAPS WERE SIMPLY WRONG.
|
|
91
|
+
|
|
92
|
+
I had `Esc`, `Print Screen`, `Scroll Lock` and `Pause` here: PC keys, read off a blurred
|
|
93
|
+
photograph and believed because they looked like a plausible PC-ish column. The sharp reference
|
|
94
|
+
(`DSC08836-scaled.jpeg`) shows what is actually there, and it is all 3270:
|
|
95
|
+
|
|
96
|
+
Attn | Clear
|
|
97
|
+
Cursr | (blank)
|
|
98
|
+
Sel |
|
|
99
|
+
Ex Sel| Erase EOF
|
|
100
|
+
Print | Test <- `Test` is the SHIFTED legend on a blank cap
|
|
101
|
+
(blk) | (blk)
|
|
102
|
+
|
|
103
|
+
**TWO OF THEM ARE FUNCTIONS WE ALREADY IMPLEMENT**, so they become LIVE KEYS rather than grey
|
|
104
|
+
scenery: `Erase EOF` is `eraseEOF`, which the compact keypad has carried all along as `ErEOF`.
|
|
105
|
+
That is the most valuable thing this photograph bought -- not geometry, but two working keys
|
|
106
|
+
where I had drawn dead ones.
|
|
107
|
+
|
|
108
|
+
`Ex Sel` IS *NOT* IMPLEMENTED, and the distinction matters: it is Erase Input on some units and
|
|
109
|
+
a field-select on others, and `keypad.ts` records Erase Input as `ErInp` separately. Rather
|
|
110
|
+
than guess which this cap is, it stays inert with its real name -- the same discipline the
|
|
111
|
+
unreadable caps got.
|
|
112
|
+
|
|
113
|
+
`Print`'s SHIFTED LEGEND IS `Ident`, and the blank cap beside it carries `Test`. Both were read
|
|
114
|
+
by the user on this photograph. Neither has an action here, so they are recorded as legends on
|
|
115
|
+
inert caps: documented as existing and unimplemented rather than silently absent.
|
|
116
|
+
|
|
117
|
+
`Attn`'s OWN SHIFTED LEGEND IS STILL UNREADABLE even here -- it is front-printed and blurred in
|
|
118
|
+
every photograph so far -- as is the row-1 blank's. Those two remain unidentified.
|
|
119
|
+
*/
|
|
120
|
+
key("Attn", { kind: "attn" }, "special-left", 0, 0, 1, "Attention"),
|
|
121
|
+
key("Clr", { kind: "clear" }, "special-left", 0, 1, 1, "Clear"),
|
|
122
|
+
dead("Cur\nSel", "special-left", 1, 0, 1, "Cursor Select (not implemented: light-pen work)"),
|
|
123
|
+
dead("", "special-left", 1, 1, 1, "blank, right of Cursor Select"),
|
|
124
|
+
dead("Ex\nSel", "special-left", 2, 0, 1, "Ex Sel (not implemented: ambiguous function)"),
|
|
125
|
+
// A REAL 3270 FUNCTION WE IMPLEMENT -- the compact keypad has carried it as `ErEOF` all along.
|
|
126
|
+
key("Ers\nEOF", { kind: "eraseEOF" }, "special-left", 2, 1, 1, "Erase EOF"),
|
|
127
|
+
{
|
|
128
|
+
label: "Prnt",
|
|
129
|
+
shiftLabel: "Idnt",
|
|
130
|
+
action: void 0,
|
|
131
|
+
region: "special-left",
|
|
132
|
+
row: 3,
|
|
133
|
+
x: 0,
|
|
134
|
+
width: 1,
|
|
135
|
+
name: "Print (shifted: Ident, not implemented)"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
label: "",
|
|
139
|
+
shiftLabel: "Test",
|
|
140
|
+
action: void 0,
|
|
141
|
+
region: "special-left",
|
|
142
|
+
row: 3,
|
|
143
|
+
x: 1,
|
|
144
|
+
width: 1,
|
|
145
|
+
name: "blank (shifted: Test, not implemented)"
|
|
146
|
+
},
|
|
147
|
+
/*
|
|
148
|
+
THE TWO BLANK CAPS AT THE BOTTOM, AND `Xfer` GOES BACK ON ONE OF THEM.
|
|
149
|
+
|
|
150
|
+
It had moved to the cursor cluster's third cap on the user's reasoning that the cap had no
|
|
151
|
+
unshifted function -- but this photograph IDENTIFIES that cap: it carries the
|
|
152
|
+
`a`-with-a-squiggle glyph, which is delete-to-end-of-line. So it is a real key after all, and
|
|
153
|
+
`Xfer` returns here, to a cap this file has CONFIRMED blank under maximum zoom.
|
|
154
|
+
*/
|
|
155
|
+
key("Xfer", { kind: "transferForm" }, "special-left", 4, 0, 1, "File transfer"),
|
|
156
|
+
dead("", "special-left", 4, 1, 1, "blank, bottom right of the special bar"),
|
|
157
|
+
/*
|
|
158
|
+
THE MAIN BLOCK, five rows.
|
|
159
|
+
|
|
160
|
+
ROW 0, the number row: `| @ # $ % not & * ( )` over `1`-`0`, then `_`/`-`, `+`/`=`, Backspace.
|
|
161
|
+
THE NOT SIGN ON 6 IS THE PROOF THIS IS A 3270 LAYOUT and not a PC one; a PC has `^` there.
|
|
162
|
+
`1` carries `|`, where a PC has `!`.
|
|
163
|
+
*/
|
|
164
|
+
ch("1", 0, 0, "|"),
|
|
165
|
+
ch("2", 0, 1, "@"),
|
|
166
|
+
ch("3", 0, 2, "#"),
|
|
167
|
+
ch("4", 0, 3, "$"),
|
|
168
|
+
ch("5", 0, 4, "%"),
|
|
169
|
+
/*
|
|
170
|
+
`cent` OVER `6`, CORRECTED 2026-10-22 FROM A SECOND PHOTOGRAPH.
|
|
171
|
+
|
|
172
|
+
I first read this as U+00AC NOT SIGN off a blurred cap, and reasoned about EBCDIC to justify
|
|
173
|
+
it. A sharp photograph of another 122-key 3279 -- an IBM 4978, a DIFFERENT layout but the same
|
|
174
|
+
typewriter character set -- shows `cent` unambiguously over `6`, and re-zooming the original
|
|
175
|
+
cap is consistent with that rather than with a not-sign.
|
|
176
|
+
|
|
177
|
+
TWO KEYBOARDS AGREEING BEATS ONE BLURRED CAP PLUS AN ARGUMENT, which is the lesson: the
|
|
178
|
+
EBCDIC reasoning was sound and still produced the wrong glyph, because it explained a reading
|
|
179
|
+
rather than testing it.
|
|
180
|
+
*/
|
|
181
|
+
ch("6", 0, 5, "\xA2"),
|
|
182
|
+
ch("7", 0, 6, "&"),
|
|
183
|
+
ch("8", 0, 7, "*"),
|
|
184
|
+
ch("9", 0, 8, "("),
|
|
185
|
+
/*
|
|
186
|
+
`)` OVER `0` -- NO LONGER AN INFERENCE, CONFIRMED 2026-10-22.
|
|
187
|
+
|
|
188
|
+
The cap is BLANK on the first reference unit (no legend at all under maximum zoom, while
|
|
189
|
+
`_`/`-` beside it is legible, so it is missing or worn smooth), and `)` was originally guessed
|
|
190
|
+
from the `9`/`(` pattern. A sharp photograph of an IBM 4978 shows `)` over `0` directly, so
|
|
191
|
+
the guess is now a reading -- from a different keyboard, which is worth stating.
|
|
192
|
+
*/
|
|
193
|
+
ch("0", 0, 9, ")"),
|
|
194
|
+
ch("-", 0, 10, "_"),
|
|
195
|
+
/*
|
|
196
|
+
`+` OVER `=`, CONFIRMED by the 4978's own `+`/`=` cap -- though note that keyboard carries an
|
|
197
|
+
EXTRA cap to the right of it, a bullet over plus-minus, which this one does not have. Two
|
|
198
|
+
122-key 3279 keyboards, two different furniture layouts; the CHARACTER SET agrees where both
|
|
199
|
+
have the key, which is exactly how far a cross-check can be trusted.
|
|
200
|
+
*/
|
|
201
|
+
ch("=", 0, 11, "+"),
|
|
202
|
+
// BACKSPACE IS 3 UNITS, which is what closes row 0 at 15 -- and the photograph shows it as the
|
|
203
|
+
// widest cap on that row, running to the right edge above the Return key.
|
|
204
|
+
key("\u2190", { kind: "backspace" }, "main", 0, 12, 3, "Backspace"),
|
|
205
|
+
/*
|
|
206
|
+
ROWS 1-4, AS A TRUE RECTANGLE -- REWRITTEN 2026-10-22 FROM A SHARP REFERENCE PHOTOGRAPH
|
|
207
|
+
(`DSC08836-scaled.jpeg`), which is the same 5271 and finally legible.
|
|
208
|
+
|
|
209
|
+
## EVERY ROW ENDS AT EXACTLY 15 UNITS, AND THAT IS WHY
|
|
210
|
+
|
|
211
|
+
The block used to end at 14, 15, 13.75, 13.5 and 14 units -- ragged by up to 1.5 units, which
|
|
212
|
+
the user saw immediately: "can we make the main keyboard an actual rectangle?" A real keyboard
|
|
213
|
+
is a rectangle because its leading keys STAGGER and its edge keys are WIDE, not because it has
|
|
214
|
+
dead space, so that is what this now models.
|
|
215
|
+
|
|
216
|
+
## THE STAGGER IS MEASURED, NOT EYEBALLED
|
|
217
|
+
|
|
218
|
+
`Z` MUST CENTRE BETWEEN `A` AND `S`, which the user named as the test. With Caps at 1.75 units
|
|
219
|
+
`A` centres at 2.25 and `S` at 3.25, so `Z` must centre at 2.75 -- which a 2.25-unit left
|
|
220
|
+
Shift gives exactly. Tab 1.5, Caps 1.75, Shift 2.25 is the classic ANSI-ish progression and it
|
|
221
|
+
falls out of that one requirement rather than being copied from a PC.
|
|
222
|
+
|
|
223
|
+
## THE RETURN KEY SPANS TWO ROWS, WHICH IS WHAT CLOSES THE RECTANGLE
|
|
224
|
+
|
|
225
|
+
The photograph shows the bent-arrow Return as one tall cap covering the QWERTY and home rows,
|
|
226
|
+
with a SEPARATE small `Enter` on the bottom row. Without the span, row 2 ends 1.25 units short
|
|
227
|
+
and no amount of widening the punctuation keys looks right. `height: 2` is declared on its top
|
|
228
|
+
row and the home row simply has no cap at that `x` -- see `KeyCap.height`.
|
|
229
|
+
|
|
230
|
+
IT IS DRAWN AS A RECTANGLE AND NOT AS THE INVERTED L THE HARDWARE HAS, which is the one place
|
|
231
|
+
this table knowingly departs from the photograph. The L's overhanging half cannot be made
|
|
232
|
+
clickable without covering the `{` below it, and the user's call (2026-10-23) was that a
|
|
233
|
+
key-shaped dead region is worse than an oversized gap. The cap's own comment has the detail.
|
|
234
|
+
*/
|
|
235
|
+
// ROW 1: Tab, QWERTY, two punctuation caps, and the TALL Return.
|
|
236
|
+
key("\u2192|", { kind: "tab" }, "main", 1, 0, 1.5, "Tab"),
|
|
237
|
+
...letters("QWERTYUIOP", 1, 1.5),
|
|
238
|
+
// `!` over a NOT SIGN -- confirmed again on this photograph, right of `P`.
|
|
239
|
+
ch("\xAC", 1, 11.5, "!"),
|
|
240
|
+
// A BROKEN BAR (U+00A6) over the backslash, distinct from the solid `|` on the `1` key.
|
|
241
|
+
ch("\\", 1, 12.5, "\xA6"),
|
|
242
|
+
/*
|
|
243
|
+
RETURN, TWO ROWS TALL AND 1.5 UNITS WIDE.
|
|
244
|
+
|
|
245
|
+
`newline`, NOT `enter`, and that is the 3270 distinction this table has carried from the start:
|
|
246
|
+
`Enter` is the AID that submits the screen and has its own cap on row 4 (and another on the
|
|
247
|
+
numeric pad); this one moves the cursor to the next input field. `keymap.ts` records that no
|
|
248
|
+
TUI chord could express Newline because Ctrl-J IS 0x0a -- a cap has no such spelling problem,
|
|
249
|
+
which is half the reason this surface is worth having.
|
|
250
|
+
*/
|
|
251
|
+
{
|
|
252
|
+
label: "\u21B5",
|
|
253
|
+
action: { kind: "newline" },
|
|
254
|
+
/*
|
|
255
|
+
1.25 UNITS WIDE, WHICH IS THE GEOMETRY THAT KEEPS THE `{` CLICKABLE.
|
|
256
|
+
|
|
257
|
+
It was width 1.5 -- and that OVERLAPPED the `{` cap below it by 0.25 units, because the home
|
|
258
|
+
row's content runs further right than the QWERTY row's (13.75 against 13.5, Caps being 1.75
|
|
259
|
+
where Tab is 1.5). The user saw the `{` lose its border.
|
|
260
|
+
|
|
261
|
+
A `clip-path` notch in `ui.css` was the first fix and it was NOT ENOUGH: a clip changes what
|
|
262
|
+
is PAINTED and not what the box covers, so the Return still sat on top of the `{`. The
|
|
263
|
+
harness said `OVERLAPS by 6px` once it compared boxes instead of hit-testing.
|
|
264
|
+
|
|
265
|
+
1.25 UNITS REMOVES THE OVERLAP AND KEEPS THE RECTANGLE: the home row is 13.75 + 1.25 = 15,
|
|
266
|
+
and the QWERTY row is 13.5 of content, a 0.25-unit gap, then the same 1.25 = 15.
|
|
267
|
+
|
|
268
|
+
THE `x` IS NOT WHAT DOES THAT, AND SAYING SO HERE IS THE POINT -- this comment claimed it was
|
|
269
|
+
until a mutation (2026-10-23) showed otherwise. `ui.css` lifts a tall cap out of flow with
|
|
270
|
+
`position: absolute; right: 0`, so its `x` is NEVER READ and the WIDTH alone sets its left
|
|
271
|
+
edge. 13.75 is recorded because it is where the key really is on the hardware and because
|
|
272
|
+
`x` is how every other cap is placed; change the WIDTH if the left edge must move.
|
|
273
|
+
|
|
274
|
+
AND THE 0.25-UNIT GAP IS LEFT EMPTY, by the user's decision on 2026-10-23. The hardware key
|
|
275
|
+
is an inverted L that fills it; drawn here, the overhang is a region that looks like a key
|
|
276
|
+
and cannot be clicked -- the `{` needs the box to stay narrow. An oversized gap reads as
|
|
277
|
+
deliberate where a dead half-key does not. See `ui.css` for the full note.
|
|
278
|
+
*/
|
|
279
|
+
region: "main",
|
|
280
|
+
row: 1,
|
|
281
|
+
x: 13.75,
|
|
282
|
+
width: 1.25,
|
|
283
|
+
height: 2,
|
|
284
|
+
name: "Newline"
|
|
285
|
+
},
|
|
286
|
+
// ROW 2: Caps (inert), ASDFGHJKL, three punctuation caps. The Return above covers 13.5-15.
|
|
287
|
+
dead("\u21EA", "main", 2, 0, 1.75, "Caps Lock (not implemented: see the header)"),
|
|
288
|
+
...letters("ASDFGHJKL", 2, 1.75),
|
|
289
|
+
ch(";", 2, 10.75, ":"),
|
|
290
|
+
ch("'", 2, 11.75, '"'),
|
|
291
|
+
ch("{", 2, 12.75, "}"),
|
|
292
|
+
/*
|
|
293
|
+
ROW 3: a 2.25-UNIT SHIFT, ZXCVBNM, three punctuation caps, and a WIDE right Shift.
|
|
294
|
+
|
|
295
|
+
BOTH SHIFT CAPS DRIVE THE SAME ONE-SHOT STATE -- neither is distinguishable to us, and they are
|
|
296
|
+
matched by NAME in `canvas/src/keyboardUi.ts`. The left cap's printed legend is `>`/`<` on the
|
|
297
|
+
hardware; it is drawn as `Shft` so the surface reads as a keyboard rather than a puzzle.
|
|
298
|
+
*/
|
|
299
|
+
shift(3, 0, 2.25, "left"),
|
|
300
|
+
...letters("ZXCVBNM", 3, 2.25),
|
|
301
|
+
ch(",", 3, 9.25, "'"),
|
|
302
|
+
ch(".", 3, 10.25, ":"),
|
|
303
|
+
ch("/", 3, 11.25, "?"),
|
|
304
|
+
shift(3, 12.25, 2.75, "right"),
|
|
305
|
+
/*
|
|
306
|
+
ROW 4: Reset, Alt, the spacebar, Alt, a blank, and `Enter`.
|
|
307
|
+
|
|
308
|
+
`Reset` CARRIES `DvCntl` AS ITS SHIFTED LEGEND -- front-printed on the cap, read on this
|
|
309
|
+
photograph and identified by the user as Device Control. We have no action for it, so it is
|
|
310
|
+
recorded as a `shiftLabel` on a cap whose unshifted function we DO implement: pressing Shift
|
|
311
|
+
re-letters it, and clicking it still sends `reset`. That is honest -- the shifted function is
|
|
312
|
+
documented as existing and unimplemented, rather than silently absent.
|
|
313
|
+
*/
|
|
314
|
+
{
|
|
315
|
+
label: "Reset",
|
|
316
|
+
shiftLabel: "DvCn",
|
|
317
|
+
action: { kind: "reset" },
|
|
318
|
+
region: "main",
|
|
319
|
+
row: 4,
|
|
320
|
+
x: 0,
|
|
321
|
+
width: 1.5,
|
|
322
|
+
name: "Reset (shifted: Device Control, not implemented)"
|
|
323
|
+
},
|
|
324
|
+
dead("Alt", "main", 4, 1.5, 1.25, "Alt, left (not a 3270 key)"),
|
|
325
|
+
{
|
|
326
|
+
label: "",
|
|
327
|
+
action: { kind: "type", text: " " },
|
|
328
|
+
region: "main",
|
|
329
|
+
row: 4,
|
|
330
|
+
x: 2.75,
|
|
331
|
+
width: 7,
|
|
332
|
+
name: "Space"
|
|
333
|
+
},
|
|
334
|
+
dead("Alt", "main", 4, 9.75, 1.25, "Alt, right (not a 3270 key)"),
|
|
335
|
+
dead("", "main", 4, 11, 1.25, "blank, right of the spacebar"),
|
|
336
|
+
key("Enter", { kind: "enter" }, "main", 4, 12.25, 2.75, "Enter"),
|
|
337
|
+
/*
|
|
338
|
+
THE CURSOR CLUSTER.
|
|
339
|
+
|
|
340
|
+
`Dup`+`PA1` and `Field Mark`+`PA2` are DOUBLE-LEGEND CAPS on the hardware -- the lower legend
|
|
341
|
+
is the key's alternate function. We bind the UPPER one, because `Dup` and `Field Mark` are
|
|
342
|
+
typed characters this client implements (`keymap.ts`: "Dup and Field Mark are TYPED CHARACTERS,
|
|
343
|
+
not AIDs") while PA1/PA2 already have caps in the keypad and chords of their own. The lower
|
|
344
|
+
legend is drawn in the label so the cap matches the photograph.
|
|
345
|
+
|
|
346
|
+
THE THREE GLYPH KEYS ABOVE THE ARROWS COULD NOT BE READ at this resolution -- a left-pointing
|
|
347
|
+
arrow, a house-like glyph and a third mark. They are INERT with descriptive names, per this
|
|
348
|
+
file's header rule, and are the first thing to fix if a better photograph turns up.
|
|
349
|
+
*/
|
|
350
|
+
/*
|
|
351
|
+
THE CURSOR CLUSTER, AS A DIAMOND -- REWRITTEN 2026-10-22 FROM THE SHARP REFERENCE.
|
|
352
|
+
|
|
353
|
+
The user prefers this to the modern inverted-T and the photograph shows why: the arrows sit in
|
|
354
|
+
a 3x3 diamond with a CENTRE CAP between them, which is what the hardware actually has.
|
|
355
|
+
|
|
356
|
+
Dup/PA1 | FieldMark/PA2 | PA3
|
|
357
|
+
BackTab | Home | DelEOL
|
|
358
|
+
| up |
|
|
359
|
+
left | (centre) | right
|
|
360
|
+
| down |
|
|
361
|
+
|
|
362
|
+
## WHAT THIS PHOTOGRAPH IDENTIFIED, ALL OF IT NEW
|
|
363
|
+
|
|
364
|
+
- THE THIRD CAP OF ROW 1 is the `a`-with-a-squiggle glyph, which the user read as
|
|
365
|
+
delete-to-end-of-line. We have no such action -- `delete` is a single character and
|
|
366
|
+
`eraseEOF` erases to the end of the FIELD, not the line -- so it is inert with its real
|
|
367
|
+
name. **AND THAT IS WHY `Xfer` MOVED OFF IT**: it is a real key, not a spare.
|
|
368
|
+
- THE CENTRE CAP carries a DIAGONAL ARROW, which the user read as cursor-to-top-left. That is
|
|
369
|
+
plausibly `Home` -- but `Home` already has its own cap two rows up, and a keyboard does not
|
|
370
|
+
usually spend two caps on one function. Inert, with the ambiguity recorded rather than
|
|
371
|
+
guessed away.
|
|
372
|
+
- THE SHIFTED LEGENDS ON LEFT AND RIGHT ARE DOUBLE ARROWS, which the user read as
|
|
373
|
+
beginning/end of line. No action exists for either, so they are `shiftLabel`s on live cursor
|
|
374
|
+
keys: Shift re-letters the cap, and clicking still moves the cursor one position. The
|
|
375
|
+
shifted function is documented as existing and unimplemented.
|
|
376
|
+
*/
|
|
377
|
+
key("Dup\nPA1", { kind: "dup" }, "cursor", 0, 0, 1, "Dup"),
|
|
378
|
+
key("Fld\nMrk\nPA2", { kind: "fieldMark" }, "cursor", 0, 1, 1, "Field Mark"),
|
|
379
|
+
key("PA3", { kind: "pa", n: 3 }, "cursor", 0, 2, 1, "PA3"),
|
|
380
|
+
/*
|
|
381
|
+
REVERSE TAB: `U+21E4 LEFTWARDS ARROW TO BAR`, AND THE DIRECTION WAS MIRRORED BEFORE.
|
|
382
|
+
|
|
383
|
+
I had `←|` -- bar on the RIGHT, as if the arrow left a tab stop. The user caught it and the
|
|
384
|
+
reference photograph is unambiguous: the BAR IS AT THE LEFT with the arrowhead terminating
|
|
385
|
+
into it, because you are going back TO a tab stop, not back FROM one. The semantics were
|
|
386
|
+
backwards, not just the drawing.
|
|
387
|
+
*/
|
|
388
|
+
key("\u21E4", { kind: "backTab" }, "cursor", 1, 0, 1, "Back Tab (cursor cluster)"),
|
|
389
|
+
/*
|
|
390
|
+
`U+21A5 UPWARDS ARROW FROM BAR` FOR INSERT -- AND THIS CAP WAS MISIDENTIFIED AS `Home`.
|
|
391
|
+
|
|
392
|
+
The glyph is a lowercase `a` beneath a WIDE CARET, which the user reads as Insert; the caret is
|
|
393
|
+
the classic insertion mark and the `a` is the character being inserted. I had drawn it as a
|
|
394
|
+
house and called it Home, which was a guess from the shape alone.
|
|
395
|
+
|
|
396
|
+
`toggleInsert` IS AN ACTION WE HAVE, so this becomes a working key rather than scenery -- the
|
|
397
|
+
compact keypad carries it as `Ins`.
|
|
398
|
+
|
|
399
|
+
THE GLYPH IS AN APPROXIMATION, NOT A TRANSCRIPTION: there is no Unicode point for an `a` under
|
|
400
|
+
a caret, so this is the closest available stand-in, and an SVG of the real mark is the someday
|
|
401
|
+
fix.
|
|
402
|
+
|
|
403
|
+
`U+2041 CARET INSERTION POINT`, AND IT IS THE THIRD CHOICE -- the first two were rejected for
|
|
404
|
+
different reasons, which is why all three are recorded.
|
|
405
|
+
|
|
406
|
+
1. `U+21A5 UPWARDS ARROW FROM BAR` was mine, and it is the wrong SHAPE: a thin solid arrow
|
|
407
|
+
with a bar at its foot, where a 12x crop of the cap shows a wide HOLLOW caret.
|
|
408
|
+
2. `U+21E7 UPWARDS WHITE ARROW` was the user's suggestion and matches the shape exactly --
|
|
409
|
+
but **U+21E7 IS THE SHIFT KEY**, universally, and this keyboard HAS two Shift caps. A
|
|
410
|
+
glyph that means Shift on a key that means Insert is worse than one that merely looks
|
|
411
|
+
wrong: it is actively misleading, and the shape being right makes the confusion more
|
|
412
|
+
likely rather than less.
|
|
413
|
+
3. `U+2041 CARET INSERTION POINT` is the proofreader's insertion mark, so right shape and right
|
|
414
|
+
meaning -- and it DOES NOT RENDER. Measured in the harness: 2.3px against 7.5px for a
|
|
415
|
+
glyph known to be present, i.e. a near-blank fallback. General Punctuation is not what the
|
|
416
|
+
symbol fonts in `ui.css` were chosen for.
|
|
417
|
+
4. `U+2303 UP ARROWHEAD` measures 7.5px, exactly matching the reference glyph, and it is a
|
|
418
|
+
hollow caret that means "control/up" rather than Shift. Right shape, available, and
|
|
419
|
+
unclaimed on this keyboard.
|
|
420
|
+
|
|
421
|
+
THE LESSON IS THAT A GLYPH HAS THREE PROPERTIES and all of them matter: what it looks like,
|
|
422
|
+
what it already means elsewhere, and whether the font actually has it. Choice 1 failed the
|
|
423
|
+
first, choice 2 the second, choice 3 the third.
|
|
424
|
+
|
|
425
|
+
STILL AN APPROXIMATION: the hardware draws an `a` tucked under the caret's apex, and no
|
|
426
|
+
Unicode point does that. An SVG remains the someday fix.
|
|
427
|
+
*/
|
|
428
|
+
key("\u2303", { kind: "toggleInsert" }, "cursor", 1, 1, 1, "Insert (approximate glyph)"),
|
|
429
|
+
/*
|
|
430
|
+
`U+219B RIGHTWARDS ARROW WITH STROKE` FOR DELETE CHARACTER, SHIFTED TO TWO OF THEM.
|
|
431
|
+
|
|
432
|
+
The cap is a lowercase `a` with a squiggle struck diagonally up-and-right, and its shifted
|
|
433
|
+
legend is visibly LONGER -- the user reads it as the squiggle running through three `a`s, i.e.
|
|
434
|
+
the same operation extended to the end of the line.
|
|
435
|
+
|
|
436
|
+
I HAD `U+232B ERASE TO THE LEFT`, WHICH WAS WRONG IN MEANING: the user points out the stroke
|
|
437
|
+
goes up and to the RIGHT, so this deletes forward, not backward. `delete` is exactly that
|
|
438
|
+
action and we have it, so the unshifted key is LIVE; the shifted form is
|
|
439
|
+
delete-to-end-of-line, which we do NOT have, and the doubled glyph records it the way the
|
|
440
|
+
doubled arrows record start/end of line on the cursor keys.
|
|
441
|
+
|
|
442
|
+
BOTH GLYPHS ARE APPROXIMATIONS, not transcriptions -- no Unicode point draws an `a` with a
|
|
443
|
+
diagonal strike. Doubling for the shifted form follows the hardware's own idiom and this
|
|
444
|
+
table's existing one.
|
|
445
|
+
*/
|
|
446
|
+
{
|
|
447
|
+
label: "\u219B",
|
|
448
|
+
shiftLabel: "\u219B\u219B",
|
|
449
|
+
action: { kind: "delete" },
|
|
450
|
+
region: "cursor",
|
|
451
|
+
row: 1,
|
|
452
|
+
x: 2,
|
|
453
|
+
width: 1,
|
|
454
|
+
name: "Delete character (shifted: delete to end of line, not implemented)"
|
|
455
|
+
},
|
|
456
|
+
// THE DIAMOND. `up` is centred on its own row; `left`, the centre cap and `right` share the
|
|
457
|
+
// next; `down` is centred below. The row offsets come from the table and `keyboardUi.ts`
|
|
458
|
+
// renders them -- see its note on `padding-left`.
|
|
459
|
+
{
|
|
460
|
+
label: "\u2191",
|
|
461
|
+
action: { kind: "up" },
|
|
462
|
+
region: "cursor",
|
|
463
|
+
row: 2,
|
|
464
|
+
x: 1,
|
|
465
|
+
width: 1,
|
|
466
|
+
name: "Cursor up"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
label: "\u2190",
|
|
470
|
+
shiftLabel: "\u219E",
|
|
471
|
+
action: { kind: "left" },
|
|
472
|
+
region: "cursor",
|
|
473
|
+
row: 3,
|
|
474
|
+
x: 0,
|
|
475
|
+
width: 1,
|
|
476
|
+
name: "Cursor left (shifted: start of line, not implemented)"
|
|
477
|
+
},
|
|
478
|
+
dead("\u2196", "cursor", 3, 1, 1, "diagonal arrow, function unidentified"),
|
|
479
|
+
{
|
|
480
|
+
label: "\u2192",
|
|
481
|
+
shiftLabel: "\u21A0",
|
|
482
|
+
action: { kind: "right" },
|
|
483
|
+
region: "cursor",
|
|
484
|
+
row: 3,
|
|
485
|
+
x: 2,
|
|
486
|
+
width: 1,
|
|
487
|
+
name: "Cursor right (shifted: end of line, not implemented)"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
label: "\u2193",
|
|
491
|
+
action: { kind: "down" },
|
|
492
|
+
region: "cursor",
|
|
493
|
+
row: 4,
|
|
494
|
+
x: 1,
|
|
495
|
+
width: 1,
|
|
496
|
+
name: "Cursor down"
|
|
497
|
+
},
|
|
498
|
+
/*
|
|
499
|
+
THE NUMERIC KEYPAD.
|
|
500
|
+
|
|
501
|
+
The digits are typed characters. The right-hand column could not be fully resolved: the top
|
|
502
|
+
cap reads like `Space`, below it a tab-like glyph, then `-`, then a tall `Enter`. `Enter` is
|
|
503
|
+
legible and is bound; the other three are INERT with descriptive names rather than guessed.
|
|
504
|
+
*/
|
|
505
|
+
// `ch()` builds a `main` cap, so the region is overridden here rather than adding a parameter
|
|
506
|
+
// for one caller. The digits are the same typed characters as the main block's.
|
|
507
|
+
...[..."789"].map((c, i) => ({ ...ch(c, 0, i), region: "numpad" })),
|
|
508
|
+
...[..."456"].map((c, i) => ({ ...ch(c, 1, i), region: "numpad" })),
|
|
509
|
+
...[..."123"].map((c, i) => ({ ...ch(c, 2, i), region: "numpad" })),
|
|
510
|
+
{ ...ch("0", 3, 0), region: "numpad", width: 2, name: "0 (numeric pad)" },
|
|
511
|
+
{ ...ch(".", 3, 2), region: "numpad", name: ". (numeric pad)" },
|
|
512
|
+
/*
|
|
513
|
+
THE NUMERIC PAD'S RIGHT-HAND COLUMN -- READ AT LAST, 2026-10-22, FROM THE ORIGINAL PHOTOGRAPH.
|
|
514
|
+
|
|
515
|
+
These three were inert with `unread` names because I could not resolve them. A 6x crop of the
|
|
516
|
+
SAME image settles all three: `Space`, a FIELD TAB arrow, and `-`, with `Enter` below. **The
|
|
517
|
+
pixels were always there; I gave up on the zoom too early**, which is worth recording next to
|
|
518
|
+
the entries it cost.
|
|
519
|
+
|
|
520
|
+
A SECOND KEYBOARD CONFIRMED NOTHING HERE, and that is the interesting part: the IBM 3270 PC
|
|
521
|
+
(the layout this one otherwise matches exactly) has a PC NUMPAD in this position -- 7/Home,
|
|
522
|
+
8/up, 9/PgUp, with Esc/NumLk/ScrLk above. It is a PC with 3270 emulation, so its numpad is a
|
|
523
|
+
PC's. Transplanting those legends would have been the worst kind of cross-check: a keyboard
|
|
524
|
+
that agrees everywhere else, differing silently in exactly the place I needed.
|
|
525
|
+
*/
|
|
526
|
+
key("Spc", { kind: "type", text: " " }, "numpad", 0, 3, 1, "Space (numeric pad)"),
|
|
527
|
+
// FIELD TAB, the same `→|` glyph the main block's Tab carries.
|
|
528
|
+
key("\u2192|", { kind: "tab" }, "numpad", 1, 3, 1, "Tab (numeric pad)"),
|
|
529
|
+
ch2("-", "numpad", 2, 3, "- (numeric pad)"),
|
|
530
|
+
key("Ent", { kind: "enter" }, "numpad", 3, 3, 1, "Enter (numeric pad)")
|
|
531
|
+
]);
|
|
532
|
+
|
|
533
|
+
// packages/canvas/dist/keyboardUi.js
|
|
534
|
+
var isShiftCap = (cap) => cap.name.startsWith(SHIFT_NAME);
|
|
535
|
+
function createKeyboardUi(deps) {
|
|
536
|
+
let armed = false;
|
|
537
|
+
const shiftButtons = [];
|
|
538
|
+
const dualCaps = [];
|
|
539
|
+
const paintShift = () => {
|
|
540
|
+
for (const button of shiftButtons) {
|
|
541
|
+
if (armed)
|
|
542
|
+
deps.setAttr(button, "data-armed", "true");
|
|
543
|
+
else
|
|
544
|
+
deps.removeAttr(button, "data-armed");
|
|
545
|
+
}
|
|
546
|
+
for (const cap of dualCaps) {
|
|
547
|
+
deps.setText(cap.el, armed ? cap.shifted : cap.plain);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const regions = /* @__PURE__ */ new Map();
|
|
551
|
+
for (const cap of KEYBOARD_3279) {
|
|
552
|
+
const rows = regions.get(cap.region) ?? /* @__PURE__ */ new Map();
|
|
553
|
+
const row = rows.get(cap.row) ?? [];
|
|
554
|
+
row.push(cap);
|
|
555
|
+
rows.set(cap.row, row);
|
|
556
|
+
regions.set(cap.region, rows);
|
|
557
|
+
}
|
|
558
|
+
const bands = deps.create("div");
|
|
559
|
+
deps.setAttr(bands, "class", "keyboard-bands");
|
|
560
|
+
const topBand = deps.create("div");
|
|
561
|
+
deps.setAttr(topBand, "class", "keyboard-band");
|
|
562
|
+
deps.setAttr(topBand, "id", "keyboard-band-pf");
|
|
563
|
+
const lowerBand = deps.create("div");
|
|
564
|
+
deps.setAttr(lowerBand, "class", "keyboard-band");
|
|
565
|
+
deps.setAttr(lowerBand, "id", "keyboard-band-main");
|
|
566
|
+
deps.append(bands, topBand);
|
|
567
|
+
deps.append(bands, lowerBand);
|
|
568
|
+
deps.append(deps.root, bands);
|
|
569
|
+
for (const [region, rows] of regions) {
|
|
570
|
+
const section = deps.create("div");
|
|
571
|
+
deps.setAttr(section, "class", "keyboard-region");
|
|
572
|
+
deps.setAttr(section, "id", `keyboard-${region}`);
|
|
573
|
+
for (const [, caps] of [...rows].sort((a, b) => a[0] - b[0])) {
|
|
574
|
+
const line = deps.create("div");
|
|
575
|
+
deps.setAttr(line, "class", "keyboard-row");
|
|
576
|
+
const firstX = Math.min(...caps.map((c) => c.x));
|
|
577
|
+
if (firstX > 0) {
|
|
578
|
+
deps.setAttr(line, "style", `padding-left:calc(${firstX} * (var(--kb-pitch) + 2px))`);
|
|
579
|
+
}
|
|
580
|
+
for (const cap of caps) {
|
|
581
|
+
const button = deps.create("button");
|
|
582
|
+
deps.setAttr(button, "type", "button");
|
|
583
|
+
deps.setAttr(button, "data-key", cap.name);
|
|
584
|
+
const height = cap.height ?? 1;
|
|
585
|
+
deps.setAttr(button, "style", height > 1 ? `--kb-w:${cap.width};--kb-h:${height}` : `--kb-w:${cap.width}`);
|
|
586
|
+
deps.setText(button, cap.label);
|
|
587
|
+
deps.setTitle(button, cap.name);
|
|
588
|
+
if (isShiftCap(cap)) {
|
|
589
|
+
shiftButtons.push(button);
|
|
590
|
+
deps.setAttr(button, "data-shift", "true");
|
|
591
|
+
deps.onClick(button, () => {
|
|
592
|
+
armed = !armed;
|
|
593
|
+
paintShift();
|
|
594
|
+
});
|
|
595
|
+
deps.append(line, button);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (cap.action === void 0) {
|
|
599
|
+
deps.setAttr(button, "disabled", "");
|
|
600
|
+
deps.append(line, button);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const action = cap.action;
|
|
604
|
+
const shiftLabel = cap.shiftLabel;
|
|
605
|
+
if (shiftLabel !== void 0) {
|
|
606
|
+
dualCaps.push({ el: button, plain: cap.label, shifted: shiftLabel });
|
|
607
|
+
}
|
|
608
|
+
const isCharacter = action.kind === "type";
|
|
609
|
+
deps.onClick(button, () => {
|
|
610
|
+
if (isCharacter && armed) {
|
|
611
|
+
armed = false;
|
|
612
|
+
paintShift();
|
|
613
|
+
deps.sendAction(shiftLabel !== void 0 ? { kind: "type", text: shiftLabel } : action);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
deps.sendAction(action);
|
|
617
|
+
});
|
|
618
|
+
deps.append(line, button);
|
|
619
|
+
}
|
|
620
|
+
deps.append(section, line);
|
|
621
|
+
}
|
|
622
|
+
deps.append(region === "pf" ? topBand : lowerBand, section);
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
shiftArmed: () => armed,
|
|
626
|
+
disarmShift: () => {
|
|
627
|
+
armed = false;
|
|
628
|
+
paintShift();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// packages/canvas/dist/keys.js
|
|
634
|
+
function layoutCharacter(e) {
|
|
635
|
+
if (e.getModifierState?.("AltGraph") !== true)
|
|
636
|
+
return null;
|
|
637
|
+
if ([...e.key].length === 1)
|
|
638
|
+
return { kind: "type", text: e.key };
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
var NAMED = Object.freeze({
|
|
642
|
+
Enter: { kind: "enter" },
|
|
643
|
+
ArrowLeft: { kind: "left" },
|
|
644
|
+
ArrowRight: { kind: "right" },
|
|
645
|
+
ArrowUp: { kind: "up" },
|
|
646
|
+
ArrowDown: { kind: "down" },
|
|
647
|
+
Home: { kind: "home" },
|
|
648
|
+
Backspace: { kind: "backspace" },
|
|
649
|
+
Delete: { kind: "delete" },
|
|
650
|
+
// vt220 Select/End bound to EraseEOF, following c3270 -- a CHOICE, not a derivation.
|
|
651
|
+
End: { kind: "eraseEOF" },
|
|
652
|
+
// x3270's Toggle(insertMode), fb-x3270:210.
|
|
653
|
+
Insert: { kind: "toggleInsert" }
|
|
654
|
+
});
|
|
655
|
+
var MODIFIERS = /* @__PURE__ */ new Set([
|
|
656
|
+
"Shift",
|
|
657
|
+
"Control",
|
|
658
|
+
"Alt",
|
|
659
|
+
"Meta",
|
|
660
|
+
"CapsLock",
|
|
661
|
+
"AltGraph",
|
|
662
|
+
"NumLock",
|
|
663
|
+
"ScrollLock"
|
|
664
|
+
]);
|
|
665
|
+
var CTRL = Object.freeze({
|
|
666
|
+
c: { kind: "clear" },
|
|
667
|
+
r: { kind: "reset" },
|
|
668
|
+
u: { kind: "eraseInput" },
|
|
669
|
+
"]": { kind: "quit" },
|
|
670
|
+
a: { kind: "attn" },
|
|
671
|
+
d: { kind: "dup" },
|
|
672
|
+
f: { kind: "fieldMark" },
|
|
673
|
+
k: { kind: "toggleKeypad" },
|
|
674
|
+
// The full 122-key keyboard, 2026-10-22. Free in both mappers -- the taken chords are
|
|
675
|
+
// C R U ] A D F K T -- and claiming it also suppresses Firefox's bookmarks sidebar, which is
|
|
676
|
+
// correct: `renderer.ts` calls `preventDefault` only for keys we claimed, so claiming it is
|
|
677
|
+
// what makes it ours. Intercepted CLIENT-SIDE in both DOM front ends, exactly as `toggleKeypad`
|
|
678
|
+
// is, and the gateway returns before `applyAction` for it -- see `web/src/main.ts`.
|
|
679
|
+
b: { kind: "toggleKeyboard" },
|
|
680
|
+
/**
|
|
681
|
+
* Ctrl-T, the transfer form -- `BINDING_INTENT`'s own spelling, now that the GUI has a dialog.
|
|
682
|
+
*
|
|
683
|
+
* THE GUI HAD THIS CHORD DEAD WHILE THE WINDOW WORKED, which is how this was found: a real
|
|
684
|
+
* Electron run with `TN3270_GUI_KEYS=Ctrl+T` printed `keys: sent Ctrl+T` and NO `action:` line
|
|
685
|
+
* at all, where a click on the `Xfer` keypad button opened the window fine. The keypad route
|
|
686
|
+
* went through `KEYPAD_KEYS` and never through this table, so the one that was missing was also
|
|
687
|
+
* the one no harness drove.
|
|
688
|
+
*
|
|
689
|
+
* ACCEPTED BY THE WEB FRONT END TOO, since this mapper is shared, and that is safe rather than
|
|
690
|
+
* merely tolerable: `protocol.ts:129-131` REJECTS the kind at decode with a per-client `error`
|
|
691
|
+
* frame, and `web/src/main.ts:151-154` answers a decode failure on that one socket without
|
|
692
|
+
* touching the session or the process. The browser could already produce this kind from the
|
|
693
|
+
* `Xfer` button it draws today, so this adds a second route to an answer that already exists --
|
|
694
|
+
* no new failure mode, and the refusal names its reason. When stage 4 gives the gateway a
|
|
695
|
+
* transfer path that rejection becomes an interception, and this entry needs no change.
|
|
696
|
+
*/
|
|
697
|
+
t: { kind: "transferForm" }
|
|
698
|
+
});
|
|
699
|
+
var PA_CODES = Object.freeze({
|
|
700
|
+
Digit1: { kind: "pa", n: 1 },
|
|
701
|
+
Digit2: { kind: "pa", n: 2 },
|
|
702
|
+
Digit3: { kind: "pa", n: 3 }
|
|
703
|
+
});
|
|
704
|
+
function actionForKey(e) {
|
|
705
|
+
if (MODIFIERS.has(e.key))
|
|
706
|
+
return null;
|
|
707
|
+
if (e.shiftKey && !e.altKey && e.ctrlKey !== e.metaKey && e.code === "KeyC") {
|
|
708
|
+
return { kind: "requestCopy" };
|
|
709
|
+
}
|
|
710
|
+
if (e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
711
|
+
return CTRL[e.key.toLowerCase()] ?? null;
|
|
712
|
+
}
|
|
713
|
+
if (e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
714
|
+
if (e.code === "KeyK")
|
|
715
|
+
return { kind: "toggleKeypad" };
|
|
716
|
+
if (e.code === "KeyM")
|
|
717
|
+
return { kind: "geometryMenu" };
|
|
718
|
+
return PA_CODES[e.code] ?? layoutCharacter(e);
|
|
719
|
+
}
|
|
720
|
+
if (e.metaKey || e.altKey)
|
|
721
|
+
return layoutCharacter(e);
|
|
722
|
+
if (e.key === "Tab")
|
|
723
|
+
return e.shiftKey ? { kind: "backTab" } : { kind: "tab" };
|
|
724
|
+
const fn = /^F(\d{1,2})$/.exec(e.key);
|
|
725
|
+
if (fn !== null) {
|
|
726
|
+
const n = Number(fn[1]);
|
|
727
|
+
if (n < 1 || n > 12)
|
|
728
|
+
return null;
|
|
729
|
+
return { kind: "pf", n: e.shiftKey ? n + 12 : n };
|
|
730
|
+
}
|
|
731
|
+
const named = NAMED[e.key];
|
|
732
|
+
if (named !== void 0)
|
|
733
|
+
return named;
|
|
734
|
+
if ([...e.key].length === 1)
|
|
735
|
+
return { kind: "type", text: e.key };
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// packages/canvas/dist/forwardKeys.js
|
|
740
|
+
function isEditableTarget(target) {
|
|
741
|
+
const tag = typeof target?.tagName === "string" ? target.tagName : "";
|
|
742
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target?.isContentEditable === true;
|
|
743
|
+
}
|
|
744
|
+
function forwardKeys(deps) {
|
|
745
|
+
deps.on("keydown", (raw) => {
|
|
746
|
+
const e = raw;
|
|
747
|
+
if (isEditableTarget(e.target))
|
|
748
|
+
return;
|
|
749
|
+
const action = actionForKey(e);
|
|
750
|
+
if (action === null)
|
|
751
|
+
return;
|
|
752
|
+
e.preventDefault();
|
|
753
|
+
deps.sendAction(action);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// packages/gui/dist/keyboardBoot.js
|
|
758
|
+
var root = document.getElementById("keyboard-root");
|
|
759
|
+
if (root === null) {
|
|
760
|
+
throw new Error("keyboard window: #keyboard-root is missing from keyboard.html");
|
|
761
|
+
}
|
|
762
|
+
var bridge = window.tn3270keyboard;
|
|
763
|
+
if (bridge === void 0) {
|
|
764
|
+
throw new Error("keyboard window: the tn3270keyboard bridge is absent (preload failed to load?)");
|
|
765
|
+
}
|
|
766
|
+
createKeyboardUi({
|
|
767
|
+
root,
|
|
768
|
+
create: (tag) => document.createElement(tag),
|
|
769
|
+
append: (parent, child) => {
|
|
770
|
+
parent.appendChild(child);
|
|
771
|
+
},
|
|
772
|
+
setText: (el, text) => {
|
|
773
|
+
el.textContent = text;
|
|
774
|
+
},
|
|
775
|
+
setTitle: (el, title) => {
|
|
776
|
+
el.title = title;
|
|
777
|
+
},
|
|
778
|
+
setAttr: (el, name, value) => {
|
|
779
|
+
el.setAttribute(name, value);
|
|
780
|
+
},
|
|
781
|
+
removeAttr: (el, name) => {
|
|
782
|
+
el.removeAttribute(name);
|
|
783
|
+
},
|
|
784
|
+
onClick: (el, fn) => {
|
|
785
|
+
el.addEventListener("click", fn);
|
|
786
|
+
},
|
|
787
|
+
sendAction: (action) => {
|
|
788
|
+
bridge.sendAction(action);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
forwardKeys({
|
|
792
|
+
on: (type, handler) => {
|
|
793
|
+
window.addEventListener(type, handler);
|
|
794
|
+
},
|
|
795
|
+
sendAction: (action) => {
|
|
796
|
+
bridge.sendAction(action);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
//# sourceMappingURL=keyboardBoot.js.map
|