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,284 @@
1
+ import { CMD, SNA_CMD, ORDER, FA, EXT_ATTR, decodeAddress } from './constants.js';
2
+ import { ebcdicToChar } from '../tn5250/ebcdic.js';
3
+ /**
4
+ * Parses 3270 data stream records and updates the screen buffer.
5
+ */
6
+ export class TN3270Parser {
7
+ screen;
8
+ constructor(screen) {
9
+ this.screen = screen;
10
+ }
11
+ /**
12
+ * Parse a 3270 data stream record.
13
+ * Returns true if the screen was modified.
14
+ */
15
+ parseRecord(record) {
16
+ if (record.length < 1)
17
+ return false;
18
+ const cmd = record[0];
19
+ let modified = false;
20
+ switch (cmd) {
21
+ case CMD.WRITE:
22
+ case SNA_CMD.WRITE:
23
+ modified = this.parseWrite(record, 1, false);
24
+ break;
25
+ case CMD.ERASE_WRITE:
26
+ case SNA_CMD.ERASE_WRITE:
27
+ this.screen.clear();
28
+ modified = this.parseWrite(record, 1, false);
29
+ break;
30
+ case CMD.ERASE_WRITE_ALTERNATE:
31
+ case SNA_CMD.ERASE_WRITE_ALTERNATE:
32
+ this.screen.clear();
33
+ modified = this.parseWrite(record, 1, false);
34
+ break;
35
+ case CMD.ERASE_ALL_UNPROTECTED:
36
+ case SNA_CMD.ERASE_ALL_UNPROTECTED:
37
+ this.screen.clearUnprotected();
38
+ modified = true;
39
+ break;
40
+ case CMD.WRITE_STRUCTURED_FIELD:
41
+ case SNA_CMD.WRITE_STRUCTURED_FIELD:
42
+ // Structured fields — parse each SF
43
+ modified = this.parseStructuredFields(record, 1);
44
+ break;
45
+ case CMD.READ_BUFFER:
46
+ case CMD.READ_MODIFIED:
47
+ case CMD.READ_MODIFIED_ALL:
48
+ case SNA_CMD.READ_BUFFER:
49
+ case SNA_CMD.READ_MODIFIED:
50
+ case SNA_CMD.READ_MODIFIED_ALL:
51
+ // Read commands — don't modify screen, but may need to trigger a response
52
+ // Handled at the handler level
53
+ break;
54
+ default:
55
+ // Try parsing as a write command anyway (some servers omit the command byte)
56
+ if (record.length > 1) {
57
+ modified = this.parseWrite(record, 0, true);
58
+ }
59
+ break;
60
+ }
61
+ if (modified) {
62
+ this.screen.rebuildFields();
63
+ }
64
+ return modified;
65
+ }
66
+ /**
67
+ * Parse a Write or Erase/Write command body.
68
+ * Starts at `offset` which points to the WCC byte.
69
+ */
70
+ parseWrite(data, offset, skipWCC) {
71
+ let pos = offset;
72
+ // Parse WCC (Write Control Character)
73
+ if (!skipWCC && pos < data.length) {
74
+ const wcc = data[pos++];
75
+ // WCC bit 0: reset MDT flags
76
+ if (wcc & 0x01) {
77
+ for (const field of this.screen.fields) {
78
+ field.modified = false;
79
+ // Clear MDT bit in attribute buffer too
80
+ const attr = this.screen.attrBuffer[field.attrAddr];
81
+ if (attr !== 0) {
82
+ this.screen.attrBuffer[field.attrAddr] = attr & ~FA.MDT;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ let modified = false;
88
+ // Parse orders and data
89
+ while (pos < data.length) {
90
+ const byte = data[pos];
91
+ switch (byte) {
92
+ case ORDER.SBA: {
93
+ // Set Buffer Address: 2 address bytes follow
94
+ if (pos + 2 >= data.length)
95
+ return modified;
96
+ pos++;
97
+ const addr = decodeAddress(data[pos], data[pos + 1]);
98
+ pos += 2;
99
+ this.screen.currentAddr = addr % this.screen.size;
100
+ break;
101
+ }
102
+ case ORDER.SF: {
103
+ // Start Field: 1 attribute byte follows
104
+ if (pos + 1 >= data.length)
105
+ return modified;
106
+ pos++;
107
+ const attr = data[pos++];
108
+ this.screen.setFieldAttribute(this.screen.currentAddr, attr);
109
+ this.screen.currentAddr = (this.screen.currentAddr + 1) % this.screen.size;
110
+ modified = true;
111
+ break;
112
+ }
113
+ case ORDER.SFE: {
114
+ // Start Field Extended: pair count, then (type, value) pairs
115
+ if (pos + 1 >= data.length)
116
+ return modified;
117
+ pos++;
118
+ const pairCount = data[pos++];
119
+ let fieldAttr = 0;
120
+ let extHighlight = 0;
121
+ let extColor = 0;
122
+ for (let i = 0; i < pairCount && pos + 1 < data.length; i++) {
123
+ const attrType = data[pos++];
124
+ const attrValue = data[pos++];
125
+ if (attrType === 0xC0) {
126
+ // Basic field attribute
127
+ fieldAttr = attrValue;
128
+ }
129
+ else if (attrType === EXT_ATTR.HIGHLIGHT) {
130
+ extHighlight = attrValue;
131
+ }
132
+ else if (attrType === EXT_ATTR.COLOR) {
133
+ extColor = attrValue;
134
+ }
135
+ // Other extended attributes ignored for now
136
+ }
137
+ this.screen.setFieldAttribute(this.screen.currentAddr, fieldAttr || FA.PROTECTED);
138
+ this.screen.highlightBuffer[this.screen.currentAddr] = extHighlight;
139
+ this.screen.colorBuffer[this.screen.currentAddr] = extColor;
140
+ this.screen.currentAddr = (this.screen.currentAddr + 1) % this.screen.size;
141
+ modified = true;
142
+ break;
143
+ }
144
+ case ORDER.SA: {
145
+ // Set Attribute: type + value
146
+ if (pos + 2 >= data.length)
147
+ return modified;
148
+ pos++;
149
+ const saType = data[pos++];
150
+ const saValue = data[pos++];
151
+ // SA affects subsequent characters until next SA/SF
152
+ if (saType === EXT_ATTR.HIGHLIGHT) {
153
+ this.screen.highlightBuffer[this.screen.currentAddr] = saValue;
154
+ }
155
+ else if (saType === EXT_ATTR.COLOR) {
156
+ this.screen.colorBuffer[this.screen.currentAddr] = saValue;
157
+ }
158
+ break;
159
+ }
160
+ case ORDER.MF: {
161
+ // Modify Field: pair count, then (type, value) pairs
162
+ if (pos + 1 >= data.length)
163
+ return modified;
164
+ pos++;
165
+ const mfPairCount = data[pos++];
166
+ for (let i = 0; i < mfPairCount && pos + 1 < data.length; i++) {
167
+ pos += 2; // Skip type + value
168
+ }
169
+ break;
170
+ }
171
+ case ORDER.IC: {
172
+ // Insert Cursor: set cursor to current buffer address
173
+ pos++;
174
+ this.screen.cursorAddr = this.screen.currentAddr;
175
+ break;
176
+ }
177
+ case ORDER.PT: {
178
+ // Program Tab: advance to next unprotected field
179
+ pos++;
180
+ this.advanceToNextUnprotected();
181
+ break;
182
+ }
183
+ case ORDER.RA: {
184
+ // Repeat to Address: 2 address bytes + 1 char byte
185
+ if (pos + 3 >= data.length)
186
+ return modified;
187
+ pos++;
188
+ const targetAddr = decodeAddress(data[pos], data[pos + 1]);
189
+ pos += 2;
190
+ const charByte = data[pos++];
191
+ let repeatChar;
192
+ if (charByte === ORDER.GE && pos < data.length) {
193
+ // Graphic escape — next byte is an APL/graphic char
194
+ repeatChar = ebcdicToChar(data[pos++]);
195
+ }
196
+ else {
197
+ repeatChar = ebcdicToChar(charByte);
198
+ }
199
+ const target = targetAddr % this.screen.size;
200
+ let addr = this.screen.currentAddr;
201
+ while (addr !== target) {
202
+ this.screen.setCharAt(addr, repeatChar);
203
+ addr = (addr + 1) % this.screen.size;
204
+ }
205
+ this.screen.currentAddr = target;
206
+ modified = true;
207
+ break;
208
+ }
209
+ case ORDER.EUA: {
210
+ // Erase Unprotected to Address: 2 address bytes
211
+ if (pos + 2 >= data.length)
212
+ return modified;
213
+ pos++;
214
+ const euaTarget = decodeAddress(data[pos], data[pos + 1]);
215
+ pos += 2;
216
+ const euaEnd = euaTarget % this.screen.size;
217
+ let euaAddr = this.screen.currentAddr;
218
+ while (euaAddr !== euaEnd) {
219
+ // Only erase if position is in an unprotected field
220
+ const field = this.screen.getFieldAt(euaAddr);
221
+ if (field && !this.screen.isProtected(field)) {
222
+ this.screen.setCharAt(euaAddr, ' ');
223
+ }
224
+ euaAddr = (euaAddr + 1) % this.screen.size;
225
+ }
226
+ this.screen.currentAddr = euaEnd;
227
+ modified = true;
228
+ break;
229
+ }
230
+ case ORDER.GE: {
231
+ // Graphic Escape: next byte is a graphic character
232
+ pos++;
233
+ if (pos < data.length) {
234
+ const geChar = ebcdicToChar(data[pos++]);
235
+ this.screen.setCharAt(this.screen.currentAddr, geChar);
236
+ this.screen.currentAddr = (this.screen.currentAddr + 1) % this.screen.size;
237
+ modified = true;
238
+ }
239
+ break;
240
+ }
241
+ default: {
242
+ // Regular EBCDIC character data
243
+ const ch = ebcdicToChar(byte);
244
+ this.screen.setCharAt(this.screen.currentAddr, ch);
245
+ this.screen.currentAddr = (this.screen.currentAddr + 1) % this.screen.size;
246
+ pos++;
247
+ modified = true;
248
+ break;
249
+ }
250
+ }
251
+ }
252
+ return modified;
253
+ }
254
+ /** Parse structured fields */
255
+ parseStructuredFields(data, offset) {
256
+ let pos = offset;
257
+ let modified = false;
258
+ while (pos + 2 < data.length) {
259
+ const sfLen = (data[pos] << 8) | data[pos + 1];
260
+ if (sfLen < 3 || pos + sfLen > data.length)
261
+ break;
262
+ // Skip the structured field (we may implement query reply later)
263
+ pos += sfLen;
264
+ modified = true;
265
+ }
266
+ return modified;
267
+ }
268
+ /** Advance cursor to the start of the next unprotected field */
269
+ advanceToNextUnprotected() {
270
+ const startAddr = this.screen.currentAddr;
271
+ let addr = (startAddr + 1) % this.screen.size;
272
+ while (addr !== startAddr) {
273
+ // Check if there's a field attribute at this position
274
+ if (this.screen.attrBuffer[addr] !== 0) {
275
+ const field = this.screen.fields.find(f => f.attrAddr === addr);
276
+ if (field && !this.screen.isProtected(field)) {
277
+ this.screen.currentAddr = field.startAddr;
278
+ return;
279
+ }
280
+ }
281
+ addr = (addr + 1) % this.screen.size;
282
+ }
283
+ }
284
+ }
@@ -0,0 +1,89 @@
1
+ export interface FieldDef3270 {
2
+ /** Buffer address where the field attribute byte is */
3
+ attrAddr: number;
4
+ /** Buffer address where the field data starts (attrAddr + 1) */
5
+ startAddr: number;
6
+ /** Field attribute byte */
7
+ attribute: number;
8
+ /** Extended attributes (highlight, color, etc.) */
9
+ extHighlight: number;
10
+ extColor: number;
11
+ /** Length of the field data (not including attribute byte) */
12
+ length: number;
13
+ /** Whether the MDT (Modified Data Tag) is set */
14
+ modified: boolean;
15
+ }
16
+ export declare class ScreenBuffer3270 {
17
+ rows: number;
18
+ cols: number;
19
+ /** Character buffer (EBCDIC decoded to UTF-8) */
20
+ buffer: string[];
21
+ /** Field attribute at each position (0 = no field attribute here) */
22
+ attrBuffer: number[];
23
+ /** Extended highlight per cell */
24
+ highlightBuffer: number[];
25
+ /** Extended color per cell */
26
+ colorBuffer: number[];
27
+ /** Field definitions */
28
+ fields: FieldDef3270[];
29
+ cursorAddr: number;
30
+ /** Current buffer address for write operations */
31
+ currentAddr: number;
32
+ constructor(rows?: 24, cols?: 80);
33
+ get size(): number;
34
+ get cursorRow(): number;
35
+ get cursorCol(): number;
36
+ addrToRowCol(addr: number): {
37
+ row: number;
38
+ col: number;
39
+ };
40
+ rowColToAddr(row: number, col: number): number;
41
+ /** Clear entire screen */
42
+ clear(): void;
43
+ /** Clear all unprotected fields */
44
+ clearUnprotected(): void;
45
+ /** Set character at buffer address */
46
+ setCharAt(addr: number, char: string): void;
47
+ /** Get character at buffer address */
48
+ getCharAt(addr: number): string;
49
+ /** Place a field attribute at an address */
50
+ setFieldAttribute(addr: number, attr: number): void;
51
+ /** Check if field is protected */
52
+ isProtected(field: FieldDef3270): boolean;
53
+ /** Check if field is numeric */
54
+ isNumeric(field: FieldDef3270): boolean;
55
+ /** Check if field is hidden (non-display) */
56
+ isHidden(field: FieldDef3270): boolean;
57
+ /** Check if field is intensified */
58
+ isIntensified(field: FieldDef3270): boolean;
59
+ /** Get the field containing the given address */
60
+ getFieldAt(addr: number): FieldDef3270 | null;
61
+ /** Get the field at cursor position */
62
+ getFieldAtCursor(): FieldDef3270 | null;
63
+ /** Get field value as string */
64
+ getFieldValue(field: FieldDef3270): string;
65
+ /**
66
+ * Rebuild field list from attribute bytes in the buffer.
67
+ * Called after processing a write command.
68
+ */
69
+ rebuildFields(): void;
70
+ /** Convert to protocol-agnostic ScreenData for the frontend */
71
+ toScreenData(): {
72
+ content: string;
73
+ cursor_row: number;
74
+ cursor_col: number;
75
+ rows: number;
76
+ cols: number;
77
+ fields: {
78
+ row: number;
79
+ col: number;
80
+ length: number;
81
+ is_input: boolean;
82
+ is_protected: boolean;
83
+ is_highlighted: true | undefined;
84
+ is_reverse: undefined;
85
+ }[];
86
+ screen_signature: string;
87
+ timestamp: string;
88
+ };
89
+ }
@@ -0,0 +1,207 @@
1
+ import { createHash } from 'crypto';
2
+ import { FA, SCREEN } from './constants.js';
3
+ export class ScreenBuffer3270 {
4
+ rows;
5
+ cols;
6
+ /** Character buffer (EBCDIC decoded to UTF-8) */
7
+ buffer;
8
+ /** Field attribute at each position (0 = no field attribute here) */
9
+ attrBuffer;
10
+ /** Extended highlight per cell */
11
+ highlightBuffer;
12
+ /** Extended color per cell */
13
+ colorBuffer;
14
+ /** Field definitions */
15
+ fields = [];
16
+ cursorAddr = 0;
17
+ /** Current buffer address for write operations */
18
+ currentAddr = 0;
19
+ constructor(rows = SCREEN.MODEL_2_ROWS, cols = SCREEN.MODEL_2_COLS) {
20
+ this.rows = rows;
21
+ this.cols = cols;
22
+ const size = rows * cols;
23
+ this.buffer = new Array(size).fill(' ');
24
+ this.attrBuffer = new Array(size).fill(0);
25
+ this.highlightBuffer = new Array(size).fill(0);
26
+ this.colorBuffer = new Array(size).fill(0);
27
+ }
28
+ get size() {
29
+ return this.rows * this.cols;
30
+ }
31
+ get cursorRow() {
32
+ return Math.floor(this.cursorAddr / this.cols);
33
+ }
34
+ get cursorCol() {
35
+ return this.cursorAddr % this.cols;
36
+ }
37
+ addrToRowCol(addr) {
38
+ return {
39
+ row: Math.floor(addr / this.cols),
40
+ col: addr % this.cols,
41
+ };
42
+ }
43
+ rowColToAddr(row, col) {
44
+ return row * this.cols + col;
45
+ }
46
+ /** Clear entire screen */
47
+ clear() {
48
+ this.buffer.fill(' ');
49
+ this.attrBuffer.fill(0);
50
+ this.highlightBuffer.fill(0);
51
+ this.colorBuffer.fill(0);
52
+ this.fields = [];
53
+ this.cursorAddr = 0;
54
+ this.currentAddr = 0;
55
+ }
56
+ /** Clear all unprotected fields */
57
+ clearUnprotected() {
58
+ for (const field of this.fields) {
59
+ if (!this.isProtected(field)) {
60
+ for (let i = 0; i < field.length; i++) {
61
+ const addr = (field.startAddr + i) % this.size;
62
+ this.buffer[addr] = ' ';
63
+ }
64
+ field.modified = false;
65
+ }
66
+ }
67
+ }
68
+ /** Set character at buffer address */
69
+ setCharAt(addr, char) {
70
+ const a = addr % this.size;
71
+ this.buffer[a] = char;
72
+ }
73
+ /** Get character at buffer address */
74
+ getCharAt(addr) {
75
+ return this.buffer[addr % this.size];
76
+ }
77
+ /** Place a field attribute at an address */
78
+ setFieldAttribute(addr, attr) {
79
+ const a = addr % this.size;
80
+ this.attrBuffer[a] = attr;
81
+ this.buffer[a] = ' '; // attribute byte displays as space
82
+ }
83
+ /** Check if field is protected */
84
+ isProtected(field) {
85
+ return (field.attribute & FA.PROTECTED) !== 0;
86
+ }
87
+ /** Check if field is numeric */
88
+ isNumeric(field) {
89
+ return (field.attribute & FA.NUMERIC) !== 0;
90
+ }
91
+ /** Check if field is hidden (non-display) */
92
+ isHidden(field) {
93
+ return (field.attribute & FA.DISPLAY_MASK) === FA.DISPLAY_HIDDEN;
94
+ }
95
+ /** Check if field is intensified */
96
+ isIntensified(field) {
97
+ return (field.attribute & FA.DISPLAY_MASK) === FA.DISPLAY_INTENSIFIED;
98
+ }
99
+ /** Get the field containing the given address */
100
+ getFieldAt(addr) {
101
+ for (const field of this.fields) {
102
+ const start = field.startAddr;
103
+ const end = (start + field.length) % this.size;
104
+ if (start <= end) {
105
+ if (addr >= start && addr < end)
106
+ return field;
107
+ }
108
+ else {
109
+ // Field wraps around screen
110
+ if (addr >= start || addr < end)
111
+ return field;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ /** Get the field at cursor position */
117
+ getFieldAtCursor() {
118
+ return this.getFieldAt(this.cursorAddr);
119
+ }
120
+ /** Get field value as string */
121
+ getFieldValue(field) {
122
+ let value = '';
123
+ for (let i = 0; i < field.length; i++) {
124
+ value += this.buffer[(field.startAddr + i) % this.size];
125
+ }
126
+ return value;
127
+ }
128
+ /**
129
+ * Rebuild field list from attribute bytes in the buffer.
130
+ * Called after processing a write command.
131
+ */
132
+ rebuildFields() {
133
+ this.fields = [];
134
+ const attrPositions = [];
135
+ // Find all field attribute positions
136
+ for (let i = 0; i < this.size; i++) {
137
+ if (this.attrBuffer[i] !== 0) {
138
+ attrPositions.push(i);
139
+ }
140
+ }
141
+ if (attrPositions.length === 0)
142
+ return;
143
+ // Build fields: each field goes from (attrPos + 1) to the next attrPos
144
+ for (let i = 0; i < attrPositions.length; i++) {
145
+ const attrAddr = attrPositions[i];
146
+ const startAddr = (attrAddr + 1) % this.size;
147
+ const nextAttrAddr = attrPositions[(i + 1) % attrPositions.length];
148
+ let length;
149
+ if (i + 1 < attrPositions.length) {
150
+ length = nextAttrAddr - startAddr;
151
+ if (length < 0)
152
+ length += this.size;
153
+ }
154
+ else if (attrPositions.length > 1) {
155
+ // Last field wraps to first attribute
156
+ length = attrPositions[0] - startAddr;
157
+ if (length < 0)
158
+ length += this.size;
159
+ }
160
+ else {
161
+ // Single field covers entire screen minus 1
162
+ length = this.size - 1;
163
+ }
164
+ this.fields.push({
165
+ attrAddr,
166
+ startAddr,
167
+ attribute: this.attrBuffer[attrAddr],
168
+ extHighlight: 0,
169
+ extColor: 0,
170
+ length,
171
+ modified: (this.attrBuffer[attrAddr] & FA.MDT) !== 0,
172
+ });
173
+ }
174
+ }
175
+ /** Convert to protocol-agnostic ScreenData for the frontend */
176
+ toScreenData() {
177
+ const lines = [];
178
+ for (let r = 0; r < this.rows; r++) {
179
+ const start = r * this.cols;
180
+ lines.push(this.buffer.slice(start, start + this.cols).join(''));
181
+ }
182
+ const content = lines.join('\n');
183
+ const fields = this.fields.map(f => {
184
+ const { row, col } = this.addrToRowCol(f.startAddr);
185
+ return {
186
+ row,
187
+ col,
188
+ length: f.length,
189
+ is_input: !this.isProtected(f),
190
+ is_protected: this.isProtected(f),
191
+ is_highlighted: this.isIntensified(f) || undefined,
192
+ is_reverse: undefined,
193
+ };
194
+ });
195
+ const hash = createHash('md5').update(content).digest('hex').substring(0, 12);
196
+ return {
197
+ content,
198
+ cursor_row: this.cursorRow,
199
+ cursor_col: this.cursorCol,
200
+ rows: this.rows,
201
+ cols: this.cols,
202
+ fields,
203
+ screen_signature: hash,
204
+ timestamp: new Date().toISOString(),
205
+ };
206
+ }
207
+ }
@@ -0,0 +1,41 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface ConnectionEvents {
3
+ connected: () => void;
4
+ disconnected: () => void;
5
+ data: (record: Buffer) => void;
6
+ error: (err: Error) => void;
7
+ }
8
+ /**
9
+ * Manages raw TCP socket to IBM i, handles Telnet negotiation,
10
+ * and extracts 5250 data records (delimited by IAC EOR).
11
+ */
12
+ export declare class TN5250Connection extends EventEmitter {
13
+ private socket;
14
+ private host;
15
+ private port;
16
+ private connected;
17
+ private recvBuffer;
18
+ private negotiationDone;
19
+ get isConnected(): boolean;
20
+ get remoteHost(): string;
21
+ get remotePort(): number;
22
+ connect(host: string, port: number): Promise<void>;
23
+ disconnect(): void;
24
+ /** Send raw bytes over the socket */
25
+ sendRaw(data: Buffer): void;
26
+ private cleanup;
27
+ private onData;
28
+ private processBuffer;
29
+ /** Find IAC SE sequence for subnegotiation end */
30
+ private findSubnegEnd;
31
+ /** Find IAC EOR (end of 5250 record) */
32
+ private findRecordEnd;
33
+ /** Remove IAC IAC escaping from data */
34
+ private unescapeIAC;
35
+ private handleNegotiation;
36
+ private handleSubnegotiation;
37
+ private sendTerminalType;
38
+ private sendEnviron;
39
+ private handleTN5250ESubneg;
40
+ private sendTelnet;
41
+ }