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,101 @@
|
|
|
1
|
+
import type { ScreenData } from '../protocols/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Character attributes for a single cell in the VT screen buffer.
|
|
4
|
+
*/
|
|
5
|
+
export interface CellAttrs {
|
|
6
|
+
bold: boolean;
|
|
7
|
+
dim: boolean;
|
|
8
|
+
italic: boolean;
|
|
9
|
+
underline: boolean;
|
|
10
|
+
blink: boolean;
|
|
11
|
+
reverse: boolean;
|
|
12
|
+
hidden: boolean;
|
|
13
|
+
strikethrough: boolean;
|
|
14
|
+
fg: number;
|
|
15
|
+
bg: number;
|
|
16
|
+
}
|
|
17
|
+
/** Create a default (reset) attribute set */
|
|
18
|
+
export declare function defaultAttrs(): CellAttrs;
|
|
19
|
+
/** Synthetic field detected from VT screen content */
|
|
20
|
+
export interface SyntheticField {
|
|
21
|
+
row: number;
|
|
22
|
+
col: number;
|
|
23
|
+
length: number;
|
|
24
|
+
is_input: boolean;
|
|
25
|
+
is_protected: boolean;
|
|
26
|
+
is_highlighted?: boolean;
|
|
27
|
+
is_reverse?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* VT terminal screen buffer.
|
|
31
|
+
*
|
|
32
|
+
* A simple character grid with per-cell attributes. No native field concept —
|
|
33
|
+
* fields are detected synthetically by scanning for common patterns like
|
|
34
|
+
* "Label: ____" or prompt strings.
|
|
35
|
+
*/
|
|
36
|
+
export declare class VTScreenBuffer {
|
|
37
|
+
rows: number;
|
|
38
|
+
cols: number;
|
|
39
|
+
/** Character grid */
|
|
40
|
+
buffer: string[];
|
|
41
|
+
/** Per-cell attributes */
|
|
42
|
+
attrs: CellAttrs[];
|
|
43
|
+
cursorRow: number;
|
|
44
|
+
cursorCol: number;
|
|
45
|
+
/** Scroll region (top and bottom row, inclusive, 0-indexed) */
|
|
46
|
+
scrollTop: number;
|
|
47
|
+
scrollBottom: number;
|
|
48
|
+
/** Saved cursor state (DECSC / DECRC) */
|
|
49
|
+
private savedCursor;
|
|
50
|
+
/** Current drawing attributes (applied to new characters) */
|
|
51
|
+
currentAttrs: CellAttrs;
|
|
52
|
+
/** Line-wrapping mode (DECAWM) */
|
|
53
|
+
autoWrap: boolean;
|
|
54
|
+
/** Origin mode (DECOM) — cursor addressing relative to scroll region */
|
|
55
|
+
originMode: boolean;
|
|
56
|
+
/** Pending wrap — the next printable char wraps to next line */
|
|
57
|
+
pendingWrap: boolean;
|
|
58
|
+
constructor(rows?: number, cols?: number);
|
|
59
|
+
get size(): number;
|
|
60
|
+
offset(row: number, col: number): number;
|
|
61
|
+
/** Write a character at the current cursor position and advance cursor */
|
|
62
|
+
writeChar(ch: string): void;
|
|
63
|
+
/** Set a character at a specific position without moving cursor */
|
|
64
|
+
setChar(row: number, col: number, ch: string, cellAttrs?: CellAttrs): void;
|
|
65
|
+
getChar(row: number, col: number): string;
|
|
66
|
+
setCursor(row: number, col: number): void;
|
|
67
|
+
private clampRow;
|
|
68
|
+
private clampCol;
|
|
69
|
+
lineFeed(): void;
|
|
70
|
+
reverseLineFeed(): void;
|
|
71
|
+
/** Scroll the scroll region up by n lines (new blank lines at bottom) */
|
|
72
|
+
scrollUp(n: number): void;
|
|
73
|
+
/** Scroll the scroll region down by n lines (new blank lines at top) */
|
|
74
|
+
scrollDown(n: number): void;
|
|
75
|
+
/** ED — Erase in Display */
|
|
76
|
+
eraseInDisplay(mode: number): void;
|
|
77
|
+
/** EL — Erase in Line */
|
|
78
|
+
eraseInLine(mode: number): void;
|
|
79
|
+
/** ECH — Erase Characters */
|
|
80
|
+
eraseCharacters(n: number): void;
|
|
81
|
+
private eraseRange;
|
|
82
|
+
/** IL — Insert n blank lines at cursor row */
|
|
83
|
+
insertLines(n: number): void;
|
|
84
|
+
/** DL — Delete n lines at cursor row */
|
|
85
|
+
deleteLines(n: number): void;
|
|
86
|
+
/** ICH — Insert n blank characters at cursor */
|
|
87
|
+
insertCharacters(n: number): void;
|
|
88
|
+
/** DCH — Delete n characters at cursor */
|
|
89
|
+
deleteCharacters(n: number): void;
|
|
90
|
+
saveCursor(): void;
|
|
91
|
+
restoreCursor(): void;
|
|
92
|
+
reset(): void;
|
|
93
|
+
tabForward(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Detect synthetic fields from the screen content. Best-effort heuristic —
|
|
96
|
+
* VT terminals have no native field concept.
|
|
97
|
+
*/
|
|
98
|
+
detectFields(): SyntheticField[];
|
|
99
|
+
/** Convert to the protocol-agnostic ScreenData format */
|
|
100
|
+
toScreenData(): ScreenData;
|
|
101
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { DEFAULT_ROWS, DEFAULT_COLS } from './constants.js';
|
|
3
|
+
/** Create a default (reset) attribute set */
|
|
4
|
+
export function defaultAttrs() {
|
|
5
|
+
return {
|
|
6
|
+
bold: false,
|
|
7
|
+
dim: false,
|
|
8
|
+
italic: false,
|
|
9
|
+
underline: false,
|
|
10
|
+
blink: false,
|
|
11
|
+
reverse: false,
|
|
12
|
+
hidden: false,
|
|
13
|
+
strikethrough: false,
|
|
14
|
+
fg: 8, // default
|
|
15
|
+
bg: 8, // default
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* VT terminal screen buffer.
|
|
20
|
+
*
|
|
21
|
+
* A simple character grid with per-cell attributes. No native field concept —
|
|
22
|
+
* fields are detected synthetically by scanning for common patterns like
|
|
23
|
+
* "Label: ____" or prompt strings.
|
|
24
|
+
*/
|
|
25
|
+
export class VTScreenBuffer {
|
|
26
|
+
rows;
|
|
27
|
+
cols;
|
|
28
|
+
/** Character grid */
|
|
29
|
+
buffer;
|
|
30
|
+
/** Per-cell attributes */
|
|
31
|
+
attrs;
|
|
32
|
+
cursorRow = 0;
|
|
33
|
+
cursorCol = 0;
|
|
34
|
+
/** Scroll region (top and bottom row, inclusive, 0-indexed) */
|
|
35
|
+
scrollTop = 0;
|
|
36
|
+
scrollBottom;
|
|
37
|
+
/** Saved cursor state (DECSC / DECRC) */
|
|
38
|
+
savedCursor = null;
|
|
39
|
+
/** Current drawing attributes (applied to new characters) */
|
|
40
|
+
currentAttrs;
|
|
41
|
+
/** Line-wrapping mode (DECAWM) */
|
|
42
|
+
autoWrap = true;
|
|
43
|
+
/** Origin mode (DECOM) — cursor addressing relative to scroll region */
|
|
44
|
+
originMode = false;
|
|
45
|
+
/** Pending wrap — the next printable char wraps to next line */
|
|
46
|
+
pendingWrap = false;
|
|
47
|
+
constructor(rows = DEFAULT_ROWS, cols = DEFAULT_COLS) {
|
|
48
|
+
this.rows = rows;
|
|
49
|
+
this.cols = cols;
|
|
50
|
+
this.scrollBottom = rows - 1;
|
|
51
|
+
const size = rows * cols;
|
|
52
|
+
this.buffer = new Array(size).fill(' ');
|
|
53
|
+
this.attrs = new Array(size);
|
|
54
|
+
for (let i = 0; i < size; i++) {
|
|
55
|
+
this.attrs[i] = defaultAttrs();
|
|
56
|
+
}
|
|
57
|
+
this.currentAttrs = defaultAttrs();
|
|
58
|
+
}
|
|
59
|
+
get size() {
|
|
60
|
+
return this.rows * this.cols;
|
|
61
|
+
}
|
|
62
|
+
offset(row, col) {
|
|
63
|
+
return row * this.cols + col;
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Character operations
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/** Write a character at the current cursor position and advance cursor */
|
|
69
|
+
writeChar(ch) {
|
|
70
|
+
if (this.pendingWrap) {
|
|
71
|
+
if (this.autoWrap) {
|
|
72
|
+
this.cursorCol = 0;
|
|
73
|
+
this.lineFeed();
|
|
74
|
+
}
|
|
75
|
+
this.pendingWrap = false;
|
|
76
|
+
}
|
|
77
|
+
const off = this.offset(this.cursorRow, this.cursorCol);
|
|
78
|
+
if (off >= 0 && off < this.size) {
|
|
79
|
+
this.buffer[off] = ch;
|
|
80
|
+
this.attrs[off] = { ...this.currentAttrs };
|
|
81
|
+
}
|
|
82
|
+
if (this.cursorCol < this.cols - 1) {
|
|
83
|
+
this.cursorCol++;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// At last column — set pending wrap flag
|
|
87
|
+
this.pendingWrap = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Set a character at a specific position without moving cursor */
|
|
91
|
+
setChar(row, col, ch, cellAttrs) {
|
|
92
|
+
const off = this.offset(row, col);
|
|
93
|
+
if (off >= 0 && off < this.size) {
|
|
94
|
+
this.buffer[off] = ch;
|
|
95
|
+
if (cellAttrs)
|
|
96
|
+
this.attrs[off] = { ...cellAttrs };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getChar(row, col) {
|
|
100
|
+
const off = this.offset(row, col);
|
|
101
|
+
return off >= 0 && off < this.size ? this.buffer[off] : ' ';
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Cursor movement
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
setCursor(row, col) {
|
|
107
|
+
this.cursorRow = this.clampRow(row);
|
|
108
|
+
this.cursorCol = this.clampCol(col);
|
|
109
|
+
this.pendingWrap = false;
|
|
110
|
+
}
|
|
111
|
+
clampRow(row) {
|
|
112
|
+
return Math.max(0, Math.min(this.rows - 1, row));
|
|
113
|
+
}
|
|
114
|
+
clampCol(col) {
|
|
115
|
+
return Math.max(0, Math.min(this.cols - 1, col));
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Line feed / scrolling
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
lineFeed() {
|
|
121
|
+
if (this.cursorRow === this.scrollBottom) {
|
|
122
|
+
this.scrollUp(1);
|
|
123
|
+
}
|
|
124
|
+
else if (this.cursorRow < this.rows - 1) {
|
|
125
|
+
this.cursorRow++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
reverseLineFeed() {
|
|
129
|
+
if (this.cursorRow === this.scrollTop) {
|
|
130
|
+
this.scrollDown(1);
|
|
131
|
+
}
|
|
132
|
+
else if (this.cursorRow > 0) {
|
|
133
|
+
this.cursorRow--;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Scroll the scroll region up by n lines (new blank lines at bottom) */
|
|
137
|
+
scrollUp(n) {
|
|
138
|
+
for (let i = 0; i < n; i++) {
|
|
139
|
+
for (let r = this.scrollTop; r < this.scrollBottom; r++) {
|
|
140
|
+
const dstOff = r * this.cols;
|
|
141
|
+
const srcOff = (r + 1) * this.cols;
|
|
142
|
+
for (let c = 0; c < this.cols; c++) {
|
|
143
|
+
this.buffer[dstOff + c] = this.buffer[srcOff + c];
|
|
144
|
+
this.attrs[dstOff + c] = this.attrs[srcOff + c];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const bottomOff = this.scrollBottom * this.cols;
|
|
148
|
+
for (let c = 0; c < this.cols; c++) {
|
|
149
|
+
this.buffer[bottomOff + c] = ' ';
|
|
150
|
+
this.attrs[bottomOff + c] = defaultAttrs();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Scroll the scroll region down by n lines (new blank lines at top) */
|
|
155
|
+
scrollDown(n) {
|
|
156
|
+
for (let i = 0; i < n; i++) {
|
|
157
|
+
for (let r = this.scrollBottom; r > this.scrollTop; r--) {
|
|
158
|
+
const dstOff = r * this.cols;
|
|
159
|
+
const srcOff = (r - 1) * this.cols;
|
|
160
|
+
for (let c = 0; c < this.cols; c++) {
|
|
161
|
+
this.buffer[dstOff + c] = this.buffer[srcOff + c];
|
|
162
|
+
this.attrs[dstOff + c] = this.attrs[srcOff + c];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const topOff = this.scrollTop * this.cols;
|
|
166
|
+
for (let c = 0; c < this.cols; c++) {
|
|
167
|
+
this.buffer[topOff + c] = ' ';
|
|
168
|
+
this.attrs[topOff + c] = defaultAttrs();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Erase operations
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/** ED — Erase in Display */
|
|
176
|
+
eraseInDisplay(mode) {
|
|
177
|
+
switch (mode) {
|
|
178
|
+
case 0:
|
|
179
|
+
this.eraseRange(this.offset(this.cursorRow, this.cursorCol), this.size);
|
|
180
|
+
break;
|
|
181
|
+
case 1:
|
|
182
|
+
this.eraseRange(0, this.offset(this.cursorRow, this.cursorCol) + 1);
|
|
183
|
+
break;
|
|
184
|
+
case 2:
|
|
185
|
+
case 3:
|
|
186
|
+
this.eraseRange(0, this.size);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** EL — Erase in Line */
|
|
191
|
+
eraseInLine(mode) {
|
|
192
|
+
const rowStart = this.cursorRow * this.cols;
|
|
193
|
+
switch (mode) {
|
|
194
|
+
case 0:
|
|
195
|
+
this.eraseRange(this.offset(this.cursorRow, this.cursorCol), rowStart + this.cols);
|
|
196
|
+
break;
|
|
197
|
+
case 1:
|
|
198
|
+
this.eraseRange(rowStart, this.offset(this.cursorRow, this.cursorCol) + 1);
|
|
199
|
+
break;
|
|
200
|
+
case 2:
|
|
201
|
+
this.eraseRange(rowStart, rowStart + this.cols);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** ECH — Erase Characters */
|
|
206
|
+
eraseCharacters(n) {
|
|
207
|
+
const start = this.offset(this.cursorRow, this.cursorCol);
|
|
208
|
+
const end = Math.min(start + n, this.cursorRow * this.cols + this.cols);
|
|
209
|
+
this.eraseRange(start, end);
|
|
210
|
+
}
|
|
211
|
+
eraseRange(start, end) {
|
|
212
|
+
for (let i = start; i < end && i < this.size; i++) {
|
|
213
|
+
this.buffer[i] = ' ';
|
|
214
|
+
this.attrs[i] = defaultAttrs();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Insert / Delete lines and characters
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
/** IL — Insert n blank lines at cursor row */
|
|
221
|
+
insertLines(n) {
|
|
222
|
+
if (this.cursorRow < this.scrollTop || this.cursorRow > this.scrollBottom)
|
|
223
|
+
return;
|
|
224
|
+
const oldTop = this.scrollTop;
|
|
225
|
+
this.scrollTop = this.cursorRow;
|
|
226
|
+
this.scrollDown(n);
|
|
227
|
+
this.scrollTop = oldTop;
|
|
228
|
+
}
|
|
229
|
+
/** DL — Delete n lines at cursor row */
|
|
230
|
+
deleteLines(n) {
|
|
231
|
+
if (this.cursorRow < this.scrollTop || this.cursorRow > this.scrollBottom)
|
|
232
|
+
return;
|
|
233
|
+
const oldTop = this.scrollTop;
|
|
234
|
+
this.scrollTop = this.cursorRow;
|
|
235
|
+
this.scrollUp(n);
|
|
236
|
+
this.scrollTop = oldTop;
|
|
237
|
+
}
|
|
238
|
+
/** ICH — Insert n blank characters at cursor */
|
|
239
|
+
insertCharacters(n) {
|
|
240
|
+
const rowOff = this.cursorRow * this.cols;
|
|
241
|
+
const curOff = rowOff + this.cursorCol;
|
|
242
|
+
const endOff = rowOff + this.cols;
|
|
243
|
+
for (let i = endOff - 1; i >= curOff + n; i--) {
|
|
244
|
+
this.buffer[i] = this.buffer[i - n];
|
|
245
|
+
this.attrs[i] = this.attrs[i - n];
|
|
246
|
+
}
|
|
247
|
+
for (let i = curOff; i < curOff + n && i < endOff; i++) {
|
|
248
|
+
this.buffer[i] = ' ';
|
|
249
|
+
this.attrs[i] = defaultAttrs();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** DCH — Delete n characters at cursor */
|
|
253
|
+
deleteCharacters(n) {
|
|
254
|
+
const rowOff = this.cursorRow * this.cols;
|
|
255
|
+
const curOff = rowOff + this.cursorCol;
|
|
256
|
+
const endOff = rowOff + this.cols;
|
|
257
|
+
for (let i = curOff; i < endOff - n; i++) {
|
|
258
|
+
this.buffer[i] = this.buffer[i + n];
|
|
259
|
+
this.attrs[i] = this.attrs[i + n];
|
|
260
|
+
}
|
|
261
|
+
for (let i = endOff - n; i < endOff; i++) {
|
|
262
|
+
this.buffer[i] = ' ';
|
|
263
|
+
this.attrs[i] = defaultAttrs();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Cursor save / restore (DECSC / DECRC)
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
saveCursor() {
|
|
270
|
+
this.savedCursor = {
|
|
271
|
+
row: this.cursorRow,
|
|
272
|
+
col: this.cursorCol,
|
|
273
|
+
attrs: { ...this.currentAttrs },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
restoreCursor() {
|
|
277
|
+
if (this.savedCursor) {
|
|
278
|
+
this.cursorRow = this.savedCursor.row;
|
|
279
|
+
this.cursorCol = this.savedCursor.col;
|
|
280
|
+
this.currentAttrs = { ...this.savedCursor.attrs };
|
|
281
|
+
this.pendingWrap = false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Full reset
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
reset() {
|
|
288
|
+
this.buffer.fill(' ');
|
|
289
|
+
for (let i = 0; i < this.size; i++) {
|
|
290
|
+
this.attrs[i] = defaultAttrs();
|
|
291
|
+
}
|
|
292
|
+
this.cursorRow = 0;
|
|
293
|
+
this.cursorCol = 0;
|
|
294
|
+
this.scrollTop = 0;
|
|
295
|
+
this.scrollBottom = this.rows - 1;
|
|
296
|
+
this.currentAttrs = defaultAttrs();
|
|
297
|
+
this.autoWrap = true;
|
|
298
|
+
this.originMode = false;
|
|
299
|
+
this.pendingWrap = false;
|
|
300
|
+
this.savedCursor = null;
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Tab stops
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
tabForward() {
|
|
306
|
+
const nextTab = (Math.floor(this.cursorCol / 8) + 1) * 8;
|
|
307
|
+
this.cursorCol = Math.min(nextTab, this.cols - 1);
|
|
308
|
+
this.pendingWrap = false;
|
|
309
|
+
}
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Synthetic field detection
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
/**
|
|
314
|
+
* Detect synthetic fields from the screen content. Best-effort heuristic —
|
|
315
|
+
* VT terminals have no native field concept.
|
|
316
|
+
*/
|
|
317
|
+
detectFields() {
|
|
318
|
+
const fields = [];
|
|
319
|
+
for (let r = 0; r < this.rows; r++) {
|
|
320
|
+
const rowStart = r * this.cols;
|
|
321
|
+
const line = this.buffer.slice(rowStart, rowStart + this.cols).join('');
|
|
322
|
+
// Pattern 1: Underscore runs (3+ consecutive underscores)
|
|
323
|
+
const underscoreRe = /_{3,}/g;
|
|
324
|
+
let match;
|
|
325
|
+
while ((match = underscoreRe.exec(line)) !== null) {
|
|
326
|
+
fields.push({
|
|
327
|
+
row: r,
|
|
328
|
+
col: match.index,
|
|
329
|
+
length: match[0].length,
|
|
330
|
+
is_input: true,
|
|
331
|
+
is_protected: false,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// Pattern 2: Reverse-video runs (possible input fields)
|
|
335
|
+
let inReverse = false;
|
|
336
|
+
let reverseStart = 0;
|
|
337
|
+
for (let c = 0; c < this.cols; c++) {
|
|
338
|
+
const a = this.attrs[rowStart + c];
|
|
339
|
+
if (a.reverse && !inReverse) {
|
|
340
|
+
inReverse = true;
|
|
341
|
+
reverseStart = c;
|
|
342
|
+
}
|
|
343
|
+
else if (!a.reverse && inReverse) {
|
|
344
|
+
inReverse = false;
|
|
345
|
+
const len = c - reverseStart;
|
|
346
|
+
if (len >= 3) {
|
|
347
|
+
fields.push({
|
|
348
|
+
row: r,
|
|
349
|
+
col: reverseStart,
|
|
350
|
+
length: len,
|
|
351
|
+
is_input: true,
|
|
352
|
+
is_protected: false,
|
|
353
|
+
is_reverse: true,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (inReverse) {
|
|
359
|
+
const len = this.cols - reverseStart;
|
|
360
|
+
if (len >= 3) {
|
|
361
|
+
fields.push({
|
|
362
|
+
row: r,
|
|
363
|
+
col: reverseStart,
|
|
364
|
+
length: len,
|
|
365
|
+
is_input: true,
|
|
366
|
+
is_protected: false,
|
|
367
|
+
is_reverse: true,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Pattern 3: Prompt detection
|
|
372
|
+
const promptRe = /\b(Username|Login|Password|User|Passwd|Account|Host|Port)\s*:\s*/gi;
|
|
373
|
+
let pm;
|
|
374
|
+
while ((pm = promptRe.exec(line)) !== null) {
|
|
375
|
+
const afterPrompt = pm.index + pm[0].length;
|
|
376
|
+
const remaining = line.substring(afterPrompt);
|
|
377
|
+
const inputLen = remaining.length - remaining.trimEnd().length || remaining.length;
|
|
378
|
+
if (inputLen > 0) {
|
|
379
|
+
fields.push({
|
|
380
|
+
row: r,
|
|
381
|
+
col: afterPrompt,
|
|
382
|
+
length: Math.min(inputLen, this.cols - afterPrompt),
|
|
383
|
+
is_input: true,
|
|
384
|
+
is_protected: false,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Deduplicate overlapping fields
|
|
390
|
+
const deduped = [];
|
|
391
|
+
for (const f of fields) {
|
|
392
|
+
const overlaps = deduped.some((d) => d.row === f.row &&
|
|
393
|
+
f.col < d.col + d.length &&
|
|
394
|
+
f.col + f.length > d.col);
|
|
395
|
+
if (!overlaps)
|
|
396
|
+
deduped.push(f);
|
|
397
|
+
}
|
|
398
|
+
return deduped;
|
|
399
|
+
}
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
// Serialization
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
/** Convert to the protocol-agnostic ScreenData format */
|
|
404
|
+
toScreenData() {
|
|
405
|
+
const lines = [];
|
|
406
|
+
for (let r = 0; r < this.rows; r++) {
|
|
407
|
+
const start = r * this.cols;
|
|
408
|
+
lines.push(this.buffer.slice(start, start + this.cols).join(''));
|
|
409
|
+
}
|
|
410
|
+
const content = lines.join('\n');
|
|
411
|
+
const fields = this.detectFields();
|
|
412
|
+
const hash = createHash('md5').update(content).digest('hex').substring(0, 12);
|
|
413
|
+
return {
|
|
414
|
+
content,
|
|
415
|
+
cursor_row: this.cursorRow,
|
|
416
|
+
cursor_col: this.cursorCol,
|
|
417
|
+
rows: this.rows,
|
|
418
|
+
cols: this.cols,
|
|
419
|
+
fields,
|
|
420
|
+
screen_signature: hash,
|
|
421
|
+
timestamp: new Date().toISOString(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { Server as HttpServer } from 'http';
|
|
3
|
+
import { Session } from './session.js';
|
|
4
|
+
export declare function setupWebSocket(server: HttpServer): WebSocketServer;
|
|
5
|
+
/** Subscribe to a session's events and push to connected WS clients */
|
|
6
|
+
export declare function bindSessionToWebSocket(session: Session): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { getSession, getDefaultSession } from './session.js';
|
|
4
|
+
const clients = new Set();
|
|
5
|
+
export function setupWebSocket(server) {
|
|
6
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
7
|
+
wss.on('connection', (ws, req) => {
|
|
8
|
+
// Extract sessionId from query params
|
|
9
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
10
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
11
|
+
const client = { ws, sessionId };
|
|
12
|
+
clients.add(client);
|
|
13
|
+
ws.on('close', () => {
|
|
14
|
+
clients.delete(client);
|
|
15
|
+
});
|
|
16
|
+
ws.on('error', () => {
|
|
17
|
+
clients.delete(client);
|
|
18
|
+
});
|
|
19
|
+
// Send current screen immediately if a session is available
|
|
20
|
+
const session = sessionId ? getSession(sessionId) : getDefaultSession();
|
|
21
|
+
if (session && session.status.connected) {
|
|
22
|
+
ws.send(JSON.stringify({ type: 'screen', data: session.getScreenData() }));
|
|
23
|
+
ws.send(JSON.stringify({ type: 'status', data: session.status }));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return wss;
|
|
27
|
+
}
|
|
28
|
+
/** Subscribe to a session's events and push to connected WS clients */
|
|
29
|
+
export function bindSessionToWebSocket(session) {
|
|
30
|
+
session.on('screenChange', (screenData) => {
|
|
31
|
+
const msg = JSON.stringify({ type: 'screen', data: screenData });
|
|
32
|
+
broadcastToSession(session.id, msg);
|
|
33
|
+
});
|
|
34
|
+
session.on('statusChange', (status) => {
|
|
35
|
+
const msg = JSON.stringify({ type: 'status', data: status });
|
|
36
|
+
broadcastToSession(session.id, msg);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function broadcastToSession(sessionId, message) {
|
|
40
|
+
for (const client of clients) {
|
|
41
|
+
if (client.ws.readyState !== WebSocket.OPEN)
|
|
42
|
+
continue;
|
|
43
|
+
// Send to clients that are either:
|
|
44
|
+
// 1. Explicitly bound to this session
|
|
45
|
+
// 2. Not bound to any session (will receive from default/single session)
|
|
46
|
+
if (client.sessionId === sessionId || client.sessionId === null) {
|
|
47
|
+
client.ws.send(message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "green-screen-proxy",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "WebSocket/REST proxy server for green-screen-react — connects browsers to TN5250, TN3270, VT220, and HP 6530 hosts over TCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"green-screen-proxy": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/server.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx watch src/server.ts",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "node dist/server.js",
|
|
17
|
+
"lint": "tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"terminal",
|
|
22
|
+
"proxy",
|
|
23
|
+
"tn5250",
|
|
24
|
+
"tn3270",
|
|
25
|
+
"vt220",
|
|
26
|
+
"hp6530",
|
|
27
|
+
"ibm-i",
|
|
28
|
+
"as400",
|
|
29
|
+
"mainframe",
|
|
30
|
+
"green-screen",
|
|
31
|
+
"telnet",
|
|
32
|
+
"websocket"
|
|
33
|
+
],
|
|
34
|
+
"author": "VisionBridge Solutions",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/visionbridge-solutions/green-screen-react.git",
|
|
39
|
+
"directory": "proxy"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/visionbridge-solutions/green-screen-react#proxy-setup",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/visionbridge-solutions/green-screen-react/issues"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"cors": "^2.8.5",
|
|
47
|
+
"express": "^4.21.0",
|
|
48
|
+
"ws": "^8.18.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/cors": "^2.8.17",
|
|
52
|
+
"@types/express": "^4.17.21",
|
|
53
|
+
"@types/ws": "^8.5.12",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^5.7.0"
|
|
56
|
+
}
|
|
57
|
+
}
|