green-screen-proxy 1.1.1 → 1.2.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.
Files changed (91) hide show
  1. package/README.md +93 -8
  2. package/dist/cli.js +0 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/controller.d.ts +3 -0
  5. package/dist/controller.d.ts.map +1 -1
  6. package/dist/controller.js +14 -5
  7. package/dist/controller.js.map +1 -1
  8. package/dist/index.d.ts +1 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +18 -16
  11. package/dist/index.js.map +1 -1
  12. package/dist/protocols/tn3270-handler.d.ts +2 -1
  13. package/dist/protocols/tn3270-handler.d.ts.map +1 -1
  14. package/dist/protocols/tn3270-handler.js +10 -2
  15. package/dist/protocols/tn3270-handler.js.map +1 -1
  16. package/dist/protocols/tn5250-handler.d.ts +3 -2
  17. package/dist/protocols/tn5250-handler.d.ts.map +1 -1
  18. package/dist/protocols/tn5250-handler.js +85 -26
  19. package/dist/protocols/tn5250-handler.js.map +1 -1
  20. package/dist/protocols/types.d.ts +30 -2
  21. package/dist/protocols/types.d.ts.map +1 -1
  22. package/dist/protocols/types.js +32 -0
  23. package/dist/protocols/types.js.map +1 -1
  24. package/dist/routes.d.ts.map +1 -1
  25. package/dist/routes.js +261 -30
  26. package/dist/routes.js.map +1 -1
  27. package/dist/server.js +1 -5
  28. package/dist/server.js.map +1 -1
  29. package/dist/session-store.d.ts +68 -0
  30. package/dist/session-store.d.ts.map +1 -0
  31. package/dist/session-store.js +40 -0
  32. package/dist/session-store.js.map +1 -0
  33. package/dist/session.d.ts +32 -2
  34. package/dist/session.d.ts.map +1 -1
  35. package/dist/session.js +105 -9
  36. package/dist/session.js.map +1 -1
  37. package/dist/standalone.d.ts +3 -0
  38. package/dist/standalone.d.ts.map +1 -0
  39. package/dist/standalone.js +6 -0
  40. package/dist/standalone.js.map +1 -0
  41. package/dist/tn3270/connection.d.ts +1 -1
  42. package/dist/tn3270/connection.d.ts.map +1 -1
  43. package/dist/tn3270/connection.js +2 -2
  44. package/dist/tn3270/connection.js.map +1 -1
  45. package/dist/tn3270/screen.d.ts +1 -0
  46. package/dist/tn3270/screen.d.ts.map +1 -1
  47. package/dist/tn3270/screen.js +1 -0
  48. package/dist/tn3270/screen.js.map +1 -1
  49. package/dist/tn5250/connection.d.ts +6 -1
  50. package/dist/tn5250/connection.d.ts.map +1 -1
  51. package/dist/tn5250/connection.js +27 -3
  52. package/dist/tn5250/connection.js.map +1 -1
  53. package/dist/tn5250/constants.d.ts +39 -3
  54. package/dist/tn5250/constants.d.ts.map +1 -1
  55. package/dist/tn5250/constants.js +51 -3
  56. package/dist/tn5250/constants.js.map +1 -1
  57. package/dist/tn5250/ebcdic-jp-builtin.d.ts +45 -0
  58. package/dist/tn5250/ebcdic-jp-builtin.d.ts.map +1 -0
  59. package/dist/tn5250/ebcdic-jp-builtin.js +124 -0
  60. package/dist/tn5250/ebcdic-jp-builtin.js.map +1 -0
  61. package/dist/tn5250/ebcdic-jp.d.ts +61 -0
  62. package/dist/tn5250/ebcdic-jp.d.ts.map +1 -0
  63. package/dist/tn5250/ebcdic-jp.js +188 -0
  64. package/dist/tn5250/ebcdic-jp.js.map +1 -0
  65. package/dist/tn5250/ebcdic.d.ts +13 -4
  66. package/dist/tn5250/ebcdic.d.ts.map +1 -1
  67. package/dist/tn5250/ebcdic.js +30 -8
  68. package/dist/tn5250/ebcdic.js.map +1 -1
  69. package/dist/tn5250/encoder.d.ts +41 -9
  70. package/dist/tn5250/encoder.d.ts.map +1 -1
  71. package/dist/tn5250/encoder.js +228 -41
  72. package/dist/tn5250/encoder.js.map +1 -1
  73. package/dist/tn5250/parser.d.ts +14 -0
  74. package/dist/tn5250/parser.d.ts.map +1 -1
  75. package/dist/tn5250/parser.js +428 -53
  76. package/dist/tn5250/parser.js.map +1 -1
  77. package/dist/tn5250/screen.d.ts +144 -24
  78. package/dist/tn5250/screen.d.ts.map +1 -1
  79. package/dist/tn5250/screen.js +245 -13
  80. package/dist/tn5250/screen.js.map +1 -1
  81. package/dist/ui/assets/index-B51sr7HL.js +56 -0
  82. package/dist/ui/assets/index-B9wpEWAh.css +1 -0
  83. package/dist/ui/assets/index-BrUnECmE.css +1 -0
  84. package/dist/ui/assets/index-CDBbEXbH.js +56 -0
  85. package/dist/ui/index.html +16 -0
  86. package/dist/websocket.d.ts +3 -0
  87. package/dist/websocket.d.ts.map +1 -1
  88. package/dist/websocket.js +90 -1
  89. package/dist/websocket.js.map +1 -1
  90. package/dist/worker/index.js +5573 -0
  91. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { CMD, ORDER, OPCODE, ATTR, WDSF_TYPE, WDSF_CLASS } from './constants.js';
