green-screen-proxy 1.0.3 → 1.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/dist/controller.d.ts +2 -0
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +37 -5
- package/dist/controller.js.map +1 -1
- package/dist/protocols/tn5250-handler.d.ts +14 -1
- package/dist/protocols/tn5250-handler.d.ts.map +1 -1
- package/dist/protocols/tn5250-handler.js +204 -16
- package/dist/protocols/tn5250-handler.js.map +1 -1
- package/dist/routes.js +2 -2
- package/dist/routes.js.map +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -2
- package/dist/session.js.map +1 -1
- package/dist/tn5250/connection.d.ts +2 -1
- package/dist/tn5250/connection.d.ts.map +1 -1
- package/dist/tn5250/connection.js +5 -3
- package/dist/tn5250/connection.js.map +1 -1
- package/dist/tn5250/constants.d.ts +15 -0
- package/dist/tn5250/constants.d.ts.map +1 -1
- package/dist/tn5250/constants.js +18 -0
- package/dist/tn5250/constants.js.map +1 -1
- package/dist/tn5250/encoder.d.ts +11 -0
- package/dist/tn5250/encoder.d.ts.map +1 -1
- package/dist/tn5250/encoder.js +107 -5
- package/dist/tn5250/encoder.js.map +1 -1
- package/dist/tn5250/parser.d.ts +64 -6
- package/dist/tn5250/parser.d.ts.map +1 -1
- package/dist/tn5250/parser.js +579 -134
- package/dist/tn5250/parser.js.map +1 -1
- package/dist/tn5250/screen.d.ts +93 -0
- package/dist/tn5250/screen.d.ts.map +1 -1
- package/dist/tn5250/screen.js +283 -12
- package/dist/tn5250/screen.js.map +1 -1
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +90 -19
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/tn5250/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CMD, ORDER, OPCODE, ATTR } from './constants.js';
|
|
1
|
+
import { CMD, ORDER, OPCODE, ATTR, WDSF_TYPE, WDSF_CLASS } from './constants.js';
|
|
2
2
|
import { ebcdicToChar, ebcdicSymbolChar } from './ebcdic.js';
|
|
3
3
|
/**
|
|
4
4
|
* Parses 5250 data stream records and updates the screen buffer.
|
|
@@ -7,6 +7,15 @@ export class TN5250Parser {
|
|
|
7
7
|
screen;
|
|
8
8
|
/** When true, the next SF order should clear stale fields first */
|
|
9
9
|
pendingFieldsClear = false;
|
|
10
|
+
/** Set when a WSF 5250 Query is received; handler should send query reply */
|
|
11
|
+
pendingQueryReply = false;
|
|
12
|
+
/** Set when an IC order was applied during the last parseOrders call.
|
|
13
|
+
* When true, calculateFieldLengths should NOT override the cursor. */
|
|
14
|
+
icApplied = false;
|
|
15
|
+
/** Active window offset — after CREATE_WINDOW, SBA/RA/EA addresses are
|
|
16
|
+
* relative to the window content area, not the full screen. */
|
|
17
|
+
winRowOff = 0;
|
|
18
|
+
winColOff = 0;
|
|
10
19
|
constructor(screen) {
|
|
11
20
|
this.screen = screen;
|
|
12
21
|
}
|
|
@@ -15,6 +24,7 @@ export class TN5250Parser {
|
|
|
15
24
|
* Returns true if the screen was modified.
|
|
16
25
|
*/
|
|
17
26
|
parseRecord(record) {
|
|
27
|
+
this.icApplied = false;
|
|
18
28
|
if (record.length < 2)
|
|
19
29
|
return false;
|
|
20
30
|
// 5250 record header:
|
|
@@ -67,10 +77,21 @@ export class TN5250Parser {
|
|
|
67
77
|
case OPCODE.INVITE:
|
|
68
78
|
break;
|
|
69
79
|
case OPCODE.SAVE_SCREEN:
|
|
80
|
+
this.screen.saveState();
|
|
81
|
+
if (record.length > dataOffset) {
|
|
82
|
+
modified = this.parseCommandsFromOffset(record, dataOffset);
|
|
83
|
+
}
|
|
84
|
+
modified = true;
|
|
85
|
+
break;
|
|
70
86
|
case OPCODE.RESTORE_SCREEN:
|
|
87
|
+
this.screen.restoreState();
|
|
88
|
+
this.screen.windowList = [];
|
|
89
|
+
this.winRowOff = 0;
|
|
90
|
+
this.winColOff = 0;
|
|
71
91
|
if (record.length > dataOffset) {
|
|
72
92
|
modified = this.parseCommandsFromOffset(record, dataOffset);
|
|
73
93
|
}
|
|
94
|
+
modified = true;
|
|
74
95
|
break;
|
|
75
96
|
default:
|
|
76
97
|
if (record.length > dataOffset) {
|
|
@@ -119,6 +140,9 @@ export class TN5250Parser {
|
|
|
119
140
|
case CMD.CLEAR_UNIT:
|
|
120
141
|
case CMD.CLEAR_UNIT_ALT:
|
|
121
142
|
this.screen.clear();
|
|
143
|
+
this.screen.windowList = [];
|
|
144
|
+
this.winRowOff = 0;
|
|
145
|
+
this.winColOff = 0;
|
|
122
146
|
pos++;
|
|
123
147
|
modified = true;
|
|
124
148
|
break;
|
|
@@ -133,25 +157,62 @@ export class TN5250Parser {
|
|
|
133
157
|
if (pos + 1 < data.length) {
|
|
134
158
|
const cc1 = data[pos++];
|
|
135
159
|
const cc2 = data[pos++];
|
|
136
|
-
// CC1
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
160
|
+
// CC1 upper 3 bits (0xE0 mask) control MDT reset + null fill.
|
|
161
|
+
// Per lib5250 session.c:820-851, this is a 7-value switch:
|
|
162
|
+
let resetNonBypassMdt = false;
|
|
163
|
+
let resetAllMdt = false;
|
|
164
|
+
let nullNonBypassMdt = false;
|
|
165
|
+
let nullNonBypass = false;
|
|
166
|
+
// CC1: lock keyboard for all values except 0x00
|
|
167
|
+
// Per lib5250 session.c:853-858
|
|
168
|
+
if ((cc1 & 0xE0) !== 0x00) {
|
|
169
|
+
this.screen.keyboardLocked = true;
|
|
143
170
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
switch (cc1 & 0xE0) {
|
|
172
|
+
case 0x00: break; // no action (don't lock keyboard)
|
|
173
|
+
case 0x20: break; // reserved / no action in lib5250
|
|
174
|
+
case 0x40:
|
|
175
|
+
resetNonBypassMdt = true;
|
|
176
|
+
break;
|
|
177
|
+
case 0x60:
|
|
178
|
+
resetAllMdt = true;
|
|
179
|
+
break;
|
|
180
|
+
case 0x80:
|
|
181
|
+
nullNonBypassMdt = true;
|
|
182
|
+
break;
|
|
183
|
+
case 0xA0:
|
|
184
|
+
resetNonBypassMdt = true;
|
|
185
|
+
nullNonBypass = true;
|
|
186
|
+
break;
|
|
187
|
+
case 0xC0:
|
|
188
|
+
resetNonBypassMdt = true;
|
|
189
|
+
nullNonBypassMdt = true;
|
|
190
|
+
break;
|
|
191
|
+
case 0xE0:
|
|
192
|
+
resetAllMdt = true;
|
|
193
|
+
nullNonBypass = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
for (const f of this.screen.fields) {
|
|
197
|
+
const isInput = this.screen.isInputField(f);
|
|
198
|
+
// Null fill: clear input field content
|
|
199
|
+
if (isInput && (nullNonBypass || (nullNonBypassMdt && f.modified))) {
|
|
200
|
+
const start = this.screen.offset(f.row, f.col);
|
|
201
|
+
for (let i = start; i < start + f.length; i++) {
|
|
202
|
+
this.screen.buffer[i] = ' ';
|
|
152
203
|
}
|
|
153
204
|
}
|
|
205
|
+
// MDT reset
|
|
206
|
+
if (resetAllMdt || (resetNonBypassMdt && isInput)) {
|
|
207
|
+
f.modified = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Mark that subsequent SF orders in this WTD should clear stale fields
|
|
211
|
+
if (resetAllMdt || resetNonBypassMdt) {
|
|
212
|
+
this.pendingFieldsClear = true;
|
|
154
213
|
}
|
|
214
|
+
// CC2 handling per lib5250 session.c:1033-1066
|
|
215
|
+
this.handleCC2(cc2);
|
|
155
216
|
}
|
|
156
217
|
// Parse orders and data following WTD
|
|
157
218
|
pos = this.parseOrders(data, pos);
|
|
@@ -160,33 +221,92 @@ export class TN5250Parser {
|
|
|
160
221
|
}
|
|
161
222
|
case CMD.WRITE_ERROR_CODE:
|
|
162
223
|
case CMD.WRITE_ERROR_CODE_WIN: {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
224
|
+
// Per lib5250 session.c:733-799: writes error message to message line
|
|
225
|
+
pos++;
|
|
226
|
+
// WRITE_ERROR_CODE_WIN has 2 extra bytes (start/end window)
|
|
227
|
+
if (cmd === CMD.WRITE_ERROR_CODE_WIN && pos + 1 < data.length) {
|
|
228
|
+
pos += 2; // skip startwin, endwin
|
|
229
|
+
}
|
|
230
|
+
// Save cursor so Reset can restore it after unlocking
|
|
231
|
+
this.screen.savedCursorBeforeError = { row: this.screen.cursorRow, col: this.screen.cursorCol };
|
|
232
|
+
// Message line is the last row (row = rows - 1)
|
|
233
|
+
const msgRow = this.screen.rows - 1;
|
|
234
|
+
let msgCol = 0;
|
|
235
|
+
let endY = this.screen.cursorRow;
|
|
236
|
+
let endX = this.screen.cursorCol;
|
|
237
|
+
// Parse error message data: printable chars go to message line,
|
|
238
|
+
// IC sets cursor position (but NOT pending insert per spec)
|
|
239
|
+
while (pos < data.length) {
|
|
240
|
+
const c = data[pos];
|
|
241
|
+
if (c === 0x27) { // ESC — end of error code data
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (c === ORDER.IC && pos + 2 < data.length) {
|
|
245
|
+
// IC within error code: just move cursor, don't set pending insert
|
|
246
|
+
pos++;
|
|
247
|
+
endY = data[pos++] - 1;
|
|
248
|
+
endX = data[pos++] - 1;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// Printable EBCDIC character → write to message line
|
|
252
|
+
if (c >= 0x40 || c === 0x00) {
|
|
253
|
+
const ch = ebcdicToChar(c);
|
|
254
|
+
const addr = this.screen.offset(msgRow, msgCol);
|
|
255
|
+
if (addr < this.screen.size) {
|
|
256
|
+
this.screen.setCharAt(addr, ch);
|
|
257
|
+
}
|
|
258
|
+
msgCol++;
|
|
259
|
+
}
|
|
260
|
+
pos++;
|
|
261
|
+
}
|
|
262
|
+
// Set cursor to the IC position from within the error code
|
|
263
|
+
if (endY >= 0 && endY < this.screen.rows && endX >= 0 && endX < this.screen.cols) {
|
|
264
|
+
this.screen.cursorRow = endY;
|
|
265
|
+
this.screen.cursorCol = endX;
|
|
266
|
+
}
|
|
267
|
+
// Lock keyboard after error
|
|
268
|
+
this.screen.keyboardLocked = true;
|
|
167
269
|
modified = true;
|
|
168
270
|
break;
|
|
169
271
|
}
|
|
170
272
|
case CMD.WRITE_STRUCTURED_FIELD: {
|
|
171
273
|
pos++;
|
|
172
|
-
//
|
|
274
|
+
// WSF: length(2) + class(1) + type(1) + data
|
|
275
|
+
// Per lib5250 session.c:2220-2275
|
|
173
276
|
if (pos + 1 < data.length) {
|
|
174
277
|
const sfLen = (data[pos] << 8) | data[pos + 1];
|
|
175
|
-
pos
|
|
278
|
+
const sfEnd = pos + sfLen;
|
|
279
|
+
// Check for 5250 Query (class 0xD9, type 0x70)
|
|
280
|
+
if (pos + 3 < data.length && data[pos + 2] === WDSF_CLASS && data[pos + 3] === 0x70) {
|
|
281
|
+
this.pendingQueryReply = true;
|
|
282
|
+
}
|
|
283
|
+
pos = Math.min(sfEnd, data.length);
|
|
176
284
|
}
|
|
177
285
|
break;
|
|
178
286
|
}
|
|
179
287
|
case CMD.ROLL: {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
288
|
+
// ROLL: 3 bytes — direction, topRow(1-based), bottomRow(1-based)
|
|
289
|
+
// Per lib5250 session.c:1463-1487
|
|
290
|
+
pos++;
|
|
291
|
+
if (pos + 2 < data.length) {
|
|
292
|
+
const direction = data[pos++];
|
|
293
|
+
const top = data[pos++] - 1; // convert 1-based to 0-based
|
|
294
|
+
const bot = data[pos++] - 1;
|
|
295
|
+
let lines = direction & 0x1F;
|
|
296
|
+
if ((direction & 0x80) === 0) {
|
|
297
|
+
lines = -lines; // scroll up (negative)
|
|
298
|
+
}
|
|
299
|
+
if (lines !== 0 && top >= 0 && bot >= top && bot < this.screen.rows) {
|
|
300
|
+
this.rollBuffer(top, bot, lines);
|
|
301
|
+
}
|
|
186
302
|
modified = true;
|
|
187
303
|
}
|
|
188
304
|
break;
|
|
189
305
|
}
|
|
306
|
+
case 0x04:
|
|
307
|
+
// Sub-record marker — skip (appears between commands in some hosts)
|
|
308
|
+
pos++;
|
|
309
|
+
break;
|
|
190
310
|
default:
|
|
191
311
|
// Not a recognized command at this position
|
|
192
312
|
// Try treating remaining data as orders/text
|
|
@@ -206,6 +326,8 @@ export class TN5250Parser {
|
|
|
206
326
|
let currentAttr = ATTR.NORMAL;
|
|
207
327
|
let useSymbolCharSet = false; // SA type 0x22 can switch to APL/symbol CGCS
|
|
208
328
|
let afterSBA = false; // Track if we just processed an SBA order (field attrs follow SBA)
|
|
329
|
+
let pendingICRow = -1; // IC stores pending cursor position, applied after WTD
|
|
330
|
+
let pendingICCol = -1;
|
|
209
331
|
while (pos < data.length) {
|
|
210
332
|
const byte = data[pos];
|
|
211
333
|
// Within a WTD, all bytes are orders or EBCDIC data.
|
|
@@ -214,83 +336,133 @@ export class TN5250Parser {
|
|
|
214
336
|
// parseOrders consumes data until the end of the buffer.
|
|
215
337
|
switch (byte) {
|
|
216
338
|
case ORDER.SBA: {
|
|
217
|
-
// Set Buffer Address: 2 bytes follow (row, col)
|
|
339
|
+
// Set Buffer Address: 2 bytes follow (row, col) — 1-based from host
|
|
218
340
|
if (pos + 2 >= data.length)
|
|
219
341
|
return data.length;
|
|
220
342
|
pos++;
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
343
|
+
const rawRow = data[pos++];
|
|
344
|
+
const rawCol = data[pos++];
|
|
345
|
+
const row = rawRow - 1 + this.winRowOff;
|
|
346
|
+
const col = rawCol - 1 + this.winColOff;
|
|
347
|
+
if (row >= 0 && row < this.screen.rows && col >= 0 && col < this.screen.cols) {
|
|
348
|
+
currentAddr = this.screen.offset(row, col);
|
|
349
|
+
}
|
|
224
350
|
afterSBA = true; // Field attribute may follow
|
|
225
351
|
continue; // skip the afterSBA = false at end of loop
|
|
226
352
|
}
|
|
227
353
|
case ORDER.IC: {
|
|
228
|
-
// Insert Cursor:
|
|
354
|
+
// Insert Cursor: 2 bytes follow (row, col) — 1-based from host.
|
|
355
|
+
// Per lib5250, IC stores a pending position applied after WTD.
|
|
356
|
+
if (pos + 2 >= data.length)
|
|
357
|
+
return data.length;
|
|
229
358
|
pos++;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
359
|
+
const icRow = data[pos++] - 1 + this.winRowOff;
|
|
360
|
+
const icCol = data[pos++] - 1 + this.winColOff;
|
|
361
|
+
pendingICRow = icRow;
|
|
362
|
+
pendingICCol = icCol;
|
|
233
363
|
break;
|
|
234
364
|
}
|
|
235
365
|
case ORDER.MC: {
|
|
236
|
-
// Move Cursor: 2 bytes follow (row, col)
|
|
366
|
+
// Move Cursor: 2 bytes follow (row, col) — 1-based from host
|
|
237
367
|
if (pos + 2 >= data.length)
|
|
238
368
|
return data.length;
|
|
239
369
|
pos++;
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
this.screen.
|
|
243
|
-
|
|
244
|
-
|
|
370
|
+
const mcRow = data[pos++] - 1 + this.winRowOff;
|
|
371
|
+
const mcCol = data[pos++] - 1 + this.winColOff;
|
|
372
|
+
if (mcRow >= 0 && mcRow < this.screen.rows && mcCol >= 0 && mcCol < this.screen.cols) {
|
|
373
|
+
this.screen.cursorRow = mcRow;
|
|
374
|
+
this.screen.cursorCol = mcCol;
|
|
375
|
+
currentAddr = this.screen.offset(mcRow, mcCol);
|
|
376
|
+
}
|
|
245
377
|
break;
|
|
246
378
|
}
|
|
247
379
|
case ORDER.RA: {
|
|
248
|
-
// Repeat to Address:
|
|
380
|
+
// Repeat to Address: 3 bytes (row, col, char) — 1-based address
|
|
249
381
|
if (pos + 3 >= data.length)
|
|
250
382
|
return data.length;
|
|
251
383
|
pos++;
|
|
252
|
-
const
|
|
253
|
-
const
|
|
384
|
+
const raRow = data[pos++] - 1 + this.winRowOff;
|
|
385
|
+
const raCol = data[pos++] - 1 + this.winColOff;
|
|
254
386
|
const charByte = data[pos++];
|
|
255
|
-
const targetAddr = this.screen.offset(
|
|
387
|
+
const targetAddr = this.screen.offset(raRow, raCol);
|
|
256
388
|
const ch = ebcdicToChar(charByte);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
currentAddr
|
|
389
|
+
// lib5250 uses addch which wraps; we fill up to target (inclusive of current pos)
|
|
390
|
+
if (targetAddr >= currentAddr) {
|
|
391
|
+
while (currentAddr < targetAddr && currentAddr < this.screen.size) {
|
|
392
|
+
this.screen.setCharAt(currentAddr, ch);
|
|
393
|
+
currentAddr++;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Wrap-around: fill to end of screen, then from start to target
|
|
398
|
+
while (currentAddr < this.screen.size) {
|
|
399
|
+
this.screen.setCharAt(currentAddr, ch);
|
|
400
|
+
currentAddr++;
|
|
401
|
+
}
|
|
402
|
+
currentAddr = 0;
|
|
403
|
+
while (currentAddr < targetAddr && currentAddr < this.screen.size) {
|
|
404
|
+
this.screen.setCharAt(currentAddr, ch);
|
|
405
|
+
currentAddr++;
|
|
406
|
+
}
|
|
260
407
|
}
|
|
261
408
|
break;
|
|
262
409
|
}
|
|
263
410
|
case ORDER.EA: {
|
|
264
|
-
// Erase to Address:
|
|
411
|
+
// Erase to Address: 2 bytes — row(1-based), col(1-based)
|
|
412
|
+
// Per 5250 spec (5494 Functions Reference) and lib5250: EA takes
|
|
413
|
+
// only row + col, erases from current address to target address.
|
|
265
414
|
if (pos + 2 >= data.length)
|
|
266
415
|
return data.length;
|
|
267
416
|
pos++;
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
this.screen.
|
|
273
|
-
|
|
417
|
+
const eaRow = data[pos++] - 1 + this.winRowOff;
|
|
418
|
+
const eaCol = data[pos++] - 1 + this.winColOff;
|
|
419
|
+
const eaTarget = this.screen.offset(eaRow, eaCol);
|
|
420
|
+
if (eaTarget >= currentAddr) {
|
|
421
|
+
while (currentAddr < eaTarget && currentAddr < this.screen.size) {
|
|
422
|
+
this.screen.setCharAt(currentAddr, ' ');
|
|
423
|
+
currentAddr++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Wrap-around
|
|
428
|
+
while (currentAddr < this.screen.size) {
|
|
429
|
+
this.screen.setCharAt(currentAddr, ' ');
|
|
430
|
+
currentAddr++;
|
|
431
|
+
}
|
|
432
|
+
currentAddr = 0;
|
|
433
|
+
while (currentAddr < eaTarget && currentAddr < this.screen.size) {
|
|
434
|
+
this.screen.setCharAt(currentAddr, ' ');
|
|
435
|
+
currentAddr++;
|
|
436
|
+
}
|
|
274
437
|
}
|
|
438
|
+
currentAddr = eaTarget;
|
|
275
439
|
break;
|
|
276
440
|
}
|
|
277
441
|
case ORDER.SOH: {
|
|
278
442
|
// Start of Header: variable-length header for input fields.
|
|
443
|
+
// Per lib5250: SOH clears format table and pending insert cursor.
|
|
279
444
|
// Format: [0x01] [length] [data...]
|
|
280
|
-
// The length byte
|
|
445
|
+
// The length byte specifies the number of data bytes following it
|
|
446
|
+
// (NOT including SOH or length byte itself).
|
|
281
447
|
if (pos + 1 >= data.length)
|
|
282
448
|
return data.length;
|
|
283
449
|
pos++;
|
|
450
|
+
this.screen.fields = [];
|
|
451
|
+
this.screen.selectionFields = [];
|
|
452
|
+
pendingICRow = -1;
|
|
453
|
+
pendingICCol = -1;
|
|
284
454
|
const hdrLen = data[pos++];
|
|
285
|
-
pos += Math.max(0, hdrLen
|
|
455
|
+
pos += Math.max(0, hdrLen);
|
|
286
456
|
break;
|
|
287
457
|
}
|
|
288
458
|
case ORDER.TD: {
|
|
289
|
-
// Transparent Data: length
|
|
290
|
-
|
|
459
|
+
// Transparent Data: 2-byte length followed by raw data
|
|
460
|
+
// Per lib5250 session.c:2002-2019
|
|
461
|
+
if (pos + 2 >= data.length)
|
|
291
462
|
return data.length;
|
|
292
463
|
pos++;
|
|
293
|
-
const tdLen = data[pos
|
|
464
|
+
const tdLen = (data[pos] << 8) | data[pos + 1];
|
|
465
|
+
pos += 2;
|
|
294
466
|
for (let i = 0; i < tdLen && pos < data.length; i++) {
|
|
295
467
|
this.screen.setCharAt(currentAddr++, ebcdicToChar(data[pos++]));
|
|
296
468
|
}
|
|
@@ -332,6 +504,46 @@ export class TN5250Parser {
|
|
|
332
504
|
// Preserve afterSBA — SA sets color/highlight context before a field attribute
|
|
333
505
|
continue;
|
|
334
506
|
}
|
|
507
|
+
case ORDER.WDSF: {
|
|
508
|
+
// Write Display Structured Field: 2-byte length + class + type + data
|
|
509
|
+
// Per lib5250 session.c:1909-1984 (0x15 within WTD)
|
|
510
|
+
if (pos + 2 >= data.length)
|
|
511
|
+
return data.length;
|
|
512
|
+
pos++;
|
|
513
|
+
const wdsfLen = (data[pos] << 8) | data[pos + 1];
|
|
514
|
+
const wdsfEnd = pos + Math.max(2, wdsfLen);
|
|
515
|
+
pos += 2; // past length bytes
|
|
516
|
+
if (pos + 1 < data.length) {
|
|
517
|
+
const wdsfClass = data[pos++];
|
|
518
|
+
const wdsfType = data[pos++];
|
|
519
|
+
if (wdsfClass === WDSF_CLASS) {
|
|
520
|
+
switch (wdsfType) {
|
|
521
|
+
case WDSF_TYPE.CREATE_WINDOW:
|
|
522
|
+
this.parseCreateWindow(data, pos, wdsfEnd);
|
|
523
|
+
break;
|
|
524
|
+
case WDSF_TYPE.DEFINE_SELECTION_FIELD:
|
|
525
|
+
this.parseDefineSelectionField(data, pos, wdsfEnd);
|
|
526
|
+
break;
|
|
527
|
+
case WDSF_TYPE.REM_GUI_WINDOW:
|
|
528
|
+
if (this.screen.windowList.length > 0) {
|
|
529
|
+
this.screen.windowList.pop();
|
|
530
|
+
}
|
|
531
|
+
this.winRowOff = 0;
|
|
532
|
+
this.winColOff = 0;
|
|
533
|
+
break;
|
|
534
|
+
case WDSF_TYPE.REM_ALL_GUI_CONSTRUCTS:
|
|
535
|
+
this.screen.windowList = [];
|
|
536
|
+
this.winRowOff = 0;
|
|
537
|
+
this.winColOff = 0;
|
|
538
|
+
break;
|
|
539
|
+
default:
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
pos = Math.min(wdsfEnd, data.length);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
335
547
|
case ORDER.SF: {
|
|
336
548
|
// Start Field: explicit SF order with FFW + optional FCW
|
|
337
549
|
pos++;
|
|
@@ -351,8 +563,8 @@ export class TN5250Parser {
|
|
|
351
563
|
if (currentAddr < this.screen.size) {
|
|
352
564
|
this.screen.setCharAt(currentAddr, ch);
|
|
353
565
|
this.screen.setAttrAt(currentAddr, currentAttr);
|
|
354
|
-
currentAddr++;
|
|
355
566
|
}
|
|
567
|
+
currentAddr++;
|
|
356
568
|
pos++;
|
|
357
569
|
}
|
|
358
570
|
break;
|
|
@@ -360,6 +572,12 @@ export class TN5250Parser {
|
|
|
360
572
|
}
|
|
361
573
|
afterSBA = false;
|
|
362
574
|
}
|
|
575
|
+
// Apply deferred IC cursor position (last IC wins, per lib5250)
|
|
576
|
+
if (pendingICRow >= 0 && pendingICCol >= 0) {
|
|
577
|
+
this.screen.cursorRow = pendingICRow;
|
|
578
|
+
this.screen.cursorCol = pendingICCol;
|
|
579
|
+
this.icApplied = true;
|
|
580
|
+
}
|
|
363
581
|
return pos;
|
|
364
582
|
}
|
|
365
583
|
/**
|
|
@@ -397,64 +615,59 @@ export class TN5250Parser {
|
|
|
397
615
|
return pos;
|
|
398
616
|
}
|
|
399
617
|
/**
|
|
400
|
-
* Parse SF (Start Field) order
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
618
|
+
* Parse SF (Start Field) order.
|
|
619
|
+
* Per lib5250 session.c:1499-1797:
|
|
620
|
+
* First byte: if (byte & 0xE0) != 0x20 → input field with FFW
|
|
621
|
+
* if (byte & 0xE0) == 0x20 → output field, byte IS the attribute
|
|
622
|
+
* Input field: FFW1, FFW2, then loop reading FCW pairs while (byte & 0xE0) != 0x20,
|
|
623
|
+
* then attribute byte (0x20-0x3F), then 2-byte field length.
|
|
624
|
+
* Output field: attribute byte, then 2-byte field length.
|
|
404
625
|
*/
|
|
405
626
|
parseStartField(data, pos, addr, displayAttr) {
|
|
406
627
|
if (addr < this.screen.size) {
|
|
407
628
|
this.screen.setCharAt(addr, ' ');
|
|
408
629
|
}
|
|
409
|
-
|
|
410
|
-
if (pos + 1 >= data.length)
|
|
630
|
+
if (pos >= data.length)
|
|
411
631
|
return pos;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
632
|
+
let curByte = data[pos++];
|
|
633
|
+
let ffw1 = 0, ffw2 = 0, fcw1 = 0, fcw2 = 0;
|
|
634
|
+
let inputField = false;
|
|
635
|
+
if ((curByte & 0xE0) !== 0x20) {
|
|
636
|
+
// Input field: curByte is FFW1
|
|
637
|
+
inputField = true;
|
|
638
|
+
ffw1 = curByte;
|
|
639
|
+
if (pos >= data.length)
|
|
640
|
+
return pos;
|
|
641
|
+
ffw2 = data[pos++];
|
|
642
|
+
// Read FCW pairs: keep reading while next byte is NOT in attribute range
|
|
643
|
+
if (pos >= data.length)
|
|
644
|
+
return pos;
|
|
645
|
+
curByte = data[pos++];
|
|
646
|
+
while ((curByte & 0xE0) !== 0x20 && pos < data.length) {
|
|
647
|
+
fcw1 = curByte;
|
|
421
648
|
fcw2 = data[pos++];
|
|
649
|
+
if (pos >= data.length)
|
|
650
|
+
return pos;
|
|
651
|
+
curByte = data[pos++];
|
|
422
652
|
}
|
|
423
653
|
}
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
fieldDisplayAttr = this.decodeDisplayAttr(attrByte, displayAttr);
|
|
434
|
-
}
|
|
654
|
+
// else: output field, curByte is already the attribute byte
|
|
655
|
+
// curByte is now the attribute byte (0x20-0x3F)
|
|
656
|
+
const rawAttrByte = curByte;
|
|
657
|
+
const fieldDisplayAttr = this.decodeDisplayAttr(rawAttrByte, displayAttr);
|
|
658
|
+
// Read 2-byte field length (always present after attribute byte per lib5250)
|
|
659
|
+
let fieldLength = 0;
|
|
660
|
+
if (pos + 1 < data.length) {
|
|
661
|
+
fieldLength = (data[pos] << 8) | data[pos + 1];
|
|
662
|
+
pos += 2;
|
|
435
663
|
}
|
|
436
664
|
const fieldStartAddr = addr + 1;
|
|
437
665
|
const { row, col } = this.screen.toRowCol(fieldStartAddr);
|
|
438
|
-
// After SF + FFW + optional FCW + ATTR, the host may include a few stale
|
|
439
|
-
// bytes (field content initializers like nulls) before the next SBA order.
|
|
440
|
-
// These should not be displayed. Scan ahead (up to 4 bytes) for the next
|
|
441
|
-
// SBA — if found, skip everything in between.
|
|
442
|
-
{
|
|
443
|
-
let scan = pos;
|
|
444
|
-
const limit = Math.min(pos + 4, data.length);
|
|
445
|
-
while (scan < limit) {
|
|
446
|
-
if (data[scan] === ORDER.SBA) {
|
|
447
|
-
pos = scan; // skip stale bytes
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
scan++;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
666
|
const field = {
|
|
454
667
|
row,
|
|
455
668
|
col,
|
|
456
|
-
length:
|
|
457
|
-
ffw1,
|
|
669
|
+
length: fieldLength,
|
|
670
|
+
ffw1: inputField ? ffw1 : 0x20, // BYPASS bit for output fields
|
|
458
671
|
ffw2,
|
|
459
672
|
fcw1,
|
|
460
673
|
fcw2,
|
|
@@ -468,19 +681,249 @@ export class TN5250Parser {
|
|
|
468
681
|
}
|
|
469
682
|
/**
|
|
470
683
|
* Decode a display attribute byte (0x20–0x3F) into an ATTR constant.
|
|
471
|
-
*
|
|
472
|
-
*
|
|
684
|
+
*
|
|
685
|
+
* 5250 field attribute byte layout (bits of lower nibble):
|
|
686
|
+
* Lower 3 bits (0x07) determine display type:
|
|
687
|
+
* 0 = normal, 1 = column separator, 2 = high intensity,
|
|
688
|
+
* 3 = column separator + HI, 4 = underscore, 5 = underscore + reverse,
|
|
689
|
+
* 6 = underscore + HI, 7 = non-display
|
|
690
|
+
* Bit 3 (0x08): when set, indicates color field (RED, TURQ, etc.)
|
|
691
|
+
* Color is determined by the full byte value (0x28=RED, 0x30=TURQ, etc.)
|
|
692
|
+
* For color fields, the lower 3 bits still encode the display type.
|
|
473
693
|
*/
|
|
474
694
|
decodeDisplayAttr(attrByte, displayAttr = ATTR.NORMAL) {
|
|
475
|
-
|
|
695
|
+
const type = attrByte & 0x07;
|
|
696
|
+
if (type === 0x07)
|
|
476
697
|
return ATTR.NON_DISPLAY;
|
|
477
|
-
if (
|
|
478
|
-
return ATTR.UNDERSCORE;
|
|
479
|
-
if (
|
|
698
|
+
if (type >= 0x04)
|
|
699
|
+
return ATTR.UNDERSCORE; // 4, 5, 6 all have underscore
|
|
700
|
+
if (type === 0x02 || type === 0x03)
|
|
480
701
|
return ATTR.HIGH_INTENSITY;
|
|
481
|
-
if (
|
|
482
|
-
return ATTR.
|
|
483
|
-
|
|
702
|
+
if (type === 0x01)
|
|
703
|
+
return ATTR.COLUMN_SEPARATOR;
|
|
704
|
+
// type === 0x00: normal or color field
|
|
705
|
+
// Bit 3 (0x08) set = color field, treat as normal display (green on most terminals)
|
|
706
|
+
return ATTR.NORMAL;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Parse CREATE_WINDOW structured field data.
|
|
710
|
+
* Per lib5250 session.c:3129-3349.
|
|
711
|
+
* Window position = current cursor position. Renders border and erases content area.
|
|
712
|
+
*/
|
|
713
|
+
parseCreateWindow(data, pos, end) {
|
|
714
|
+
if (pos + 4 >= end)
|
|
715
|
+
return;
|
|
716
|
+
const _flagbyte1 = data[pos++]; // 0x80=cursor restricted, 0x40=pull-down
|
|
717
|
+
pos += 2; // 2 reserved bytes
|
|
718
|
+
const depth = data[pos++]; // content height
|
|
719
|
+
const width = data[pos++]; // content width
|
|
720
|
+
// Parse border minor structures per lib5250 session.c:3129-3349
|
|
721
|
+
let borderChars = { ul: '.', top: '.', ur: '.', left: ':', right: ':', ll: ':', bot: '.', lr: ':' };
|
|
722
|
+
let titleText = '';
|
|
723
|
+
let footerText = '';
|
|
724
|
+
while (pos < end) {
|
|
725
|
+
if (pos + 2 > end)
|
|
726
|
+
break;
|
|
727
|
+
const minorLen = data[pos];
|
|
728
|
+
if (minorLen < 2 || pos + minorLen > end)
|
|
729
|
+
break;
|
|
730
|
+
const minorType = data[pos + 1];
|
|
731
|
+
if (minorType === 0x01 && minorLen >= 13) {
|
|
732
|
+
// Border Presentation: flags(1) + mono_attr(1) + color_attr(1) + 8 border chars
|
|
733
|
+
borderChars = {
|
|
734
|
+
ul: ebcdicToChar(data[pos + 5]),
|
|
735
|
+
top: ebcdicToChar(data[pos + 6]),
|
|
736
|
+
ur: ebcdicToChar(data[pos + 7]),
|
|
737
|
+
left: ebcdicToChar(data[pos + 8]),
|
|
738
|
+
right: ebcdicToChar(data[pos + 9]),
|
|
739
|
+
ll: ebcdicToChar(data[pos + 10]),
|
|
740
|
+
bot: ebcdicToChar(data[pos + 11]),
|
|
741
|
+
lr: ebcdicToChar(data[pos + 12]),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
else if (minorType === 0x10 && minorLen > 6) {
|
|
745
|
+
// Window Title/Footer: flags(1) + mono_attr(1) + color_attr(1) + reserved(1) + text
|
|
746
|
+
const isFooter = (data[pos + 2] & 0x40) !== 0;
|
|
747
|
+
let text = '';
|
|
748
|
+
for (let i = 6; i < minorLen; i++) {
|
|
749
|
+
text += ebcdicToChar(data[pos + i]);
|
|
750
|
+
}
|
|
751
|
+
if (isFooter)
|
|
752
|
+
footerText = text;
|
|
753
|
+
else
|
|
754
|
+
titleText = text;
|
|
755
|
+
}
|
|
756
|
+
pos += minorLen;
|
|
757
|
+
}
|
|
758
|
+
const winRow = this.screen.cursorRow;
|
|
759
|
+
const winCol = this.screen.cursorCol;
|
|
760
|
+
this.screen.windowList.push({ row: winRow, col: winCol, height: depth, width: width });
|
|
761
|
+
// Render border with custom characters from host
|
|
762
|
+
this.screen.renderWindowBorderCustom(winRow, winCol, depth, width, borderChars, titleText, footerText);
|
|
763
|
+
// Erase content area inside the border
|
|
764
|
+
this.screen.eraseRegion(winRow + 1, winCol + 1, winRow + depth, winCol + width);
|
|
765
|
+
// Set window offset — subsequent SBA/RA/EA addresses are relative to
|
|
766
|
+
// the window content area (row+1, col+1)
|
|
767
|
+
this.winRowOff = winRow + 1;
|
|
768
|
+
this.winColOff = winCol + 1;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Parse DEFINE_SELECTION_FIELD structured field data.
|
|
772
|
+
* Per lib5250 session.c:2930-3128 (tn5250_session_handle_define_selection).
|
|
773
|
+
*
|
|
774
|
+
* 16-byte fixed header:
|
|
775
|
+
* [0] flagbyte1 [1] flagbyte2 [2] flagbyte3 [3] fieldtype
|
|
776
|
+
* [4-8] reserved (5 bytes)
|
|
777
|
+
* [9] itemsize [10] height [11] items [12] padding
|
|
778
|
+
* [13] separator [14] selectionchar [15] cancelaid
|
|
779
|
+
*
|
|
780
|
+
* Followed by minor structures (type 0x10 = choice text, etc.)
|
|
781
|
+
*/
|
|
782
|
+
parseDefineSelectionField(data, pos, end) {
|
|
783
|
+
if (pos + 16 > end)
|
|
784
|
+
return;
|
|
785
|
+
const _flagbyte1 = data[pos++];
|
|
786
|
+
const _flagbyte2 = data[pos++];
|
|
787
|
+
const _flagbyte3 = data[pos++];
|
|
788
|
+
const fieldType = data[pos++]; // 0x01=menubar, 0x11=single, 0x12=multi, etc.
|
|
789
|
+
pos += 5; // 5 reserved bytes
|
|
790
|
+
const itemSize = data[pos++]; // width of each choice text
|
|
791
|
+
const numRows = data[pos++]; // visible rows
|
|
792
|
+
const numItems = data[pos++]; // total choices
|
|
793
|
+
const _padding = data[pos++];
|
|
794
|
+
const _separator = data[pos++];
|
|
795
|
+
const _selectionChar = data[pos++];
|
|
796
|
+
const _cancelAid = data[pos++];
|
|
797
|
+
const baseRow = this.screen.cursorRow;
|
|
798
|
+
const baseCol = this.screen.cursorCol;
|
|
799
|
+
const choices = [];
|
|
800
|
+
let choiceIndex = 0;
|
|
801
|
+
// Parse minor structures
|
|
802
|
+
while (pos < end) {
|
|
803
|
+
if (pos + 2 > end)
|
|
804
|
+
break;
|
|
805
|
+
const minorLen = data[pos];
|
|
806
|
+
if (minorLen < 2 || pos + minorLen > end)
|
|
807
|
+
break;
|
|
808
|
+
const minorType = data[pos + 1];
|
|
809
|
+
if (minorType === 0x10 && minorLen >= 5) {
|
|
810
|
+
// Choice text: flagbyte1(1) + flagbyte2(1) + flagbyte3(1) + optional fields + text
|
|
811
|
+
// Per lib5250: flagbyte3 bits 7-5 determine if this choice is used.
|
|
812
|
+
// If all zero, the entire minor structure is ignored.
|
|
813
|
+
const choiceFlagbyte1 = data[pos + 2];
|
|
814
|
+
const _choiceFlagbyte2 = data[pos + 3];
|
|
815
|
+
const choiceFlagbyte3 = data[pos + 4];
|
|
816
|
+
// Skip if flagbyte3 bits 7-5 are all zero (choice not applicable)
|
|
817
|
+
if ((choiceFlagbyte3 & 0xE0) !== 0) {
|
|
818
|
+
// Calculate text offset: skip flags, optional mnemonic/AID/numeric fields
|
|
819
|
+
let textOff = 5;
|
|
820
|
+
if (choiceFlagbyte1 & 0x08)
|
|
821
|
+
textOff++; // mnemonic offset
|
|
822
|
+
if (choiceFlagbyte1 & 0x04)
|
|
823
|
+
textOff++; // AID byte
|
|
824
|
+
const numericSel = choiceFlagbyte1 & 0x03;
|
|
825
|
+
if (numericSel === 1)
|
|
826
|
+
textOff++; // single-digit numeric
|
|
827
|
+
else if (numericSel === 2)
|
|
828
|
+
textOff += 2; // double-digit numeric
|
|
829
|
+
let text = '';
|
|
830
|
+
for (let i = textOff; i < minorLen; i++) {
|
|
831
|
+
text += ebcdicToChar(data[pos + i]);
|
|
832
|
+
}
|
|
833
|
+
const choiceRow = baseRow + choiceIndex;
|
|
834
|
+
choices.push({ text, row: choiceRow, col: baseCol });
|
|
835
|
+
choiceIndex++;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
pos += minorLen;
|
|
839
|
+
}
|
|
840
|
+
this.screen.selectionFields.push({
|
|
841
|
+
row: baseRow,
|
|
842
|
+
col: baseCol,
|
|
843
|
+
numRows,
|
|
844
|
+
numCols: itemSize,
|
|
845
|
+
choices,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Roll (scroll) screen buffer rows within [top, bot] by `lines` rows.
|
|
850
|
+
* Negative = scroll up, positive = scroll down.
|
|
851
|
+
* Per lib5250 dbuffer.c:869-899.
|
|
852
|
+
*/
|
|
853
|
+
rollBuffer(top, bot, lines) {
|
|
854
|
+
const cols = this.screen.cols;
|
|
855
|
+
if (lines < 0) {
|
|
856
|
+
// Scroll up: move rows upward
|
|
857
|
+
for (let r = top; r <= bot; r++) {
|
|
858
|
+
if (r + lines >= top) {
|
|
859
|
+
const dstOff = (r + lines) * cols;
|
|
860
|
+
const srcOff = r * cols;
|
|
861
|
+
for (let c = 0; c < cols; c++) {
|
|
862
|
+
this.screen.buffer[dstOff + c] = this.screen.buffer[srcOff + c];
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// Clear vacated rows at bottom
|
|
867
|
+
for (let r = bot + lines + 1; r <= bot; r++) {
|
|
868
|
+
if (r >= top) {
|
|
869
|
+
const off = r * cols;
|
|
870
|
+
for (let c = 0; c < cols; c++) {
|
|
871
|
+
this.screen.buffer[off + c] = ' ';
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
// Scroll down: move rows downward
|
|
878
|
+
for (let r = bot; r >= top; r--) {
|
|
879
|
+
if (r + lines <= bot) {
|
|
880
|
+
const dstOff = (r + lines) * cols;
|
|
881
|
+
const srcOff = r * cols;
|
|
882
|
+
for (let c = 0; c < cols; c++) {
|
|
883
|
+
this.screen.buffer[dstOff + c] = this.screen.buffer[srcOff + c];
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
// Clear vacated rows at top
|
|
888
|
+
for (let r = top; r < top + lines; r++) {
|
|
889
|
+
if (r <= bot) {
|
|
890
|
+
const off = r * cols;
|
|
891
|
+
for (let c = 0; c < cols; c++) {
|
|
892
|
+
this.screen.buffer[off + c] = ' ';
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Handle CC2 (Control Character 2) from WTD.
|
|
900
|
+
* Per lib5250 session.c:1033-1066.
|
|
901
|
+
*
|
|
902
|
+
* CC2 bit flags:
|
|
903
|
+
* 0x40 IC_ULOCK — suppress IC cursor positioning when unlocking
|
|
904
|
+
* 0x20 CLR_BLINK — clear blink
|
|
905
|
+
* 0x10 SET_BLINK — set blink
|
|
906
|
+
* 0x08 UNLOCK — unlock keyboard
|
|
907
|
+
* 0x04 ALARM — sound audible alarm
|
|
908
|
+
* 0x02 MESSAGE_OFF — clear message waiting indicator
|
|
909
|
+
* 0x01 MESSAGE_ON — set message waiting indicator
|
|
910
|
+
*/
|
|
911
|
+
handleCC2(cc2) {
|
|
912
|
+
// Message waiting indicator
|
|
913
|
+
if (cc2 & 0x01) {
|
|
914
|
+
this.screen.messageWaiting = true;
|
|
915
|
+
}
|
|
916
|
+
if ((cc2 & 0x02) && !(cc2 & 0x01)) {
|
|
917
|
+
this.screen.messageWaiting = false;
|
|
918
|
+
}
|
|
919
|
+
// Alarm
|
|
920
|
+
if (cc2 & 0x04) {
|
|
921
|
+
this.screen.pendingAlarm = true;
|
|
922
|
+
}
|
|
923
|
+
// Unlock keyboard
|
|
924
|
+
if (cc2 & 0x08) {
|
|
925
|
+
this.screen.keyboardLocked = false;
|
|
926
|
+
}
|
|
484
927
|
}
|
|
485
928
|
/** Clear stale fields when the first SF order arrives after a Reset MDT WTD */
|
|
486
929
|
clearStaleFieldsOnce() {
|
|
@@ -505,6 +948,10 @@ export class TN5250Parser {
|
|
|
505
948
|
});
|
|
506
949
|
for (let i = 0; i < fields.length; i++) {
|
|
507
950
|
const current = fields[i];
|
|
951
|
+
// If the field already has an explicit length from SF order, keep it
|
|
952
|
+
if (current.length > 0)
|
|
953
|
+
continue;
|
|
954
|
+
// Otherwise infer length from adjacent field positions (bare field attributes)
|
|
508
955
|
const currentStart = this.screen.offset(current.row, current.col);
|
|
509
956
|
if (i + 1 < fields.length) {
|
|
510
957
|
const next = fields[i + 1];
|
|
@@ -526,28 +973,26 @@ export class TN5250Parser {
|
|
|
526
973
|
if (current.length <= 0)
|
|
527
974
|
current.length = 1;
|
|
528
975
|
}
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
976
|
+
// Cursor homing per lib5250 display.c:614-635 (set_cursor_home):
|
|
977
|
+
// 1. If cursor is inside a field (input or protected) — trust it.
|
|
978
|
+
// 2. If cursor is outside all fields, nudge to the first functional
|
|
979
|
+
// input field (with native underscore). This avoids landing on
|
|
980
|
+
// UIM NON_DISPLAY artifact fields that aren't interactive.
|
|
981
|
+
// 3. If no functional fields exist, leave cursor where IC put it.
|
|
533
982
|
{
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
});
|
|
545
|
-
if (!inTarget) {
|
|
546
|
-
const after = targets.find(f => this.screen.offset(f.row, f.col) >= cursorAddr);
|
|
547
|
-
const target = after || targets[targets.length - 1];
|
|
548
|
-
this.screen.cursorRow = target.row;
|
|
549
|
-
this.screen.cursorCol = target.col;
|
|
983
|
+
const cursorAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
|
|
984
|
+
const cursorInField = fields.some(f => {
|
|
985
|
+
const start = this.screen.offset(f.row, f.col);
|
|
986
|
+
return cursorAddr >= start && cursorAddr < start + f.length;
|
|
987
|
+
});
|
|
988
|
+
if (!cursorInField) {
|
|
989
|
+
const functional = fields.find(f => this.screen.isInputField(f) && this.screen.hasNativeUnderscore(f));
|
|
990
|
+
if (functional) {
|
|
991
|
+
this.screen.cursorRow = functional.row;
|
|
992
|
+
this.screen.cursorCol = functional.col;
|
|
550
993
|
}
|
|
994
|
+
// If no functional fields, leave cursor at IC position — the screen
|
|
995
|
+
// may not have interactive input (e.g. DSPMSG with only F-key actions)
|
|
551
996
|
}
|
|
552
997
|
}
|
|
553
998
|
// Deduplicate fields at the same position (keep the last one)
|