green-screen-proxy 1.2.6 → 1.3.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/dist/protocols/tn5250-handler.d.ts.map +1 -1
- package/dist/protocols/tn5250-handler.js +39 -26
- package/dist/protocols/tn5250-handler.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -1
- package/dist/routes.js.map +1 -1
- package/dist/tn5250/connection.d.ts +9 -1
- package/dist/tn5250/connection.d.ts.map +1 -1
- package/dist/tn5250/connection.js +35 -10
- package/dist/tn5250/connection.js.map +1 -1
- package/dist/tn5250/constants.d.ts +7 -0
- package/dist/tn5250/constants.d.ts.map +1 -1
- package/dist/tn5250/constants.js +8 -0
- package/dist/tn5250/constants.js.map +1 -1
- package/dist/tn5250/encoder.d.ts +12 -0
- package/dist/tn5250/encoder.d.ts.map +1 -1
- package/dist/tn5250/encoder.js +44 -0
- package/dist/tn5250/encoder.js.map +1 -1
- package/dist/tn5250/parser.d.ts +25 -6
- package/dist/tn5250/parser.d.ts.map +1 -1
- package/dist/tn5250/parser.js +335 -157
- package/dist/tn5250/parser.js.map +1 -1
- package/dist/tn5250/screen.d.ts +39 -2
- package/dist/tn5250/screen.d.ts.map +1 -1
- package/dist/tn5250/screen.js +65 -3
- package/dist/tn5250/screen.js.map +1 -1
- package/package.json +1 -1
package/dist/tn5250/parser.js
CHANGED
|
@@ -17,6 +17,9 @@ export class TN5250Parser {
|
|
|
17
17
|
* relative to the window content area, not the full screen. */
|
|
18
18
|
winRowOff = 0;
|
|
19
19
|
winColOff = 0;
|
|
20
|
+
/** Set during parseRecord when opcode is RESTORE_SCREEN; used by
|
|
21
|
+
* parseOrders for post-WTD cursor positioning per lib5250. */
|
|
22
|
+
isRestoreScreen = false;
|
|
20
23
|
constructor(screen) {
|
|
21
24
|
this.screen = screen;
|
|
22
25
|
}
|
|
@@ -26,6 +29,7 @@ export class TN5250Parser {
|
|
|
26
29
|
*/
|
|
27
30
|
parseRecord(record) {
|
|
28
31
|
this.icApplied = false;
|
|
32
|
+
this.isRestoreScreen = false;
|
|
29
33
|
if (record.length < 2)
|
|
30
34
|
return false;
|
|
31
35
|
// 5250 record header:
|
|
@@ -85,6 +89,7 @@ export class TN5250Parser {
|
|
|
85
89
|
modified = true;
|
|
86
90
|
break;
|
|
87
91
|
case OPCODE.RESTORE_SCREEN:
|
|
92
|
+
this.isRestoreScreen = true;
|
|
88
93
|
this.screen.restoreState();
|
|
89
94
|
this.screen.windowList = [];
|
|
90
95
|
this.winRowOff = 0;
|
|
@@ -109,12 +114,38 @@ export class TN5250Parser {
|
|
|
109
114
|
// the first known command byte — handles non-GDS hosts.
|
|
110
115
|
return this.parseCommandsFromOffset(record, 0);
|
|
111
116
|
}
|
|
117
|
+
/** Check if a byte is a known 5250 command code. */
|
|
118
|
+
static isKnownCommand(b) {
|
|
119
|
+
return (b === CMD.WRITE_TO_DISPLAY ||
|
|
120
|
+
b === CMD.CLEAR_UNIT ||
|
|
121
|
+
b === CMD.CLEAR_UNIT_ALT ||
|
|
122
|
+
b === CMD.CLEAR_FORMAT_TABLE ||
|
|
123
|
+
b === CMD.WRITE_STRUCTURED_FIELD ||
|
|
124
|
+
b === CMD.WRITE_ERROR_CODE ||
|
|
125
|
+
b === CMD.WRITE_ERROR_CODE_WIN ||
|
|
126
|
+
b === CMD.READ_INPUT_FIELDS ||
|
|
127
|
+
b === CMD.READ_MDT_FIELDS ||
|
|
128
|
+
b === CMD.READ_MDT_FIELDS_ALT ||
|
|
129
|
+
b === CMD.READ_SCREEN_IMMEDIATE ||
|
|
130
|
+
b === CMD.READ_IMMEDIATE ||
|
|
131
|
+
b === CMD.READ_IMMEDIATE_ALT ||
|
|
132
|
+
b === CMD.SAVE_SCREEN ||
|
|
133
|
+
b === CMD.SAVE_PARTIAL_SCREEN ||
|
|
134
|
+
b === CMD.RESTORE_SCREEN ||
|
|
135
|
+
b === CMD.RESTORE_PARTIAL_SCREEN ||
|
|
136
|
+
b === CMD.ROLL ||
|
|
137
|
+
b === CMD.READ_SCREEN_EXTENDED ||
|
|
138
|
+
b === CMD.READ_SCREEN_PRINT ||
|
|
139
|
+
b === CMD.READ_SCREEN_PRINT_EXTENDED ||
|
|
140
|
+
b === CMD.READ_SCREEN_PRINT_GRID ||
|
|
141
|
+
b === CMD.READ_SCREEN_PRINT_EXT_GRID);
|
|
142
|
+
}
|
|
112
143
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
144
|
+
* Parse commands from a data region.
|
|
145
|
+
*
|
|
146
|
+
* Per lib5250 session.c:620-629 each command is preceded by an ESC byte
|
|
147
|
+
* (0x04). We look for ESC+command pairs. As a fallback for non-conforming
|
|
148
|
+
* servers (e.g. pub400.com) we also accept a bare known-command byte.
|
|
118
149
|
*
|
|
119
150
|
* CRITICAL: the recognition list MUST include every command the host
|
|
120
151
|
* may send alone in its own record. Missing READ_* here would cause a
|
|
@@ -123,34 +154,43 @@ export class TN5250Parser {
|
|
|
123
154
|
* Read handler is the only path that clears `keyboardLocked`.
|
|
124
155
|
*/
|
|
125
156
|
parseCommandsFromOffset(data, start) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
157
|
+
let pos = start;
|
|
158
|
+
let modified = false;
|
|
159
|
+
while (pos < data.length) {
|
|
160
|
+
const b = data[pos];
|
|
161
|
+
if (b === 0x04) {
|
|
162
|
+
// ESC byte — per lib5250, next byte is the command
|
|
163
|
+
if (pos + 1 < data.length && TN5250Parser.isKnownCommand(data[pos + 1])) {
|
|
164
|
+
pos++; // skip ESC, parseCommands starts at the command byte
|
|
165
|
+
const result = this.parseCommands(data, pos);
|
|
166
|
+
modified = modified || result.modified;
|
|
167
|
+
pos = result.pos;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Stray 0x04 without a valid command following — skip it
|
|
171
|
+
pos++;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Fallback: bare command byte without ESC prefix (non-conforming servers)
|
|
176
|
+
if (TN5250Parser.isKnownCommand(b)) {
|
|
177
|
+
const result = this.parseCommands(data, pos);
|
|
178
|
+
modified = modified || result.modified;
|
|
179
|
+
pos = result.pos;
|
|
129
180
|
continue;
|
|
130
|
-
// Known command bytes — hand off to normal parsing
|
|
131
|
-
const b = data[i];
|
|
132
|
-
if (b === CMD.WRITE_TO_DISPLAY ||
|
|
133
|
-
b === CMD.CLEAR_UNIT ||
|
|
134
|
-
b === CMD.CLEAR_UNIT_ALT ||
|
|
135
|
-
b === CMD.CLEAR_FORMAT_TABLE ||
|
|
136
|
-
b === CMD.WRITE_STRUCTURED_FIELD ||
|
|
137
|
-
b === CMD.WRITE_ERROR_CODE ||
|
|
138
|
-
b === CMD.WRITE_ERROR_CODE_WIN ||
|
|
139
|
-
b === CMD.READ_INPUT_FIELDS ||
|
|
140
|
-
b === CMD.READ_MDT_FIELDS ||
|
|
141
|
-
b === CMD.READ_MDT_FIELDS_ALT ||
|
|
142
|
-
b === CMD.READ_SCREEN_IMMEDIATE ||
|
|
143
|
-
b === CMD.READ_IMMEDIATE ||
|
|
144
|
-
b === CMD.READ_IMMEDIATE_ALT ||
|
|
145
|
-
b === CMD.SAVE_SCREEN ||
|
|
146
|
-
b === CMD.RESTORE_SCREEN ||
|
|
147
|
-
b === CMD.ROLL) {
|
|
148
|
-
return this.parseCommands(data, i);
|
|
149
181
|
}
|
|
182
|
+
// Unknown byte at command level — skip
|
|
183
|
+
pos++;
|
|
150
184
|
}
|
|
151
|
-
return
|
|
185
|
+
return modified;
|
|
152
186
|
}
|
|
153
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Parse one or more 5250 commands starting at offset.
|
|
189
|
+
* Returns the new position and whether the screen was modified.
|
|
190
|
+
* The command loop terminates when ESC (0x04) is encountered (it is
|
|
191
|
+
* NOT consumed — the caller re-dispatches it as the start of the next
|
|
192
|
+
* ESC+command pair, per lib5250 session.c:964-967).
|
|
193
|
+
*/
|
|
154
194
|
parseCommands(data, offset) {
|
|
155
195
|
let pos = offset;
|
|
156
196
|
let modified = false;
|
|
@@ -163,6 +203,7 @@ export class TN5250Parser {
|
|
|
163
203
|
this.screen.resize(24, 80);
|
|
164
204
|
this.screen.keyboardLocked = true;
|
|
165
205
|
this.screen.insertMode = false;
|
|
206
|
+
this.screen.pendingInsert = false;
|
|
166
207
|
this.screen.savedCursorBeforeError = null;
|
|
167
208
|
this.screen.windowList = [];
|
|
168
209
|
this.screen.selectionFields = [];
|
|
@@ -181,6 +222,7 @@ export class TN5250Parser {
|
|
|
181
222
|
this.screen.resize(27, 132);
|
|
182
223
|
this.screen.keyboardLocked = true;
|
|
183
224
|
this.screen.insertMode = false;
|
|
225
|
+
this.screen.pendingInsert = false;
|
|
184
226
|
this.screen.savedCursorBeforeError = null;
|
|
185
227
|
this.screen.windowList = [];
|
|
186
228
|
this.screen.selectionFields = [];
|
|
@@ -191,81 +233,44 @@ export class TN5250Parser {
|
|
|
191
233
|
this.winRowOff = 0;
|
|
192
234
|
this.winColOff = 0;
|
|
193
235
|
pos++;
|
|
194
|
-
//
|
|
195
|
-
if (pos < data.length)
|
|
196
|
-
pos
|
|
236
|
+
// Read and validate the flag byte (per lib5250 session.c:1148-1156)
|
|
237
|
+
if (pos < data.length) {
|
|
238
|
+
const flag = data[pos++];
|
|
239
|
+
if (flag !== 0x00 && flag !== 0x80) {
|
|
240
|
+
// Per lib5250: invalid flag; log but continue
|
|
241
|
+
}
|
|
242
|
+
}
|
|
197
243
|
modified = true;
|
|
198
244
|
break;
|
|
199
245
|
case CMD.CLEAR_FORMAT_TABLE:
|
|
246
|
+
// Per lib5250 display.c:1949-1956: clear format table, reset cursor,
|
|
247
|
+
// lock keyboard, and clear insert mode.
|
|
200
248
|
this.screen.fields = [];
|
|
249
|
+
this.screen.cursorRow = 0;
|
|
250
|
+
this.screen.cursorCol = 0;
|
|
251
|
+
this.screen.keyboardLocked = true;
|
|
252
|
+
this.screen.insertMode = false;
|
|
201
253
|
pos++;
|
|
202
254
|
modified = true;
|
|
203
255
|
break;
|
|
204
256
|
case CMD.WRITE_TO_DISPLAY: {
|
|
205
257
|
pos++; // skip command byte
|
|
206
258
|
// WTD has a CC (control character) — 2 bytes
|
|
259
|
+
let wtdCC2 = 0;
|
|
207
260
|
if (pos + 1 < data.length) {
|
|
208
261
|
const cc1 = data[pos++];
|
|
209
|
-
|
|
210
|
-
// CC1
|
|
211
|
-
|
|
212
|
-
let resetNonBypassMdt = false;
|
|
213
|
-
let resetAllMdt = false;
|
|
214
|
-
let nullNonBypassMdt = false;
|
|
215
|
-
let nullNonBypass = false;
|
|
216
|
-
// CC1: lock keyboard for all values except 0x00
|
|
217
|
-
// Per lib5250 session.c:853-858
|
|
218
|
-
if ((cc1 & 0xE0) !== 0x00) {
|
|
219
|
-
this.screen.keyboardLocked = true;
|
|
220
|
-
}
|
|
221
|
-
switch (cc1 & 0xE0) {
|
|
222
|
-
case 0x00: break; // no action (don't lock keyboard)
|
|
223
|
-
case 0x20: break; // reserved / no action in lib5250
|
|
224
|
-
case 0x40:
|
|
225
|
-
resetNonBypassMdt = true;
|
|
226
|
-
break;
|
|
227
|
-
case 0x60:
|
|
228
|
-
resetAllMdt = true;
|
|
229
|
-
break;
|
|
230
|
-
case 0x80:
|
|
231
|
-
nullNonBypassMdt = true;
|
|
232
|
-
break;
|
|
233
|
-
case 0xA0:
|
|
234
|
-
resetNonBypassMdt = true;
|
|
235
|
-
nullNonBypass = true;
|
|
236
|
-
break;
|
|
237
|
-
case 0xC0:
|
|
238
|
-
resetNonBypassMdt = true;
|
|
239
|
-
nullNonBypassMdt = true;
|
|
240
|
-
break;
|
|
241
|
-
case 0xE0:
|
|
242
|
-
resetAllMdt = true;
|
|
243
|
-
nullNonBypass = true;
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
for (const f of this.screen.fields) {
|
|
247
|
-
const isInput = this.screen.isInputField(f);
|
|
248
|
-
// Null fill: clear input field content
|
|
249
|
-
if (isInput && (nullNonBypass || (nullNonBypassMdt && f.modified))) {
|
|
250
|
-
const start = this.screen.offset(f.row, f.col);
|
|
251
|
-
for (let i = start; i < start + f.length; i++) {
|
|
252
|
-
this.screen.buffer[i] = ' ';
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// MDT reset
|
|
256
|
-
if (resetAllMdt || (resetNonBypassMdt && isInput)) {
|
|
257
|
-
f.modified = false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
262
|
+
wtdCC2 = data[pos++];
|
|
263
|
+
// CC1 handling (shared with Read commands)
|
|
264
|
+
this.handleCC1(cc1);
|
|
260
265
|
// Mark that subsequent SF orders in this WTD should clear stale fields
|
|
261
|
-
|
|
262
|
-
this.pendingFieldsClear = true;
|
|
263
|
-
}
|
|
266
|
+
// (done after handleCC1 since it checks MDT reset flags)
|
|
264
267
|
// CC2 handling per lib5250 session.c:1033-1066
|
|
265
|
-
|
|
268
|
+
// Note: CC2 is handled AFTER orders in lib5250 (session.c:1018),
|
|
269
|
+
// but we handle message/alarm bits here; cursor logic is in parseOrders.
|
|
270
|
+
this.handleCC2(wtdCC2);
|
|
266
271
|
}
|
|
267
272
|
// Parse orders and data following WTD
|
|
268
|
-
pos = this.parseOrders(data, pos);
|
|
273
|
+
pos = this.parseOrders(data, pos, wtdCC2, this.isRestoreScreen);
|
|
269
274
|
modified = true;
|
|
270
275
|
break;
|
|
271
276
|
}
|
|
@@ -292,8 +297,8 @@ export class TN5250Parser {
|
|
|
292
297
|
// IC sets cursor position (but NOT pending insert per spec)
|
|
293
298
|
while (pos < data.length) {
|
|
294
299
|
const c = data[pos];
|
|
295
|
-
if (c ===
|
|
296
|
-
break;
|
|
300
|
+
if (c === 0x04) { // ESC — end of error code data (per lib5250 session.c:765-767)
|
|
301
|
+
break; // don't advance pos; command loop will see 0x04 for next dispatch
|
|
297
302
|
}
|
|
298
303
|
if (c === ORDER.IC && pos + 2 < data.length) {
|
|
299
304
|
// IC within error code: just move cursor, don't set pending insert
|
|
@@ -344,22 +349,23 @@ export class TN5250Parser {
|
|
|
344
349
|
case CMD.READ_IMMEDIATE:
|
|
345
350
|
case CMD.READ_IMMEDIATE_ALT: {
|
|
346
351
|
// Per lib5250 session.c:2328-2354 (tn5250_session_read_cmd):
|
|
347
|
-
// Reads 2 CC bytes, handles CC1/CC2,
|
|
348
|
-
//
|
|
349
|
-
//
|
|
352
|
+
// Reads 2 CC bytes, handles CC1/CC2, clears X_SYSTEM/X_CLOCK,
|
|
353
|
+
// and unlocks the keyboard only if in normal LOCKED state
|
|
354
|
+
// (not POSTHELP/error state).
|
|
350
355
|
const readOp = cmd;
|
|
351
356
|
pos++;
|
|
352
357
|
if (pos + 1 < data.length) {
|
|
353
358
|
const cc1 = data[pos++];
|
|
354
359
|
const cc2 = data[pos++];
|
|
355
|
-
|
|
356
|
-
if ((cc1 & 0xE0) !== 0x00) {
|
|
357
|
-
this.screen.keyboardLocked = true;
|
|
358
|
-
}
|
|
360
|
+
this.handleCC1(cc1);
|
|
359
361
|
this.handleCC2(cc2);
|
|
360
362
|
}
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
+
// Per lib5250 session.c:2348-2351: only unlock if in normal
|
|
364
|
+
// locked state (not error/POSTHELP). We use savedCursorBeforeError
|
|
365
|
+
// as a proxy for the POSTHELP state.
|
|
366
|
+
if (this.screen.savedCursorBeforeError === null) {
|
|
367
|
+
this.screen.keyboardLocked = false;
|
|
368
|
+
}
|
|
363
369
|
this.screen.readOpcode = readOp;
|
|
364
370
|
modified = true;
|
|
365
371
|
break;
|
|
@@ -383,10 +389,21 @@ export class TN5250Parser {
|
|
|
383
389
|
}
|
|
384
390
|
break;
|
|
385
391
|
}
|
|
386
|
-
|
|
387
|
-
|
|
392
|
+
// Commands recognized by lib5250 but not processed (logged only)
|
|
393
|
+
case CMD.SAVE_PARTIAL_SCREEN:
|
|
394
|
+
case CMD.RESTORE_PARTIAL_SCREEN:
|
|
395
|
+
case CMD.READ_SCREEN_EXTENDED:
|
|
396
|
+
case CMD.READ_SCREEN_PRINT:
|
|
397
|
+
case CMD.READ_SCREEN_PRINT_EXTENDED:
|
|
398
|
+
case CMD.READ_SCREEN_PRINT_GRID:
|
|
399
|
+
case CMD.READ_SCREEN_PRINT_EXT_GRID:
|
|
400
|
+
// Per lib5250 session.c: these commands are logged but ignored.
|
|
388
401
|
pos++;
|
|
389
402
|
break;
|
|
403
|
+
case 0x04:
|
|
404
|
+
// ESC byte — per lib5250 session.c:964-967, return to caller for
|
|
405
|
+
// re-dispatch as the start of the next ESC+command pair.
|
|
406
|
+
return { pos, modified };
|
|
390
407
|
default:
|
|
391
408
|
// Not a recognized command at this position
|
|
392
409
|
// Try treating remaining data as orders/text
|
|
@@ -395,13 +412,15 @@ export class TN5250Parser {
|
|
|
395
412
|
break;
|
|
396
413
|
}
|
|
397
414
|
}
|
|
398
|
-
return modified;
|
|
415
|
+
return { pos, modified };
|
|
399
416
|
}
|
|
400
417
|
/**
|
|
401
418
|
* Parse orders and text data within a WTD (or similar) command.
|
|
402
419
|
* Updates the screen buffer and returns the new position.
|
|
403
420
|
*/
|
|
404
|
-
parseOrders(data, pos) {
|
|
421
|
+
parseOrders(data, pos, cc2 = 0, isRestoreScreen = false) {
|
|
422
|
+
const oldCursorRow = this.screen.cursorRow;
|
|
423
|
+
const oldCursorCol = this.screen.cursorCol;
|
|
405
424
|
let currentAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
|
|
406
425
|
let currentAttr = ATTR.NORMAL;
|
|
407
426
|
let useSymbolCharSet = false; // SA type 0x22 can switch to APL/symbol CGCS
|
|
@@ -424,6 +443,8 @@ export class TN5250Parser {
|
|
|
424
443
|
: null;
|
|
425
444
|
// Helper: write a single rendered character at currentAddr, applying
|
|
426
445
|
// current attributes and resetting the DBCS continuation flag for SBCS.
|
|
446
|
+
// Per lib5250 dbuffer.c:672-680 (addch + right): wraps around to 0
|
|
447
|
+
// when reaching the end of the screen buffer.
|
|
427
448
|
const writeChar = (ch) => {
|
|
428
449
|
if (currentAddr < this.screen.size) {
|
|
429
450
|
this.screen.setCharAt(currentAddr, ch);
|
|
@@ -432,6 +453,8 @@ export class TN5250Parser {
|
|
|
432
453
|
this.screen.dbcsCont[currentAddr] = false;
|
|
433
454
|
}
|
|
434
455
|
currentAddr++;
|
|
456
|
+
if (currentAddr >= this.screen.size)
|
|
457
|
+
currentAddr = 0;
|
|
435
458
|
};
|
|
436
459
|
// Helper: write a DBCS character — glyph in cell N, empty continuation
|
|
437
460
|
// in cell N+1, both flagged as DBCS.
|
|
@@ -443,6 +466,8 @@ export class TN5250Parser {
|
|
|
443
466
|
this.screen.dbcsCont[currentAddr] = false;
|
|
444
467
|
}
|
|
445
468
|
currentAddr++;
|
|
469
|
+
if (currentAddr >= this.screen.size)
|
|
470
|
+
currentAddr = 0;
|
|
446
471
|
if (currentAddr < this.screen.size) {
|
|
447
472
|
this.screen.setCharAt(currentAddr, '');
|
|
448
473
|
this.screen.setAttrAt(currentAddr, currentAttr);
|
|
@@ -450,6 +475,8 @@ export class TN5250Parser {
|
|
|
450
475
|
this.screen.dbcsCont[currentAddr] = true;
|
|
451
476
|
}
|
|
452
477
|
currentAddr++;
|
|
478
|
+
if (currentAddr >= this.screen.size)
|
|
479
|
+
currentAddr = 0;
|
|
453
480
|
};
|
|
454
481
|
while (pos < data.length) {
|
|
455
482
|
const byte = data[pos];
|
|
@@ -492,20 +519,25 @@ export class TN5250Parser {
|
|
|
492
519
|
const icCol = data[pos++] - 1 + this.winColOff;
|
|
493
520
|
pendingICRow = icRow;
|
|
494
521
|
pendingICCol = icCol;
|
|
522
|
+
// Per lib5250 display.c:458-462: IC sets a persistent position
|
|
523
|
+
// that setCursorHome uses on subsequent WTDs without IC.
|
|
524
|
+
this.screen.pendingInsert = true;
|
|
525
|
+
this.screen.pendingInsertRow = icRow;
|
|
526
|
+
this.screen.pendingInsertCol = icCol;
|
|
495
527
|
break;
|
|
496
528
|
}
|
|
497
529
|
case ORDER.MC: {
|
|
498
|
-
// Move Cursor: 2 bytes follow (row, col) — 1-based from host
|
|
530
|
+
// Move Cursor: 2 bytes follow (row, col) — 1-based from host.
|
|
531
|
+
// Per lib5250 session.c:2024-2047: MC does NOT actually move the
|
|
532
|
+
// cursor or update the display address. It only stores end_y/end_x
|
|
533
|
+
// for post-WTD cursor positioning (same as IC).
|
|
499
534
|
if (pos + 2 >= data.length)
|
|
500
535
|
return data.length;
|
|
501
536
|
pos++;
|
|
502
537
|
const mcRow = data[pos++] - 1 + this.winRowOff;
|
|
503
538
|
const mcCol = data[pos++] - 1 + this.winColOff;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this.screen.cursorCol = mcCol;
|
|
507
|
-
currentAddr = this.screen.offset(mcRow, mcCol);
|
|
508
|
-
}
|
|
539
|
+
pendingICRow = mcRow;
|
|
540
|
+
pendingICCol = mcCol;
|
|
509
541
|
break;
|
|
510
542
|
}
|
|
511
543
|
case ORDER.RA: {
|
|
@@ -521,22 +553,27 @@ export class TN5250Parser {
|
|
|
521
553
|
const charByte = data[pos++];
|
|
522
554
|
const targetAddr = this.screen.offset(raRow, raCol);
|
|
523
555
|
const ch = ebcdicToChar(charByte, this.screen.codePage);
|
|
556
|
+
// Per lib5250 addch semantics: set char, attribute, and ext attrs
|
|
557
|
+
const raExt = snapExt();
|
|
558
|
+
const raFill = () => {
|
|
559
|
+
this.screen.setCharAt(currentAddr, ch);
|
|
560
|
+
this.screen.setAttrAt(currentAddr, currentAttr);
|
|
561
|
+
this.screen.setExtAttrAt(currentAddr, raExt);
|
|
562
|
+
currentAddr++;
|
|
563
|
+
};
|
|
524
564
|
if (targetAddr >= currentAddr) {
|
|
525
565
|
while (currentAddr <= targetAddr && currentAddr < this.screen.size) {
|
|
526
|
-
|
|
527
|
-
currentAddr++;
|
|
566
|
+
raFill();
|
|
528
567
|
}
|
|
529
568
|
}
|
|
530
569
|
else {
|
|
531
570
|
// Wrap-around: fill to end of screen, then from start to target (inclusive)
|
|
532
571
|
while (currentAddr < this.screen.size) {
|
|
533
|
-
|
|
534
|
-
currentAddr++;
|
|
572
|
+
raFill();
|
|
535
573
|
}
|
|
536
574
|
currentAddr = 0;
|
|
537
575
|
while (currentAddr <= targetAddr && currentAddr < this.screen.size) {
|
|
538
|
-
|
|
539
|
-
currentAddr++;
|
|
576
|
+
raFill();
|
|
540
577
|
}
|
|
541
578
|
}
|
|
542
579
|
break;
|
|
@@ -552,18 +589,15 @@ export class TN5250Parser {
|
|
|
552
589
|
const eaRow = data[pos++] - 1 + this.winRowOff;
|
|
553
590
|
const eaCol = data[pos++] - 1 + this.winColOff;
|
|
554
591
|
const eaLength = data[pos++];
|
|
555
|
-
// Consume (length-1) attribute bytes
|
|
592
|
+
// Consume (length-1) attribute bytes. Per lib5250 session.c:2131,
|
|
593
|
+
// only the LAST attribute byte is checked for 0xFF.
|
|
556
594
|
const attrCount = Math.max(0, eaLength - 1);
|
|
557
|
-
let
|
|
595
|
+
let lastAttr = 0;
|
|
558
596
|
for (let i = 0; i < attrCount && pos < data.length; i++) {
|
|
559
|
-
|
|
560
|
-
eraseAll = true;
|
|
561
|
-
pos++;
|
|
597
|
+
lastAttr = data[pos++];
|
|
562
598
|
}
|
|
563
|
-
// lib5250 only erases characters when attribute
|
|
564
|
-
|
|
565
|
-
// attribute list so the region is cleared — safe for typical hosts.
|
|
566
|
-
if (eraseAll || attrCount === 0) {
|
|
599
|
+
// lib5250 only erases characters when the last attribute is 0xFF
|
|
600
|
+
if (lastAttr === 0xFF) {
|
|
567
601
|
const eaTarget = this.screen.offset(eaRow, eaCol);
|
|
568
602
|
if (eaTarget >= currentAddr) {
|
|
569
603
|
while (currentAddr <= eaTarget && currentAddr < this.screen.size) {
|
|
@@ -593,18 +627,25 @@ export class TN5250Parser {
|
|
|
593
627
|
}
|
|
594
628
|
case ORDER.SOH: {
|
|
595
629
|
// Start of Header: variable-length header for input fields.
|
|
596
|
-
// Per lib5250: SOH clears format table
|
|
630
|
+
// Per lib5250 session.c:1810-1841: SOH clears format table, clears
|
|
631
|
+
// pending insert cursor, locks keyboard, and sets X_SYSTEM.
|
|
597
632
|
// Format: [0x01] [length] [data...]
|
|
598
633
|
// The length byte specifies the number of data bytes following it
|
|
599
|
-
// (NOT including SOH or length byte itself).
|
|
634
|
+
// (NOT including SOH or length byte itself). Must be 0-7.
|
|
600
635
|
if (pos + 1 >= data.length)
|
|
601
636
|
return data.length;
|
|
602
637
|
pos++;
|
|
603
638
|
this.screen.fields = [];
|
|
604
639
|
this.screen.selectionFields = [];
|
|
640
|
+
this.screen.keyboardLocked = true;
|
|
641
|
+
this.screen.pendingInsert = false;
|
|
605
642
|
pendingICRow = -1;
|
|
606
643
|
pendingICCol = -1;
|
|
607
644
|
const hdrLen = data[pos++];
|
|
645
|
+
if (hdrLen > 7) {
|
|
646
|
+
// Per lib5250: invalid SOH length — skip remaining
|
|
647
|
+
return data.length;
|
|
648
|
+
}
|
|
608
649
|
// Per lib5250 dbuffer.c:167-178: copy header bytes for later use
|
|
609
650
|
// (byte 3 carries the error message line row).
|
|
610
651
|
const safeLen = Math.max(0, Math.min(hdrLen, data.length - pos));
|
|
@@ -722,6 +763,9 @@ export class TN5250Parser {
|
|
|
722
763
|
// Fallback: remove the top (last) window
|
|
723
764
|
this.screen.windowList.pop();
|
|
724
765
|
}
|
|
766
|
+
// Per lib5250 session.c:3495-3502: removing a window also
|
|
767
|
+
// destroys all scrollbars.
|
|
768
|
+
this.screen.scrollbarList = [];
|
|
725
769
|
this.winRowOff = 0;
|
|
726
770
|
this.winColOff = 0;
|
|
727
771
|
break;
|
|
@@ -744,6 +788,23 @@ export class TN5250Parser {
|
|
|
744
788
|
this.winRowOff = 0;
|
|
745
789
|
this.winColOff = 0;
|
|
746
790
|
break;
|
|
791
|
+
case WDSF_TYPE.WRITE_DATA: {
|
|
792
|
+
// Per lib5250 session.c:3575-3613: write data to the current
|
|
793
|
+
// field position. Flag byte followed by EBCDIC data bytes.
|
|
794
|
+
if (pos < wdsfEnd) {
|
|
795
|
+
const _flagbyte = data[pos++]; // 0x80 = write to entry field
|
|
796
|
+
while (pos < wdsfEnd) {
|
|
797
|
+
const ch = ebcdicToChar(data[pos++], this.screen.codePage);
|
|
798
|
+
if (currentAddr < this.screen.size) {
|
|
799
|
+
this.screen.setCharAt(currentAddr, ch);
|
|
800
|
+
}
|
|
801
|
+
currentAddr++;
|
|
802
|
+
if (currentAddr >= this.screen.size)
|
|
803
|
+
currentAddr = 0;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
747
808
|
default:
|
|
748
809
|
break;
|
|
749
810
|
}
|
|
@@ -828,12 +889,27 @@ export class TN5250Parser {
|
|
|
828
889
|
}
|
|
829
890
|
afterSBA = false;
|
|
830
891
|
}
|
|
831
|
-
//
|
|
832
|
-
|
|
892
|
+
// Post-WTD cursor positioning per lib5250 session.c:998-1018.
|
|
893
|
+
// Three-branch logic using CC2 bit 0x40 (IC_ULOCK):
|
|
894
|
+
const icUlock = (cc2 & 0x40) !== 0;
|
|
895
|
+
const willUnlock = (cc2 & 0x08) !== 0;
|
|
896
|
+
const hasIC = pendingICRow >= 0 && pendingICCol >= 0;
|
|
897
|
+
if (hasIC && !icUlock) {
|
|
898
|
+
// IC/MC position wins
|
|
833
899
|
this.screen.cursorRow = pendingICRow;
|
|
834
900
|
this.screen.cursorCol = pendingICCol;
|
|
835
901
|
this.icApplied = true;
|
|
836
902
|
}
|
|
903
|
+
else if ((willUnlock && !icUlock) || isRestoreScreen) {
|
|
904
|
+
// Cursor home: first input field, or (0,0)
|
|
905
|
+
this.screen.setCursorHome();
|
|
906
|
+
this.icApplied = true;
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
// Keep original pre-WTD cursor position
|
|
910
|
+
this.screen.cursorRow = oldCursorRow;
|
|
911
|
+
this.screen.cursorCol = oldCursorCol;
|
|
912
|
+
}
|
|
837
913
|
return pos;
|
|
838
914
|
}
|
|
839
915
|
/**
|
|
@@ -1011,7 +1087,8 @@ export class TN5250Parser {
|
|
|
1011
1087
|
fcw2,
|
|
1012
1088
|
attribute: fieldDisplayAttr,
|
|
1013
1089
|
rawAttrByte,
|
|
1014
|
-
|
|
1090
|
+
// Per lib5250 field.h:148: MDT is bit 3 (0x08) of ffw1 (FFW high byte)
|
|
1091
|
+
modified: inputField ? (ffw1 & 0x08) !== 0 : false,
|
|
1015
1092
|
continuous: continuous || undefined,
|
|
1016
1093
|
continuedFirst: contFirst || undefined,
|
|
1017
1094
|
continuedMiddle: contMiddle || undefined,
|
|
@@ -1035,7 +1112,34 @@ export class TN5250Parser {
|
|
|
1035
1112
|
lightandattn: lightandattn || undefined,
|
|
1036
1113
|
};
|
|
1037
1114
|
this.clearStaleFieldsOnce();
|
|
1038
|
-
|
|
1115
|
+
// Per lib5250 session.c:1717-1724: if a field already exists at this
|
|
1116
|
+
// position, modify it rather than adding a duplicate.
|
|
1117
|
+
if (inputField) {
|
|
1118
|
+
const existing = this.screen.fields.find(f => f.row === row && f.col === col);
|
|
1119
|
+
if (existing) {
|
|
1120
|
+
existing.ffw1 = ffw1;
|
|
1121
|
+
existing.ffw2 = ffw2;
|
|
1122
|
+
existing.attribute = fieldDisplayAttr;
|
|
1123
|
+
existing.rawAttrByte = rawAttrByte;
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
this.screen.fields.push(field);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
this.screen.fields.push(field);
|
|
1131
|
+
}
|
|
1132
|
+
// Per lib5250 session.c:1766-1795: for input fields, write 0x20 (space)
|
|
1133
|
+
// at the position after the end of the field (field separator marker),
|
|
1134
|
+
// so adjacent fields render correctly.
|
|
1135
|
+
if (inputField && fieldLength > 0) {
|
|
1136
|
+
let endAddr = this.screen.offset(row, col) + fieldLength;
|
|
1137
|
+
if (endAddr >= this.screen.size)
|
|
1138
|
+
endAddr -= this.screen.size;
|
|
1139
|
+
if (endAddr < this.screen.size) {
|
|
1140
|
+
this.screen.setCharAt(endAddr, ' ');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1039
1143
|
return pos;
|
|
1040
1144
|
}
|
|
1041
1145
|
/**
|
|
@@ -1179,37 +1283,39 @@ export class TN5250Parser {
|
|
|
1179
1283
|
const choiceFlagbyte1 = data[pos + 2];
|
|
1180
1284
|
const _choiceFlagbyte2 = data[pos + 3];
|
|
1181
1285
|
const choiceFlagbyte3 = data[pos + 4];
|
|
1182
|
-
//
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
const choiceRow = baseRow + choiceIndex;
|
|
1200
|
-
choices.push({ text, row: choiceRow, col: baseCol });
|
|
1201
|
-
choiceIndex++;
|
|
1286
|
+
// Per lib5250 session.c:2885-2910: choice availability is determined
|
|
1287
|
+
// by flagbyte1 bits 6-7 (0xC0), not flagbyte3. Include all choices.
|
|
1288
|
+
// Calculate text offset: skip flags, optional mnemonic/AID/numeric fields
|
|
1289
|
+
let textOff = 5;
|
|
1290
|
+
if (choiceFlagbyte1 & 0x08)
|
|
1291
|
+
textOff++; // mnemonic offset
|
|
1292
|
+
if (choiceFlagbyte1 & 0x04)
|
|
1293
|
+
textOff++; // AID byte
|
|
1294
|
+
const numericSel = choiceFlagbyte1 & 0x03;
|
|
1295
|
+
if (numericSel === 1)
|
|
1296
|
+
textOff++; // single-digit numeric
|
|
1297
|
+
else if (numericSel === 2)
|
|
1298
|
+
textOff += 2; // double-digit numeric
|
|
1299
|
+
let text = '';
|
|
1300
|
+
for (let i = textOff; i < minorLen; i++) {
|
|
1301
|
+
text += ebcdicToChar(data[pos + i]);
|
|
1202
1302
|
}
|
|
1303
|
+
const choiceRow = baseRow + choiceIndex;
|
|
1304
|
+
choices.push({ text, row: choiceRow, col: baseCol });
|
|
1305
|
+
choiceIndex++;
|
|
1203
1306
|
}
|
|
1204
1307
|
pos += minorLen;
|
|
1205
1308
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1309
|
+
// Per lib5250 session.c:2619-2624: if a selection field already exists at
|
|
1310
|
+
// this position, redefine it rather than creating a duplicate.
|
|
1311
|
+
const existingSel = this.screen.selectionFields.findIndex(s => s.row === baseRow && s.col === baseCol);
|
|
1312
|
+
const selDef = { row: baseRow, col: baseCol, numRows, numCols: itemSize, choices };
|
|
1313
|
+
if (existingSel >= 0) {
|
|
1314
|
+
this.screen.selectionFields[existingSel] = selDef;
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
this.screen.selectionFields.push(selDef);
|
|
1318
|
+
}
|
|
1213
1319
|
}
|
|
1214
1320
|
/**
|
|
1215
1321
|
* Parse DEFINE_SCROLL_BAR structured field data.
|
|
@@ -1347,6 +1453,66 @@ export class TN5250Parser {
|
|
|
1347
1453
|
this.screen.keyboardLocked = false;
|
|
1348
1454
|
}
|
|
1349
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Handle CC1 byte (first control character of WTD / Read commands).
|
|
1458
|
+
* Per lib5250 session.c:812-878.
|
|
1459
|
+
*
|
|
1460
|
+
* CC1 upper 3 bits (0xE0 mask) control keyboard lock, MDT reset,
|
|
1461
|
+
* and null-fill operations on fields.
|
|
1462
|
+
*/
|
|
1463
|
+
handleCC1(cc1) {
|
|
1464
|
+
let resetNonBypassMdt = false;
|
|
1465
|
+
let resetAllMdt = false;
|
|
1466
|
+
let nullNonBypassMdt = false;
|
|
1467
|
+
let nullNonBypass = false;
|
|
1468
|
+
// Lock keyboard for all CC1 values except 0x00
|
|
1469
|
+
if ((cc1 & 0xE0) !== 0x00) {
|
|
1470
|
+
this.screen.keyboardLocked = true;
|
|
1471
|
+
}
|
|
1472
|
+
switch (cc1 & 0xE0) {
|
|
1473
|
+
case 0x00: break;
|
|
1474
|
+
case 0x20: break; // reserved
|
|
1475
|
+
case 0x40:
|
|
1476
|
+
resetNonBypassMdt = true;
|
|
1477
|
+
break;
|
|
1478
|
+
case 0x60:
|
|
1479
|
+
resetAllMdt = true;
|
|
1480
|
+
break;
|
|
1481
|
+
case 0x80:
|
|
1482
|
+
nullNonBypassMdt = true;
|
|
1483
|
+
break;
|
|
1484
|
+
case 0xA0:
|
|
1485
|
+
resetNonBypassMdt = true;
|
|
1486
|
+
nullNonBypass = true;
|
|
1487
|
+
break;
|
|
1488
|
+
case 0xC0:
|
|
1489
|
+
resetNonBypassMdt = true;
|
|
1490
|
+
nullNonBypassMdt = true;
|
|
1491
|
+
break;
|
|
1492
|
+
case 0xE0:
|
|
1493
|
+
resetAllMdt = true;
|
|
1494
|
+
nullNonBypass = true;
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
for (const f of this.screen.fields) {
|
|
1498
|
+
const isInput = this.screen.isInputField(f);
|
|
1499
|
+
// Null fill: clear input field content
|
|
1500
|
+
if (isInput && (nullNonBypass || (nullNonBypassMdt && f.modified))) {
|
|
1501
|
+
const start = this.screen.offset(f.row, f.col);
|
|
1502
|
+
for (let i = start; i < start + f.length; i++) {
|
|
1503
|
+
this.screen.buffer[i] = ' ';
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
// MDT reset
|
|
1507
|
+
if (resetAllMdt || (resetNonBypassMdt && isInput)) {
|
|
1508
|
+
f.modified = false;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
// Mark that subsequent SF orders in this WTD should clear stale fields
|
|
1512
|
+
if (resetAllMdt || resetNonBypassMdt) {
|
|
1513
|
+
this.pendingFieldsClear = true;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1350
1516
|
/** Clear stale fields when the first SF order arrives after a Reset MDT WTD */
|
|
1351
1517
|
clearStaleFieldsOnce() {
|
|
1352
1518
|
if (this.pendingFieldsClear) {
|
|
@@ -1448,6 +1614,18 @@ export class TN5250Parser {
|
|
|
1448
1614
|
f.ffw1 |= 0x20;
|
|
1449
1615
|
continue;
|
|
1450
1616
|
}
|
|
1617
|
+
// Native underscore attribute (lower 3 bits = 4, 5, or 6 → UL,
|
|
1618
|
+
// UL+RI, UL+HI) is a strong signal that this is a real input field.
|
|
1619
|
+
// UIM popup screens (e.g. "Exit Interactive SQL", CRTLIB prompter)
|
|
1620
|
+
// use short underscore-attribute fields pre-populated with default
|
|
1621
|
+
// values like "1". Without this check the empty-content rule below
|
|
1622
|
+
// demotes them, which strips the cursor and breaks TAB walking on
|
|
1623
|
+
// those screens. Decoration/label fields don't carry the underscore
|
|
1624
|
+
// attribute, so this exception is safe.
|
|
1625
|
+
const attrType = f.rawAttrByte & 0x07;
|
|
1626
|
+
const hasNativeUnderscore = attrType >= 0x04 && attrType < 0x07;
|
|
1627
|
+
if (hasNativeUnderscore)
|
|
1628
|
+
continue;
|
|
1451
1629
|
// (a) Same-row termination check
|
|
1452
1630
|
const next = sortedFields[i + 1];
|
|
1453
1631
|
const sameRowTerminated = !!next && next.row === f.row;
|