green-screen-proxy 0.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.
Files changed (67) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +32 -0
  3. package/dist/hp6530/connection.d.ts +51 -0
  4. package/dist/hp6530/connection.js +258 -0
  5. package/dist/hp6530/constants.d.ts +64 -0
  6. package/dist/hp6530/constants.js +135 -0
  7. package/dist/hp6530/encoder.d.ts +37 -0
  8. package/dist/hp6530/encoder.js +89 -0
  9. package/dist/hp6530/parser.d.ts +45 -0
  10. package/dist/hp6530/parser.js +255 -0
  11. package/dist/hp6530/screen.d.ts +104 -0
  12. package/dist/hp6530/screen.js +252 -0
  13. package/dist/mock/mock-routes.d.ts +2 -0
  14. package/dist/mock/mock-routes.js +231 -0
  15. package/dist/protocols/hp6530-handler.d.ts +29 -0
  16. package/dist/protocols/hp6530-handler.js +64 -0
  17. package/dist/protocols/index.d.ts +11 -0
  18. package/dist/protocols/index.js +27 -0
  19. package/dist/protocols/tn3270-handler.d.ts +26 -0
  20. package/dist/protocols/tn3270-handler.js +61 -0
  21. package/dist/protocols/tn5250-handler.d.ts +26 -0
  22. package/dist/protocols/tn5250-handler.js +62 -0
  23. package/dist/protocols/types.d.ts +59 -0
  24. package/dist/protocols/types.js +7 -0
  25. package/dist/protocols/vt-handler.d.ts +30 -0
  26. package/dist/protocols/vt-handler.js +67 -0
  27. package/dist/routes.d.ts +2 -0
  28. package/dist/routes.js +141 -0
  29. package/dist/server.d.ts +1 -0
  30. package/dist/server.js +34 -0
  31. package/dist/session.d.ts +32 -0
  32. package/dist/session.js +88 -0
  33. package/dist/tn3270/connection.d.ts +31 -0
  34. package/dist/tn3270/connection.js +266 -0
  35. package/dist/tn3270/constants.d.ts +262 -0
  36. package/dist/tn3270/constants.js +261 -0
  37. package/dist/tn3270/encoder.d.ts +24 -0
  38. package/dist/tn3270/encoder.js +97 -0
  39. package/dist/tn3270/parser.d.ts +22 -0
  40. package/dist/tn3270/parser.js +284 -0
  41. package/dist/tn3270/screen.d.ts +89 -0
  42. package/dist/tn3270/screen.js +207 -0
  43. package/dist/tn5250/connection.d.ts +41 -0
  44. package/dist/tn5250/connection.js +254 -0
  45. package/dist/tn5250/constants.d.ts +128 -0
  46. package/dist/tn5250/constants.js +156 -0
  47. package/dist/tn5250/ebcdic.d.ts +10 -0
  48. package/dist/tn5250/ebcdic.js +89 -0
  49. package/dist/tn5250/encoder.d.ts +30 -0
  50. package/dist/tn5250/encoder.js +121 -0
  51. package/dist/tn5250/parser.d.ts +33 -0
  52. package/dist/tn5250/parser.js +412 -0
  53. package/dist/tn5250/screen.d.ts +80 -0
  54. package/dist/tn5250/screen.js +155 -0
  55. package/dist/vt/connection.d.ts +45 -0
  56. package/dist/vt/connection.js +229 -0
  57. package/dist/vt/constants.d.ts +97 -0
  58. package/dist/vt/constants.js +163 -0
  59. package/dist/vt/encoder.d.ts +30 -0
  60. package/dist/vt/encoder.js +55 -0
  61. package/dist/vt/parser.d.ts +36 -0
  62. package/dist/vt/parser.js +534 -0
  63. package/dist/vt/screen.d.ts +101 -0
  64. package/dist/vt/screen.js +424 -0
  65. package/dist/websocket.d.ts +6 -0
  66. package/dist/websocket.js +50 -0
  67. package/package.json +57 -0
