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
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ const { values } = parseArgs({
4
+ options: {
5
+ mock: { type: 'boolean', default: false },
6
+ port: { type: 'string', default: '' },
7
+ help: { type: 'boolean', short: 'h', default: false },
8
+ },
9
+ });
10
+ if (values.help) {
11
+ console.log(`green-screen-proxy — WebSocket/REST proxy for legacy terminal connections
12
+
13
+ Usage: green-screen-proxy [options]
14
+
15
+ Options:
16
+ --mock Run with mock data (no real host connection needed)
17
+ --port NUM Port to listen on (default: 3001, or PORT env var)
18
+ -h, --help Show this help message
19
+
20
+ Examples:
21
+ npx green-screen-proxy # Start proxy on port 3001
22
+ npx green-screen-proxy --mock # Start with mock screens
23
+ npx green-screen-proxy --port 8080 # Start on port 8080`);
24
+ process.exit(0);
25
+ }
26
+ if (values.port) {
27
+ process.env.PORT = values.port;
28
+ }
29
+ if (values.mock) {
30
+ process.argv.push('--mock');
31
+ }
32
+ await import('./server.js');
@@ -0,0 +1,51 @@
1
+ import { EventEmitter } from 'events';
2
+ export interface ConnectionEvents {
3
+ connected: () => void;
4
+ disconnected: () => void;
5
+ data: (data: Buffer) => void;
6
+ error: (err: Error) => void;
7
+ }
8
+ /**
9
+ * Manages a TCP/Telnet connection to an HP NonStop (Tandem) system.
10
+ *
11
+ * HP 6530 terminals use standard Telnet negotiation but differ from
12
+ * TN5250/TN3270 in that data is stream-based — there are no IAC EOR
13
+ * delimited records. Instead, the host sends escape sequences inline
14
+ * and block-mode data transfer uses DC1 (XON) / DC3 (XOFF) for flow
15
+ * control.
16
+ */
17
+ export declare class HP6530Connection extends EventEmitter {
18
+ private socket;
19
+ private host;
20
+ private port;
21
+ private _connected;
22
+ private recvBuffer;
23
+ /** Whether we are in XOFF state (host asked us to pause sending) */
24
+ private xoff;
25
+ /** Queue of outbound data waiting for XON */
26
+ private sendQueue;
27
+ get isConnected(): boolean;
28
+ get remoteHost(): string;
29
+ get remotePort(): number;
30
+ connect(host: string, port: number): Promise<void>;
31
+ disconnect(): void;
32
+ /** Send raw bytes over the socket, respecting XON/XOFF flow control */
33
+ sendRaw(data: Buffer): void;
34
+ private cleanup;
35
+ private onData;
36
+ /**
37
+ * Process the receive buffer. We need to:
38
+ * 1. Handle Telnet IAC sequences (negotiation)
39
+ * 2. Handle DC1/DC3 flow control characters
40
+ * 3. Pass remaining data (escape sequences + printable chars) to the parser
41
+ */
42
+ private processBuffer;
43
+ private emitData;
44
+ private flushSendQueue;
45
+ /** Find IAC SE sequence for subnegotiation end */
46
+ private findSubnegEnd;
47
+ private handleNegotiation;
48
+ private handleSubnegotiation;
49
+ private sendTerminalType;
50
+ private sendTelnet;
51
+ }
@@ -0,0 +1,258 @@
1
+ import * as net from 'net';
2
+ import { EventEmitter } from 'events';
3
+ import { TELNET } from '../tn5250/constants.js';
4
+ import { TERMINAL_TYPE, CTRL } from './constants.js';
5
+ /**
6
+ * Manages a TCP/Telnet connection to an HP NonStop (Tandem) system.
7
+ *
8
+ * HP 6530 terminals use standard Telnet negotiation but differ from
9
+ * TN5250/TN3270 in that data is stream-based — there are no IAC EOR
10
+ * delimited records. Instead, the host sends escape sequences inline
11
+ * and block-mode data transfer uses DC1 (XON) / DC3 (XOFF) for flow
12
+ * control.
13
+ */
14
+ export class HP6530Connection extends EventEmitter {
15
+ socket = null;
16
+ host = '';
17
+ port = 23;
18
+ _connected = false;
19
+ recvBuffer = Buffer.alloc(0);
20
+ /** Whether we are in XOFF state (host asked us to pause sending) */
21
+ xoff = false;
22
+ /** Queue of outbound data waiting for XON */
23
+ sendQueue = [];
24
+ get isConnected() {
25
+ return this._connected;
26
+ }
27
+ get remoteHost() {
28
+ return this.host;
29
+ }
30
+ get remotePort() {
31
+ return this.port;
32
+ }
33
+ connect(host, port) {
34
+ return new Promise((resolve, reject) => {
35
+ if (this.socket) {
36
+ this.disconnect();
37
+ }
38
+ this.host = host;
39
+ this.port = port;
40
+ this.recvBuffer = Buffer.alloc(0);
41
+ this.xoff = false;
42
+ this.sendQueue = [];
43
+ this.socket = new net.Socket();
44
+ this.socket.setTimeout(30000);
45
+ const onError = (err) => {
46
+ this.cleanup();
47
+ reject(err);
48
+ };
49
+ this.socket.once('error', onError);
50
+ this.socket.connect(port, host, () => {
51
+ this._connected = true;
52
+ this.socket.removeListener('error', onError);
53
+ this.socket.on('error', (err) => {
54
+ this.emit('error', err);
55
+ this.cleanup();
56
+ });
57
+ this.socket.on('close', () => {
58
+ this.cleanup();
59
+ this.emit('disconnected');
60
+ });
61
+ this.socket.on('timeout', () => {
62
+ this.emit('error', new Error('Connection timeout'));
63
+ });
64
+ this.socket.on('data', (data) => this.onData(data));
65
+ this.emit('connected');
66
+ resolve();
67
+ });
68
+ });
69
+ }
70
+ disconnect() {
71
+ if (this.socket) {
72
+ this.socket.destroy();
73
+ this.cleanup();
74
+ }
75
+ }
76
+ /** Send raw bytes over the socket, respecting XON/XOFF flow control */
77
+ sendRaw(data) {
78
+ if (!this.socket || !this._connected)
79
+ return;
80
+ if (this.xoff) {
81
+ // Queue data until we receive XON
82
+ this.sendQueue.push(data);
83
+ return;
84
+ }
85
+ this.socket.write(data);
86
+ }
87
+ cleanup() {
88
+ this._connected = false;
89
+ this.xoff = false;
90
+ this.sendQueue = [];
91
+ if (this.socket) {
92
+ this.socket.removeAllListeners();
93
+ this.socket.destroy();
94
+ this.socket = null;
95
+ }
96
+ }
97
+ onData(data) {
98
+ this.recvBuffer = Buffer.concat([this.recvBuffer, data]);
99
+ this.processBuffer();
100
+ }
101
+ /**
102
+ * Process the receive buffer. We need to:
103
+ * 1. Handle Telnet IAC sequences (negotiation)
104
+ * 2. Handle DC1/DC3 flow control characters
105
+ * 3. Pass remaining data (escape sequences + printable chars) to the parser
106
+ */
107
+ processBuffer() {
108
+ while (this.recvBuffer.length > 0) {
109
+ const byte = this.recvBuffer[0];
110
+ // --- Telnet IAC handling ---
111
+ if (byte === TELNET.IAC) {
112
+ if (this.recvBuffer.length < 2)
113
+ return; // need more data
114
+ const cmd = this.recvBuffer[1];
115
+ // IAC IAC = escaped 0xFF literal
116
+ if (cmd === TELNET.IAC) {
117
+ // Emit as data
118
+ this.emitData(Buffer.from([0xFF]));
119
+ this.recvBuffer = this.recvBuffer.subarray(2);
120
+ continue;
121
+ }
122
+ // Subnegotiation: IAC SB ... IAC SE
123
+ if (cmd === TELNET.SB) {
124
+ const seIdx = this.findSubnegEnd();
125
+ if (seIdx === -1)
126
+ return; // wait for more data
127
+ const subData = this.recvBuffer.subarray(2, seIdx);
128
+ this.recvBuffer = this.recvBuffer.subarray(seIdx + 2);
129
+ this.handleSubnegotiation(subData);
130
+ continue;
131
+ }
132
+ // DO / DONT / WILL / WONT: 3 bytes
133
+ if (cmd === TELNET.DO || cmd === TELNET.DONT ||
134
+ cmd === TELNET.WILL || cmd === TELNET.WONT) {
135
+ if (this.recvBuffer.length < 3)
136
+ return;
137
+ const option = this.recvBuffer[2];
138
+ this.recvBuffer = this.recvBuffer.subarray(3);
139
+ this.handleNegotiation(cmd, option);
140
+ continue;
141
+ }
142
+ // Other IAC command (2 bytes), skip
143
+ this.recvBuffer = this.recvBuffer.subarray(2);
144
+ continue;
145
+ }
146
+ // --- XON/XOFF flow control ---
147
+ if (byte === CTRL.DC3) {
148
+ // XOFF: host asks us to stop sending
149
+ this.xoff = true;
150
+ this.recvBuffer = this.recvBuffer.subarray(1);
151
+ continue;
152
+ }
153
+ if (byte === CTRL.DC1) {
154
+ // XON: host allows us to resume sending
155
+ this.xoff = false;
156
+ this.recvBuffer = this.recvBuffer.subarray(1);
157
+ this.flushSendQueue();
158
+ continue;
159
+ }
160
+ // --- Regular data (escape sequences + printable characters) ---
161
+ // Find the next IAC or flow control character to know how much data to emit
162
+ let end = 1;
163
+ while (end < this.recvBuffer.length) {
164
+ const b = this.recvBuffer[end];
165
+ if (b === TELNET.IAC || b === CTRL.DC1 || b === CTRL.DC3)
166
+ break;
167
+ end++;
168
+ }
169
+ const chunk = this.recvBuffer.subarray(0, end);
170
+ this.recvBuffer = this.recvBuffer.subarray(end);
171
+ this.emitData(Buffer.from(chunk));
172
+ }
173
+ }
174
+ emitData(data) {
175
+ if (data.length > 0) {
176
+ this.emit('data', data);
177
+ }
178
+ }
179
+ flushSendQueue() {
180
+ if (!this.socket || !this._connected)
181
+ return;
182
+ while (this.sendQueue.length > 0 && !this.xoff) {
183
+ const queued = this.sendQueue.shift();
184
+ this.socket.write(queued);
185
+ }
186
+ }
187
+ /** Find IAC SE sequence for subnegotiation end */
188
+ findSubnegEnd() {
189
+ for (let i = 2; i < this.recvBuffer.length - 1; i++) {
190
+ if (this.recvBuffer[i] === TELNET.IAC && this.recvBuffer[i + 1] === TELNET.SE) {
191
+ return i;
192
+ }
193
+ }
194
+ return -1;
195
+ }
196
+ handleNegotiation(cmd, option) {
197
+ switch (cmd) {
198
+ case TELNET.DO:
199
+ // Server asks us to enable an option
200
+ if (option === TELNET.OPT_TTYPE ||
201
+ option === TELNET.OPT_BINARY ||
202
+ option === 0x03 /* SGA */ ||
203
+ option === 0x01 /* ECHO */) {
204
+ this.sendTelnet(TELNET.WILL, option);
205
+ }
206
+ else {
207
+ this.sendTelnet(TELNET.WONT, option);
208
+ }
209
+ break;
210
+ case TELNET.WILL:
211
+ // Server offers an option
212
+ if (option === TELNET.OPT_BINARY ||
213
+ option === 0x03 /* SGA */ ||
214
+ option === 0x01 /* ECHO */) {
215
+ this.sendTelnet(TELNET.DO, option);
216
+ }
217
+ else {
218
+ this.sendTelnet(TELNET.DONT, option);
219
+ }
220
+ break;
221
+ case TELNET.DONT:
222
+ this.sendTelnet(TELNET.WONT, option);
223
+ break;
224
+ case TELNET.WONT:
225
+ this.sendTelnet(TELNET.DONT, option);
226
+ break;
227
+ }
228
+ }
229
+ handleSubnegotiation(data) {
230
+ if (data.length === 0)
231
+ return;
232
+ const option = data[0];
233
+ if (option === TELNET.OPT_TTYPE && data.length >= 2 && data[1] === TELNET.TTYPE_SEND) {
234
+ this.sendTerminalType();
235
+ }
236
+ }
237
+ sendTerminalType() {
238
+ const typeStr = TERMINAL_TYPE;
239
+ const buf = Buffer.alloc(4 + typeStr.length + 2);
240
+ let i = 0;
241
+ buf[i++] = TELNET.IAC;
242
+ buf[i++] = TELNET.SB;
243
+ buf[i++] = TELNET.OPT_TTYPE;
244
+ buf[i++] = TELNET.TTYPE_IS;
245
+ for (let j = 0; j < typeStr.length; j++) {
246
+ buf[i++] = typeStr.charCodeAt(j);
247
+ }
248
+ buf[i++] = TELNET.IAC;
249
+ buf[i++] = TELNET.SE;
250
+ this.sendRaw(buf);
251
+ }
252
+ sendTelnet(cmd, option) {
253
+ if (this.socket && this._connected) {
254
+ // Bypass flow control for Telnet negotiation
255
+ this.socket.write(Buffer.from([TELNET.IAC, cmd, option]));
256
+ }
257
+ }
258
+ }
@@ -0,0 +1,64 @@
1
+ export { TELNET } from '../tn5250/constants.js';
2
+ /** Terminal type string for Telnet TTYPE negotiation */
3
+ export declare const TERMINAL_TYPE = "T6530";
4
+ /** Alternative terminal type */
5
+ export declare const TERMINAL_TYPE_ALT = "HP700/96";
6
+ /** Screen dimensions */
7
+ export declare const SCREEN: {
8
+ readonly ROWS: 24;
9
+ readonly COLS: 80;
10
+ };
11
+ export declare const CTRL: {
12
+ readonly NUL: 0;
13
+ readonly SOH: 1;
14
+ readonly STX: 2;
15
+ readonly ETX: 3;
16
+ readonly EOT: 4;
17
+ readonly ENQ: 5;
18
+ readonly ACK: 6;
19
+ readonly BEL: 7;
20
+ readonly BS: 8;
21
+ readonly HT: 9;
22
+ readonly LF: 10;
23
+ readonly VT: 11;
24
+ readonly FF: 12;
25
+ readonly CR: 13;
26
+ readonly SO: 14;
27
+ readonly SI: 15;
28
+ readonly DC1: 17;
29
+ readonly DC3: 19;
30
+ readonly ESC: 27;
31
+ readonly DEL: 127;
32
+ };
33
+ /** Direct cursor address: ESC [ row ; col H (VT-style CUP) */
34
+ export declare const ESC_CUP_PREFIX: Buffer<ArrayBuffer>;
35
+ export declare const ESC_CUP_SEP = 59;
36
+ export declare const ESC_CUP_SUFFIX = 72;
37
+ /** Clear to end of display */
38
+ export declare const ESC_CLEAR_EOS: Buffer<ArrayBuffer>;
39
+ /** Clear to end of line */
40
+ export declare const ESC_CLEAR_EOL: Buffer<ArrayBuffer>;
41
+ /** Start protected field */
42
+ export declare const ESC_PROTECT_START: Buffer<ArrayBuffer>;
43
+ /** End protected field (start unprotected) */
44
+ export declare const ESC_PROTECT_END: Buffer<ArrayBuffer>;
45
+ export declare const ATTR: {
46
+ readonly NORMAL: 64;
47
+ readonly HALF_BRIGHT: 66;
48
+ readonly UNDERLINE: 68;
49
+ readonly BLINK: 72;
50
+ readonly INVERSE: 74;
51
+ readonly UNDERLINE_INVERSE: 76;
52
+ };
53
+ /** Attribute escape sequence prefix: ESC & d */
54
+ export declare const ESC_ATTR_PREFIX: Buffer<ArrayBuffer>;
55
+ /** Map of attribute code to human-readable name */
56
+ export declare const ATTR_NAMES: Record<number, string>;
57
+ /** F1–F8: ESC p through ESC w (0x70–0x77) */
58
+ /** F9–F16: ESC ` through ESC g (0x60–0x67) — varies by model */
59
+ /** SF1–SF16: shifted versions — ESC P through ESC W (0x50–0x57) for SF1-SF8,
60
+ * ESC H through ESC O (0x48–0x4F) for SF9-SF16 — varies by model */
61
+ export declare const FKEY_SEQUENCES: Record<string, Buffer>;
62
+ export declare const ARROW_SEQUENCES: Record<string, Buffer>;
63
+ export declare const KEY_TO_SEQUENCE: Record<string, Buffer>;
64
+ export declare const FKEY_BYTE_TO_NAME: Record<number, string>;
@@ -0,0 +1,135 @@
1
+ // === Telnet Constants (reuse from TN5250) ===
2
+ export { TELNET } from '../tn5250/constants.js';
3
+ // === HP 6530 Terminal Constants ===
4
+ /** Terminal type string for Telnet TTYPE negotiation */
5
+ export const TERMINAL_TYPE = 'T6530';
6
+ /** Alternative terminal type */
7
+ export const TERMINAL_TYPE_ALT = 'HP700/96';
8
+ /** Screen dimensions */
9
+ export const SCREEN = {
10
+ ROWS: 24,
11
+ COLS: 80,
12
+ };
13
+ // === Control Characters ===
14
+ export const CTRL = {
15
+ NUL: 0x00,
16
+ SOH: 0x01, // Start of header
17
+ STX: 0x02, // Start of text
18
+ ETX: 0x03, // End of text
19
+ EOT: 0x04, // End of transmission
20
+ ENQ: 0x05, // Enquiry
21
+ ACK: 0x06, // Acknowledge
22
+ BEL: 0x07, // Bell
23
+ BS: 0x08, // Backspace
24
+ HT: 0x09, // Horizontal tab
25
+ LF: 0x0A, // Line feed
26
+ VT: 0x0B, // Vertical tab
27
+ FF: 0x0C, // Form feed
28
+ CR: 0x0D, // Carriage return
29
+ SO: 0x0E, // Shift out
30
+ SI: 0x0F, // Shift in
31
+ DC1: 0x11, // XON — Device control 1 (resume transmission)
32
+ DC3: 0x13, // XOFF — Device control 3 (pause transmission)
33
+ ESC: 0x1B, // Escape
34
+ DEL: 0x7F, // Delete
35
+ };
36
+ // === 6530 Escape Sequences ===
37
+ // These are the byte sequences sent from the host to control the terminal.
38
+ /** Direct cursor address: ESC [ row ; col H (VT-style CUP) */
39
+ export const ESC_CUP_PREFIX = Buffer.from([0x1B, 0x5B]); // ESC [
40
+ export const ESC_CUP_SEP = 0x3B; // ';'
41
+ export const ESC_CUP_SUFFIX = 0x48; // 'H'
42
+ /** Clear to end of display */
43
+ export const ESC_CLEAR_EOS = Buffer.from([0x1B, 0x4A]); // ESC J
44
+ /** Clear to end of line */
45
+ export const ESC_CLEAR_EOL = Buffer.from([0x1B, 0x4B]); // ESC K
46
+ /** Start protected field */
47
+ export const ESC_PROTECT_START = Buffer.from([0x1B, 0x29]); // ESC )
48
+ /** End protected field (start unprotected) */
49
+ export const ESC_PROTECT_END = Buffer.from([0x1B, 0x28]); // ESC (
50
+ // === Display Attribute Escape Sequences ===
51
+ // Format: ESC & d <code>
52
+ export const ATTR = {
53
+ NORMAL: 0x40, // '@' — Normal display / reset attributes
54
+ HALF_BRIGHT: 0x42, // 'B' — Dim
55
+ UNDERLINE: 0x44, // 'D' — Underline
56
+ BLINK: 0x48, // 'H' — Blink
57
+ INVERSE: 0x4A, // 'J' — Inverse video
58
+ UNDERLINE_INVERSE: 0x4C, // 'L' — Underline + inverse
59
+ };
60
+ /** Attribute escape sequence prefix: ESC & d */
61
+ export const ESC_ATTR_PREFIX = Buffer.from([0x1B, 0x26, 0x64]); // ESC & d
62
+ /** Map of attribute code to human-readable name */
63
+ export const ATTR_NAMES = {
64
+ [ATTR.NORMAL]: 'normal',
65
+ [ATTR.HALF_BRIGHT]: 'half_bright',
66
+ [ATTR.UNDERLINE]: 'underline',
67
+ [ATTR.BLINK]: 'blink',
68
+ [ATTR.INVERSE]: 'inverse',
69
+ [ATTR.UNDERLINE_INVERSE]: 'underline_inverse',
70
+ };
71
+ // === Function Key Escape Sequences ===
72
+ // Sequences sent from the terminal to the host when function keys are pressed.
73
+ /** F1–F8: ESC p through ESC w (0x70–0x77) */
74
+ /** F9–F16: ESC ` through ESC g (0x60–0x67) — varies by model */
75
+ /** SF1–SF16: shifted versions — ESC P through ESC W (0x50–0x57) for SF1-SF8,
76
+ * ESC H through ESC O (0x48–0x4F) for SF9-SF16 — varies by model */
77
+ export const FKEY_SEQUENCES = {
78
+ // F1–F8: ESC p through ESC w
79
+ F1: Buffer.from([0x1B, 0x70]), // ESC p
80
+ F2: Buffer.from([0x1B, 0x71]), // ESC q
81
+ F3: Buffer.from([0x1B, 0x72]), // ESC r
82
+ F4: Buffer.from([0x1B, 0x73]), // ESC s
83
+ F5: Buffer.from([0x1B, 0x74]), // ESC t
84
+ F6: Buffer.from([0x1B, 0x75]), // ESC u
85
+ F7: Buffer.from([0x1B, 0x76]), // ESC v
86
+ F8: Buffer.from([0x1B, 0x77]), // ESC w
87
+ // F9–F16: ESC ` through ESC g
88
+ F9: Buffer.from([0x1B, 0x60]), // ESC `
89
+ F10: Buffer.from([0x1B, 0x61]), // ESC a
90
+ F11: Buffer.from([0x1B, 0x62]), // ESC b
91
+ F12: Buffer.from([0x1B, 0x63]), // ESC c
92
+ F13: Buffer.from([0x1B, 0x64]), // ESC d
93
+ F14: Buffer.from([0x1B, 0x65]), // ESC e
94
+ F15: Buffer.from([0x1B, 0x66]), // ESC f
95
+ F16: Buffer.from([0x1B, 0x67]), // ESC g
96
+ // SF1–SF8 (shifted): ESC P through ESC W
97
+ SF1: Buffer.from([0x1B, 0x50]), // ESC P
98
+ SF2: Buffer.from([0x1B, 0x51]), // ESC Q
99
+ SF3: Buffer.from([0x1B, 0x52]), // ESC R
100
+ SF4: Buffer.from([0x1B, 0x53]), // ESC S
101
+ SF5: Buffer.from([0x1B, 0x54]), // ESC T
102
+ SF6: Buffer.from([0x1B, 0x55]), // ESC U
103
+ SF7: Buffer.from([0x1B, 0x56]), // ESC V
104
+ SF8: Buffer.from([0x1B, 0x57]), // ESC W
105
+ // SF9–SF16 (shifted): ESC H through ESC O
106
+ SF9: Buffer.from([0x1B, 0x48]), // ESC H
107
+ SF10: Buffer.from([0x1B, 0x49]), // ESC I
108
+ SF11: Buffer.from([0x1B, 0x4A]), // ESC J (note: conflicts with clear EOS in host→terminal direction)
109
+ SF12: Buffer.from([0x1B, 0x4B]), // ESC K
110
+ SF13: Buffer.from([0x1B, 0x4C]), // ESC L
111
+ SF14: Buffer.from([0x1B, 0x4D]), // ESC M
112
+ SF15: Buffer.from([0x1B, 0x4E]), // ESC N
113
+ SF16: Buffer.from([0x1B, 0x4F]), // ESC O
114
+ };
115
+ // === Arrow Key Sequences (terminal → host) ===
116
+ export const ARROW_SEQUENCES = {
117
+ UP: Buffer.from([0x1B, 0x41]), // ESC A
118
+ DOWN: Buffer.from([0x1B, 0x42]), // ESC B
119
+ RIGHT: Buffer.from([0x1B, 0x43]), // ESC C
120
+ LEFT: Buffer.from([0x1B, 0x44]), // ESC D
121
+ };
122
+ // === Combined key-to-sequence map ===
123
+ export const KEY_TO_SEQUENCE = {
124
+ ...FKEY_SEQUENCES,
125
+ ...ARROW_SEQUENCES,
126
+ ENTER: Buffer.from([CTRL.CR]),
127
+ TAB: Buffer.from([CTRL.HT]),
128
+ BACKSPACE: Buffer.from([CTRL.BS]),
129
+ DELETE: Buffer.from([CTRL.DEL]),
130
+ };
131
+ // === Reverse lookup: sequence second byte → function key name (for F-keys only) ===
132
+ export const FKEY_BYTE_TO_NAME = {};
133
+ for (const [name, seq] of Object.entries(FKEY_SEQUENCES)) {
134
+ FKEY_BYTE_TO_NAME[seq[1]] = name;
135
+ }
@@ -0,0 +1,37 @@
1
+ import { HP6530Screen } from './screen.js';
2
+ /**
3
+ * Encodes client input into HP 6530 wire-format data for sending
4
+ * to an HP NonStop (Tandem) host.
5
+ *
6
+ * In block mode, the terminal collects all modified unprotected field
7
+ * data and sends it as a single transmission when an action key
8
+ * (ENTER, function key) is pressed.
9
+ */
10
+ export declare class HP6530Encoder {
11
+ private screen;
12
+ constructor(screen: HP6530Screen);
13
+ /**
14
+ * Build a block-mode response for a key press.
15
+ *
16
+ * For action keys (ENTER, function keys), we:
17
+ * 1. Send the function key escape sequence
18
+ * 2. Preceded by all modified unprotected field data
19
+ *
20
+ * The field data format for block-mode transmission:
21
+ * DC1 (XON) signals start of block
22
+ * For each modified field: position + data
23
+ * Followed by the key sequence
24
+ * CR signals end of input
25
+ *
26
+ * Returns a Buffer ready to send, or null if the key is unknown.
27
+ */
28
+ buildKeyResponse(keyName: string): Buffer | null;
29
+ /**
30
+ * Insert text at the current cursor position in the current
31
+ * unprotected field. Updates the screen buffer and marks the
32
+ * field as modified.
33
+ *
34
+ * Returns true if text was successfully inserted.
35
+ */
36
+ insertText(text: string): boolean;
37
+ }
@@ -0,0 +1,89 @@
1
+ import { KEY_TO_SEQUENCE, CTRL } from './constants.js';
2
+ /**
3
+ * Encodes client input into HP 6530 wire-format data for sending
4
+ * to an HP NonStop (Tandem) host.
5
+ *
6
+ * In block mode, the terminal collects all modified unprotected field
7
+ * data and sends it as a single transmission when an action key
8
+ * (ENTER, function key) is pressed.
9
+ */
10
+ export class HP6530Encoder {
11
+ screen;
12
+ constructor(screen) {
13
+ this.screen = screen;
14
+ }
15
+ /**
16
+ * Build a block-mode response for a key press.
17
+ *
18
+ * For action keys (ENTER, function keys), we:
19
+ * 1. Send the function key escape sequence
20
+ * 2. Preceded by all modified unprotected field data
21
+ *
22
+ * The field data format for block-mode transmission:
23
+ * DC1 (XON) signals start of block
24
+ * For each modified field: position + data
25
+ * Followed by the key sequence
26
+ * CR signals end of input
27
+ *
28
+ * Returns a Buffer ready to send, or null if the key is unknown.
29
+ */
30
+ buildKeyResponse(keyName) {
31
+ const keySeq = KEY_TO_SEQUENCE[keyName];
32
+ if (!keySeq)
33
+ return null;
34
+ const parts = [];
35
+ // For block-mode action keys (ENTER, function keys), collect field data
36
+ const isActionKey = keyName === 'ENTER' ||
37
+ keyName.startsWith('F') ||
38
+ keyName.startsWith('SF');
39
+ if (isActionKey && this.screen.fields.length > 0) {
40
+ // Collect all modified unprotected field data
41
+ for (const field of this.screen.fields) {
42
+ if (field.isProtected || !field.modified)
43
+ continue;
44
+ const value = this.screen.getFieldValue(field);
45
+ // Trim trailing spaces
46
+ const trimmed = value.replace(/\s+$/, '');
47
+ if (trimmed.length > 0) {
48
+ parts.push(Buffer.from(trimmed, 'ascii'));
49
+ }
50
+ // Field separator: HT between fields
51
+ parts.push(Buffer.from([CTRL.HT]));
52
+ }
53
+ }
54
+ // Append the key sequence itself
55
+ parts.push(keySeq);
56
+ // For ENTER in block mode, append CR if not already the key sequence
57
+ if (keyName !== 'ENTER' && isActionKey) {
58
+ parts.push(Buffer.from([CTRL.CR]));
59
+ }
60
+ return Buffer.concat(parts);
61
+ }
62
+ /**
63
+ * Insert text at the current cursor position in the current
64
+ * unprotected field. Updates the screen buffer and marks the
65
+ * field as modified.
66
+ *
67
+ * Returns true if text was successfully inserted.
68
+ */
69
+ insertText(text) {
70
+ const field = this.screen.getFieldAtCursor();
71
+ if (!field || field.isProtected)
72
+ return false;
73
+ const fieldStart = this.screen.offset(field.row, field.col);
74
+ let cursorOffset = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
75
+ const fieldEnd = fieldStart + field.length;
76
+ for (const ch of text) {
77
+ if (cursorOffset >= fieldEnd)
78
+ break;
79
+ this.screen.buffer[cursorOffset] = ch;
80
+ cursorOffset++;
81
+ }
82
+ // Update cursor position
83
+ const newPos = this.screen.toRowCol(Math.min(cursorOffset, fieldEnd - 1));
84
+ this.screen.cursorRow = newPos.row;
85
+ this.screen.cursorCol = newPos.col;
86
+ field.modified = true;
87
+ return true;
88
+ }
89
+ }