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.
@@ -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
- * Handle records with non-standard framing (e.g. opcode 0x04 from
114
- * pub400.com). These records contain valid commands (CLEAR_UNIT, WTD,
115
- * READ_*, etc.) but with extra sub-record marker bytes (0x04) between
116
- * the GDS header and the actual commands. We scan for the first known
117
- * command byte, skipping 0x04 markers, and hand off to parseCommands.
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
- for (let i = start; i < data.length; i++) {
127
- // Skip sub-record markers (0x04)
128
- if (data[i] === 0x04)
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 false;
185
+ return modified;
152
186
  }
153
- /** Parse one or more 5250 commands starting at offset */
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
- // Skip the trailing flag byte (per 5250 spec)
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
- const cc2 = data[pos++];
210
- // CC1 upper 3 bits (0xE0 mask) control MDT reset + null fill.
211
- // Per lib5250 session.c:820-851, this is a 7-value switch:
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
- if (resetAllMdt || resetNonBypassMdt) {
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
- this.handleCC2(cc2);
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 === 0x27) { // ESC — end of error code data
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, unlocks the keyboard (if
348
- // normally locked), and sets read_opcode. The CC bytes have the
349
- // same semantics as WTD CC1/CC2.
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
- // Lock keyboard if CC1 non-zero (same as WTD behavior)
356
- if ((cc1 & 0xE0) !== 0x00) {
357
- this.screen.keyboardLocked = true;
358
- }
360
+ this.handleCC1(cc1);
359
361
  this.handleCC2(cc2);
360
362
  }
361
- // Unlock keyboard on Read command host is requesting input
362
- this.screen.keyboardLocked = false;
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
- case 0x04:
387
- // Sub-record marker — skip (appears between commands in some hosts)
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
- if (mcRow >= 0 && mcRow < this.screen.rows && mcCol >= 0 && mcCol < this.screen.cols) {
505
- this.screen.cursorRow = mcRow;
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
- this.screen.setCharAt(currentAddr, ch);
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
- this.screen.setCharAt(currentAddr, ch);
534
- currentAddr++;
572
+ raFill();
535
573
  }
536
574
  currentAddr = 0;
537
575
  while (currentAddr <= targetAddr && currentAddr < this.screen.size) {
538
- this.screen.setCharAt(currentAddr, ch);
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 eraseAll = false;
595
+ let lastAttr = 0;
558
596
  for (let i = 0; i < attrCount && pos < data.length; i++) {
559
- if (data[pos] === 0xFF)
560
- eraseAll = true;
561
- pos++;
597
+ lastAttr = data[pos++];
562
598
  }
563
- // lib5250 only erases characters when attribute 0xFF is present
564
- // (we don't track extended attributes separately). Default on any
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 and pending insert cursor.
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
- // Apply deferred IC cursor position (last IC wins, per lib5250)
832
- if (pendingICRow >= 0 && pendingICCol >= 0) {
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
- modified: false,
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
- this.screen.fields.push(field);
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
- // Skip if flagbyte3 bits 7-5 are all zero (choice not applicable)
1183
- if ((choiceFlagbyte3 & 0xE0) !== 0) {
1184
- // Calculate text offset: skip flags, optional mnemonic/AID/numeric fields
1185
- let textOff = 5;
1186
- if (choiceFlagbyte1 & 0x08)
1187
- textOff++; // mnemonic offset
1188
- if (choiceFlagbyte1 & 0x04)
1189
- textOff++; // AID byte
1190
- const numericSel = choiceFlagbyte1 & 0x03;
1191
- if (numericSel === 1)
1192
- textOff++; // single-digit numeric
1193
- else if (numericSel === 2)
1194
- textOff += 2; // double-digit numeric
1195
- let text = '';
1196
- for (let i = textOff; i < minorLen; i++) {
1197
- text += ebcdicToChar(data[pos + i]);
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
- this.screen.selectionFields.push({
1207
- row: baseRow,
1208
- col: baseCol,
1209
- numRows,
1210
- numCols: itemSize,
1211
- choices,
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;