@@ -0,0 +1,121 @@
1
+ import { TELNET, KEY_TO_AID, AID } from './constants.js';
2
+ import { charToEbcdic, EBCDIC_SPACE } from './ebcdic.js';
3
+ /**
4
+ * Encodes client responses (aid keys + field data) into 5250 data stream
5
+ * for sending back to the IBM i host.
6
+ */
7
+ export class TN5250Encoder {
8
+ screen;
9
+ constructor(screen) {
10
+ this.screen = screen;
11
+ }
12
+ /**
13
+ * Build a 5250 input response for an aid key press.
14
+ * Collects modified field data and builds the response record.
15
+ * Returns a Buffer ready to send over the TCP socket (with Telnet EOR framing).
16
+ */
17
+ buildAidResponse(keyName) {
18
+ const aidByte = KEY_TO_AID[keyName];
19
+ if (aidByte === undefined)
20
+ return null;
21
+ const parts = [];
22
+ // Row and column of cursor (1-based for the protocol)
23
+ const cursorRow = this.screen.cursorRow;
24
+ 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);
30
+ // Cursor position + AID byte
31
+ parts.push(Buffer.from([cursorRow, cursorCol, aidByte]));
32
+ // For certain aid keys (like SysReq), no field data is sent
33
+ if (aidByte === AID.SYS_REQUEST || aidByte === AID.CLEAR) {
34
+ return this.wrapWithEOR(Buffer.concat(parts));
35
+ }
36
+ // Collect modified fields and append their data
37
+ for (const field of this.screen.fields) {
38
+ if (!field.modified)
39
+ continue;
40
+ if (!this.screen.isInputField(field))
41
+ continue;
42
+ // SBA order to indicate field position
43
+ parts.push(Buffer.from([0x11, field.row, field.col])); // SBA + row + col
44
+ // Field data in EBCDIC
45
+ const value = this.screen.getFieldValue(field);
46
+ const ebcdicData = Buffer.alloc(value.length);
47
+ for (let i = 0; i < value.length; i++) {
48
+ ebcdicData[i] = charToEbcdic(value[i]);
49
+ }
50
+ // Trim trailing spaces
51
+ let trimLen = ebcdicData.length;
52
+ while (trimLen > 0 && ebcdicData[trimLen - 1] === EBCDIC_SPACE) {
53
+ trimLen--;
54
+ }
55
+ if (trimLen > 0) {
56
+ parts.push(ebcdicData.subarray(0, trimLen));
57
+ }
58
+ }
59
+ return this.wrapWithEOR(Buffer.concat(parts));
60
+ }
61
+ /**
62
+ * Build a GDS (General Data Stream) header for a response.
63
+ */
64
+ 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]);
73
+ }
74
+ /**
75
+ * Wrap data with Telnet IAC EOR framing.
76
+ * Also escapes any 0xFF bytes in the data as IAC IAC.
77
+ */
78
+ wrapWithEOR(data) {
79
+ // Update the record length in the GDS header
80
+ if (data.length >= 2) {
81
+ const len = data.length;
82
+ data[0] = (len >> 8) & 0xFF;
83
+ data[1] = len & 0xFF;
84
+ }
85
+ // Escape IAC bytes in data and append IAC EOR
86
+ const escaped = [];
87
+ for (let i = 0; i < data.length; i++) {
88
+ escaped.push(data[i]);
89
+ if (data[i] === TELNET.IAC) {
90
+ escaped.push(TELNET.IAC); // escape
91
+ }
92
+ }
93
+ escaped.push(TELNET.IAC, TELNET.EOR);
94
+ return Buffer.from(escaped);
95
+ }
96
+ /**
97
+ * Insert text at the current cursor position in the current field.
98
+ * Updates the screen buffer and marks the field as modified.
99
+ * Returns true if text was successfully inserted.
100
+ */
101
+ insertText(text) {
102
+ const field = this.screen.getFieldAtCursor();
103
+ if (!field || !this.screen.isInputField(field))
104
+ return false;
105
+ const fieldStart = this.screen.offset(field.row, field.col);
106
+ let cursorOffset = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
107
+ const fieldEnd = fieldStart + field.length;
108
+ for (const ch of text) {
109
+ if (cursorOffset >= fieldEnd)
110
+ break; // Field is full
111
+ this.screen.buffer[cursorOffset] = ch;
112
+ cursorOffset++;
113
+ }
114
+ // Update cursor position
115
+ const newPos = this.screen.toRowCol(Math.min(cursorOffset, fieldEnd - 1));
116
+ this.screen.cursorRow = newPos.row;
117
+ this.screen.cursorCol = newPos.col;
118
+ field.modified = true;
119
+ return true;
120
+ }
121
+ }
@@ -0,0 +1,33 @@
1
+ import { ScreenBuffer } from './screen.js';
2
+ /**
3
+ * Parses 5250 data stream records and updates the screen buffer.
4
+ */
5
+ export declare class TN5250Parser {
6
+ private screen;
7
+ constructor(screen: ScreenBuffer);
8
+ /**
9
+ * Parse a complete 5250 record (after Telnet framing is removed).
10
+ * Returns true if the screen was modified.
11
+ */
12
+ parseRecord(record: Buffer): boolean;
13
+ /** Try to parse data that doesn't have a proper GDS header */
14
+ private tryParseRawData;
15
+ /** Parse one or more 5250 commands starting at offset */
16
+ private parseCommands;
17
+ /**
18
+ * Parse orders and text data within a WTD (or similar) command.
19
+ * Updates the screen buffer and returns the new position.
20
+ */
21
+ 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 */
25
+ private parseFieldAttribute;
26
+ /** Parse explicit SF order field definition */
27
+ private parseFieldDefinition;
28
+ /**
29
+ * After parsing a complete screen, calculate field lengths.
30
+ * Call this after all records for a screen have been parsed.
31
+ */
32
+ calculateFieldLengths(): void;
33
+ }
@@ -0,0 +1,412 @@
1
+ import { CMD, ORDER, OPCODE, ATTR } from './constants.js';
2
+ import { ebcdicToChar } from './ebcdic.js';
3
+ /**
4
+ * Parses 5250 data stream records and updates the screen buffer.
5
+ */
6
+ export class TN5250Parser {
7
+ screen;
8
+ constructor(screen) {
9
+ this.screen = screen;
10
+ }
11
+ /**
12
+ * Parse a complete 5250 record (after Telnet framing is removed).
13
+ * Returns true if the screen was modified.
14
+ */
15
+ parseRecord(record) {
16
+ if (record.length < 2)
17
+ return false;
18
+ // 5250 record header:
19
+ // Bytes 0-1: record length
20
+ // Byte 2: record type (high byte)
21
+ // Byte 3: record type (low byte) — together 0x12A0 for GDS
22
+ // Byte 4: reserved (variable flag)
23
+ // Byte 5: reserved
24
+ // Byte 6: opcode
25
+ // Remaining: data
26
+ // Some records may be shorter or have different formats
27
+ // Try to parse the GDS header
28
+ if (record.length < 7) {
29
+ // Too short for a full GDS header — might be a short record
30
+ return false;
31
+ }
32
+ const recordLen = (record[0] << 8) | record[1];
33
+ const recordType = (record[2] << 8) | record[3];
34
+ const varFlag = record[4];
35
+ const reserved = record[5];
36
+ const opcode = record[6];
37
+ // Check for GDS record type
38
+ if (recordType !== 0x12A0) {
39
+ // Not a GDS record — might be raw data or something else
40
+ return this.tryParseRawData(record);
41
+ }
42
+ let modified = false;
43
+ switch (opcode) {
44
+ case OPCODE.OUTPUT:
45
+ case OPCODE.PUT_GET:
46
+ // Contains one or more 5250 commands after the header
47
+ modified = this.parseCommands(record, 7);
48
+ break;
49
+ case OPCODE.INVITE:
50
+ // Invite: server is ready for input (no screen data typically)
51
+ break;
52
+ case OPCODE.SAVE_SCREEN:
53
+ case OPCODE.RESTORE_SCREEN:
54
+ // We don't implement save/restore, just ignore
55
+ break;
56
+ default:
57
+ // Try parsing as commands anyway
58
+ if (record.length > 7) {
59
+ modified = this.parseCommands(record, 7);
60
+ }
61
+ break;
62
+ }
63
+ return modified;
64
+ }
65
+ /** Try to parse data that doesn't have a proper GDS header */
66
+ tryParseRawData(record) {
67
+ // Some servers send command data without the full GDS wrapper
68
+ return this.parseCommands(record, 0);
69
+ }
70
+ /** Parse one or more 5250 commands starting at offset */
71
+ parseCommands(data, offset) {
72
+ let pos = offset;
73
+ let modified = false;
74
+ while (pos < data.length) {
75
+ const cmd = data[pos];
76
+ switch (cmd) {
77
+ case CMD.CLEAR_UNIT:
78
+ case CMD.CLEAR_UNIT_ALT:
79
+ this.screen.clear();
80
+ pos++;
81
+ modified = true;
82
+ break;
83
+ case CMD.CLEAR_FORMAT_TABLE:
84
+ this.screen.fields = [];
85
+ pos++;
86
+ modified = true;
87
+ break;
88
+ case CMD.WRITE_TO_DISPLAY: {
89
+ pos++; // skip command byte
90
+ // WTD has a CC (control character) — 2 bytes
91
+ if (pos + 1 < data.length) {
92
+ const cc1 = data[pos++];
93
+ const cc2 = data[pos++];
94
+ // CC1 bit 5: Reset MDT flags
95
+ if (cc1 & 0x20) {
96
+ for (const f of this.screen.fields) {
97
+ f.modified = false;
98
+ }
99
+ }
100
+ // CC1 bit 6: Clear all input fields (null fill)
101
+ if (cc1 & 0x40) {
102
+ for (const f of this.screen.fields) {
103
+ if (this.screen.isInputField(f)) {
104
+ const start = this.screen.offset(f.row, f.col);
105
+ for (let i = start; i < start + f.length; i++) {
106
+ this.screen.buffer[i] = ' ';
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ // Parse orders and data following WTD
113
+ pos = this.parseOrders(data, pos);
114
+ modified = true;
115
+ break;
116
+ }
117
+ case CMD.WRITE_ERROR_CODE:
118
+ case CMD.WRITE_ERROR_CODE_WIN: {
119
+ pos++; // skip command byte
120
+ // Skip the error line data — just advance past it
121
+ // Error code commands are followed by data until next command
122
+ pos = this.parseOrders(data, pos);
123
+ modified = true;
124
+ break;
125
+ }
126
+ case CMD.WRITE_STRUCTURED_FIELD: {
127
+ pos++;
128
+ // Structured fields have their own length prefix
129
+ if (pos + 1 < data.length) {
130
+ const sfLen = (data[pos] << 8) | data[pos + 1];
131
+ pos += sfLen; // skip the entire structured field
132
+ }
133
+ break;
134
+ }
135
+ case CMD.ROLL: {
136
+ pos++; // skip command byte
137
+ if (pos + 1 < data.length) {
138
+ const rollCC = data[pos++];
139
+ const rollCount = data[pos++];
140
+ // Simple roll: move content up or down
141
+ // For now just mark as modified
142
+ modified = true;
143
+ }
144
+ break;
145
+ }
146
+ default:
147
+ // Not a recognized command at this position
148
+ // Try treating remaining data as orders/text
149
+ pos = this.parseOrders(data, pos);
150
+ modified = true;
151
+ break;
152
+ }
153
+ }
154
+ return modified;
155
+ }
156
+ /**
157
+ * Parse orders and text data within a WTD (or similar) command.
158
+ * Updates the screen buffer and returns the new position.
159
+ */
160
+ parseOrders(data, pos) {
161
+ let currentAddr = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
162
+ let currentAttr = ATTR.NORMAL;
163
+ while (pos < data.length) {
164
+ 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
+ }
171
+ switch (byte) {
172
+ case ORDER.SBA: {
173
+ // Set Buffer Address: 2 bytes follow (row, col)
174
+ if (pos + 2 >= data.length)
175
+ return data.length;
176
+ pos++;
177
+ const row = data[pos++];
178
+ const col = data[pos++];
179
+ currentAddr = this.screen.offset(row, col);
180
+ break;
181
+ }
182
+ case ORDER.IC: {
183
+ // Insert Cursor: set cursor to current address
184
+ pos++;
185
+ const { row, col } = this.screen.toRowCol(currentAddr);
186
+ this.screen.cursorRow = row;
187
+ this.screen.cursorCol = col;
188
+ break;
189
+ }
190
+ case ORDER.MC: {
191
+ // Move Cursor: 2 bytes follow (row, col)
192
+ if (pos + 2 >= data.length)
193
+ return data.length;
194
+ pos++;
195
+ const row = data[pos++];
196
+ const col = data[pos++];
197
+ this.screen.cursorRow = row;
198
+ this.screen.cursorCol = col;
199
+ currentAddr = this.screen.offset(row, col);
200
+ break;
201
+ }
202
+ case ORDER.RA: {
203
+ // Repeat to Address: repeat a char up to an address
204
+ if (pos + 3 >= data.length)
205
+ return data.length;
206
+ pos++;
207
+ const toRow = data[pos++];
208
+ const toCol = data[pos++];
209
+ const charByte = data[pos++];
210
+ const targetAddr = this.screen.offset(toRow, toCol);
211
+ const ch = ebcdicToChar(charByte);
212
+ while (currentAddr < targetAddr && currentAddr < this.screen.size) {
213
+ this.screen.setCharAt(currentAddr, ch);
214
+ currentAddr++;
215
+ }
216
+ break;
217
+ }
218
+ case ORDER.EA: {
219
+ // Erase to Address: fill with spaces up to an address
220
+ if (pos + 2 >= data.length)
221
+ return data.length;
222
+ pos++;
223
+ const toRow = data[pos++];
224
+ const toCol = data[pos++];
225
+ const targetAddr = this.screen.offset(toRow, toCol);
226
+ while (currentAddr < targetAddr && currentAddr < this.screen.size) {
227
+ this.screen.setCharAt(currentAddr, ' ');
228
+ currentAddr++;
229
+ }
230
+ break;
231
+ }
232
+ case ORDER.SOH: {
233
+ // Start of Header: variable-length header for input fields
234
+ if (pos + 1 >= data.length)
235
+ return data.length;
236
+ pos++;
237
+ 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
240
+ break;
241
+ }
242
+ case ORDER.TD: {
243
+ // Transparent Data: length byte followed by raw data
244
+ if (pos + 1 >= data.length)
245
+ return data.length;
246
+ pos++;
247
+ const tdLen = data[pos++];
248
+ for (let i = 0; i < tdLen && pos < data.length; i++) {
249
+ this.screen.setCharAt(currentAddr++, ebcdicToChar(data[pos++]));
250
+ }
251
+ break;
252
+ }
253
+ case ORDER.WEA: {
254
+ // Write Extended Attribute: 2 bytes (attr type + value)
255
+ if (pos + 2 >= data.length)
256
+ return data.length;
257
+ pos++;
258
+ const attrType = data[pos++];
259
+ const attrValue = data[pos++];
260
+ // Apply attribute at current position
261
+ this.screen.setAttrAt(currentAddr, attrValue);
262
+ break;
263
+ }
264
+ case ORDER.SA: {
265
+ // Set Attribute: 2 bytes (attr type + value)
266
+ if (pos + 2 >= data.length)
267
+ return data.length;
268
+ pos++;
269
+ const saType = data[pos++];
270
+ const saValue = data[pos++];
271
+ currentAttr = saValue;
272
+ break;
273
+ }
274
+ 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:
279
+ pos++;
280
+ pos = this.parseFieldDefinition(data, pos, currentAddr, currentAttr);
281
+ break;
282
+ }
283
+ default: {
284
+ // Check for field attribute bytes (0x20-0x3F)
285
+ if (byte >= 0x20 && byte <= 0x3F && this.isLikelyFieldAttribute(data, pos)) {
286
+ pos = this.parseFieldAttribute(data, pos, currentAddr, currentAttr);
287
+ }
288
+ else {
289
+ // Regular EBCDIC character data
290
+ const ch = ebcdicToChar(byte);
291
+ if (currentAddr < this.screen.size) {
292
+ this.screen.setCharAt(currentAddr, ch);
293
+ this.screen.setAttrAt(currentAddr, currentAttr);
294
+ currentAddr++;
295
+ }
296
+ pos++;
297
+ }
298
+ break;
299
+ }
300
+ }
301
+ }
302
+ return pos;
303
+ }
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 */
324
+ parseFieldAttribute(data, pos, addr, displayAttr) {
325
+ const attrByte = data[pos++];
326
+ // The attribute byte occupies a position on screen (but is not displayed)
327
+ // Mark this position with the attribute
328
+ if (addr < this.screen.size) {
329
+ this.screen.setCharAt(addr, ' '); // Attribute position shows as space
330
+ }
331
+ // Parse FFW (Field Format Word) — 2 bytes
332
+ if (pos + 1 >= data.length)
333
+ return pos;
334
+ const ffw1 = data[pos++];
335
+ const ffw2 = data[pos++];
336
+ // Check for FCW (Field Control Word) — optional, 2 bytes
337
+ let fcw1 = 0, fcw2 = 0;
338
+ if (pos + 1 < data.length) {
339
+ // FCW is present if the first byte has bit 7 set and is not another order
340
+ const maybeFcw = data[pos];
341
+ if (maybeFcw >= 0x80 && maybeFcw !== 0xFF) {
342
+ fcw1 = data[pos++];
343
+ fcw2 = data[pos++];
344
+ }
345
+ }
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
351
+ 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;
361
+ const field = {
362
+ row,
363
+ col,
364
+ length: 0, // Will be calculated
365
+ ffw1,
366
+ ffw2,
367
+ fcw1,
368
+ fcw2,
369
+ attribute: fieldDisplayAttr,
370
+ modified: false,
371
+ };
372
+ this.screen.fields.push(field);
373
+ return pos;
374
+ }
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);
379
+ }
380
+ /**
381
+ * After parsing a complete screen, calculate field lengths.
382
+ * Call this after all records for a screen have been parsed.
383
+ */
384
+ calculateFieldLengths() {
385
+ const fields = this.screen.fields;
386
+ if (fields.length === 0)
387
+ return;
388
+ for (let i = 0; i < fields.length; i++) {
389
+ const current = fields[i];
390
+ const currentStart = this.screen.offset(current.row, current.col);
391
+ if (i + 1 < fields.length) {
392
+ const next = fields[i + 1];
393
+ const nextStart = this.screen.offset(next.row, next.col);
394
+ // Length extends to just before the next field's attribute byte
395
+ // (the attribute byte is 1 position before the next field's start)
396
+ current.length = Math.max(0, nextStart - currentStart - 1);
397
+ }
398
+ 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);
402
+ // Cap at a reasonable maximum
403
+ if (current.length > this.screen.cols * 2) {
404
+ current.length = this.screen.cols - current.col;
405
+ }
406
+ }
407
+ // Ensure minimum length of 1
408
+ if (current.length <= 0)
409
+ current.length = 1;
410
+ }
411
+ }
412
+ }
@@ -0,0 +1,80 @@
1
+ export interface FieldDef {
2
+ row: number;
3
+ col: number;
4
+ length: number;
5
+ ffw1: number;
6
+ ffw2: number;
7
+ fcw1: number;
8
+ fcw2: number;
9
+ attribute: number;
10
+ modified: boolean;
11
+ }
12
+ export declare class ScreenBuffer {
13
+ rows: number;
14
+ cols: number;
15
+ /** Character buffer stored as UTF-8 characters */
16
+ buffer: string[];
17
+ /** Attribute buffer (display attribute per cell) */
18
+ attrBuffer: number[];
19
+ fields: FieldDef[];
20
+ cursorRow: number;
21
+ cursorCol: number;
22
+ constructor(rows?: 24, cols?: 80);
23
+ get size(): number;
24
+ /** Convert row,col to linear offset */
25
+ offset(row: number, col: number): number;
26
+ /** Convert linear offset to row,col */
27
+ toRowCol(offset: number): {
28
+ row: number;
29
+ col: number;
30
+ };
31
+ /** Set a character at row,col */
32
+ setChar(row: number, col: number, char: string): void;
33
+ /** Get a character at row,col */
34
+ getChar(row: number, col: number): string;
35
+ /** Set attribute at row,col */
36
+ setAttr(row: number, col: number, attr: number): void;
37
+ /** Set character at a linear address */
38
+ setCharAt(addr: number, char: string): void;
39
+ /** Set attribute at a linear address */
40
+ setAttrAt(addr: number, attr: number): void;
41
+ /** Clear the entire screen */
42
+ clear(): void;
43
+ /** Fill range [start, end) with a character */
44
+ fillRange(start: number, end: number, char: string): void;
45
+ /** Get the content of a field as a string */
46
+ getFieldValue(field: FieldDef): string;
47
+ /** Set the content of a field */
48
+ setFieldValue(field: FieldDef, value: string): void;
49
+ /** Find the field at cursor position */
50
+ getFieldAtCursor(): FieldDef | null;
51
+ /** Find the field containing a given position */
52
+ getFieldAt(row: number, col: number): FieldDef | null;
53
+ /** Whether a field is an input field (not bypass/protected) */
54
+ isInputField(field: FieldDef): boolean;
55
+ /** Whether a field is highlighted (high intensity) */
56
+ isHighlighted(field: FieldDef): boolean;
57
+ /** Whether a field is reverse video */
58
+ isReverse(field: FieldDef): boolean;
59
+ /** Whether a field is non-display (password) */
60
+ isNonDisplay(field: FieldDef): boolean;
61
+ /** Convert screen buffer to the ScreenData format expected by the frontend */
62
+ toScreenData(): {
63
+ content: string;
64
+ cursor_row: number;
65
+ cursor_col: number;
66
+ rows: number;
67
+ cols: number;
68
+ fields: Array<{
69
+ row: number;
70
+ col: number;
71
+ length: number;
72
+ is_input: boolean;
73
+ is_protected: boolean;
74
+ is_highlighted?: boolean;
75
+ is_reverse?: boolean;
76
+ }>;
77
+ screen_signature: string;
78
+ timestamp: string;
79
+ };
80
+ }