green-screen-proxy 0.3.0 → 1.0.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 (146) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +23 -4
  5. package/dist/cli.js.map +1 -0
  6. package/dist/deploy.d.ts +2 -0
  7. package/dist/deploy.d.ts.map +1 -0
  8. package/dist/deploy.js +252 -0
  9. package/dist/deploy.js.map +1 -0
  10. package/dist/hp6530/connection.d.ts +1 -0
  11. package/dist/hp6530/connection.d.ts.map +1 -0
  12. package/dist/hp6530/connection.js +1 -0
  13. package/dist/hp6530/connection.js.map +1 -0
  14. package/dist/hp6530/constants.d.ts +1 -0
  15. package/dist/hp6530/constants.d.ts.map +1 -0
  16. package/dist/hp6530/constants.js +1 -0
  17. package/dist/hp6530/constants.js.map +1 -0
  18. package/dist/hp6530/encoder.d.ts +1 -0
  19. package/dist/hp6530/encoder.d.ts.map +1 -0
  20. package/dist/hp6530/encoder.js +1 -0
  21. package/dist/hp6530/encoder.js.map +1 -0
  22. package/dist/hp6530/parser.d.ts +1 -0
  23. package/dist/hp6530/parser.d.ts.map +1 -0
  24. package/dist/hp6530/parser.js +17 -15
  25. package/dist/hp6530/parser.js.map +1 -0
  26. package/dist/hp6530/screen.d.ts +1 -0
  27. package/dist/hp6530/screen.d.ts.map +1 -0
  28. package/dist/hp6530/screen.js +1 -0
  29. package/dist/hp6530/screen.js.map +1 -0
  30. package/dist/index.d.ts +36 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +81 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/mock/mock-routes.d.ts +1 -0
  35. package/dist/mock/mock-routes.d.ts.map +1 -0
  36. package/dist/mock/mock-routes.js +1 -0
  37. package/dist/mock/mock-routes.js.map +1 -0
  38. package/dist/protocols/hp6530-handler.d.ts +3 -1
  39. package/dist/protocols/hp6530-handler.d.ts.map +1 -0
  40. package/dist/protocols/hp6530-handler.js +1 -0
  41. package/dist/protocols/hp6530-handler.js.map +1 -0
  42. package/dist/protocols/index.d.ts +5 -2
  43. package/dist/protocols/index.d.ts.map +1 -0
  44. package/dist/protocols/index.js +1 -0
  45. package/dist/protocols/index.js.map +1 -0
  46. package/dist/protocols/tn3270-handler.d.ts +3 -1
  47. package/dist/protocols/tn3270-handler.d.ts.map +1 -0
  48. package/dist/protocols/tn3270-handler.js +1 -0
  49. package/dist/protocols/tn3270-handler.js.map +1 -0
  50. package/dist/protocols/tn5250-handler.d.ts +25 -1
  51. package/dist/protocols/tn5250-handler.d.ts.map +1 -0
  52. package/dist/protocols/tn5250-handler.js +150 -1
  53. package/dist/protocols/tn5250-handler.js.map +1 -0
  54. package/dist/protocols/types.d.ts +3 -23
  55. package/dist/protocols/types.d.ts.map +1 -0
  56. package/dist/protocols/types.js +1 -0
  57. package/dist/protocols/types.js.map +1 -0
  58. package/dist/protocols/vt-handler.d.ts +3 -1
  59. package/dist/protocols/vt-handler.d.ts.map +1 -0
  60. package/dist/protocols/vt-handler.js +1 -0
  61. package/dist/protocols/vt-handler.js.map +1 -0
  62. package/dist/routes.d.ts +1 -0
  63. package/dist/routes.d.ts.map +1 -0
  64. package/dist/routes.js +1 -0
  65. package/dist/routes.js.map +1 -0
  66. package/dist/server.d.ts +1 -0
  67. package/dist/server.d.ts.map +1 -0
  68. package/dist/server.js +13 -28
  69. package/dist/server.js.map +1 -0
  70. package/dist/session.d.ts +5 -10
  71. package/dist/session.d.ts.map +1 -0
  72. package/dist/session.js +1 -0
  73. package/dist/session.js.map +1 -0
  74. package/dist/standalone.d.ts +3 -0
  75. package/dist/standalone.d.ts.map +1 -0
  76. package/dist/standalone.js +6 -0
  77. package/dist/standalone.js.map +1 -0
  78. package/dist/tn3270/connection.d.ts +1 -0
  79. package/dist/tn3270/connection.d.ts.map +1 -0
  80. package/dist/tn3270/connection.js +1 -0
  81. package/dist/tn3270/connection.js.map +1 -0
  82. package/dist/tn3270/constants.d.ts +1 -0
  83. package/dist/tn3270/constants.d.ts.map +1 -0
  84. package/dist/tn3270/constants.js +1 -0
  85. package/dist/tn3270/constants.js.map +1 -0
  86. package/dist/tn3270/encoder.d.ts +1 -0
  87. package/dist/tn3270/encoder.d.ts.map +1 -0
  88. package/dist/tn3270/encoder.js +1 -0
  89. package/dist/tn3270/encoder.js.map +1 -0
  90. package/dist/tn3270/parser.d.ts +1 -0
  91. package/dist/tn3270/parser.d.ts.map +1 -0
  92. package/dist/tn3270/parser.js +1 -0
  93. package/dist/tn3270/parser.js.map +1 -0
  94. package/dist/tn3270/screen.d.ts +1 -0
  95. package/dist/tn3270/screen.d.ts.map +1 -0
  96. package/dist/tn3270/screen.js +1 -0
  97. package/dist/tn3270/screen.js.map +1 -0
  98. package/dist/tn5250/connection.d.ts +1 -0
  99. package/dist/tn5250/connection.d.ts.map +1 -0
  100. package/dist/tn5250/connection.js +7 -8
  101. package/dist/tn5250/connection.js.map +1 -0
  102. package/dist/tn5250/constants.d.ts +1 -0
  103. package/dist/tn5250/constants.d.ts.map +1 -0
  104. package/dist/tn5250/constants.js +1 -0
  105. package/dist/tn5250/constants.js.map +1 -0
  106. package/dist/tn5250/ebcdic.d.ts +2 -0
  107. package/dist/tn5250/ebcdic.d.ts.map +1 -0
  108. package/dist/tn5250/ebcdic.js +25 -0
  109. package/dist/tn5250/ebcdic.js.map +1 -0
  110. package/dist/tn5250/encoder.d.ts +10 -1
  111. package/dist/tn5250/encoder.d.ts.map +1 -0
  112. package/dist/tn5250/encoder.js +17 -19
  113. package/dist/tn5250/encoder.js.map +1 -0
  114. package/dist/tn5250/parser.d.ts +31 -5
  115. package/dist/tn5250/parser.d.ts.map +1 -0
  116. package/dist/tn5250/parser.js +234 -79
  117. package/dist/tn5250/parser.js.map +1 -0
  118. package/dist/tn5250/screen.d.ts +12 -0
  119. package/dist/tn5250/screen.d.ts.map +1 -0
  120. package/dist/tn5250/screen.js +29 -2
  121. package/dist/tn5250/screen.js.map +1 -0
  122. package/dist/vt/connection.d.ts +1 -0
  123. package/dist/vt/connection.d.ts.map +1 -0
  124. package/dist/vt/connection.js +1 -0
  125. package/dist/vt/connection.js.map +1 -0
  126. package/dist/vt/constants.d.ts +1 -0
  127. package/dist/vt/constants.d.ts.map +1 -0
  128. package/dist/vt/constants.js +1 -0
  129. package/dist/vt/constants.js.map +1 -0
  130. package/dist/vt/encoder.d.ts +1 -0
  131. package/dist/vt/encoder.d.ts.map +1 -0
  132. package/dist/vt/encoder.js +1 -0
  133. package/dist/vt/encoder.js.map +1 -0
  134. package/dist/vt/parser.d.ts +1 -0
  135. package/dist/vt/parser.d.ts.map +1 -0
  136. package/dist/vt/parser.js +35 -33
  137. package/dist/vt/parser.js.map +1 -0
  138. package/dist/vt/screen.d.ts +1 -0
  139. package/dist/vt/screen.d.ts.map +1 -0
  140. package/dist/vt/screen.js +1 -0
  141. package/dist/vt/screen.js.map +1 -0
  142. package/dist/websocket.d.ts +1 -0
  143. package/dist/websocket.d.ts.map +1 -0
  144. package/dist/websocket.js +241 -14
  145. package/dist/websocket.js.map +1 -0
  146. package/package.json +9 -6
