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.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/hp6530/connection.d.ts +51 -0
- package/dist/hp6530/connection.js +258 -0
- package/dist/hp6530/constants.d.ts +64 -0
- package/dist/hp6530/constants.js +135 -0
- package/dist/hp6530/encoder.d.ts +37 -0
- package/dist/hp6530/encoder.js +89 -0
- package/dist/hp6530/parser.d.ts +45 -0
- package/dist/hp6530/parser.js +255 -0
- package/dist/hp6530/screen.d.ts +104 -0
- package/dist/hp6530/screen.js +252 -0
- package/dist/mock/mock-routes.d.ts +2 -0
- package/dist/mock/mock-routes.js +231 -0
- package/dist/protocols/hp6530-handler.d.ts +29 -0
- package/dist/protocols/hp6530-handler.js +64 -0
- package/dist/protocols/index.d.ts +11 -0
- package/dist/protocols/index.js +27 -0
- package/dist/protocols/tn3270-handler.d.ts +26 -0
- package/dist/protocols/tn3270-handler.js +61 -0
- package/dist/protocols/tn5250-handler.d.ts +26 -0
- package/dist/protocols/tn5250-handler.js +62 -0
- package/dist/protocols/types.d.ts +59 -0
- package/dist/protocols/types.js +7 -0
- package/dist/protocols/vt-handler.d.ts +30 -0
- package/dist/protocols/vt-handler.js +67 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +141 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +34 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +88 -0
- package/dist/tn3270/connection.d.ts +31 -0
- package/dist/tn3270/connection.js +266 -0
- package/dist/tn3270/constants.d.ts +262 -0
- package/dist/tn3270/constants.js +261 -0
- package/dist/tn3270/encoder.d.ts +24 -0
- package/dist/tn3270/encoder.js +97 -0
- package/dist/tn3270/parser.d.ts +22 -0
- package/dist/tn3270/parser.js +284 -0
- package/dist/tn3270/screen.d.ts +89 -0
- package/dist/tn3270/screen.js +207 -0
- package/dist/tn5250/connection.d.ts +41 -0
- package/dist/tn5250/connection.js +254 -0
- package/dist/tn5250/constants.d.ts +128 -0
- package/dist/tn5250/constants.js +156 -0
- package/dist/tn5250/ebcdic.d.ts +10 -0
- package/dist/tn5250/ebcdic.js +89 -0
- package/dist/tn5250/encoder.d.ts +30 -0
- package/dist/tn5250/encoder.js +121 -0
- package/dist/tn5250/parser.d.ts +33 -0
- package/dist/tn5250/parser.js +412 -0
- package/dist/tn5250/screen.d.ts +80 -0
- package/dist/tn5250/screen.js +155 -0
- package/dist/vt/connection.d.ts +45 -0
- package/dist/vt/connection.js +229 -0
- package/dist/vt/constants.d.ts +97 -0
- package/dist/vt/constants.js +163 -0
- package/dist/vt/encoder.d.ts +30 -0
- package/dist/vt/encoder.js +55 -0
- package/dist/vt/parser.d.ts +36 -0
- package/dist/vt/parser.js +534 -0
- package/dist/vt/screen.d.ts +101 -0
- package/dist/vt/screen.js +424 -0
- package/dist/websocket.d.ts +6 -0
- package/dist/websocket.js +50 -0
- 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
|
+
}
|