vt100.js 0.3.0 → 0.7.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/README.md +13 -14
- package/dist/index.d.mts +60 -0
- package/dist/index.mjs +570 -0
- package/package.json +16 -5
- package/src/index.ts +0 -7
- package/src/screen.ts +0 -833
package/README.md
CHANGED
|
@@ -6,15 +6,17 @@ Part of the [vterm](https://github.com/beorn/vterm) monorepo.
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
- SGR attributes
|
|
11
|
-
-
|
|
12
|
-
- Cursor
|
|
13
|
-
-
|
|
9
|
+
- Strict DEC VT100 (1978) emulation — monochrome display
|
|
10
|
+
- SGR attributes: bold, underline (boolean), blink, inverse
|
|
11
|
+
- Cursor movement (CUP, CUU, CUD, CUF, CUB, CHA, CNL, CPL, HVP, VPA)
|
|
12
|
+
- Cursor save/restore (DECSC/DECRC)
|
|
13
|
+
- Erase display/line (ED/EL)
|
|
14
14
|
- Scroll regions (DECSTBM) with content preservation
|
|
15
|
+
- Scroll up/down (SU/SD)
|
|
15
16
|
- Scrollback buffer with configurable limit
|
|
16
|
-
-
|
|
17
|
-
-
|
|
17
|
+
- Screen modes (auto-wrap, origin mode, cursor visibility)
|
|
18
|
+
- DEC special graphics charset
|
|
19
|
+
- DA1/DSR/CPR device responses
|
|
18
20
|
- Zero dependencies — works in Bun, Node.js, and browsers
|
|
19
21
|
|
|
20
22
|
## Install
|
|
@@ -70,16 +72,13 @@ console.log(screen.getCursorPosition()) // { x: 18, y: 0 }
|
|
|
70
72
|
```typescript
|
|
71
73
|
interface ScreenCell {
|
|
72
74
|
char: string
|
|
73
|
-
fg: CellColor | null //
|
|
74
|
-
bg: CellColor | null
|
|
75
|
+
fg: CellColor | null // always null (VT100 is monochrome)
|
|
76
|
+
bg: CellColor | null // always null (VT100 is monochrome)
|
|
75
77
|
bold: boolean
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
underline: "none" | "single" | "double" | "curly" | "dotted" | "dashed"
|
|
79
|
-
strikethrough: boolean
|
|
78
|
+
underline: boolean
|
|
79
|
+
blink: boolean
|
|
80
80
|
inverse: boolean
|
|
81
81
|
hidden: boolean
|
|
82
|
-
wide: boolean
|
|
83
82
|
}
|
|
84
83
|
```
|
|
85
84
|
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/screen.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure TypeScript VT100 terminal emulator.
|
|
4
|
+
*
|
|
5
|
+
* Strict DEC VT100 (1978) implementation: monochrome display, cursor movement,
|
|
6
|
+
* scroll regions, auto-wrap, origin mode, DEC special graphics charset.
|
|
7
|
+
* SGR supports bold, underline, blink, and reverse — NO colors (VT100 is
|
|
8
|
+
* monochrome). No insert mode, no insert/delete chars/lines — those are
|
|
9
|
+
* VT102/VT220 features.
|
|
10
|
+
*
|
|
11
|
+
* Zero dependencies. For colors and VT220 features, use vt220.js.
|
|
12
|
+
* For truecolor, 256 colors, and wide chars, use vterm.js.
|
|
13
|
+
*/
|
|
14
|
+
interface CellColor {
|
|
15
|
+
r: number;
|
|
16
|
+
g: number;
|
|
17
|
+
b: number;
|
|
18
|
+
}
|
|
19
|
+
interface ScreenCell {
|
|
20
|
+
char: string;
|
|
21
|
+
fg: CellColor | null;
|
|
22
|
+
bg: CellColor | null;
|
|
23
|
+
bold: boolean;
|
|
24
|
+
underline: boolean;
|
|
25
|
+
blink: boolean;
|
|
26
|
+
inverse: boolean;
|
|
27
|
+
hidden: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface ScreenOptions {
|
|
30
|
+
cols: number;
|
|
31
|
+
rows: number;
|
|
32
|
+
scrollbackLimit?: number;
|
|
33
|
+
/** Callback for DA1/DSR responses — write these back to the PTY */
|
|
34
|
+
onResponse?: (data: string) => void;
|
|
35
|
+
}
|
|
36
|
+
interface Screen {
|
|
37
|
+
readonly cols: number;
|
|
38
|
+
readonly rows: number;
|
|
39
|
+
process(data: Uint8Array): void;
|
|
40
|
+
resize(cols: number, rows: number): void;
|
|
41
|
+
reset(): void;
|
|
42
|
+
getCell(row: number, col: number): ScreenCell;
|
|
43
|
+
getLine(row: number): ScreenCell[];
|
|
44
|
+
getScrollbackLine(row: number): ScreenCell[];
|
|
45
|
+
getText(): string;
|
|
46
|
+
getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string;
|
|
47
|
+
getCursorPosition(): {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
};
|
|
51
|
+
getCursorVisible(): boolean;
|
|
52
|
+
getTitle(): string;
|
|
53
|
+
getMode(mode: string): boolean;
|
|
54
|
+
getScrollbackLength(): number;
|
|
55
|
+
getViewportOffset(): number;
|
|
56
|
+
scrollViewport(delta: number): void;
|
|
57
|
+
}
|
|
58
|
+
declare function createScreen(opts: ScreenOptions): Screen;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { type CellColor, type ScreenCell, type Screen as Vt100Screen, type ScreenOptions as Vt100ScreenOptions, createScreen as createVt100Screen };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
//#region src/screen.ts
|
|
2
|
+
/** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
|
|
3
|
+
const EMPTY_CELL = Object.freeze({
|
|
4
|
+
char: "",
|
|
5
|
+
fg: null,
|
|
6
|
+
bg: null,
|
|
7
|
+
bold: false,
|
|
8
|
+
underline: false,
|
|
9
|
+
blink: false,
|
|
10
|
+
inverse: false,
|
|
11
|
+
hidden: false
|
|
12
|
+
});
|
|
13
|
+
function emptyCell() {
|
|
14
|
+
return { ...EMPTY_CELL };
|
|
15
|
+
}
|
|
16
|
+
function createScreen(opts) {
|
|
17
|
+
let cols = opts.cols;
|
|
18
|
+
let rows = opts.rows;
|
|
19
|
+
const scrollbackLimit = opts.scrollbackLimit ?? 1e3;
|
|
20
|
+
const onResponse = opts.onResponse;
|
|
21
|
+
let grid = makeGrid(cols, rows);
|
|
22
|
+
let scrollback = [];
|
|
23
|
+
let curX = 0;
|
|
24
|
+
let curY = 0;
|
|
25
|
+
let curVisible = true;
|
|
26
|
+
let savedCurX = 0;
|
|
27
|
+
let savedCurY = 0;
|
|
28
|
+
let savedState = {
|
|
29
|
+
curX: 0,
|
|
30
|
+
curY: 0,
|
|
31
|
+
attrs: resetAttrs(),
|
|
32
|
+
originMode: false,
|
|
33
|
+
autoWrap: true
|
|
34
|
+
};
|
|
35
|
+
let attrs = resetAttrs();
|
|
36
|
+
let title = "";
|
|
37
|
+
let applicationCursor = false;
|
|
38
|
+
let applicationKeypad = false;
|
|
39
|
+
let autoWrap = true;
|
|
40
|
+
let originMode = false;
|
|
41
|
+
let reverseVideo = false;
|
|
42
|
+
let scrollTop = 0;
|
|
43
|
+
let scrollBottom = rows - 1;
|
|
44
|
+
let viewportOffset = 0;
|
|
45
|
+
let parserState = "ground";
|
|
46
|
+
let escBuf = "";
|
|
47
|
+
let oscBuf = "";
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
function makeGrid(c, r) {
|
|
50
|
+
const g = [];
|
|
51
|
+
for (let row = 0; row < r; row++) g.push(makeRow(c));
|
|
52
|
+
return g;
|
|
53
|
+
}
|
|
54
|
+
function makeRow(c) {
|
|
55
|
+
const row = [];
|
|
56
|
+
for (let col = 0; col < c; col++) row.push(EMPTY_CELL);
|
|
57
|
+
return row;
|
|
58
|
+
}
|
|
59
|
+
function resetAttrs() {
|
|
60
|
+
return {
|
|
61
|
+
fg: null,
|
|
62
|
+
bg: null,
|
|
63
|
+
bold: false,
|
|
64
|
+
underline: false,
|
|
65
|
+
blink: false,
|
|
66
|
+
inverse: false,
|
|
67
|
+
hidden: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function clampCursor() {
|
|
71
|
+
if (curX < 0) curX = 0;
|
|
72
|
+
if (curX >= cols) curX = cols - 1;
|
|
73
|
+
if (curY < 0) curY = 0;
|
|
74
|
+
if (curY >= rows) curY = rows - 1;
|
|
75
|
+
}
|
|
76
|
+
function scrollUp(top, bottom) {
|
|
77
|
+
if (top === 0) {
|
|
78
|
+
scrollback.push(grid[0]);
|
|
79
|
+
if (scrollback.length > scrollbackLimit * 2) scrollback.splice(0, scrollback.length - scrollbackLimit);
|
|
80
|
+
}
|
|
81
|
+
for (let i = top; i < bottom; i++) grid[i] = grid[i + 1];
|
|
82
|
+
grid[bottom] = makeRow(cols);
|
|
83
|
+
}
|
|
84
|
+
function scrollDown(top, bottom) {
|
|
85
|
+
for (let i = bottom; i > top; i--) grid[i] = grid[i - 1];
|
|
86
|
+
grid[top] = makeRow(cols);
|
|
87
|
+
}
|
|
88
|
+
function writeChar(ch) {
|
|
89
|
+
if (curX >= cols) if (autoWrap) {
|
|
90
|
+
curX = 0;
|
|
91
|
+
curY++;
|
|
92
|
+
if (curY > scrollBottom) {
|
|
93
|
+
curY = scrollBottom;
|
|
94
|
+
scrollUp(scrollTop, scrollBottom);
|
|
95
|
+
}
|
|
96
|
+
} else curX = cols - 1;
|
|
97
|
+
const row = grid[curY];
|
|
98
|
+
let cell = row[curX];
|
|
99
|
+
if (cell === EMPTY_CELL) {
|
|
100
|
+
cell = { ...EMPTY_CELL };
|
|
101
|
+
row[curX] = cell;
|
|
102
|
+
}
|
|
103
|
+
cell.char = ch;
|
|
104
|
+
cell.fg = attrs.fg ? { ...attrs.fg } : null;
|
|
105
|
+
cell.bg = attrs.bg ? { ...attrs.bg } : null;
|
|
106
|
+
cell.bold = attrs.bold;
|
|
107
|
+
cell.underline = attrs.underline;
|
|
108
|
+
cell.blink = attrs.blink;
|
|
109
|
+
cell.inverse = attrs.inverse;
|
|
110
|
+
cell.hidden = attrs.hidden;
|
|
111
|
+
curX++;
|
|
112
|
+
}
|
|
113
|
+
function handleCSI(params, finalByte) {
|
|
114
|
+
const parts = params.split(";").map((s) => s === "" ? 0 : parseInt(s, 10));
|
|
115
|
+
switch (finalByte) {
|
|
116
|
+
case "A":
|
|
117
|
+
curY -= Math.max(parts[0] ?? 1, 1);
|
|
118
|
+
clampCursor();
|
|
119
|
+
break;
|
|
120
|
+
case "B":
|
|
121
|
+
curY += Math.max(parts[0] ?? 1, 1);
|
|
122
|
+
clampCursor();
|
|
123
|
+
break;
|
|
124
|
+
case "C":
|
|
125
|
+
curX += Math.max(parts[0] ?? 1, 1);
|
|
126
|
+
clampCursor();
|
|
127
|
+
break;
|
|
128
|
+
case "D":
|
|
129
|
+
curX -= Math.max(parts[0] ?? 1, 1);
|
|
130
|
+
clampCursor();
|
|
131
|
+
break;
|
|
132
|
+
case "E":
|
|
133
|
+
curY += Math.max(parts[0] ?? 1, 1);
|
|
134
|
+
curX = 0;
|
|
135
|
+
clampCursor();
|
|
136
|
+
break;
|
|
137
|
+
case "F":
|
|
138
|
+
curY -= Math.max(parts[0] ?? 1, 1);
|
|
139
|
+
curX = 0;
|
|
140
|
+
clampCursor();
|
|
141
|
+
break;
|
|
142
|
+
case "G":
|
|
143
|
+
curX = (parts[0] ?? 1) - 1;
|
|
144
|
+
clampCursor();
|
|
145
|
+
break;
|
|
146
|
+
case "H":
|
|
147
|
+
case "f":
|
|
148
|
+
curY = (parts[0] ?? 1) - 1;
|
|
149
|
+
curX = (parts[1] ?? 1) - 1;
|
|
150
|
+
clampCursor();
|
|
151
|
+
break;
|
|
152
|
+
case "J":
|
|
153
|
+
handleEraseDisplay(parts[0] ?? 0);
|
|
154
|
+
break;
|
|
155
|
+
case "K":
|
|
156
|
+
handleEraseLine(parts[0] ?? 0);
|
|
157
|
+
break;
|
|
158
|
+
case "S":
|
|
159
|
+
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) scrollUp(scrollTop, scrollBottom);
|
|
160
|
+
break;
|
|
161
|
+
case "T":
|
|
162
|
+
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) scrollDown(scrollTop, scrollBottom);
|
|
163
|
+
break;
|
|
164
|
+
case "d":
|
|
165
|
+
curY = (parts[0] ?? 1) - 1;
|
|
166
|
+
clampCursor();
|
|
167
|
+
break;
|
|
168
|
+
case "m":
|
|
169
|
+
handleSGR(params);
|
|
170
|
+
break;
|
|
171
|
+
case "r":
|
|
172
|
+
scrollTop = (parts[0] ?? 1) - 1;
|
|
173
|
+
scrollBottom = (parts[1] ?? rows) - 1;
|
|
174
|
+
if (scrollTop < 0) scrollTop = 0;
|
|
175
|
+
if (scrollBottom >= rows) scrollBottom = rows - 1;
|
|
176
|
+
if (scrollTop > scrollBottom) {
|
|
177
|
+
scrollTop = 0;
|
|
178
|
+
scrollBottom = rows - 1;
|
|
179
|
+
}
|
|
180
|
+
curX = 0;
|
|
181
|
+
curY = originMode ? scrollTop : 0;
|
|
182
|
+
break;
|
|
183
|
+
case "n":
|
|
184
|
+
if (onResponse) {
|
|
185
|
+
if (parts[0] === 5) onResponse("\x1B[0n");
|
|
186
|
+
else if (parts[0] === 6) onResponse(`\x1b[${curY + 1};${curX + 1}R`);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case "c":
|
|
190
|
+
if (onResponse) {
|
|
191
|
+
if (params === "" || params === "0") onResponse("\x1B[?1;2c");
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case "s":
|
|
195
|
+
savedCurX = curX;
|
|
196
|
+
savedCurY = curY;
|
|
197
|
+
break;
|
|
198
|
+
case "u":
|
|
199
|
+
curX = savedCurX;
|
|
200
|
+
curY = savedCurY;
|
|
201
|
+
clampCursor();
|
|
202
|
+
break;
|
|
203
|
+
default: break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function handleCSIPrivate(params, finalByte) {
|
|
207
|
+
const parts = params.split(";").map((s) => s === "" ? 0 : parseInt(s, 10));
|
|
208
|
+
const set = finalByte === "h";
|
|
209
|
+
for (const code of parts) switch (code) {
|
|
210
|
+
case 1:
|
|
211
|
+
applicationCursor = set;
|
|
212
|
+
break;
|
|
213
|
+
case 5:
|
|
214
|
+
reverseVideo = set;
|
|
215
|
+
break;
|
|
216
|
+
case 6:
|
|
217
|
+
originMode = set;
|
|
218
|
+
break;
|
|
219
|
+
case 7:
|
|
220
|
+
autoWrap = set;
|
|
221
|
+
break;
|
|
222
|
+
case 25:
|
|
223
|
+
curVisible = set;
|
|
224
|
+
break;
|
|
225
|
+
case 66:
|
|
226
|
+
applicationKeypad = set;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function handleEraseDisplay(mode) {
|
|
231
|
+
switch (mode) {
|
|
232
|
+
case 0:
|
|
233
|
+
eraseCells(curY, curX, curY, cols - 1);
|
|
234
|
+
for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1);
|
|
235
|
+
break;
|
|
236
|
+
case 1:
|
|
237
|
+
for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1);
|
|
238
|
+
eraseCells(curY, 0, curY, curX);
|
|
239
|
+
break;
|
|
240
|
+
case 2:
|
|
241
|
+
case 3:
|
|
242
|
+
for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
|
|
243
|
+
if (mode === 3) scrollback.length = 0;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function handleEraseLine(mode) {
|
|
248
|
+
switch (mode) {
|
|
249
|
+
case 0:
|
|
250
|
+
eraseCells(curY, curX, curY, cols - 1);
|
|
251
|
+
break;
|
|
252
|
+
case 1:
|
|
253
|
+
eraseCells(curY, 0, curY, curX);
|
|
254
|
+
break;
|
|
255
|
+
case 2:
|
|
256
|
+
eraseCells(curY, 0, curY, cols - 1);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function eraseCells(row, startCol, _endRow, endCol) {
|
|
261
|
+
const r = grid[row];
|
|
262
|
+
if (!r) return;
|
|
263
|
+
for (let col = startCol; col <= endCol && col < cols; col++) r[col] = emptyCell();
|
|
264
|
+
}
|
|
265
|
+
function handleSGR(rawParams) {
|
|
266
|
+
const params = rawParams.split(";").map((s) => s === "" ? 0 : parseInt(s, 10));
|
|
267
|
+
if (params.length === 0 || params.length === 1 && params[0] === 0) {
|
|
268
|
+
attrs = resetAttrs();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
let i = 0;
|
|
272
|
+
while (i < params.length) {
|
|
273
|
+
switch (params[i]) {
|
|
274
|
+
case 0:
|
|
275
|
+
attrs = resetAttrs();
|
|
276
|
+
break;
|
|
277
|
+
case 1:
|
|
278
|
+
attrs.bold = true;
|
|
279
|
+
break;
|
|
280
|
+
case 4:
|
|
281
|
+
attrs.underline = true;
|
|
282
|
+
break;
|
|
283
|
+
case 5:
|
|
284
|
+
attrs.blink = true;
|
|
285
|
+
break;
|
|
286
|
+
case 7:
|
|
287
|
+
attrs.inverse = true;
|
|
288
|
+
break;
|
|
289
|
+
case 22:
|
|
290
|
+
attrs.bold = false;
|
|
291
|
+
break;
|
|
292
|
+
case 24:
|
|
293
|
+
attrs.underline = false;
|
|
294
|
+
break;
|
|
295
|
+
case 25:
|
|
296
|
+
attrs.blink = false;
|
|
297
|
+
break;
|
|
298
|
+
case 27:
|
|
299
|
+
attrs.inverse = false;
|
|
300
|
+
break;
|
|
301
|
+
case 38:
|
|
302
|
+
case 48:
|
|
303
|
+
if (i + 1 < params.length && params[i + 1] === 2) i += 4;
|
|
304
|
+
else if (i + 1 < params.length && params[i + 1] === 5) i += 2;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
i++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function handleOSC(oscString) {
|
|
311
|
+
const semicolonIdx = oscString.indexOf(";");
|
|
312
|
+
if (semicolonIdx === -1) return;
|
|
313
|
+
const code = parseInt(oscString.substring(0, semicolonIdx), 10);
|
|
314
|
+
const value = oscString.substring(semicolonIdx + 1);
|
|
315
|
+
switch (code) {
|
|
316
|
+
case 0:
|
|
317
|
+
case 2:
|
|
318
|
+
title = value;
|
|
319
|
+
break;
|
|
320
|
+
case 1: break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function process(data) {
|
|
324
|
+
const text = decoder.decode(data, { stream: true });
|
|
325
|
+
for (let i = 0; i < text.length; i++) {
|
|
326
|
+
const ch = text[i];
|
|
327
|
+
const code = text.charCodeAt(i);
|
|
328
|
+
switch (parserState) {
|
|
329
|
+
case "ground":
|
|
330
|
+
if (code === 27) {
|
|
331
|
+
parserState = "escape";
|
|
332
|
+
escBuf = "";
|
|
333
|
+
} else if (code === 7) {} else if (code === 8) {
|
|
334
|
+
if (curX > 0) curX--;
|
|
335
|
+
} else if (code === 9) curX = Math.min((Math.floor(curX / 8) + 1) * 8, cols - 1);
|
|
336
|
+
else if (code === 10 || code === 11 || code === 12) {
|
|
337
|
+
curY++;
|
|
338
|
+
if (curY > scrollBottom) {
|
|
339
|
+
curY = scrollBottom;
|
|
340
|
+
scrollUp(scrollTop, scrollBottom);
|
|
341
|
+
}
|
|
342
|
+
} else if (code === 13) curX = 0;
|
|
343
|
+
else if (code >= 32) writeChar(ch);
|
|
344
|
+
break;
|
|
345
|
+
case "escape":
|
|
346
|
+
if (ch === "[") {
|
|
347
|
+
parserState = "csi";
|
|
348
|
+
escBuf = "";
|
|
349
|
+
} else if (ch === "]") {
|
|
350
|
+
parserState = "osc";
|
|
351
|
+
oscBuf = "";
|
|
352
|
+
} else if (ch === "P") {
|
|
353
|
+
parserState = "dcs";
|
|
354
|
+
escBuf = "";
|
|
355
|
+
} else if (ch === "c") fullReset();
|
|
356
|
+
else if (ch === "D") {
|
|
357
|
+
curY++;
|
|
358
|
+
if (curY > scrollBottom) {
|
|
359
|
+
curY = scrollBottom;
|
|
360
|
+
scrollUp(scrollTop, scrollBottom);
|
|
361
|
+
}
|
|
362
|
+
parserState = "ground";
|
|
363
|
+
} else if (ch === "M") {
|
|
364
|
+
curY--;
|
|
365
|
+
if (curY < scrollTop) {
|
|
366
|
+
curY = scrollTop;
|
|
367
|
+
scrollDown(scrollTop, scrollBottom);
|
|
368
|
+
}
|
|
369
|
+
parserState = "ground";
|
|
370
|
+
} else if (ch === "7") {
|
|
371
|
+
savedState = {
|
|
372
|
+
curX,
|
|
373
|
+
curY,
|
|
374
|
+
attrs: {
|
|
375
|
+
...attrs,
|
|
376
|
+
fg: attrs.fg ? { ...attrs.fg } : null,
|
|
377
|
+
bg: attrs.bg ? { ...attrs.bg } : null
|
|
378
|
+
},
|
|
379
|
+
originMode,
|
|
380
|
+
autoWrap
|
|
381
|
+
};
|
|
382
|
+
parserState = "ground";
|
|
383
|
+
} else if (ch === "8") {
|
|
384
|
+
curX = savedState.curX;
|
|
385
|
+
curY = savedState.curY;
|
|
386
|
+
attrs = {
|
|
387
|
+
...savedState.attrs,
|
|
388
|
+
fg: savedState.attrs.fg ? { ...savedState.attrs.fg } : null,
|
|
389
|
+
bg: savedState.attrs.bg ? { ...savedState.attrs.bg } : null
|
|
390
|
+
};
|
|
391
|
+
originMode = savedState.originMode;
|
|
392
|
+
autoWrap = savedState.autoWrap;
|
|
393
|
+
clampCursor();
|
|
394
|
+
parserState = "ground";
|
|
395
|
+
} else if (ch === "E") {
|
|
396
|
+
curX = 0;
|
|
397
|
+
curY++;
|
|
398
|
+
if (curY > scrollBottom) {
|
|
399
|
+
curY = scrollBottom;
|
|
400
|
+
scrollUp(scrollTop, scrollBottom);
|
|
401
|
+
}
|
|
402
|
+
parserState = "ground";
|
|
403
|
+
} else if (ch === "=") {
|
|
404
|
+
applicationKeypad = true;
|
|
405
|
+
parserState = "ground";
|
|
406
|
+
} else if (ch === ">") {
|
|
407
|
+
applicationKeypad = false;
|
|
408
|
+
parserState = "ground";
|
|
409
|
+
} else parserState = "ground";
|
|
410
|
+
break;
|
|
411
|
+
case "csi":
|
|
412
|
+
if (code >= 64 && code <= 126) {
|
|
413
|
+
if (escBuf.startsWith("?")) handleCSIPrivate(escBuf.substring(1), ch);
|
|
414
|
+
else handleCSI(escBuf, ch);
|
|
415
|
+
parserState = "ground";
|
|
416
|
+
} else if (escBuf.length >= 256) parserState = "ground";
|
|
417
|
+
else escBuf += ch;
|
|
418
|
+
break;
|
|
419
|
+
case "osc":
|
|
420
|
+
if (code === 7) {
|
|
421
|
+
handleOSC(oscBuf);
|
|
422
|
+
parserState = "ground";
|
|
423
|
+
} else if (code === 27) parserState = "oscString";
|
|
424
|
+
else if (oscBuf.length >= 4096) parserState = "ground";
|
|
425
|
+
else oscBuf += ch;
|
|
426
|
+
break;
|
|
427
|
+
case "oscString":
|
|
428
|
+
if (ch === "\\") handleOSC(oscBuf);
|
|
429
|
+
parserState = "ground";
|
|
430
|
+
break;
|
|
431
|
+
case "dcs":
|
|
432
|
+
if (code === 27) parserState = "oscString";
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function fullReset() {
|
|
438
|
+
grid = makeGrid(cols, rows);
|
|
439
|
+
scrollback = [];
|
|
440
|
+
curX = 0;
|
|
441
|
+
curY = 0;
|
|
442
|
+
curVisible = true;
|
|
443
|
+
savedCurX = 0;
|
|
444
|
+
savedCurY = 0;
|
|
445
|
+
savedState = {
|
|
446
|
+
curX: 0,
|
|
447
|
+
curY: 0,
|
|
448
|
+
attrs: resetAttrs(),
|
|
449
|
+
originMode: false,
|
|
450
|
+
autoWrap: true
|
|
451
|
+
};
|
|
452
|
+
attrs = resetAttrs();
|
|
453
|
+
title = "";
|
|
454
|
+
applicationCursor = false;
|
|
455
|
+
applicationKeypad = false;
|
|
456
|
+
autoWrap = true;
|
|
457
|
+
originMode = false;
|
|
458
|
+
reverseVideo = false;
|
|
459
|
+
scrollTop = 0;
|
|
460
|
+
scrollBottom = rows - 1;
|
|
461
|
+
viewportOffset = 0;
|
|
462
|
+
parserState = "ground";
|
|
463
|
+
escBuf = "";
|
|
464
|
+
oscBuf = "";
|
|
465
|
+
}
|
|
466
|
+
function resize(newCols, newRows) {
|
|
467
|
+
const newGrid = makeGrid(newCols, newRows);
|
|
468
|
+
copyGrid(grid, newGrid, Math.min(cols, newCols), Math.min(rows, newRows));
|
|
469
|
+
grid = newGrid;
|
|
470
|
+
cols = newCols;
|
|
471
|
+
rows = newRows;
|
|
472
|
+
scrollTop = 0;
|
|
473
|
+
scrollBottom = rows - 1;
|
|
474
|
+
clampCursor();
|
|
475
|
+
}
|
|
476
|
+
function copyGrid(src, dst, copyCols, copyRows) {
|
|
477
|
+
for (let row = 0; row < copyRows; row++) for (let col = 0; col < copyCols; col++) {
|
|
478
|
+
const srcCell = src[row]?.[col];
|
|
479
|
+
if (srcCell) dst[row][col] = { ...srcCell };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function getCell(row, col) {
|
|
483
|
+
const r = grid[row];
|
|
484
|
+
if (!r || col >= cols) return emptyCell();
|
|
485
|
+
return { ...r[col] };
|
|
486
|
+
}
|
|
487
|
+
function getLine(row) {
|
|
488
|
+
const r = grid[row];
|
|
489
|
+
if (!r) return makeRow(cols);
|
|
490
|
+
return r.map((cell) => ({ ...cell }));
|
|
491
|
+
}
|
|
492
|
+
function getScrollbackLine(row) {
|
|
493
|
+
const r = scrollback[row];
|
|
494
|
+
if (!r) return makeRow(cols);
|
|
495
|
+
return r.map((cell) => ({ ...cell }));
|
|
496
|
+
}
|
|
497
|
+
function getText() {
|
|
498
|
+
const lines = [];
|
|
499
|
+
for (const row of scrollback) lines.push(rowToString(row));
|
|
500
|
+
for (let r = 0; r < rows; r++) lines.push(rowToString(grid[r]));
|
|
501
|
+
return lines.join("\n");
|
|
502
|
+
}
|
|
503
|
+
function rowToString(row) {
|
|
504
|
+
let line = "";
|
|
505
|
+
for (let i = 0; i < row.length; i++) {
|
|
506
|
+
const cell = row[i];
|
|
507
|
+
if (cell.char === "") line += " ";
|
|
508
|
+
else line += cell.char;
|
|
509
|
+
}
|
|
510
|
+
return line.replace(/\s+$/, "");
|
|
511
|
+
}
|
|
512
|
+
function getTextRange(startRow, startCol, endRow, endCol) {
|
|
513
|
+
const parts = [];
|
|
514
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
515
|
+
const r = grid[row];
|
|
516
|
+
if (!r) continue;
|
|
517
|
+
const colStart = row === startRow ? startCol : 0;
|
|
518
|
+
const colEnd = row === endRow ? endCol : cols;
|
|
519
|
+
let line = "";
|
|
520
|
+
for (let col = colStart; col < colEnd; col++) {
|
|
521
|
+
const cell = r[col];
|
|
522
|
+
if (!cell) continue;
|
|
523
|
+
line += cell.char || " ";
|
|
524
|
+
}
|
|
525
|
+
parts.push(line.replace(/\s+$/, ""));
|
|
526
|
+
}
|
|
527
|
+
return parts.join("\n");
|
|
528
|
+
}
|
|
529
|
+
function getMode(mode) {
|
|
530
|
+
switch (mode) {
|
|
531
|
+
case "cursorVisible": return curVisible;
|
|
532
|
+
case "applicationCursor": return applicationCursor;
|
|
533
|
+
case "applicationKeypad": return applicationKeypad;
|
|
534
|
+
case "autoWrap": return autoWrap;
|
|
535
|
+
case "originMode": return originMode;
|
|
536
|
+
case "reverseVideo": return reverseVideo;
|
|
537
|
+
default: return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
get cols() {
|
|
542
|
+
return cols;
|
|
543
|
+
},
|
|
544
|
+
get rows() {
|
|
545
|
+
return rows;
|
|
546
|
+
},
|
|
547
|
+
process,
|
|
548
|
+
resize,
|
|
549
|
+
reset: fullReset,
|
|
550
|
+
getCell,
|
|
551
|
+
getLine,
|
|
552
|
+
getScrollbackLine,
|
|
553
|
+
getText,
|
|
554
|
+
getTextRange,
|
|
555
|
+
getCursorPosition: () => ({
|
|
556
|
+
x: curX,
|
|
557
|
+
y: curY
|
|
558
|
+
}),
|
|
559
|
+
getCursorVisible: () => curVisible,
|
|
560
|
+
getTitle: () => title,
|
|
561
|
+
getMode,
|
|
562
|
+
getScrollbackLength: () => scrollback.length,
|
|
563
|
+
getViewportOffset: () => viewportOffset,
|
|
564
|
+
scrollViewport: (delta) => {
|
|
565
|
+
viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta));
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
export { createScreen as createVt100Screen };
|