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,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
+ }
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;