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,45 @@
|
|
|
1
|
+
import { HP6530Screen } from './screen.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses HP 6530 escape sequences from a data stream and applies
|
|
4
|
+
* them to the screen buffer.
|
|
5
|
+
*
|
|
6
|
+
* The HP 6530 uses an escape-sequence protocol that is somewhat
|
|
7
|
+
* similar to VT terminals but with HP-specific extensions for
|
|
8
|
+
* block-mode operation, protected fields, and display attributes.
|
|
9
|
+
*/
|
|
10
|
+
export declare class HP6530Parser {
|
|
11
|
+
private screen;
|
|
12
|
+
private state;
|
|
13
|
+
/** Accumulated numeric parameter for CSI sequences */
|
|
14
|
+
private csiParams;
|
|
15
|
+
constructor(screen: HP6530Screen);
|
|
16
|
+
/**
|
|
17
|
+
* Parse a chunk of data from the host.
|
|
18
|
+
* Returns true if the screen was modified.
|
|
19
|
+
*/
|
|
20
|
+
parse(data: Buffer): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Process a single byte through the state machine.
|
|
23
|
+
* Returns true if the screen was modified.
|
|
24
|
+
*/
|
|
25
|
+
private processByte;
|
|
26
|
+
/** Process a byte in NORMAL state */
|
|
27
|
+
private processNormal;
|
|
28
|
+
/** Process a byte after ESC */
|
|
29
|
+
private processEsc;
|
|
30
|
+
/**
|
|
31
|
+
* Process bytes inside a CSI sequence: ESC [ param ; param H
|
|
32
|
+
* We accumulate digits and ';' until we see the final character.
|
|
33
|
+
*/
|
|
34
|
+
private processCsi;
|
|
35
|
+
/** Process a byte after ESC & */
|
|
36
|
+
private processAmp;
|
|
37
|
+
/** Process the attribute code byte after ESC & d */
|
|
38
|
+
private processAmpD;
|
|
39
|
+
/** Handle CSI cursor position: ESC [ row ; col H */
|
|
40
|
+
private handleCursorPosition;
|
|
41
|
+
/** Handle CSI erase display: ESC [ n J */
|
|
42
|
+
private handleEraseDisplay;
|
|
43
|
+
/** Handle tab: move to next unprotected field or next tab stop */
|
|
44
|
+
private handleTab;
|
|
45
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { CTRL } from './constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses HP 6530 escape sequences from a data stream and applies
|
|
4
|
+
* them to the screen buffer.
|
|
5
|
+
*
|
|
6
|
+
* The HP 6530 uses an escape-sequence protocol that is somewhat
|
|
7
|
+
* similar to VT terminals but with HP-specific extensions for
|
|
8
|
+
* block-mode operation, protected fields, and display attributes.
|
|
9
|
+
*/
|
|
10
|
+
export class HP6530Parser {
|
|
11
|
+
screen;
|
|
12
|
+
state = 0 /* State.NORMAL */;
|
|
13
|
+
/** Accumulated numeric parameter for CSI sequences */
|
|
14
|
+
csiParams = '';
|
|
15
|
+
constructor(screen) {
|
|
16
|
+
this.screen = screen;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a chunk of data from the host.
|
|
20
|
+
* Returns true if the screen was modified.
|
|
21
|
+
*/
|
|
22
|
+
parse(data) {
|
|
23
|
+
let modified = false;
|
|
24
|
+
for (let i = 0; i < data.length; i++) {
|
|
25
|
+
const byte = data[i];
|
|
26
|
+
const changed = this.processByte(byte);
|
|
27
|
+
if (changed)
|
|
28
|
+
modified = true;
|
|
29
|
+
}
|
|
30
|
+
if (modified) {
|
|
31
|
+
this.screen.buildFields();
|
|
32
|
+
}
|
|
33
|
+
return modified;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Process a single byte through the state machine.
|
|
37
|
+
* Returns true if the screen was modified.
|
|
38
|
+
*/
|
|
39
|
+
processByte(byte) {
|
|
40
|
+
switch (this.state) {
|
|
41
|
+
case 0 /* State.NORMAL */:
|
|
42
|
+
return this.processNormal(byte);
|
|
43
|
+
case 1 /* State.ESC */:
|
|
44
|
+
return this.processEsc(byte);
|
|
45
|
+
case 2 /* State.CSI */:
|
|
46
|
+
return this.processCsi(byte);
|
|
47
|
+
case 3 /* State.AMP */:
|
|
48
|
+
return this.processAmp(byte);
|
|
49
|
+
case 4 /* State.AMP_D */:
|
|
50
|
+
return this.processAmpD(byte);
|
|
51
|
+
default:
|
|
52
|
+
this.state = 0 /* State.NORMAL */;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Process a byte in NORMAL state */
|
|
57
|
+
processNormal(byte) {
|
|
58
|
+
// ESC starts an escape sequence
|
|
59
|
+
if (byte === CTRL.ESC) {
|
|
60
|
+
this.state = 1 /* State.ESC */;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
// Control characters
|
|
64
|
+
switch (byte) {
|
|
65
|
+
case CTRL.CR:
|
|
66
|
+
this.screen.cursorCol = 0;
|
|
67
|
+
return true;
|
|
68
|
+
case CTRL.LF:
|
|
69
|
+
this.screen.cursorRow++;
|
|
70
|
+
if (this.screen.cursorRow >= this.screen.rows) {
|
|
71
|
+
this.screen.cursorRow = this.screen.rows - 1;
|
|
72
|
+
// In a real terminal this would scroll; for block mode we stay put
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
case CTRL.BS:
|
|
76
|
+
if (this.screen.cursorCol > 0) {
|
|
77
|
+
this.screen.cursorCol--;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
case CTRL.HT:
|
|
81
|
+
// Tab: advance to next tab stop (every 8 columns) or next unprotected field
|
|
82
|
+
this.handleTab();
|
|
83
|
+
return true;
|
|
84
|
+
case CTRL.BEL:
|
|
85
|
+
// Bell — no screen change
|
|
86
|
+
return false;
|
|
87
|
+
case CTRL.FF:
|
|
88
|
+
// Form feed — clear screen
|
|
89
|
+
this.screen.clear();
|
|
90
|
+
return true;
|
|
91
|
+
case CTRL.NUL:
|
|
92
|
+
// Ignore NULs
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// Printable ASCII (0x20–0x7E)
|
|
96
|
+
if (byte >= 0x20 && byte <= 0x7E) {
|
|
97
|
+
this.screen.putChar(String.fromCharCode(byte));
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
// High-bit characters (0x80–0xFF) — pass through as-is
|
|
101
|
+
if (byte >= 0x80) {
|
|
102
|
+
this.screen.putChar(String.fromCharCode(byte));
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
// Other control chars: ignore
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
/** Process a byte after ESC */
|
|
109
|
+
processEsc(byte) {
|
|
110
|
+
this.state = 0 /* State.NORMAL */;
|
|
111
|
+
switch (byte) {
|
|
112
|
+
// ESC [ — CSI (cursor addressing)
|
|
113
|
+
case 0x5B: // '['
|
|
114
|
+
this.state = 2 /* State.CSI */;
|
|
115
|
+
this.csiParams = '';
|
|
116
|
+
return false;
|
|
117
|
+
// ESC & — start of attribute sequence
|
|
118
|
+
case 0x26: // '&'
|
|
119
|
+
this.state = 3 /* State.AMP */;
|
|
120
|
+
return false;
|
|
121
|
+
// ESC J — Clear to end of display
|
|
122
|
+
case 0x4A: // 'J'
|
|
123
|
+
this.screen.clearToEndOfScreen();
|
|
124
|
+
return true;
|
|
125
|
+
// ESC K — Clear to end of line
|
|
126
|
+
case 0x4B: // 'K'
|
|
127
|
+
this.screen.clearToEndOfLine();
|
|
128
|
+
return true;
|
|
129
|
+
// ESC ) — Start protected field
|
|
130
|
+
case 0x29: // ')'
|
|
131
|
+
this.screen.startProtected();
|
|
132
|
+
return true;
|
|
133
|
+
// ESC ( — End protected field (start unprotected)
|
|
134
|
+
case 0x28: // '('
|
|
135
|
+
this.screen.endProtected();
|
|
136
|
+
return true;
|
|
137
|
+
default:
|
|
138
|
+
// Unknown ESC sequence — ignore
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Process bytes inside a CSI sequence: ESC [ param ; param H
|
|
144
|
+
* We accumulate digits and ';' until we see the final character.
|
|
145
|
+
*/
|
|
146
|
+
processCsi(byte) {
|
|
147
|
+
// Digits and semicolons are parameter characters
|
|
148
|
+
if ((byte >= 0x30 && byte <= 0x39) || byte === 0x3B) {
|
|
149
|
+
this.csiParams += String.fromCharCode(byte);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
// Final byte determines the command
|
|
153
|
+
this.state = 0 /* State.NORMAL */;
|
|
154
|
+
switch (byte) {
|
|
155
|
+
case 0x48: // 'H' — Cursor Position (CUP)
|
|
156
|
+
return this.handleCursorPosition();
|
|
157
|
+
case 0x4A: // 'J' — Erase in Display (ED)
|
|
158
|
+
return this.handleEraseDisplay();
|
|
159
|
+
case 0x4B: // 'K' — Erase in Line (EL)
|
|
160
|
+
this.screen.clearToEndOfLine();
|
|
161
|
+
return true;
|
|
162
|
+
default:
|
|
163
|
+
// Unknown CSI command
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Process a byte after ESC & */
|
|
168
|
+
processAmp(byte) {
|
|
169
|
+
if (byte === 0x64) { // 'd'
|
|
170
|
+
this.state = 4 /* State.AMP_D */;
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
// Unknown ESC & X sequence
|
|
174
|
+
this.state = 0 /* State.NORMAL */;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
/** Process the attribute code byte after ESC & d */
|
|
178
|
+
processAmpD(byte) {
|
|
179
|
+
this.state = 0 /* State.NORMAL */;
|
|
180
|
+
this.screen.setAttrFromCode(byte);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
/** Handle CSI cursor position: ESC [ row ; col H */
|
|
184
|
+
handleCursorPosition() {
|
|
185
|
+
const parts = this.csiParams.split(';');
|
|
186
|
+
// Parameters are 1-based; default to 1 if missing
|
|
187
|
+
const row = (parts[0] ? parseInt(parts[0], 10) : 1) - 1;
|
|
188
|
+
const col = (parts[1] ? parseInt(parts[1], 10) : 1) - 1;
|
|
189
|
+
this.screen.setCursor(row, col);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/** Handle CSI erase display: ESC [ n J */
|
|
193
|
+
handleEraseDisplay() {
|
|
194
|
+
const param = this.csiParams ? parseInt(this.csiParams, 10) : 0;
|
|
195
|
+
switch (param) {
|
|
196
|
+
case 0:
|
|
197
|
+
// Clear from cursor to end of screen
|
|
198
|
+
this.screen.clearToEndOfScreen();
|
|
199
|
+
return true;
|
|
200
|
+
case 1: {
|
|
201
|
+
// Clear from start of screen to cursor
|
|
202
|
+
const end = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol) + 1;
|
|
203
|
+
for (let i = 0; i < end && i < this.screen.size; i++) {
|
|
204
|
+
this.screen.buffer[i] = ' ';
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
case 2:
|
|
209
|
+
// Clear entire screen
|
|
210
|
+
this.screen.clear();
|
|
211
|
+
return true;
|
|
212
|
+
default:
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Handle tab: move to next unprotected field or next tab stop */
|
|
217
|
+
handleTab() {
|
|
218
|
+
// If we have fields, tab to the next unprotected field
|
|
219
|
+
if (this.screen.fields.length > 0) {
|
|
220
|
+
const curOff = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
|
|
221
|
+
// Find the next unprotected field after cursor
|
|
222
|
+
let best = null;
|
|
223
|
+
let bestOff = Infinity;
|
|
224
|
+
let wrapBest = null;
|
|
225
|
+
let wrapBestOff = Infinity;
|
|
226
|
+
for (const field of this.screen.fields) {
|
|
227
|
+
if (field.isProtected)
|
|
228
|
+
continue;
|
|
229
|
+
const fOff = this.screen.offset(field.row, field.col);
|
|
230
|
+
if (fOff > curOff && fOff < bestOff) {
|
|
231
|
+
bestOff = fOff;
|
|
232
|
+
best = { row: field.row, col: field.col };
|
|
233
|
+
}
|
|
234
|
+
if (fOff < wrapBestOff) {
|
|
235
|
+
wrapBestOff = fOff;
|
|
236
|
+
wrapBest = { row: field.row, col: field.col };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const target = best || wrapBest;
|
|
240
|
+
if (target) {
|
|
241
|
+
this.screen.setCursor(target.row, target.col);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// No fields — use tab stops every 8 columns
|
|
246
|
+
const nextTab = (Math.floor(this.screen.cursorCol / 8) + 1) * 8;
|
|
247
|
+
if (nextTab < this.screen.cols) {
|
|
248
|
+
this.screen.cursorCol = nextTab;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this.screen.cursorCol = 0;
|
|
252
|
+
this.screen.cursorRow = (this.screen.cursorRow + 1) % this.screen.rows;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Display attribute for a single cell */
|
|
2
|
+
export interface CellAttr {
|
|
3
|
+
halfBright: boolean;
|
|
4
|
+
underline: boolean;
|
|
5
|
+
blink: boolean;
|
|
6
|
+
inverse: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Field definition derived from protected/unprotected transitions */
|
|
9
|
+
export interface HP6530Field {
|
|
10
|
+
row: number;
|
|
11
|
+
col: number;
|
|
12
|
+
length: number;
|
|
13
|
+
isProtected: boolean;
|
|
14
|
+
modified: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Screen buffer for an HP 6530 terminal.
|
|
18
|
+
*
|
|
19
|
+
* Characters are stored in ASCII (not EBCDIC). The screen tracks
|
|
20
|
+
* protected/unprotected regions — input fields are the unprotected
|
|
21
|
+
* regions between protected boundaries.
|
|
22
|
+
*/
|
|
23
|
+
export declare class HP6530Screen {
|
|
24
|
+
rows: number;
|
|
25
|
+
cols: number;
|
|
26
|
+
/** Character buffer (one char per cell) */
|
|
27
|
+
buffer: string[];
|
|
28
|
+
/** Per-cell display attributes */
|
|
29
|
+
attrs: CellAttr[];
|
|
30
|
+
/** Per-cell protection state (true = protected) */
|
|
31
|
+
protected: boolean[];
|
|
32
|
+
/** Current cursor position */
|
|
33
|
+
cursorRow: number;
|
|
34
|
+
cursorCol: number;
|
|
35
|
+
/** Current attribute state (applied to newly written characters) */
|
|
36
|
+
currentAttr: CellAttr;
|
|
37
|
+
/** Current protection state (applied to newly written characters) */
|
|
38
|
+
currentProtected: boolean;
|
|
39
|
+
/** Derived field list */
|
|
40
|
+
fields: HP6530Field[];
|
|
41
|
+
constructor(rows?: 24, cols?: 80);
|
|
42
|
+
get size(): number;
|
|
43
|
+
offset(row: number, col: number): number;
|
|
44
|
+
toRowCol(offset: number): {
|
|
45
|
+
row: number;
|
|
46
|
+
col: number;
|
|
47
|
+
};
|
|
48
|
+
/** Set a character at the current cursor position and advance */
|
|
49
|
+
putChar(char: string): void;
|
|
50
|
+
/** Set a character at a specific position (no cursor advance) */
|
|
51
|
+
setChar(row: number, col: number, char: string): void;
|
|
52
|
+
/** Get a character at a specific position */
|
|
53
|
+
getChar(row: number, col: number): string;
|
|
54
|
+
/** Set cursor position */
|
|
55
|
+
setCursor(row: number, col: number): void;
|
|
56
|
+
/** Advance cursor by one position, wrapping at end of line/screen */
|
|
57
|
+
advanceCursor(): void;
|
|
58
|
+
/** Set current display attribute from an attribute code */
|
|
59
|
+
setAttrFromCode(code: number): void;
|
|
60
|
+
/** Enter protected mode */
|
|
61
|
+
startProtected(): void;
|
|
62
|
+
/** Leave protected mode (start unprotected / input region) */
|
|
63
|
+
endProtected(): void;
|
|
64
|
+
/** Clear from cursor to end of display */
|
|
65
|
+
clearToEndOfScreen(): void;
|
|
66
|
+
/** Clear from cursor to end of line */
|
|
67
|
+
clearToEndOfLine(): void;
|
|
68
|
+
/** Clear the entire screen */
|
|
69
|
+
clear(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Build the field list from protected/unprotected transitions.
|
|
72
|
+
*
|
|
73
|
+
* A "field" is a contiguous run of cells with the same protection state.
|
|
74
|
+
* Input fields are the unprotected regions; label/output fields are protected.
|
|
75
|
+
*/
|
|
76
|
+
buildFields(): void;
|
|
77
|
+
/** Get the value (text) of a field */
|
|
78
|
+
getFieldValue(field: HP6530Field): string;
|
|
79
|
+
/** Set the value of a field */
|
|
80
|
+
setFieldValue(field: HP6530Field, value: string): void;
|
|
81
|
+
/** Find the field containing the cursor */
|
|
82
|
+
getFieldAtCursor(): HP6530Field | null;
|
|
83
|
+
/** Find the field containing a given position */
|
|
84
|
+
getFieldAt(row: number, col: number): HP6530Field | null;
|
|
85
|
+
/** Convert screen to the ScreenData format for the frontend */
|
|
86
|
+
toScreenData(): {
|
|
87
|
+
content: string;
|
|
88
|
+
cursor_row: number;
|
|
89
|
+
cursor_col: number;
|
|
90
|
+
rows: number;
|
|
91
|
+
cols: number;
|
|
92
|
+
fields: Array<{
|
|
93
|
+
row: number;
|
|
94
|
+
col: number;
|
|
95
|
+
length: number;
|
|
96
|
+
is_input: boolean;
|
|
97
|
+
is_protected: boolean;
|
|
98
|
+
is_highlighted?: boolean;
|
|
99
|
+
is_reverse?: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
screen_signature: string;
|
|
102
|
+
timestamp: string;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { SCREEN, ATTR } from './constants.js';
|
|
3
|
+
const DEFAULT_ATTR = {
|
|
4
|
+
halfBright: false,
|
|
5
|
+
underline: false,
|
|
6
|
+
blink: false,
|
|
7
|
+
inverse: false,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Screen buffer for an HP 6530 terminal.
|
|
11
|
+
*
|
|
12
|
+
* Characters are stored in ASCII (not EBCDIC). The screen tracks
|
|
13
|
+
* protected/unprotected regions — input fields are the unprotected
|
|
14
|
+
* regions between protected boundaries.
|
|
15
|
+
*/
|
|
16
|
+
export class HP6530Screen {
|
|
17
|
+
rows;
|
|
18
|
+
cols;
|
|
19
|
+
/** Character buffer (one char per cell) */
|
|
20
|
+
buffer;
|
|
21
|
+
/** Per-cell display attributes */
|
|
22
|
+
attrs;
|
|
23
|
+
/** Per-cell protection state (true = protected) */
|
|
24
|
+
protected;
|
|
25
|
+
/** Current cursor position */
|
|
26
|
+
cursorRow = 0;
|
|
27
|
+
cursorCol = 0;
|
|
28
|
+
/** Current attribute state (applied to newly written characters) */
|
|
29
|
+
currentAttr = { ...DEFAULT_ATTR };
|
|
30
|
+
/** Current protection state (applied to newly written characters) */
|
|
31
|
+
currentProtected = false;
|
|
32
|
+
/** Derived field list */
|
|
33
|
+
fields = [];
|
|
34
|
+
constructor(rows = SCREEN.ROWS, cols = SCREEN.COLS) {
|
|
35
|
+
this.rows = rows;
|
|
36
|
+
this.cols = cols;
|
|
37
|
+
const size = rows * cols;
|
|
38
|
+
this.buffer = new Array(size).fill(' ');
|
|
39
|
+
this.attrs = new Array(size).fill(null).map(() => ({ ...DEFAULT_ATTR }));
|
|
40
|
+
this.protected = new Array(size).fill(false);
|
|
41
|
+
}
|
|
42
|
+
get size() {
|
|
43
|
+
return this.rows * this.cols;
|
|
44
|
+
}
|
|
45
|
+
offset(row, col) {
|
|
46
|
+
return row * this.cols + col;
|
|
47
|
+
}
|
|
48
|
+
toRowCol(offset) {
|
|
49
|
+
return {
|
|
50
|
+
row: Math.floor(offset / this.cols),
|
|
51
|
+
col: offset % this.cols,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Set a character at the current cursor position and advance */
|
|
55
|
+
putChar(char) {
|
|
56
|
+
const off = this.offset(this.cursorRow, this.cursorCol);
|
|
57
|
+
if (off >= 0 && off < this.size) {
|
|
58
|
+
this.buffer[off] = char;
|
|
59
|
+
this.attrs[off] = { ...this.currentAttr };
|
|
60
|
+
this.protected[off] = this.currentProtected;
|
|
61
|
+
}
|
|
62
|
+
this.advanceCursor();
|
|
63
|
+
}
|
|
64
|
+
/** Set a character at a specific position (no cursor advance) */
|
|
65
|
+
setChar(row, col, char) {
|
|
66
|
+
const off = this.offset(row, col);
|
|
67
|
+
if (off >= 0 && off < this.size) {
|
|
68
|
+
this.buffer[off] = char;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Get a character at a specific position */
|
|
72
|
+
getChar(row, col) {
|
|
73
|
+
const off = this.offset(row, col);
|
|
74
|
+
return off >= 0 && off < this.size ? this.buffer[off] : ' ';
|
|
75
|
+
}
|
|
76
|
+
/** Set cursor position */
|
|
77
|
+
setCursor(row, col) {
|
|
78
|
+
this.cursorRow = Math.max(0, Math.min(row, this.rows - 1));
|
|
79
|
+
this.cursorCol = Math.max(0, Math.min(col, this.cols - 1));
|
|
80
|
+
}
|
|
81
|
+
/** Advance cursor by one position, wrapping at end of line/screen */
|
|
82
|
+
advanceCursor() {
|
|
83
|
+
this.cursorCol++;
|
|
84
|
+
if (this.cursorCol >= this.cols) {
|
|
85
|
+
this.cursorCol = 0;
|
|
86
|
+
this.cursorRow++;
|
|
87
|
+
if (this.cursorRow >= this.rows) {
|
|
88
|
+
this.cursorRow = 0; // wrap to top
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Set current display attribute from an attribute code */
|
|
93
|
+
setAttrFromCode(code) {
|
|
94
|
+
switch (code) {
|
|
95
|
+
case ATTR.NORMAL:
|
|
96
|
+
this.currentAttr = { ...DEFAULT_ATTR };
|
|
97
|
+
break;
|
|
98
|
+
case ATTR.HALF_BRIGHT:
|
|
99
|
+
this.currentAttr = { ...DEFAULT_ATTR, halfBright: true };
|
|
100
|
+
break;
|
|
101
|
+
case ATTR.UNDERLINE:
|
|
102
|
+
this.currentAttr = { ...DEFAULT_ATTR, underline: true };
|
|
103
|
+
break;
|
|
104
|
+
case ATTR.BLINK:
|
|
105
|
+
this.currentAttr = { ...DEFAULT_ATTR, blink: true };
|
|
106
|
+
break;
|
|
107
|
+
case ATTR.INVERSE:
|
|
108
|
+
this.currentAttr = { ...DEFAULT_ATTR, inverse: true };
|
|
109
|
+
break;
|
|
110
|
+
case ATTR.UNDERLINE_INVERSE:
|
|
111
|
+
this.currentAttr = { ...DEFAULT_ATTR, underline: true, inverse: true };
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Enter protected mode */
|
|
116
|
+
startProtected() {
|
|
117
|
+
this.currentProtected = true;
|
|
118
|
+
}
|
|
119
|
+
/** Leave protected mode (start unprotected / input region) */
|
|
120
|
+
endProtected() {
|
|
121
|
+
this.currentProtected = false;
|
|
122
|
+
}
|
|
123
|
+
/** Clear from cursor to end of display */
|
|
124
|
+
clearToEndOfScreen() {
|
|
125
|
+
const start = this.offset(this.cursorRow, this.cursorCol);
|
|
126
|
+
for (let i = start; i < this.size; i++) {
|
|
127
|
+
this.buffer[i] = ' ';
|
|
128
|
+
this.attrs[i] = { ...DEFAULT_ATTR };
|
|
129
|
+
this.protected[i] = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Clear from cursor to end of line */
|
|
133
|
+
clearToEndOfLine() {
|
|
134
|
+
const start = this.offset(this.cursorRow, this.cursorCol);
|
|
135
|
+
const lineEnd = this.offset(this.cursorRow, this.cols - 1) + 1;
|
|
136
|
+
for (let i = start; i < lineEnd && i < this.size; i++) {
|
|
137
|
+
this.buffer[i] = ' ';
|
|
138
|
+
this.attrs[i] = { ...DEFAULT_ATTR };
|
|
139
|
+
this.protected[i] = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Clear the entire screen */
|
|
143
|
+
clear() {
|
|
144
|
+
this.buffer.fill(' ');
|
|
145
|
+
this.attrs = new Array(this.size).fill(null).map(() => ({ ...DEFAULT_ATTR }));
|
|
146
|
+
this.protected.fill(false);
|
|
147
|
+
this.fields = [];
|
|
148
|
+
this.cursorRow = 0;
|
|
149
|
+
this.cursorCol = 0;
|
|
150
|
+
this.currentAttr = { ...DEFAULT_ATTR };
|
|
151
|
+
this.currentProtected = false;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the field list from protected/unprotected transitions.
|
|
155
|
+
*
|
|
156
|
+
* A "field" is a contiguous run of cells with the same protection state.
|
|
157
|
+
* Input fields are the unprotected regions; label/output fields are protected.
|
|
158
|
+
*/
|
|
159
|
+
buildFields() {
|
|
160
|
+
this.fields = [];
|
|
161
|
+
if (this.size === 0)
|
|
162
|
+
return;
|
|
163
|
+
let currentProt = this.protected[0];
|
|
164
|
+
let startOff = 0;
|
|
165
|
+
for (let off = 1; off <= this.size; off++) {
|
|
166
|
+
const prot = off < this.size ? this.protected[off] : !currentProt; // force flush at end
|
|
167
|
+
if (prot !== currentProt) {
|
|
168
|
+
const pos = this.toRowCol(startOff);
|
|
169
|
+
this.fields.push({
|
|
170
|
+
row: pos.row,
|
|
171
|
+
col: pos.col,
|
|
172
|
+
length: off - startOff,
|
|
173
|
+
isProtected: currentProt,
|
|
174
|
+
modified: false,
|
|
175
|
+
});
|
|
176
|
+
currentProt = prot;
|
|
177
|
+
startOff = off;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Get the value (text) of a field */
|
|
182
|
+
getFieldValue(field) {
|
|
183
|
+
const start = this.offset(field.row, field.col);
|
|
184
|
+
return this.buffer.slice(start, start + field.length).join('');
|
|
185
|
+
}
|
|
186
|
+
/** Set the value of a field */
|
|
187
|
+
setFieldValue(field, value) {
|
|
188
|
+
const start = this.offset(field.row, field.col);
|
|
189
|
+
for (let i = 0; i < field.length; i++) {
|
|
190
|
+
this.buffer[start + i] = i < value.length ? value[i] : ' ';
|
|
191
|
+
}
|
|
192
|
+
field.modified = true;
|
|
193
|
+
}
|
|
194
|
+
/** Find the field containing the cursor */
|
|
195
|
+
getFieldAtCursor() {
|
|
196
|
+
return this.getFieldAt(this.cursorRow, this.cursorCol);
|
|
197
|
+
}
|
|
198
|
+
/** Find the field containing a given position */
|
|
199
|
+
getFieldAt(row, col) {
|
|
200
|
+
const pos = this.offset(row, col);
|
|
201
|
+
for (const field of this.fields) {
|
|
202
|
+
const start = this.offset(field.row, field.col);
|
|
203
|
+
if (pos >= start && pos < start + field.length) {
|
|
204
|
+
return field;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
/** Convert screen to the ScreenData format for the frontend */
|
|
210
|
+
toScreenData() {
|
|
211
|
+
// Build content as newline-separated rows
|
|
212
|
+
const lines = [];
|
|
213
|
+
for (let r = 0; r < this.rows; r++) {
|
|
214
|
+
const start = r * this.cols;
|
|
215
|
+
lines.push(this.buffer.slice(start, start + this.cols).join(''));
|
|
216
|
+
}
|
|
217
|
+
const content = lines.join('\n');
|
|
218
|
+
// Map fields to frontend format
|
|
219
|
+
const fields = this.fields.map(f => {
|
|
220
|
+
// Check if any cell in the field has special attributes
|
|
221
|
+
const start = this.offset(f.row, f.col);
|
|
222
|
+
let hasInverse = false;
|
|
223
|
+
let hasHighlight = false;
|
|
224
|
+
for (let i = start; i < start + f.length && i < this.size; i++) {
|
|
225
|
+
if (this.attrs[i].inverse)
|
|
226
|
+
hasInverse = true;
|
|
227
|
+
if (this.attrs[i].halfBright)
|
|
228
|
+
hasHighlight = true;
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
row: f.row,
|
|
232
|
+
col: f.col,
|
|
233
|
+
length: f.length,
|
|
234
|
+
is_input: !f.isProtected,
|
|
235
|
+
is_protected: f.isProtected,
|
|
236
|
+
is_highlighted: hasHighlight || undefined,
|
|
237
|
+
is_reverse: hasInverse || undefined,
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
const hash = createHash('md5').update(content).digest('hex').substring(0, 12);
|
|
241
|
+
return {
|
|
242
|
+
content,
|
|
243
|
+
cursor_row: this.cursorRow,
|
|
244
|
+
cursor_col: this.cursorCol,
|
|
245
|
+
rows: this.rows,
|
|
246
|
+
cols: this.cols,
|
|
247
|
+
fields,
|
|
248
|
+
screen_signature: hash,
|
|
249
|
+
timestamp: new Date().toISOString(),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|