vt100.js 0.1.0 → 0.1.2

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/index.mjs ADDED
@@ -0,0 +1,564 @@
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 getText() {
493
+ const lines = [];
494
+ for (const row of scrollback) lines.push(rowToString(row));
495
+ for (let r = 0; r < rows; r++) lines.push(rowToString(grid[r]));
496
+ return lines.join("\n");
497
+ }
498
+ function rowToString(row) {
499
+ let line = "";
500
+ for (let i = 0; i < row.length; i++) {
501
+ const cell = row[i];
502
+ if (cell.char === "") line += " ";
503
+ else line += cell.char;
504
+ }
505
+ return line.replace(/\s+$/, "");
506
+ }
507
+ function getTextRange(startRow, startCol, endRow, endCol) {
508
+ const parts = [];
509
+ for (let row = startRow; row <= endRow; row++) {
510
+ const r = grid[row];
511
+ if (!r) continue;
512
+ const colStart = row === startRow ? startCol : 0;
513
+ const colEnd = row === endRow ? endCol : cols;
514
+ let line = "";
515
+ for (let col = colStart; col < colEnd; col++) {
516
+ const cell = r[col];
517
+ if (!cell) continue;
518
+ line += cell.char || " ";
519
+ }
520
+ parts.push(line.replace(/\s+$/, ""));
521
+ }
522
+ return parts.join("\n");
523
+ }
524
+ function getMode(mode) {
525
+ switch (mode) {
526
+ case "cursorVisible": return curVisible;
527
+ case "applicationCursor": return applicationCursor;
528
+ case "applicationKeypad": return applicationKeypad;
529
+ case "autoWrap": return autoWrap;
530
+ case "originMode": return originMode;
531
+ case "reverseVideo": return reverseVideo;
532
+ default: return false;
533
+ }
534
+ }
535
+ return {
536
+ get cols() {
537
+ return cols;
538
+ },
539
+ get rows() {
540
+ return rows;
541
+ },
542
+ process,
543
+ resize,
544
+ reset: fullReset,
545
+ getCell,
546
+ getLine,
547
+ getText,
548
+ getTextRange,
549
+ getCursorPosition: () => ({
550
+ x: curX,
551
+ y: curY
552
+ }),
553
+ getCursorVisible: () => curVisible,
554
+ getTitle: () => title,
555
+ getMode,
556
+ getScrollbackLength: () => scrollback.length,
557
+ getViewportOffset: () => viewportOffset,
558
+ scrollViewport: (delta) => {
559
+ viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta));
560
+ }
561
+ };
562
+ }
563
+ //#endregion
564
+ export { createScreen as createVt100Screen };
package/package.json CHANGED
@@ -1,26 +1,45 @@
1
1
  {
2
2
  "name": "vt100.js",
3
- "version": "0.1.0",
4
- "description": "Pure TypeScript VT100 terminal emulator. Zero dependencies, headless, fast.",
5
- "keywords": ["vt100", "terminal-emulator", "ansi", "typescript", "headless", "zero-dependency", "tui"],
6
- "homepage": "https://github.com/beorn/vt100",
7
- "bugs": "https://github.com/beorn/vt100/issues",
3
+ "version": "0.1.2",
4
+ "description": "VT100 terminal emulator monochrome, cursor, scroll regions. Pure TypeScript, zero dependencies.",
5
+ "keywords": [
6
+ "ansi",
7
+ "headless",
8
+ "terminal-emulator",
9
+ "tui",
10
+ "typescript",
11
+ "vt100",
12
+ "zero-dependency"
13
+ ],
14
+ "homepage": "https://github.com/beorn/vterm/tree/main/packages/vt100",
15
+ "bugs": "https://github.com/beorn/vterm/issues",
8
16
  "license": "MIT",
9
- "author": "Beorn",
10
- "repository": "github:beorn/vt100",
17
+ "author": "Bjørn Stabell",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/beorn/vterm.git",
21
+ "directory": "packages/vt100"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
11
26
  "type": "module",
12
- "module": "./src/index.ts",
13
27
  "exports": {
14
- ".": "./src/index.ts"
28
+ ".": {
29
+ "types": "./dist/index.d.mts",
30
+ "import": "./dist/index.mjs"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
15
35
  },
16
- "files": ["src"],
17
- "publishConfig": { "access": "public" },
18
- "engines": { "bun": ">=1.0.0", "node": ">=18" },
19
- "devDependencies": {
20
- "vitest": "^4.0.0"
36
+ "tsdown": {
37
+ "clean": true,
38
+ "dts": true,
39
+ "entry": "src/index.ts",
40
+ "format": "esm"
21
41
  },
22
- "scripts": {
23
- "test": "vitest run",
24
- "typecheck": "tsc --noEmit"
42
+ "engines": {
43
+ "node": ">=23.6.0"
25
44
  }
26
- }
45
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Beorn
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export {
2
- createScreen as createVt100Screen,
3
- type Screen as Vt100Screen,
4
- type ScreenOptions as Vt100ScreenOptions,
5
- type ScreenCell,
6
- type CellColor,
7
- type UnderlineStyle,
8
- } from "./screen.ts"