2
2
  import { ebcdicToChar, ebcdicSymbolChar } from './ebcdic.js';
3
+ import { SI, SO, decodeDbcsPair } from './ebcdic-jp.js';
3
4
  /**
4
5
  * Parses 5250 data stream records and updates the screen buffer.
5
6
  */
@@ -103,8 +104,10 @@ export class TN5250Parser {
103
104
  }
104
105
  /** Try to parse data that doesn't have a proper GDS header */
105
106
  tryParseRawData(record) {
106
- // Some servers send command data without the full GDS wrapper
107
- return this.parseCommands(record, 0);
107
+ // Some servers send command data without the full GDS wrapper.
108
+ // Use parseCommandsFromOffset to skip 0x04 escape markers and find
109
+ // the first known command byte — handles non-GDS hosts.
110
+ return this.parseCommandsFromOffset(record, 0);
108
111
  }
109
112
  /**
110
113
  * Handle records with non-standard framing (e.g. opcode 0x04 from
@@ -138,12 +141,42 @@ export class TN5250Parser {
138
141
  const cmd = data[pos];
139
142
  switch (cmd) {
140
143
  case CMD.CLEAR_UNIT:
144
+ // Per lib5250 display.c:1889-1907: resize to 24x80, clear content,
145
+ // lock keyboard, clear pending insert cursor, clear message lines.
146
+ this.screen.resize(24, 80);
147
+ this.screen.keyboardLocked = true;
148
+ this.screen.insertMode = false;
149
+ this.screen.savedCursorBeforeError = null;
150
+ this.screen.windowList = [];
151
+ this.screen.selectionFields = [];
152
+ this.screen.scrollbarList = [];
153
+ this.screen.savedMsgLine = null;
154
+ this.screen.savedMsgLineRow = -1;
155
+ this.screen.headerData = [];
156
+ this.winRowOff = 0;
157
+ this.winColOff = 0;
158
+ pos++;
159
+ modified = true;
160
+ break;
141
161
  case CMD.CLEAR_UNIT_ALT:
142
- this.screen.clear();
162
+ // Per lib5250 display.c:1919-1937: resize to 27x132 and reset state.
163
+ // A flag byte follows (0x00 = alt screen, others reserved).
164
+ this.screen.resize(27, 132);
165
+ this.screen.keyboardLocked = true;
166
+ this.screen.insertMode = false;
167
+ this.screen.savedCursorBeforeError = null;
143
168
  this.screen.windowList = [];
169
+ this.screen.selectionFields = [];
170
+ this.screen.scrollbarList = [];
171
+ this.screen.savedMsgLine = null;
172
+ this.screen.savedMsgLineRow = -1;
173
+ this.screen.headerData = [];
144
174
  this.winRowOff = 0;
145
175
  this.winColOff = 0;
146
176
  pos++;
177
+ // Skip the trailing flag byte (per 5250 spec)
178
+ if (pos < data.length)
179
+ pos++;
147
180
  modified = true;
148
181
  break;
149
182
  case CMD.CLEAR_FORMAT_TABLE:
@@ -229,8 +262,12 @@ export class TN5250Parser {
229
262
  }
230
263
  // Save cursor so Reset can restore it after unlocking
231
264
  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;
265
+ // Save the current contents of the message-line row so Reset can
266
+ // restore it (per lib5250 display.c:2489-2502).
267
+ this.screen.saveMsgLine();
268
+ // Message line row is configurable via SOH header byte 3, or the
269
+ // last row by default (per lib5250 dbuffer.c:934-944).
270
+ const msgRow = this.screen.msgLineRow();
234
271
  let msgCol = 0;
235
272
  let endY = this.screen.cursorRow;
236
273
  let endX = this.screen.cursorCol;
@@ -250,7 +287,7 @@ export class TN5250Parser {
250
287
  }
251
288
  // Printable EBCDIC character → write to message line
252
289
  if (c >= 0x40 || c === 0x00) {
253
- const ch = ebcdicToChar(c);
290
+ const ch = ebcdicToChar(c, this.screen.codePage);
254
291
  const addr = this.screen.offset(msgRow, msgCol);
255
292
  if (addr < this.screen.size) {
256
293
  this.screen.setCharAt(addr, ch);
@@ -284,6 +321,32 @@ export class TN5250Parser {
284
321
  }
285
322
  break;
286
323
  }
324
+ case CMD.READ_INPUT_FIELDS:
325
+ case CMD.READ_MDT_FIELDS:
326
+ case CMD.READ_MDT_FIELDS_ALT:
327
+ case CMD.READ_IMMEDIATE:
328
+ case CMD.READ_IMMEDIATE_ALT: {
329
+ // Per lib5250 session.c:2328-2354 (tn5250_session_read_cmd):
330
+ // Reads 2 CC bytes, handles CC1/CC2, unlocks the keyboard (if
331
+ // normally locked), and sets read_opcode. The CC bytes have the
332
+ // same semantics as WTD CC1/CC2.
333
+ const readOp = cmd;
334
+ pos++;
335
+ if (pos + 1 < data.length) {
336
+ const cc1 = data[pos++];
337
+ const cc2 = data[pos++];
338
+ // Lock keyboard if CC1 non-zero (same as WTD behavior)
339
+ if ((cc1 & 0xE0) !== 0x00) {
340
+ this.screen.keyboardLocked = true;
341
+ }
342
+ this.handleCC2(cc2);
343
+ }
344
+ // Unlock keyboard on Read command — host is requesting input
345
+ this.screen.keyboardLocked = false;
346
+ this.screen.readOpcode = readOp;
347
+ modified = true;
348
+ break;
349
+ }
287
350
  case CMD.ROLL: {
288
351
  // ROLL: 3 bytes — direction, topRow(1-based), bottomRow(1-based)
289
352
  // Per lib5250 session.c:1463-1487
@@ -328,6 +391,49 @@ export class TN5250Parser {
328
391
  let afterSBA = false; // Track if we just processed an SBA order (field attrs follow SBA)
329
392
  let pendingICRow = -1; // IC stores pending cursor position, applied after WTD
330
393
  let pendingICCol = -1;
394
+ // Pending extended attributes set by WEA orders; applied to subsequent
395
+ // cells until modified/reset. Per 5250 Functions Reference the ECB
396
+ // values persist across characters until explicitly changed.
397
+ let extColor = 0;
398
+ let extHighlight = 0;
399
+ let extCharSet = 0;
400
+ // DBCS shift state: once SO (0x0E) is seen, read bytes in pairs until
401
+ // SI (0x0F). Per 5250 Functions Reference and IBM i Japanese support.
402
+ let dbcsMode = false;
403
+ let dbcsPending = -1; // first byte of a pair awaiting its companion
404
+ // Helper: build an ExtAttr snapshot, or null if nothing is set.
405
+ const snapExt = () => (extColor || extHighlight || extCharSet)
406
+ ? { color: extColor, highlight: extHighlight, charSet: extCharSet }
407
+ : null;
408
+ // Helper: write a single rendered character at currentAddr, applying
409
+ // current attributes and resetting the DBCS continuation flag for SBCS.
410
+ const writeChar = (ch) => {
411
+ if (currentAddr < this.screen.size) {
412
+ this.screen.setCharAt(currentAddr, ch);
413
+ this.screen.setAttrAt(currentAddr, currentAttr);
414
+ this.screen.setExtAttrAt(currentAddr, snapExt());
415
+ this.screen.dbcsCont[currentAddr] = false;
416
+ }
417
+ currentAddr++;
418
+ };
419
+ // Helper: write a DBCS character — glyph in cell N, empty continuation
420
+ // in cell N+1, both flagged as DBCS.
421
+ const writeDbcs = (ch) => {
422
+ if (currentAddr < this.screen.size) {
423
+ this.screen.setCharAt(currentAddr, ch);
424
+ this.screen.setAttrAt(currentAddr, currentAttr);
425
+ this.screen.setExtAttrAt(currentAddr, snapExt());
426
+ this.screen.dbcsCont[currentAddr] = false;
427
+ }
428
+ currentAddr++;
429
+ if (currentAddr < this.screen.size) {
430
+ this.screen.setCharAt(currentAddr, '');
431
+ this.screen.setAttrAt(currentAddr, currentAttr);
432
+ this.screen.setExtAttrAt(currentAddr, snapExt());
433
+ this.screen.dbcsCont[currentAddr] = true;
434
+ }
435
+ currentAddr++;
436
+ };
331
437
  while (pos < data.length) {
332
438
  const byte = data[pos];
333
439
  // Within a WTD, all bytes are orders or EBCDIC data.
@@ -377,7 +483,10 @@ export class TN5250Parser {
377
483
  break;
378
484
  }
379
485
  case ORDER.RA: {
380
- // Repeat to Address: 3 bytes (row, col, char) — 1-based address
486
+ // Repeat to Address: 3 bytes (row, col, char) — 1-based address.
487
+ // Per lib5250 session.c:2161-2207: fill from current position up to
488
+ // AND INCLUDING the target position (the loop writes the target cell
489
+ // then breaks).
381
490
  if (pos + 3 >= data.length)
382
491
  return data.length;
383
492
  pos++;
@@ -385,22 +494,21 @@ export class TN5250Parser {
385
494
  const raCol = data[pos++] - 1 + this.winColOff;
386
495
  const charByte = data[pos++];
387
496
  const targetAddr = this.screen.offset(raRow, raCol);
388
- const ch = ebcdicToChar(charByte);
389
- // lib5250 uses addch which wraps; we fill up to target (inclusive of current pos)
497
+ const ch = ebcdicToChar(charByte, this.screen.codePage);
390
498
  if (targetAddr >= currentAddr) {
391
- while (currentAddr < targetAddr && currentAddr < this.screen.size) {
499
+ while (currentAddr <= targetAddr && currentAddr < this.screen.size) {
392
500
  this.screen.setCharAt(currentAddr, ch);
393
501
  currentAddr++;
394
502
  }
395
503
  }
396
504
  else {
397
- // Wrap-around: fill to end of screen, then from start to target
505
+ // Wrap-around: fill to end of screen, then from start to target (inclusive)
398
506
  while (currentAddr < this.screen.size) {
399
507
  this.screen.setCharAt(currentAddr, ch);
400
508
  currentAddr++;
401
509
  }
402
510
  currentAddr = 0;
403
- while (currentAddr < targetAddr && currentAddr < this.screen.size) {
511
+ while (currentAddr <= targetAddr && currentAddr < this.screen.size) {
404
512
  this.screen.setCharAt(currentAddr, ch);
405
513
  currentAddr++;
406
514
  }
@@ -408,34 +516,53 @@ export class TN5250Parser {
408
516
  break;
409
517
  }
410
518
  case ORDER.EA: {
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.
414
- if (pos + 2 >= data.length)
519
+ // Erase to Address: row, col, length, then (length-1) attribute type
520
+ // bytes. Per lib5250 session.c:2088-2148 length is in [2,5]; the
521
+ // attribute list selects which kinds of attributes to erase (0xFF =
522
+ // all). Region is inclusive of the target address.
523
+ if (pos + 3 >= data.length)
415
524
  return data.length;
416
525
  pos++;
417
526
  const eaRow = data[pos++] - 1 + this.winRowOff;
418
527
  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
- }
528
+ const eaLength = data[pos++];
529
+ // Consume (length-1) attribute bytes
530
+ const attrCount = Math.max(0, eaLength - 1);
531
+ let eraseAll = false;
532
+ for (let i = 0; i < attrCount && pos < data.length; i++) {
533
+ if (data[pos] === 0xFF)
534
+ eraseAll = true;
535
+ pos++;
425
536
  }
426
- else {
427
- // Wrap-around
428
- while (currentAddr < this.screen.size) {
429
- this.screen.setCharAt(currentAddr, ' ');
430
- currentAddr++;
537
+ // lib5250 only erases characters when attribute 0xFF is present
538
+ // (we don't track extended attributes separately). Default on any
539
+ // attribute list so the region is cleared — safe for typical hosts.
540
+ if (eraseAll || attrCount === 0) {
541
+ const eaTarget = this.screen.offset(eaRow, eaCol);
542
+ if (eaTarget >= currentAddr) {
543
+ while (currentAddr <= eaTarget && currentAddr < this.screen.size) {
544
+ this.screen.setCharAt(currentAddr, ' ');
545
+ currentAddr++;
546
+ }
431
547
  }
432
- currentAddr = 0;
433
- while (currentAddr < eaTarget && currentAddr < this.screen.size) {
434
- this.screen.setCharAt(currentAddr, ' ');
435
- currentAddr++;
548
+ else {
549
+ // Wrap-around
550
+ while (currentAddr < this.screen.size) {
551
+ this.screen.setCharAt(currentAddr, ' ');
552
+ currentAddr++;
553
+ }
554
+ currentAddr = 0;
555
+ while (currentAddr <= eaTarget && currentAddr < this.screen.size) {
556
+ this.screen.setCharAt(currentAddr, ' ');
557
+ currentAddr++;
558
+ }
436
559
  }
560
+ // Per lib5250: EA sets the current display address to target+1
561
+ // (wrapping if at screen boundary).
562
+ currentAddr = eaTarget + 1;
563
+ if (currentAddr >= this.screen.size)
564
+ currentAddr = 0;
437
565
  }
438
- currentAddr = eaTarget;
439
566
  break;
440
567
  }
441
568
  case ORDER.SOH: {
@@ -452,7 +579,11 @@ export class TN5250Parser {
452
579
  pendingICRow = -1;
453
580
  pendingICCol = -1;
454
581
  const hdrLen = data[pos++];
455
- pos += Math.max(0, hdrLen);
582
+ // Per lib5250 dbuffer.c:167-178: copy header bytes for later use
583
+ // (byte 3 carries the error message line row).
584
+ const safeLen = Math.max(0, Math.min(hdrLen, data.length - pos));
585
+ this.screen.headerData = Array.from(data.subarray(pos, pos + safeLen));
586
+ pos += safeLen;
456
587
  break;
457
588
  }
458
589
  case ORDER.TD: {
@@ -464,19 +595,42 @@ export class TN5250Parser {
464
595
  const tdLen = (data[pos] << 8) | data[pos + 1];
465
596
  pos += 2;
466
597
  for (let i = 0; i < tdLen && pos < data.length; i++) {
467
- this.screen.setCharAt(currentAddr++, ebcdicToChar(data[pos++]));
598
+ this.screen.setCharAt(currentAddr++, ebcdicToChar(data[pos++], this.screen.codePage));
468
599
  }
469
600
  break;
470
601
  }
471
602
  case ORDER.WEA: {
472
- // Write Extended Attribute: 2 bytes (attr type + value)
603
+ // Write Extended Attribute: 2 bytes (attr type + value).
604
+ // Per 5250 Functions Reference:
605
+ // type 0x01 — extended color
606
+ // type 0x02 — extended highlighting (underscore/blink/reverse/col-sep)
607
+ // type 0x03 — character set (CGCS id)
608
+ // type 0x04 — transparency / field outlining
609
+ // type 0xFF with value 0xFF — reset all extended attributes
610
+ // Extended attributes persist for subsequent characters until changed.
473
611
  if (pos + 2 >= data.length)
474
612
  return data.length;
475
613
  pos++;
476
- const attrType = data[pos++];
477
- const attrValue = data[pos++];
478
- // Apply attribute at current position
479
- this.screen.setAttrAt(currentAddr, attrValue);
614
+ const extType = data[pos++];
615
+ const extValue = data[pos++];
616
+ switch (extType) {
617
+ case 0x01:
618
+ extColor = extValue;
619
+ break;
620
+ case 0x02:
621
+ extHighlight = extValue;
622
+ break;
623
+ case 0x03:
624
+ extCharSet = extValue;
625
+ break;
626
+ case 0xFF:
627
+ if (extValue === 0xFF) {
628
+ extColor = 0;
629
+ extHighlight = 0;
630
+ extCharSet = 0;
631
+ }
632
+ break;
633
+ }
480
634
  // Preserve afterSBA — WEA can appear between SBA and a field attribute
481
635
  continue;
482
636
  }
@@ -524,15 +678,43 @@ export class TN5250Parser {
524
678
  case WDSF_TYPE.DEFINE_SELECTION_FIELD:
525
679
  this.parseDefineSelectionField(data, pos, wdsfEnd);
526
680
  break;
527
- case WDSF_TYPE.REM_GUI_WINDOW:
528
- if (this.screen.windowList.length > 0) {
681
+ case WDSF_TYPE.DEFINE_SCROLL_BAR:
682
+ this.parseDefineScrollbar(data, pos, wdsfEnd);
683
+ break;
684
+ case WDSF_TYPE.REM_GUI_WINDOW: {
685
+ // Per lib5250 session.c:3465-3505: find the window containing
686
+ // the cursor (hit-test) and remove it, not the last-created.
687
+ // Note: flagbyte1, flagbyte2, reserved are read but unused.
688
+ const cy = this.screen.cursorRow;
689
+ const cx = this.screen.cursorCol;
690
+ const idx = this.screen.windowList.findIndex(w => cy >= w.row && cy <= w.row + w.height &&
691
+ cx >= w.col && cx <= w.col + w.width);
692
+ if (idx >= 0) {
693
+ this.screen.windowList.splice(idx, 1);
694
+ }
695
+ else if (this.screen.windowList.length > 0) {
696
+ // Fallback: remove the top (last) window
529
697
  this.screen.windowList.pop();
530
698
  }
531
699
  this.winRowOff = 0;
532
700
  this.winColOff = 0;
533
701
  break;
702
+ }
703
+ case WDSF_TYPE.REM_GUI_SEL_FIELD:
704
+ // Per lib5250 session.c:3095-3116: destroys all menubars /
705
+ // selection fields.
706
+ this.screen.selectionFields = [];
707
+ break;
708
+ case WDSF_TYPE.REM_GUI_SCROLL_BAR:
709
+ // Per lib5250 stream handling: clear all scrollbars.
710
+ this.screen.scrollbarList = [];
711
+ break;
534
712
  case WDSF_TYPE.REM_ALL_GUI_CONSTRUCTS:
713
+ // Per lib5250 session.c:3518-3562: destroy all windows,
714
+ // scrollbars and selection fields.
535
715
  this.screen.windowList = [];
716
+ this.screen.selectionFields = [];
717
+ this.screen.scrollbarList = [];
536
718
  this.winRowOff = 0;
537
719
  this.winColOff = 0;
538
720
  break;
@@ -557,14 +739,42 @@ export class TN5250Parser {
557
739
  pos = this.parseFieldAttribute(data, pos, currentAddr, currentAttr);
558
740
  currentAddr++; // attribute byte occupies one screen position
559
741
  }
560
- else {
561
- // Regular EBCDIC character data
562
- const ch = useSymbolCharSet ? ebcdicSymbolChar(byte) : ebcdicToChar(byte);
563
- if (currentAddr < this.screen.size) {
564
- this.screen.setCharAt(currentAddr, ch);
565
- this.screen.setAttrAt(currentAddr, currentAttr);
742
+ else if (byte === SO && !dbcsMode) {
743
+ // Shift-Out: enter DBCS Kanji mode (per IBM i Japanese DBCS/SBCS
744
+ // mixed code pages 930/939). SO itself occupies a screen cell as
745
+ // a space to preserve column alignment (matches IBM PCOMM).
746
+ dbcsMode = true;
747
+ dbcsPending = -1;
748
+ writeChar(' ');
749
+ pos++;
750
+ }
751
+ else if (byte === SI && dbcsMode) {
752
+ // Shift-In: exit DBCS mode, return to SBCS. Any orphan pending
753
+ // byte is discarded (malformed stream). SI also occupies a cell.
754
+ dbcsMode = false;
755
+ dbcsPending = -1;
756
+ writeChar(' ');
757
+ pos++;
758
+ }
759
+ else if (dbcsMode) {
760
+ // Collect DBCS byte pairs. Each pair renders as one full-width
761
+ // glyph occupying two screen cells.
762
+ if (dbcsPending < 0) {
763
+ dbcsPending = byte;
764
+ }
765
+ else {
766
+ const glyph = decodeDbcsPair(dbcsPending, byte);
767
+ writeDbcs(glyph);
768
+ dbcsPending = -1;
566
769
  }
567
- currentAddr++;
770
+ pos++;
771
+ }
772
+ else {
773
+ // Regular EBCDIC character data in the active single-byte code page.
774
+ const ch = useSymbolCharSet
775
+ ? ebcdicSymbolChar(byte)
776
+ : ebcdicToChar(byte, this.screen.codePage);
777
+ writeChar(ch);
568
778
  pos++;
569
779
  }
570
780
  break;
@@ -632,6 +842,30 @@ export class TN5250Parser {
632
842
  let curByte = data[pos++];
633
843
  let ffw1 = 0, ffw2 = 0, fcw1 = 0, fcw2 = 0;
634
844
  let inputField = false;
845
+ // Continuation / wordwrap flags collected from FCW pairs
846
+ // (per lib5250 session.c:1617-1641)
847
+ let continuous = false;
848
+ let contFirst = false;
849
+ let contMiddle = false;
850
+ let contLast = false;
851
+ let wordwrap = false;
852
+ // All other FCW metadata (per lib5250 session.c:1577-1661)
853
+ let resequence = 0;
854
+ let progressionId = 0;
855
+ let highlightEntryAttr = 0;
856
+ let transparency = 0;
857
+ let pointerAid = 0;
858
+ let forwardEdge = false;
859
+ let selfCheckMod10 = false;
860
+ let selfCheckMod11 = false;
861
+ let ideographicOnly = false;
862
+ let ideographicData = false;
863
+ let ideographicEither = false;
864
+ let ideographicOpen = false;
865
+ let magstripe = false;
866
+ let lightpen = false;
867
+ let magandlight = false;
868
+ let lightandattn = false;
635
869
  if ((curByte & 0xE0) !== 0x20) {
636
870
  // Input field: curByte is FFW1
637
871
  inputField = true;
@@ -646,6 +880,63 @@ export class TN5250Parser {
646
880
  while ((curByte & 0xE0) !== 0x20 && pos < data.length) {
647
881
  fcw1 = curByte;
648
882
  fcw2 = data[pos++];
883
+ const fcw = (fcw1 << 8) | fcw2;
884
+ // Resequence / cursor progression (per session.c:1577-1579, 1643-1645)
885
+ if (fcw1 === 0x80)
886
+ resequence = fcw2;
887
+ else if (fcw1 === 0x88)
888
+ progressionId = fcw2;
889
+ else if (fcw1 === 0x89)
890
+ highlightEntryAttr = fcw2;
891
+ else if (fcw1 === 0x84)
892
+ transparency = fcw2;
893
+ else if (fcw1 === 0x8A)
894
+ pointerAid = fcw2;
895
+ // Peripheral input (per session.c:1581-1595)
896
+ else if (fcw === 0x8101)
897
+ magstripe = true;
898
+ else if (fcw === 0x8102)
899
+ lightpen = true;
900
+ else if (fcw === 0x8103)
901
+ magandlight = true;
902
+ else if (fcw === 0x8106)
903
+ lightandattn = true;
904
+ // Ideographic / DBCS (per session.c:1597-1611)
905
+ else if (fcw === 0x8200)
906
+ ideographicOnly = true;
907
+ else if (fcw === 0x8220)
908
+ ideographicData = true;
909
+ else if (fcw === 0x8240)
910
+ ideographicEither = true;
911
+ else if (fcw === 0x8280 || fcw === 0x82C0)
912
+ ideographicOpen = true;
913
+ // Forward-edge trigger (per session.c:1617-1619)
914
+ else if (fcw === 0x8501)
915
+ forwardEdge = true;
916
+ // Continuation flags (per session.c:1621-1641)
917
+ else if (fcw === 0x8601) {
918
+ continuous = true;
919
+ contFirst = true;
920
+ }
921
+ else if (fcw === 0x8603) {
922
+ continuous = true;
923
+ contMiddle = true;
924
+ }
925
+ else if (fcw === 0x8602) {
926
+ continuous = true;
927
+ contLast = true;
928
+ }
929
+ else if (fcw === 0x8680) {
930
+ wordwrap = true;
931
+ }
932
+ // Self-check (per session.c:1655-1661)
933
+ else if (fcw === 0xB140)
934
+ selfCheckMod11 = true;
935
+ else if (fcw === 0xB1A0)
936
+ selfCheckMod10 = true;
937
+ // Per C: if this is the "last" with wordwrap flag, drop wordwrap.
938
+ if (fcw === 0x8602 && wordwrap)
939
+ wordwrap = false;
649
940
  if (pos >= data.length)
650
941
  return pos;
651
942
  curByte = data[pos++];
@@ -674,6 +965,27 @@ export class TN5250Parser {
674
965
  attribute: fieldDisplayAttr,
675
966
  rawAttrByte,
676
967
  modified: false,
968
+ continuous: continuous || undefined,
969
+ continuedFirst: contFirst || undefined,
970
+ continuedMiddle: contMiddle || undefined,
971
+ continuedLast: contLast || undefined,
972
+ wordwrap: wordwrap || undefined,
973
+ resequence: resequence || undefined,
974
+ progressionId: progressionId || undefined,
975
+ highlightEntryAttr: highlightEntryAttr || undefined,
976
+ transparency: transparency || undefined,
977
+ pointerAid: pointerAid || undefined,
978
+ forwardEdge: forwardEdge || undefined,
979
+ selfCheckMod10: selfCheckMod10 || undefined,
980
+ selfCheckMod11: selfCheckMod11 || undefined,
981
+ ideographicOnly: ideographicOnly || undefined,
982
+ ideographicData: ideographicData || undefined,
983
+ ideographicEither: ideographicEither || undefined,
984
+ ideographicOpen: ideographicOpen || undefined,
985
+ magstripe: magstripe || undefined,
986
+ lightpen: lightpen || undefined,
987
+ magandlight: magandlight || undefined,
988
+ lightandattn: lightandattn || undefined,
677
989
  };
678
990
  this.clearStaleFieldsOnce();
679
991
  this.screen.fields.push(field);
@@ -757,7 +1069,14 @@ export class TN5250Parser {
757
1069
  }
758
1070
  const winRow = this.screen.cursorRow;
759
1071
  const winCol = this.screen.cursorCol;
760
- this.screen.windowList.push({ row: winRow, col: winCol, height: depth, width: width });
1072
+ this.screen.windowList.push({
1073
+ row: winRow,
1074
+ col: winCol,
1075
+ height: depth,
1076
+ width: width,
1077
+ title: titleText || undefined,
1078
+ footer: footerText || undefined,
1079
+ });
761
1080
  // Render border with custom characters from host
762
1081
  this.screen.renderWindowBorderCustom(winRow, winCol, depth, width, borderChars, titleText, footerText);
763
1082
  // Erase content area inside the border
@@ -845,6 +1164,56 @@ export class TN5250Parser {
845
1164
  choices,
846
1165
  });
847
1166
  }
1167
+ /**
1168
+ * Parse DEFINE_SCROLL_BAR structured field data.
1169
+ * Per lib5250 session.c:3362-3451.
1170
+ *
1171
+ * Payload format (after WDSF class/type):
1172
+ * [flagbyte1] 0x80 = horizontal, else vertical
1173
+ * [reserved]
1174
+ * [totalrowscols1..4] BCD digits (1000s, 100s, 10s, 1s)
1175
+ * [sliderpos1..4] BCD digits (1000s, 100s, 10s, 1s)
1176
+ * [size]
1177
+ *
1178
+ * Position is the current cursor (1-based per lib5250 convention).
1179
+ */
1180
+ parseDefineScrollbar(data, pos, end) {
1181
+ if (pos + 11 > end)
1182
+ return;
1183
+ const flagbyte1 = data[pos++];
1184
+ pos++; // reserved
1185
+ const totalrowscols = 1000 * data[pos++] +
1186
+ 100 * data[pos++] +
1187
+ 10 * data[pos++] +
1188
+ data[pos++];
1189
+ const sliderpos = 1000 * data[pos++] +
1190
+ 100 * data[pos++] +
1191
+ 10 * data[pos++] +
1192
+ data[pos++];
1193
+ const size = data[pos++];
1194
+ const row = this.screen.cursorRow + 1;
1195
+ const col = this.screen.cursorCol + 1;
1196
+ const direction = (flagbyte1 & 0x80) !== 0 ? 1 : 0;
1197
+ // Hit-test: if an existing scrollbar is at the same position, update it
1198
+ // rather than creating a duplicate (per session.c:3385-3391).
1199
+ const existing = this.screen.scrollbarList.find(s => s.row === row && s.col === col);
1200
+ if (existing) {
1201
+ existing.direction = direction;
1202
+ existing.rowscols = totalrowscols;
1203
+ existing.sliderpos = sliderpos;
1204
+ existing.size = size;
1205
+ }
1206
+ else {
1207
+ this.screen.scrollbarList.push({
1208
+ row,
1209
+ col,
1210
+ direction,
1211
+ rowscols: totalrowscols,
1212
+ sliderpos,
1213
+ size,
1214
+ });
1215
+ }
1216
+ }
848
1217
  /**
849
1218
  * Roll (scroll) screen buffer rows within [top, bot] by `lines` rows.
850
1219
  * Negative = scroll up, positive = scroll down.
@@ -852,6 +1221,8 @@ export class TN5250Parser {
852
1221
  */