@@ -19,14 +19,10 @@ export class TN5250Encoder {
19
19
  if (aidByte === undefined)
20
20
  return null;
21
21
  const parts = [];
22
- // Row and column of cursor (1-based for the protocol)
23
22
  const cursorRow = this.screen.cursorRow;
24
23
  const cursorCol = this.screen.cursorCol;
25
- // Build the response record
26
- // Format: [row][col][aid_byte][field_data...]
27
- // GDS header (6 bytes) + response data
28
- const header = this.buildGDSHeader();
29
- parts.push(header);
24
+ // GDS header: length(2) + record_type(2) + var(1) + reserved(1) + opcode(1)
25
+ parts.push(this.buildGDSHeader());
30
26
  // Cursor position + AID byte
31
27
  parts.push(Buffer.from([cursorRow, cursorCol, aidByte]));
32
28
  // For certain aid keys (like SysReq), no field data is sent
@@ -40,16 +36,16 @@ export class TN5250Encoder {
40
36
  if (!this.screen.isInputField(field))
41
37
  continue;
42
38
  // SBA order to indicate field position
43
- parts.push(Buffer.from([0x11, field.row, field.col])); // SBA + row + col
39
+ parts.push(Buffer.from([0x11, field.row, field.col]));
44
40
  // Field data in EBCDIC
45
41
  const value = this.screen.getFieldValue(field);
46
42
  const ebcdicData = Buffer.alloc(value.length);
47
43
  for (let i = 0; i < value.length; i++) {
48
44
  ebcdicData[i] = charToEbcdic(value[i]);
49
45
  }
50
- // Trim trailing spaces
46
+ // Trim trailing spaces and nulls
51
47
  let trimLen = ebcdicData.length;
52
- while (trimLen > 0 && ebcdicData[trimLen - 1] === EBCDIC_SPACE) {
48
+ while (trimLen > 0 && (ebcdicData[trimLen - 1] === EBCDIC_SPACE || ebcdicData[trimLen - 1] === 0x00)) {
53
49
  trimLen--;
54
50
  }
55
51
  if (trimLen > 0) {
@@ -59,24 +55,25 @@ export class TN5250Encoder {
59
55
  return this.wrapWithEOR(Buffer.concat(parts));
60
56
  }
61
57
  /**
62
- * Build a GDS (General Data Stream) header for a response.
58
+ * Build a GDS header for a client response (matching tn5250j format).
59
+ * 10-byte header:
60
+ * Bytes 0-1: record length (filled by wrapWithEOR)
61
+ * Bytes 2-3: record type 0x12A0 (SNA GDS Variable)
62
+ * Bytes 4-5: reserved 0x0000
63
+ * Byte 6: sub-header length 0x04
64
+ * Byte 7: flags 0x00
65
+ * Byte 8: reserved 0x00
66
+ * Byte 9: opcode 0x03 (PUT/GET response)
63
67
  */
64
68
  buildGDSHeader() {
65
- // We'll fill in the length later, but for now use a placeholder
66
- // The actual header format:
67
- // Bytes 0-1: record length (will be filled)
68
- // Bytes 2-3: record type = 0x12A0
69
- // Byte 4: variable indicator (0x00)
70
- // Byte 5: reserved (0x00)
71
- // Byte 6: opcode (0x00 for response, 0x04 for response to save screen)
72
- return Buffer.from([0x00, 0x00, 0x12, 0xA0, 0x00, 0x00, 0x00]);
69
+ return Buffer.from([0x00, 0x00, 0x12, 0xA0, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03]);
73
70
  }
74
71
  /**
75
72
  * Wrap data with Telnet IAC EOR framing.
76
73
  * Also escapes any 0xFF bytes in the data as IAC IAC.
77
74
  */
78
75
  wrapWithEOR(data) {
79
- // Update the record length in the GDS header
76
+ // Update GDS record length in the first 2 bytes (includes itself)
80
77
  if (data.length >= 2) {
81
78
  const len = data.length;
82
79
  data[0] = (len >> 8) & 0xFF;
@@ -119,3 +116,4 @@ export class TN5250Encoder {
119
116
  return true;
120
117
  }
121
118
  }
119
+ //# sourceMappingURL=encoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encoder.js","sourceRoot":"","sources":["../../src/tn5250/encoder.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ import { ScreenBuffer } from './screen.js';
4
4
  */
5
5
  export declare class TN5250Parser {
6
6
  private screen;
7
+ /** When true, the next SF order should clear stale fields first */
8
+ private pendingFieldsClear;
7
9
  constructor(screen: ScreenBuffer);
8
10
  /**
9
11
  * Parse a complete 5250 record (after Telnet framing is removed).
@@ -12,6 +14,14 @@ export declare class TN5250Parser {
12
14
  parseRecord(record: Buffer): boolean;
13
15
  /** Try to parse data that doesn't have a proper GDS header */
14
16
  private tryParseRawData;
17
+ /**
18
+ * Handle records with non-standard framing (e.g. opcode 0x04 from
19
+ * pub400.com). These records contain valid commands (CLEAR_UNIT, WTD)
20
+ * but with extra sub-record marker bytes (0x04) between the GDS header
21
+ * and the actual commands. We scan for the first known command byte,
22
+ * skipping 0x04 markers, and hand off to parseCommands.
23
+ */
24
+ private parseCommandsFromOffset;
15
25
  /** Parse one or more 5250 commands starting at offset */
16
26
  private parseCommands;
17
27
  /**
@@ -19,15 +29,31 @@ export declare class TN5250Parser {
19
29
  * Updates the screen buffer and returns the new position.
20
30
  */
21
31
  private parseOrders;
22
- /** Try to detect if a byte in the 0x20-0x3F range is a field attribute vs regular character */
23
- private isLikelyFieldAttribute;
24
- /** Parse a field attribute byte and the following FFW/FCW */
32
+ /**
33
+ * Parse a bare field attribute byte (0x20-0x3F) that appears after SBA.
34
+ * In basic 5250, this is just 1 byte no FFW/FCW follows.
35
+ * FFW/FCW are only present with explicit SF (Start Field, 0x1D) order.
36
+ */
25
37
  private parseFieldAttribute;
26
- /** Parse explicit SF order field definition */
27
- private parseFieldDefinition;
38
+ /**
39
+ * Parse SF (Start Field) order with FFW and optional FCW.
40
+ * Format: SF(0x1D) FFW1 FFW2 [FCW1 FCW2]
41
+ * FFW1 always has bit 6 set (0x40+). No trailing attribute byte.
42
+ * Display attribute is derived from FFW2.
43
+ */
44
+ private parseStartField;
45
+ /**
46
+ * Decode a display attribute byte (0x20–0x3F) into an ATTR constant.
47
+ * Only recognises the bits that determine display type; falls back to
48
+ * the SA context (displayAttr) for modifier-only bytes (0x30, 0x38, etc.).
49
+ */
50
+ private decodeDisplayAttr;
51
+ /** Clear stale fields when the first SF order arrives after a Reset MDT WTD */
52
+ private clearStaleFieldsOnce;
28
53
  /**
29
54
  * After parsing a complete screen, calculate field lengths.
30
55
  * Call this after all records for a screen have been parsed.
31
56
  */
32
57
  calculateFieldLengths(): void;
33
58
  }
59
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/tn5250/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAY,MAAM,aAAa,CAAC;AAIrD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;IAC7B,mEAAmE;IACnE,OAAO,CAAC,kBAAkB,CAAS;gBAEvB,MAAM,EAAE,YAAY;IAIhC;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IA6EpC,8DAA8D;IAC9D,OAAO,CAAC,eAAe;IAKvB;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAiB/B,yDAAyD;IACzD,OAAO,CAAC,aAAa;IAkGrB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAqKnB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAsC3B;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAuEvB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAQzB,+EAA+E;IAC/E,OAAO,CAAC,oBAAoB;IAO5B;;;OAGG;IACH,qBAAqB,IAAI,IAAI;CA4E9B"}
@@ -1,10 +1,12 @@
1
1
  import { CMD, ORDER, OPCODE, ATTR } from './constants.js';
2
- import { ebcdicToChar } from './ebcdic.js';
2
+ import { ebcdicToChar, ebcdicSymbolChar } from './ebcdic.js';
3
3
  /**
4
4
  * Parses 5250 data stream records and updates the screen buffer.
5
5
  */
6
6
  export class TN5250Parser {
7
7
  screen;
8
+ /** When true, the next SF order should clear stale fields first */
9
+ pendingFieldsClear = false;
8
10
  constructor(screen) {
9
11
  this.screen = screen;
10
12
  }
@@ -31,32 +33,48 @@ export class TN5250Parser {
31
33
  }
32
34
  const recordLen = (record[0] << 8) | record[1];
33
35
  const recordType = (record[2] << 8) | record[3];
34
- const varFlag = record[4];
35
- const reserved = record[5];
36
- const opcode = record[6];
37
36
  // Check for GDS record type
38
37
  if (recordType !== 0x12A0) {
39
- // Not a GDS record — might be raw data or something else
40
38
  return this.tryParseRawData(record);
41
39
  }
40
+ // Determine header layout: standard 10-byte header has a 4-byte sub-header
41
+ // at bytes 6-9: [sub_header_len=0x04][flags][reserved][opcode]
42
+ // Data starts at byte 10. Older/simpler records may use 7-byte layout.
43
+ let opcode;
44
+ let dataOffset;
45
+ if (record.length >= 10 && record[6] === 0x04) {
46
+ // 10-byte header with 4-byte sub-header
47
+ opcode = record[9];
48
+ dataOffset = 10;
49
+ }
50
+ else {
51
+ // 7-byte header (opcode at byte 6)
52
+ opcode = record[6];
53
+ dataOffset = 7;
54
+ }
42
55
  let modified = false;
43
56
  switch (opcode) {
44
57
  case OPCODE.OUTPUT:
45
58
  case OPCODE.PUT_GET:
46
- // Contains one or more 5250 commands after the header
47
- modified = this.parseCommands(record, 7);
59
+ // Use parseCommandsFromOffset to scan for CLEAR_UNIT/WTD within the data.
60
+ // The data may contain sub-record markers (0x04) before the actual commands,
61
+ // which parseCommands would misinterpret. parseCommandsFromOffset handles this
62
+ // by scanning for known command bytes.
63
+ if (record.length > dataOffset) {
64
+ modified = this.parseCommandsFromOffset(record, dataOffset);
65
+ }
48
66
  break;
49
67
  case OPCODE.INVITE:
50
- // Invite: server is ready for input (no screen data typically)
51
68
  break;
52
69
  case OPCODE.SAVE_SCREEN:
53
70
  case OPCODE.RESTORE_SCREEN:
54
- // We don't implement save/restore, just ignore
71
+ if (record.length > dataOffset) {
72
+ modified = this.parseCommandsFromOffset(record, dataOffset);
73
+ }
55
74
  break;
56
75
  default:
57
- // Try parsing as commands anyway
58
- if (record.length > 7) {
59
- modified = this.parseCommands(record, 7);
76
+ if (record.length > dataOffset) {
77
+ modified = this.parseCommandsFromOffset(record, dataOffset);
60
78
  }
61
79
  break;
62
80
  }
@@ -67,6 +85,30 @@ export class TN5250Parser {
67
85
  // Some servers send command data without the full GDS wrapper
68
86
  return this.parseCommands(record, 0);
69
87
  }
88
+ /**
89
+ * Handle records with non-standard framing (e.g. opcode 0x04 from
90
+ * pub400.com). These records contain valid commands (CLEAR_UNIT, WTD)
91
+ * but with extra sub-record marker bytes (0x04) between the GDS header
92
+ * and the actual commands. We scan for the first known command byte,
93
+ * skipping 0x04 markers, and hand off to parseCommands.
94
+ */
95
+ parseCommandsFromOffset(data, start) {
96
+ for (let i = start; i < data.length; i++) {
97
+ // Skip sub-record markers (0x04)
98
+ if (data[i] === 0x04)
99
+ continue;
100
+ // Known command bytes — hand off to normal parsing
101
+ if (data[i] === CMD.WRITE_TO_DISPLAY ||
102
+ data[i] === CMD.CLEAR_UNIT ||
103
+ data[i] === CMD.CLEAR_UNIT_ALT ||
104
+ data[i] === CMD.WRITE_STRUCTURED_FIELD ||
105
+ data[i] === CMD.WRITE_ERROR_CODE ||
106
+ data[i] === CMD.WRITE_ERROR_CODE_WIN) {
107
+ return this.parseCommands(data, i);
108
+ }
109
+ }
110
+ return false;
111
+ }
70
112
  /** Parse one or more 5250 commands starting at offset */
71
113
  parseCommands(data, offset) {
72
114
  let pos = offset;
@@ -96,6 +138,8 @@ export class TN5250Parser {
96
138
  for (const f of this.screen.fields) {
97
139
  f.modified = false;
98
140
  }
141
+ // Mark that subsequent SF orders in this WTD should clear stale fields
142
+ this.pendingFieldsClear = true;
99
143
  }
100
144
  // CC1 bit 6: Clear all input fields (null fill)
101
145
  if (cc1 & 0x40) {
@@ -160,14 +204,14 @@ export class TN5250Parser {
160
204
  parseOrders(data, pos) {
161
205
  let currentAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
162
206
  let currentAttr = ATTR.NORMAL;
207
+ let useSymbolCharSet = false; // SA type 0x22 can switch to APL/symbol CGCS
208
+ let afterSBA = false; // Track if we just processed an SBA order (field attrs follow SBA)
163
209
  while (pos < data.length) {
164
210
  const byte = data[pos];
165
- // Check if we've hit another command byte stop parsing orders
166
- if (byte === CMD.WRITE_TO_DISPLAY || byte === CMD.CLEAR_UNIT ||
167
- byte === CMD.CLEAR_FORMAT_TABLE || byte === CMD.WRITE_STRUCTURED_FIELD ||
168
- byte === CMD.WRITE_ERROR_CODE || byte === CMD.ROLL) {
169
- break;
170
- }
211
+ // Within a WTD, all bytes are orders or EBCDIC data.
212
+ // Command bytes like CLEAR_UNIT (0x40 = EBCDIC space) and WTD (0x11 = SBA)
213
+ // overlap with valid order/data values, so we cannot break on them.
214
+ // parseOrders consumes data until the end of the buffer.
171
215
  switch (byte) {
172
216
  case ORDER.SBA: {
173
217
  // Set Buffer Address: 2 bytes follow (row, col)
@@ -177,7 +221,8 @@ export class TN5250Parser {
177
221
  const row = data[pos++];
178
222
  const col = data[pos++];
179
223
  currentAddr = this.screen.offset(row, col);
180
- break;
224
+ afterSBA = true; // Field attribute may follow
225
+ continue; // skip the afterSBA = false at end of loop
181
226
  }
182
227
  case ORDER.IC: {
183
228
  // Insert Cursor: set cursor to current address
@@ -230,13 +275,14 @@ export class TN5250Parser {
230
275
  break;
231
276
  }
232
277
  case ORDER.SOH: {
233
- // Start of Header: variable-length header for input fields
278
+ // Start of Header: variable-length header for input fields.
279
+ // Format: [0x01] [length] [data...]
280
+ // The length byte includes SOH byte + itself, so remaining = length - 2.
234
281
  if (pos + 1 >= data.length)
235
282
  return data.length;
236
283
  pos++;
237
284
  const hdrLen = data[pos++];
238
- // Skip header data (contains error line, etc.)
239
- pos += Math.max(0, hdrLen - 2); // length includes the length byte itself sometimes
285
+ pos += Math.max(0, hdrLen - 2);
240
286
  break;
241
287
  }
242
288
  case ORDER.TD: {
@@ -259,35 +305,49 @@ export class TN5250Parser {
259
305
  const attrValue = data[pos++];
260
306
  // Apply attribute at current position
261
307
  this.screen.setAttrAt(currentAddr, attrValue);
262
- break;
308
+ // Preserve afterSBA — WEA can appear between SBA and a field attribute
309
+ continue;
263
310
  }
264
311
  case ORDER.SA: {
265
312
  // Set Attribute: 2 bytes (attr type + value)
313
+ // Type 0x00 = all/reset, 0x20 = extended highlighting,
314
+ // 0x21 = foreground color, 0x22 = character set (CGCS).
315
+ // Only update display attribute for types that affect it.
266
316
  if (pos + 2 >= data.length)
267
317
  return data.length;
268
318
  pos++;
269
319
  const saType = data[pos++];
270
320
  const saValue = data[pos++];
271
- currentAttr = saValue;
272
- break;
321
+ if (saType === 0x00) {
322
+ currentAttr = saValue;
323
+ useSymbolCharSet = false; // reset
324
+ }
325
+ else if (saType === 0x20) {
326
+ currentAttr = saValue;
327
+ }
328
+ else if (saType === 0x22) {
329
+ // Character set (CGCS) — non-default means APL/symbol glyphs
330
+ useSymbolCharSet = saValue !== 0x00;
331
+ }
332
+ // Preserve afterSBA — SA sets color/highlight context before a field attribute
333
+ continue;
273
334
  }
274
335
  case ORDER.SF: {
275
- // Start Field: field attribute byte + FFW + optional FCW
276
- // Actually, in 5250, SF isn't always 0x1D. The field definition
277
- // comes after SBA as attribute byte (0x20-0x3F range).
278
- // But let's handle explicit SF if encountered:
336
+ // Start Field: explicit SF order with FFW + optional FCW
279
337
  pos++;
280
- pos = this.parseFieldDefinition(data, pos, currentAddr, currentAttr);
338
+ pos = this.parseStartField(data, pos, currentAddr, currentAttr);
339
+ currentAddr++; // attribute byte occupies one screen position
281
340
  break;
282
341
  }
283
342
  default: {
284
- // Check for field attribute bytes (0x20-0x3F)
285
- if (byte >= 0x20 && byte <= 0x3F && this.isLikelyFieldAttribute(data, pos)) {
343
+ // Field attribute bytes (0x20-0x3F) only appear immediately after SBA
344
+ if (afterSBA && byte >= 0x20 && byte <= 0x3F) {
286
345
  pos = this.parseFieldAttribute(data, pos, currentAddr, currentAttr);
346
+ currentAddr++; // attribute byte occupies one screen position
287
347
  }
288
348
  else {
289
349
  // Regular EBCDIC character data
290
- const ch = ebcdicToChar(byte);
350
+ const ch = useSymbolCharSet ? ebcdicSymbolChar(byte) : ebcdicToChar(byte);
291
351
  if (currentAddr < this.screen.size) {
292
352
  this.screen.setCharAt(currentAddr, ch);
293
353
  this.screen.setAttrAt(currentAddr, currentAttr);
@@ -298,84 +358,138 @@ export class TN5250Parser {
298
358
  break;
299
359
  }
300
360
  }
361
+ afterSBA = false;
301
362
  }
302
363
  return pos;
303
364
  }
304
- /** Try to detect if a byte in the 0x20-0x3F range is a field attribute vs regular character */
305
- isLikelyFieldAttribute(data, pos) {
306
- // Field attributes in 5250 are typically preceded by SBA and followed by FFW bytes
307
- // This is a heuristic in practice, the WTD command structure makes this deterministic
308
- // For the 0x20 byte specifically, it's also EBCDIC space, so we need context
309
- const byte = data[pos];
310
- // 0x20 is very common as both space and attribute — don't treat as field attr
311
- if (byte === 0x20)
312
- return false;
313
- // For other bytes in 0x21-0x3F range, check if followed by FFW-like bytes
314
- if (pos + 2 < data.length) {
315
- const next = data[pos + 1];
316
- const after = data[pos + 2];
317
- // FFW first byte typically has specific bit patterns
318
- // If followed by reasonable FFW bytes, treat as field attribute
319
- return (next & 0x40) !== 0 || next === 0x00;
320
- }
321
- return false;
322
- }
323
- /** Parse a field attribute byte and the following FFW/FCW */
365
+ /**
366
+ * Parse a bare field attribute byte (0x20-0x3F) that appears after SBA.
367
+ * In basic 5250, this is just 1 byte no FFW/FCW follows.
368
+ * FFW/FCW are only present with explicit SF (Start Field, 0x1D) order.
369
+ */
324
370
  parseFieldAttribute(data, pos, addr, displayAttr) {
325
371
  const attrByte = data[pos++];
326
372
  // The attribute byte occupies a position on screen (but is not displayed)
327
- // Mark this position with the attribute
328
373
  if (addr < this.screen.size) {
329
- this.screen.setCharAt(addr, ' '); // Attribute position shows as space
374
+ this.screen.setCharAt(addr, ' ');
375
+ }
376
+ const fieldStartAddr = addr + 1;
377
+ const { row, col } = this.screen.toRowCol(fieldStartAddr);
378
+ // Map attribute byte to display characteristic, falling back to SA context.
379
+ const fieldDisplayAttr = this.decodeDisplayAttr(attrByte, displayAttr);
380
+ // Determine input vs protected from the RAW attribute byte (not SA-enhanced).
381
+ // Lower 3 bits: 4-6 = underscore variants (input), 7 = non-display (input).
382
+ // This prevents SA context from promoting a normal/protected field to input.
383
+ const rawType = attrByte & 0x07;
384
+ const isInput = rawType >= 0x04; // underscore (4,5,6) or nondisplay (7)
385
+ const field = {
386
+ row,
387
+ col,
388
+ length: 0, // Calculated later
389
+ ffw1: isInput ? 0x00 : 0x20, // BYPASS bit set for output fields
390
+ ffw2: 0,
391
+ fcw1: 0,
392
+ fcw2: 0,
393
+ attribute: fieldDisplayAttr,
394
+ rawAttrByte: attrByte,
395
+ modified: false,
396
+ };
397
+ this.clearStaleFieldsOnce();
398
+ this.screen.fields.push(field);
399
+ return pos;
400
+ }
401
+ /**
402
+ * Parse SF (Start Field) order with FFW and optional FCW.
403
+ * Format: SF(0x1D) FFW1 FFW2 [FCW1 FCW2]
404
+ * FFW1 always has bit 6 set (0x40+). No trailing attribute byte.
405
+ * Display attribute is derived from FFW2.
406
+ */
407
+ parseStartField(data, pos, addr, displayAttr) {
408
+ if (addr < this.screen.size) {
409
+ this.screen.setCharAt(addr, ' ');
330
410
  }
331
- // Parse FFW (Field Format Word) — 2 bytes
411
+ // Parse FFW (Field Format Word) — 2 bytes (FFW1 has bit 6 set)
332
412
  if (pos + 1 >= data.length)
333
413
  return pos;
334
414
  const ffw1 = data[pos++];
335
415
  const ffw2 = data[pos++];
336
416
  // Check for FCW (Field Control Word) — optional, 2 bytes
417
+ // FCW1 has bit 7 set (>= 0x80)
337
418
  let fcw1 = 0, fcw2 = 0;
338
419
  if (pos + 1 < data.length) {
339
- // FCW is present if the first byte has bit 7 set and is not another order
340
420
  const maybeFcw = data[pos];
341
421
  if (maybeFcw >= 0x80 && maybeFcw !== 0xFF) {
342
422
  fcw1 = data[pos++];
343
423
  fcw2 = data[pos++];
344
424
  }
345
425
  }
346
- // Determine field length: from current position to next field attribute or end of screen
347
- // We'll calculate it later when all fields are known; for now use a placeholder
348
- const fieldStartAddr = addr + 1; // Field data starts after the attribute byte
349
- const { row, col } = this.screen.toRowCol(fieldStartAddr);
350
- // Determine display attribute from the attribute byte
426
+ // Consume the trailing field attribute byte (always present after FFW/FCW).
427
+ // This byte (0x20–0x3F) specifies the display attribute for the field.
351
428
  let fieldDisplayAttr = displayAttr;
352
- // Map attribute byte to display characteristics
353
- if (attrByte & 0x04)
354
- fieldDisplayAttr = ATTR.UNDERSCORE;
355
- if (attrByte & 0x08)
356
- fieldDisplayAttr = ATTR.HIGH_INTENSITY;
357
- if (attrByte & 0x01)
358
- fieldDisplayAttr = ATTR.REVERSE;
359
- if (attrByte === 0x27 || (attrByte & 0x07) === 0x07)
360
- fieldDisplayAttr = ATTR.NON_DISPLAY;
429
+ let rawAttrByte = 0;
430
+ if (pos < data.length) {
431
+ const attrByte = data[pos];
432
+ if (attrByte >= 0x20 && attrByte <= 0x3F) {
433
+ pos++;
434
+ rawAttrByte = attrByte;
435
+ fieldDisplayAttr = this.decodeDisplayAttr(attrByte, displayAttr);
436
+ }
437
+ }
438
+ const fieldStartAddr = addr + 1;
439
+ const { row, col } = this.screen.toRowCol(fieldStartAddr);
440
+ // After SF + FFW + optional FCW + ATTR, the host may include a few stale
441
+ // bytes (field content initializers like nulls) before the next SBA order.
442
+ // These should not be displayed. Scan ahead (up to 4 bytes) for the next
443
+ // SBA — if found, skip everything in between.
444
+ {
445
+ let scan = pos;
446
+ const limit = Math.min(pos + 4, data.length);
447
+ while (scan < limit) {
448
+ if (data[scan] === ORDER.SBA) {
449
+ pos = scan; // skip stale bytes
450
+ break;
451
+ }
452
+ scan++;
453
+ }
454
+ }
361
455
  const field = {
362
456
  row,
363
457
  col,
364
- length: 0, // Will be calculated
458
+ length: 0,
365
459
  ffw1,
366
460
  ffw2,
367
461
  fcw1,
368
462
  fcw2,
369
463
  attribute: fieldDisplayAttr,
464
+ rawAttrByte,
370
465
  modified: false,
371
466
  };
467
+ this.clearStaleFieldsOnce();
372
468
  this.screen.fields.push(field);
373
469
  return pos;
374
470
  }
375
- /** Parse explicit SF order field definition */
376
- parseFieldDefinition(data, pos, addr, displayAttr) {
377
- // Similar to parseFieldAttribute but for explicit SF order
378
- return this.parseFieldAttribute(data, pos - 1, addr, displayAttr);
471
+ /**
472
+ * Decode a display attribute byte (0x20–0x3F) into an ATTR constant.
473
+ * Only recognises the bits that determine display type; falls back to
474
+ * the SA context (displayAttr) for modifier-only bytes (0x30, 0x38, etc.).
475
+ */
476
+ decodeDisplayAttr(attrByte, displayAttr = ATTR.NORMAL) {
477
+ if ((attrByte & 0x07) === 0x07)
478
+ return ATTR.NON_DISPLAY;
479
+ if (attrByte & 0x04)
480
+ return ATTR.UNDERSCORE;
481
+ if (attrByte & 0x02)
482
+ return ATTR.HIGH_INTENSITY;
483
+ if (attrByte & 0x08)
484
+ return ATTR.HIGH_INTENSITY;
485
+ return displayAttr;
486
+ }
487
+ /** Clear stale fields when the first SF order arrives after a Reset MDT WTD */
488
+ clearStaleFieldsOnce() {
489
+ if (this.pendingFieldsClear) {
490
+ this.screen.fields = [];
491
+ this.pendingFieldsClear = false;
492
+ }
379
493
  }
380
494
  /**
381
495
  * After parsing a complete screen, calculate field lengths.
@@ -385,6 +499,12 @@ export class TN5250Parser {
385
499
  const fields = this.screen.fields;
386
500
  if (fields.length === 0)
387
501
  return;
502
+ // Sort fields by screen position for correct length calculation
503
+ fields.sort((a, b) => {
504
+ const posA = this.screen.offset(a.row, a.col);
505
+ const posB = this.screen.offset(b.row, b.col);
506
+ return posA - posB;
507
+ });
388
508
  for (let i = 0; i < fields.length; i++) {
389
509
  const current = fields[i];
390
510
  const currentStart = this.screen.offset(current.row, current.col);
@@ -396,9 +516,9 @@ export class TN5250Parser {
396
516
  current.length = Math.max(0, nextStart - currentStart - 1);
397
517
  }
398
518
  else {
399
- // Last field extends to end of screen or a reasonable default
400
- const endAddr = this.screen.size;
401
- current.length = Math.max(0, endAddr - currentStart);
519
+ // Last field wraps to first field (5250 screen is circular)
520
+ const firstStart = this.screen.offset(fields[0].row, fields[0].col);
521
+ current.length = Math.max(0, (this.screen.size - currentStart) + firstStart - 1);
402
522
  // Cap at a reasonable maximum
403
523
  if (current.length > this.screen.cols * 2) {
404
524
  current.length = this.screen.cols - current.col;
@@ -408,5 +528,40 @@ export class TN5250Parser {
408
528
  if (current.length <= 0)
409
529
  current.length = 1;
410
530
  }
531
+ // Ensure cursor is in a functional input field. Skip UIM framework
532
+ // artifact fields whose OWN attribute byte doesn't indicate underscore
533
+ // or non-display (they may inherit underscore from SA context but aren't
534
+ // real interactive fields — they exist in the panel header).
535
+ {
536
+ const allInputs = fields.filter(f => this.screen.isInputField(f));
537
+ if (allInputs.length > 0) {
538
+ const lastPos = this.screen.offset(allInputs[allInputs.length - 1].row, allInputs[allInputs.length - 1].col);
539
+ const functional = allInputs.filter(f => this.screen.hasNativeUnderscore(f) || this.screen.hasNativeNonDisplay(f) ||
540
+ this.screen.offset(f.row, f.col) === lastPos);
541
+ const targets = functional.length > 0 ? functional : allInputs;
542
+ const cursorAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
543
+ const inTarget = targets.some(f => {
544
+ const start = this.screen.offset(f.row, f.col);
545
+ return cursorAddr >= start && cursorAddr < start + f.length;
546
+ });
547
+ if (!inTarget) {
548
+ const after = targets.find(f => this.screen.offset(f.row, f.col) >= cursorAddr);
549
+ const target = after || targets[targets.length - 1];
550
+ this.screen.cursorRow = target.row;
551
+ this.screen.cursorCol = target.col;
552
+ }
553
+ }
554
+ }
555
+ // Deduplicate fields at the same position (keep the last one)
556
+ const seen = new Map();
557
+ for (let i = 0; i < fields.length; i++) {
558
+ const key = `${fields[i].row},${fields[i].col}`;
559
+ seen.set(key, i);
560
+ }
561
+ if (seen.size < fields.length) {
562
+ const keep = new Set(seen.values());
563
+ this.screen.fields = fields.filter((_, i) => keep.has(i));
564
+ }
411
565
  }
412
566
  }
567
+ //# sourceMappingURL=parser.js.map