853
1222
  rollBuffer(top, bot, lines) {
854
1223
  const cols = this.screen.cols;
1224
+ const buf = this.screen.buffer;
1225
+ const attr = this.screen.attrBuffer;
855
1226
  if (lines < 0) {
856
1227
  // Scroll up: move rows upward
857
1228
  for (let r = top; r <= bot; r++) {
@@ -859,7 +1230,8 @@ export class TN5250Parser {
859
1230
  const dstOff = (r + lines) * cols;
860
1231
  const srcOff = r * cols;
861
1232
  for (let c = 0; c < cols; c++) {
862
- this.screen.buffer[dstOff + c] = this.screen.buffer[srcOff + c];
1233
+ buf[dstOff + c] = buf[srcOff + c];
1234
+ attr[dstOff + c] = attr[srcOff + c];
863
1235
  }
864
1236
  }
865
1237
  }
@@ -868,7 +1240,8 @@ export class TN5250Parser {
868
1240
  if (r >= top) {
869
1241
  const off = r * cols;
870
1242
  for (let c = 0; c < cols; c++) {
871
- this.screen.buffer[off + c] = ' ';
1243
+ buf[off + c] = ' ';
1244
+ attr[off + c] = ATTR.NORMAL;
872
1245
  }
873
1246
  }
874
1247
  }
@@ -880,7 +1253,8 @@ export class TN5250Parser {
880
1253
  const dstOff = (r + lines) * cols;
881
1254
  const srcOff = r * cols;
882
1255
  for (let c = 0; c < cols; c++) {
883
- this.screen.buffer[dstOff + c] = this.screen.buffer[srcOff + c];
1256
+ buf[dstOff + c] = buf[srcOff + c];
1257
+ attr[dstOff + c] = attr[srcOff + c];
884
1258
  }
885
1259
  }
886
1260
  }
@@ -889,7 +1263,8 @@ export class TN5250Parser {
889
1263
  if (r <= bot) {
890
1264
  const off = r * cols;
891
1265
  for (let c = 0; c < cols; c++) {
892
- this.screen.buffer[off + c] = ' ';
1266
+ buf[off + c] = ' ';
1267
+ attr[off + c] = ATTR.NORMAL;
893
1268
  }
894
1269
  }
895
1270
  }