vt100.js 0.3.1 → 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/src/screen.ts DELETED
@@ -1,833 +0,0 @@
1
- /**
2
- * Pure TypeScript VT100 terminal emulator.
3
- *
4
- * Strict DEC VT100 (1978) implementation: monochrome display, cursor movement,
5
- * scroll regions, auto-wrap, origin mode, DEC special graphics charset.
6
- * SGR supports bold, underline, blink, and reverse — NO colors (VT100 is
7
- * monochrome). No insert mode, no insert/delete chars/lines — those are
8
- * VT102/VT220 features.
9
- *
10
- * Zero dependencies. For colors and VT220 features, use vt220.js.
11
- * For truecolor, 256 colors, and wide chars, use vterm.js.
12
- */
13
-
14
- // ═══════════════════════════════════════════════════════
15
- // Internal cell representation
16
- // ═══════════════════════════════════════════════════════
17
-
18
- export interface CellColor {
19
- r: number
20
- g: number
21
- b: number
22
- }
23
-
24
- export interface ScreenCell {
25
- char: string
26
- fg: CellColor | null
27
- bg: CellColor | null
28
- bold: boolean
29
- underline: boolean
30
- blink: boolean
31
- inverse: boolean
32
- hidden: boolean
33
- }
34
-
35
- /** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
36
- const EMPTY_CELL: ScreenCell = Object.freeze({
37
- char: "",
38
- fg: null,
39
- bg: null,
40
- bold: false,
41
- underline: false,
42
- blink: false,
43
- inverse: false,
44
- hidden: false,
45
- })
46
-
47
- function emptyCell(): ScreenCell {
48
- return { ...EMPTY_CELL }
49
- }
50
-
51
- // ═══════════════════════════════════════════════════════
52
- // Screen
53
- // ═══════════════════════════════════════════════════════
54
-
55
- export interface ScreenOptions {
56
- cols: number
57
- rows: number
58
- scrollbackLimit?: number
59
- /** Callback for DA1/DSR responses — write these back to the PTY */
60
- onResponse?: (data: string) => void
61
- }
62
-
63
- interface Attrs {
64
- fg: CellColor | null
65
- bg: CellColor | null
66
- bold: boolean
67
- underline: boolean
68
- blink: boolean
69
- inverse: boolean
70
- hidden: boolean
71
- }
72
-
73
- export interface Screen {
74
- readonly cols: number
75
- readonly rows: number
76
- process(data: Uint8Array): void
77
- resize(cols: number, rows: number): void
78
- reset(): void
79
- getCell(row: number, col: number): ScreenCell
80
- getLine(row: number): ScreenCell[]
81
- getText(): string
82
- getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string
83
- getCursorPosition(): { x: number; y: number }
84
- getCursorVisible(): boolean
85
- getTitle(): string
86
- getMode(mode: string): boolean
87
- getScrollbackLength(): number
88
- getViewportOffset(): number
89
- scrollViewport(delta: number): void
90
- }
91
-
92
- export function createScreen(opts: ScreenOptions): Screen {
93
- let cols = opts.cols
94
- let rows = opts.rows
95
- const scrollbackLimit = opts.scrollbackLimit ?? 1000
96
- const onResponse = opts.onResponse
97
-
98
- // Main screen buffer (no alternate screen in VT100)
99
- let grid: ScreenCell[][] = makeGrid(cols, rows)
100
- let scrollback: ScreenCell[][] = []
101
-
102
- // Cursor
103
- let curX = 0
104
- let curY = 0
105
- let curVisible = true
106
- let savedCurX = 0
107
- let savedCurY = 0
108
-
109
- // DECSC/DECRC saved state (cursor + attrs + modes)
110
- interface SavedState {
111
- curX: number
112
- curY: number
113
- attrs: Attrs
114
- originMode: boolean
115
- autoWrap: boolean
116
- }
117
- let savedState: SavedState = {
118
- curX: 0,
119
- curY: 0,
120
- attrs: resetAttrs(),
121
- originMode: false,
122
- autoWrap: true,
123
- }
124
-
125
- // Current drawing attributes
126
- let attrs: Attrs = resetAttrs()
127
-
128
- // Terminal state
129
- let title = ""
130
- let applicationCursor = false
131
- let applicationKeypad = false
132
- let autoWrap = true
133
- let originMode = false
134
- let reverseVideo = false
135
-
136
- // Scroll region (inclusive, 0-based)
137
- let scrollTop = 0
138
- let scrollBottom = rows - 1
139
-
140
- // Viewport scroll offset for scrollViewport()
141
- let viewportOffset = 0
142
-
143
- // Parser state
144
- let parserState: "ground" | "escape" | "csi" | "osc" | "dcs" | "oscString" = "ground"
145
- let escBuf = ""
146
- let oscBuf = ""
147
-
148
- // Decoder for incoming bytes
149
- const decoder = new TextDecoder()
150
-
151
- function makeGrid(c: number, r: number): ScreenCell[][] {
152
- const g: ScreenCell[][] = []
153
- for (let row = 0; row < r; row++) {
154
- g.push(makeRow(c))
155
- }
156
- return g
157
- }
158
-
159
- function makeRow(c: number): ScreenCell[] {
160
- const row: ScreenCell[] = []
161
- for (let col = 0; col < c; col++) {
162
- row.push(EMPTY_CELL)
163
- }
164
- return row
165
- }
166
-
167
- function resetAttrs(): Attrs {
168
- return {
169
- fg: null,
170
- bg: null,
171
- bold: false,
172
- underline: false,
173
- blink: false,
174
- inverse: false,
175
- hidden: false,
176
- }
177
- }
178
-
179
- function clampCursor(): void {
180
- if (curX < 0) curX = 0
181
- if (curX >= cols) curX = cols - 1
182
- if (curY < 0) curY = 0
183
- if (curY >= rows) curY = rows - 1
184
- }
185
-
186
- // ── Scrolling ──
187
-
188
- function scrollUp(top: number, bottom: number): void {
189
- // Move top row to scrollback (only if top of screen)
190
- if (top === 0) {
191
- scrollback.push(grid[0]!)
192
- // Bulk trim when exceeding 2x limit to avoid O(n) shift() on every scroll
193
- if (scrollback.length > scrollbackLimit * 2) {
194
- scrollback.splice(0, scrollback.length - scrollbackLimit)
195
- }
196
- }
197
- // Shift rows up within the region
198
- for (let i = top; i < bottom; i++) {
199
- grid[i] = grid[i + 1]!
200
- }
201
- grid[bottom] = makeRow(cols)
202
- }
203
-
204
- function scrollDown(top: number, bottom: number): void {
205
- for (let i = bottom; i > top; i--) {
206
- grid[i] = grid[i - 1]!
207
- }
208
- grid[top] = makeRow(cols)
209
- }
210
-
211
- // ── Character writing ──
212
-
213
- function writeChar(ch: string): void {
214
- // Handle autowrap at end of line
215
- if (curX >= cols) {
216
- if (autoWrap) {
217
- curX = 0
218
- curY++
219
- if (curY > scrollBottom) {
220
- curY = scrollBottom
221
- scrollUp(scrollTop, scrollBottom)
222
- }
223
- } else {
224
- curX = cols - 1
225
- }
226
- }
227
-
228
- // Copy-on-write: if cell is the shared EMPTY_CELL sentinel, create a fresh object
229
- const row = grid[curY]!
230
- let cell = row[curX]!
231
- if (cell === EMPTY_CELL) {
232
- cell = { ...EMPTY_CELL }
233
- row[curX] = cell
234
- }
235
- cell.char = ch
236
- cell.fg = attrs.fg ? { ...attrs.fg } : null
237
- cell.bg = attrs.bg ? { ...attrs.bg } : null
238
- cell.bold = attrs.bold
239
- cell.underline = attrs.underline
240
- cell.blink = attrs.blink
241
- cell.inverse = attrs.inverse
242
- cell.hidden = attrs.hidden
243
-
244
- curX++
245
- }
246
-
247
- // ── CSI handler ──
248
-
249
- function handleCSI(params: string, finalByte: string): void {
250
- const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
251
-
252
- switch (finalByte) {
253
- case "A": // CUU - Cursor Up
254
- curY -= Math.max(parts[0] ?? 1, 1)
255
- clampCursor()
256
- break
257
- case "B": // CUD - Cursor Down
258
- curY += Math.max(parts[0] ?? 1, 1)
259
- clampCursor()
260
- break
261
- case "C": // CUF - Cursor Forward
262
- curX += Math.max(parts[0] ?? 1, 1)
263
- clampCursor()
264
- break
265
- case "D": // CUB - Cursor Back
266
- curX -= Math.max(parts[0] ?? 1, 1)
267
- clampCursor()
268
- break
269
- case "E": // CNL - Cursor Next Line
270
- curY += Math.max(parts[0] ?? 1, 1)
271
- curX = 0
272
- clampCursor()
273
- break
274
- case "F": // CPL - Cursor Previous Line
275
- curY -= Math.max(parts[0] ?? 1, 1)
276
- curX = 0
277
- clampCursor()
278
- break
279
- case "G": // CHA - Cursor Horizontal Absolute
280
- curX = (parts[0] ?? 1) - 1
281
- clampCursor()
282
- break
283
- case "H": // CUP - Cursor Position
284
- case "f": // HVP - same as CUP
285
- curY = (parts[0] ?? 1) - 1
286
- curX = (parts[1] ?? 1) - 1
287
- clampCursor()
288
- break
289
- case "J": // ED - Erase in Display
290
- handleEraseDisplay(parts[0] ?? 0)
291
- break
292
- case "K": // EL - Erase in Line
293
- handleEraseLine(parts[0] ?? 0)
294
- break
295
- case "S": // SU - Scroll Up
296
- for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
297
- scrollUp(scrollTop, scrollBottom)
298
- }
299
- break
300
- case "T": // SD - Scroll Down
301
- for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
302
- scrollDown(scrollTop, scrollBottom)
303
- }
304
- break
305
- case "d": // VPA - Line Position Absolute
306
- curY = (parts[0] ?? 1) - 1
307
- clampCursor()
308
- break
309
- case "m": // SGR - Select Graphic Rendition
310
- handleSGR(params)
311
- break
312
- case "r": // DECSTBM - Set Scrolling Region
313
- scrollTop = (parts[0] ?? 1) - 1
314
- scrollBottom = (parts[1] ?? rows) - 1
315
- if (scrollTop < 0) scrollTop = 0
316
- if (scrollBottom >= rows) scrollBottom = rows - 1
317
- if (scrollTop > scrollBottom) {
318
- scrollTop = 0
319
- scrollBottom = rows - 1
320
- }
321
- curX = 0
322
- curY = originMode ? scrollTop : 0
323
- break
324
- case "n": // DSR - Device Status Report
325
- if (onResponse) {
326
- if (parts[0] === 5) {
327
- // Status report - OK
328
- onResponse("\x1b[0n")
329
- } else if (parts[0] === 6) {
330
- // CPR - Cursor position report (1-based)
331
- onResponse(`\x1b[${curY + 1};${curX + 1}R`)
332
- }
333
- }
334
- break
335
- case "c": // DA1 - Primary Device Attributes
336
- if (onResponse) {
337
- if (params === "" || params === "0") {
338
- // VT100 with Advanced Video Option (AVO)
339
- onResponse("\x1b[?1;2c")
340
- }
341
- }
342
- break
343
- case "s": // SCP - Save Cursor Position
344
- savedCurX = curX
345
- savedCurY = curY
346
- break
347
- case "u": // RCP - Restore Cursor Position
348
- curX = savedCurX
349
- curY = savedCurY
350
- clampCursor()
351
- break
352
- default:
353
- // Unknown CSI sequence — ignore
354
- break
355
- }
356
- }
357
-
358
- function handleCSIPrivate(params: string, finalByte: string): void {
359
- const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
360
- const set = finalByte === "h"
361
-
362
- for (const code of parts) {
363
- switch (code) {
364
- case 1: // DECCKM - Application Cursor
365
- applicationCursor = set
366
- break
367
- case 5: // DECSCNM - Reverse Video
368
- reverseVideo = set
369
- break
370
- case 6: // DECOM - Origin Mode
371
- originMode = set
372
- break
373
- case 7: // DECAWM - Autowrap Mode
374
- autoWrap = set
375
- break
376
- case 25: // DECTCEM - Cursor Visible
377
- curVisible = set
378
- break
379
- case 66: // DECNKM - Application Keypad
380
- applicationKeypad = set
381
- break
382
- }
383
- }
384
- }
385
-
386
- function handleEraseDisplay(mode: number): void {
387
- switch (mode) {
388
- case 0: // Erase from cursor to end
389
- eraseCells(curY, curX, curY, cols - 1)
390
- for (let row = curY + 1; row < rows; row++) {
391
- eraseCells(row, 0, row, cols - 1)
392
- }
393
- break
394
- case 1: // Erase from start to cursor
395
- for (let row = 0; row < curY; row++) {
396
- eraseCells(row, 0, row, cols - 1)
397
- }
398
- eraseCells(curY, 0, curY, curX)
399
- break
400
- case 2: // Erase entire display
401
- case 3: // Erase entire display + scrollback
402
- for (let row = 0; row < rows; row++) {
403
- eraseCells(row, 0, row, cols - 1)
404
- }
405
- if (mode === 3) {
406
- scrollback.length = 0
407
- }
408
- break
409
- }
410
- }
411
-
412
- function handleEraseLine(mode: number): void {
413
- switch (mode) {
414
- case 0: // Erase from cursor to end of line
415
- eraseCells(curY, curX, curY, cols - 1)
416
- break
417
- case 1: // Erase from start to cursor
418
- eraseCells(curY, 0, curY, curX)
419
- break
420
- case 2: // Erase entire line
421
- eraseCells(curY, 0, curY, cols - 1)
422
- break
423
- }
424
- }
425
-
426
- function eraseCells(row: number, startCol: number, _endRow: number, endCol: number): void {
427
- const r = grid[row]
428
- if (!r) return
429
- for (let col = startCol; col <= endCol && col < cols; col++) {
430
- r[col] = emptyCell()
431
- }
432
- }
433
-
434
- // ── SGR (Select Graphic Rendition) ──
435
- // VT100 is monochrome: only bold (1), underline (4), blink (5), reverse (7).
436
- // Color codes are silently ignored for forward compatibility.
437
-
438
- function handleSGR(rawParams: string): void {
439
- const params = rawParams.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
440
-
441
- if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
442
- attrs = resetAttrs()
443
- return
444
- }
445
-
446
- let i = 0
447
- while (i < params.length) {
448
- const code = params[i]!
449
- switch (code) {
450
- case 0:
451
- attrs = resetAttrs()
452
- break
453
- case 1:
454
- attrs.bold = true
455
- break
456
- case 4:
457
- attrs.underline = true
458
- break
459
- case 5: // Blink
460
- attrs.blink = true
461
- break
462
- case 7:
463
- attrs.inverse = true
464
- break
465
- case 22: // Normal intensity (turn off bold)
466
- attrs.bold = false
467
- break
468
- case 24:
469
- attrs.underline = false
470
- break
471
- case 25: // Blink off
472
- attrs.blink = false
473
- break
474
- case 27:
475
- attrs.inverse = false
476
- break
477
- // Color codes silently ignored — VT100 is monochrome
478
- // Skip extended color sequences to consume params correctly
479
- case 38:
480
- case 48:
481
- if (i + 1 < params.length && params[i + 1] === 2) {
482
- i += 4 // skip 38;2;R;G;B or 48;2;R;G;B
483
- } else if (i + 1 < params.length && params[i + 1] === 5) {
484
- i += 2 // skip 38;5;N or 48;5;N
485
- }
486
- break
487
- // All other SGR codes (colors 30-37, 40-47, 39, 49, 8, 28, etc.)
488
- // are silently ignored for forward compatibility
489
- }
490
- i++
491
- }
492
- }
493
-
494
- // ── OSC handler ──
495
-
496
- function handleOSC(oscString: string): void {
497
- const semicolonIdx = oscString.indexOf(";")
498
- if (semicolonIdx === -1) return
499
-
500
- const code = parseInt(oscString.substring(0, semicolonIdx), 10)
501
- const value = oscString.substring(semicolonIdx + 1)
502
-
503
- switch (code) {
504
- case 0: // Set icon name and window title
505
- case 2: // Set window title
506
- title = value
507
- break
508
- case 1: // Set icon name (ignore)
509
- break
510
- }
511
- }
512
-
513
- // ── Main parser ──
514
-
515
- function process(data: Uint8Array): void {
516
- const text = decoder.decode(data, { stream: true })
517
-
518
- for (let i = 0; i < text.length; i++) {
519
- const ch = text[i]!
520
- const code = text.charCodeAt(i)
521
-
522
- switch (parserState) {
523
- case "ground":
524
- if (code === 0x1b) {
525
- parserState = "escape"
526
- escBuf = ""
527
- } else if (code === 0x07) {
528
- // BEL — ignore
529
- } else if (code === 0x08) {
530
- // BS - Backspace
531
- if (curX > 0) curX--
532
- } else if (code === 0x09) {
533
- // TAB
534
- curX = Math.min((Math.floor(curX / 8) + 1) * 8, cols - 1)
535
- } else if (code === 0x0a || code === 0x0b || code === 0x0c) {
536
- // LF, VT, FF — linefeed
537
- curY++
538
- if (curY > scrollBottom) {
539
- curY = scrollBottom
540
- scrollUp(scrollTop, scrollBottom)
541
- }
542
- } else if (code === 0x0d) {
543
- // CR - Carriage Return
544
- curX = 0
545
- } else if (code >= 0x20) {
546
- writeChar(ch)
547
- }
548
- break
549
-
550
- case "escape":
551
- if (ch === "[") {
552
- parserState = "csi"
553
- escBuf = ""
554
- } else if (ch === "]") {
555
- parserState = "osc"
556
- oscBuf = ""
557
- } else if (ch === "P") {
558
- parserState = "dcs"
559
- escBuf = ""
560
- } else if (ch === "c") {
561
- // RIS - Reset to Initial State
562
- fullReset()
563
- } else if (ch === "D") {
564
- // IND - Index (move cursor down, scroll if needed)
565
- curY++
566
- if (curY > scrollBottom) {
567
- curY = scrollBottom
568
- scrollUp(scrollTop, scrollBottom)
569
- }
570
- parserState = "ground"
571
- } else if (ch === "M") {
572
- // RI - Reverse Index (move cursor up, scroll if needed)
573
- curY--
574
- if (curY < scrollTop) {
575
- curY = scrollTop
576
- scrollDown(scrollTop, scrollBottom)
577
- }
578
- parserState = "ground"
579
- } else if (ch === "7") {
580
- // DECSC - Save Cursor + attributes + modes
581
- savedState = {
582
- curX,
583
- curY,
584
- attrs: { ...attrs, fg: attrs.fg ? { ...attrs.fg } : null, bg: attrs.bg ? { ...attrs.bg } : null },
585
- originMode,
586
- autoWrap,
587
- }
588
- parserState = "ground"
589
- } else if (ch === "8") {
590
- // DECRC - Restore Cursor + attributes + modes
591
- curX = savedState.curX
592
- curY = savedState.curY
593
- attrs = {
594
- ...savedState.attrs,
595
- fg: savedState.attrs.fg ? { ...savedState.attrs.fg } : null,
596
- bg: savedState.attrs.bg ? { ...savedState.attrs.bg } : null,
597
- }
598
- originMode = savedState.originMode
599
- autoWrap = savedState.autoWrap
600
- clampCursor()
601
- parserState = "ground"
602
- } else if (ch === "E") {
603
- // NEL - Next Line
604
- curX = 0
605
- curY++
606
- if (curY > scrollBottom) {
607
- curY = scrollBottom
608
- scrollUp(scrollTop, scrollBottom)
609
- }
610
- parserState = "ground"
611
- } else if (ch === "=") {
612
- // DECKPAM - Application Keypad Mode
613
- applicationKeypad = true
614
- parserState = "ground"
615
- } else if (ch === ">") {
616
- // DECKPNM - Normal Keypad Mode
617
- applicationKeypad = false
618
- parserState = "ground"
619
- } else {
620
- // Unknown escape — return to ground
621
- parserState = "ground"
622
- }
623
- break
624
-
625
- case "csi":
626
- if (code >= 0x40 && code <= 0x7e) {
627
- // Final byte — dispatch CSI
628
- if (escBuf.startsWith("?")) {
629
- handleCSIPrivate(escBuf.substring(1), ch)
630
- } else {
631
- handleCSI(escBuf, ch)
632
- }
633
- parserState = "ground"
634
- } else if (escBuf.length >= 256) {
635
- // Buffer overflow — drop to ground to avoid unbounded accumulation
636
- parserState = "ground"
637
- } else {
638
- // Parameter or intermediate byte
639
- escBuf += ch
640
- }
641
- break
642
-
643
- case "osc":
644
- if (code === 0x07) {
645
- // BEL terminates OSC
646
- handleOSC(oscBuf)
647
- parserState = "ground"
648
- } else if (code === 0x1b) {
649
- // ESC might be start of ST (\x1b\\)
650
- parserState = "oscString"
651
- } else if (oscBuf.length >= 4096) {
652
- // Buffer overflow — drop to ground to avoid unbounded accumulation
653
- parserState = "ground"
654
- } else {
655
- oscBuf += ch
656
- }
657
- break
658
-
659
- case "oscString":
660
- if (ch === "\\") {
661
- // ST (String Terminator) — end of OSC
662
- handleOSC(oscBuf)
663
- }
664
- // Either way, back to ground
665
- parserState = "ground"
666
- break
667
-
668
- case "dcs":
669
- // Consume until ST
670
- if (code === 0x1b) {
671
- parserState = "oscString" // Reuse ST detection
672
- }
673
- break
674
- }
675
- }
676
- }
677
-
678
- function fullReset(): void {
679
- grid = makeGrid(cols, rows)
680
- scrollback = []
681
- curX = 0
682
- curY = 0
683
- curVisible = true
684
- savedCurX = 0
685
- savedCurY = 0
686
- savedState = { curX: 0, curY: 0, attrs: resetAttrs(), originMode: false, autoWrap: true }
687
- attrs = resetAttrs()
688
- title = ""
689
- applicationCursor = false
690
- applicationKeypad = false
691
- autoWrap = true
692
- originMode = false
693
- reverseVideo = false
694
- scrollTop = 0
695
- scrollBottom = rows - 1
696
- viewportOffset = 0
697
- parserState = "ground"
698
- escBuf = ""
699
- oscBuf = ""
700
- }
701
-
702
- function resize(newCols: number, newRows: number): void {
703
- const newGrid = makeGrid(newCols, newRows)
704
-
705
- // Copy content from old grid
706
- copyGrid(grid, newGrid, Math.min(cols, newCols), Math.min(rows, newRows))
707
-
708
- grid = newGrid
709
- cols = newCols
710
- rows = newRows
711
- scrollTop = 0
712
- scrollBottom = rows - 1
713
- clampCursor()
714
- }
715
-
716
- function copyGrid(src: ScreenCell[][], dst: ScreenCell[][], copyCols: number, copyRows: number): void {
717
- for (let row = 0; row < copyRows; row++) {
718
- for (let col = 0; col < copyCols; col++) {
719
- const srcCell = src[row]?.[col]
720
- if (srcCell) {
721
- dst[row]![col] = { ...srcCell }
722
- }
723
- }
724
- }
725
- }
726
-
727
- function getCell(row: number, col: number): ScreenCell {
728
- const r = grid[row]
729
- if (!r || col >= cols) return emptyCell()
730
- return { ...r[col]! }
731
- }
732
-
733
- function getLine(row: number): ScreenCell[] {
734
- const r = grid[row]
735
- if (!r) return makeRow(cols)
736
- return r.map((cell) => ({ ...cell }))
737
- }
738
-
739
- function getText(): string {
740
- const lines: string[] = []
741
-
742
- // Scrollback
743
- for (const row of scrollback) {
744
- lines.push(rowToString(row))
745
- }
746
-
747
- // Screen
748
- for (let r = 0; r < rows; r++) {
749
- lines.push(rowToString(grid[r]!))
750
- }
751
-
752
- return lines.join("\n")
753
- }
754
-
755
- function rowToString(row: ScreenCell[]): string {
756
- let line = ""
757
- for (let i = 0; i < row.length; i++) {
758
- const cell = row[i]!
759
- if (cell.char === "") {
760
- line += " "
761
- } else {
762
- line += cell.char
763
- }
764
- }
765
- return line.replace(/\s+$/, "") // Trim trailing whitespace
766
- }
767
-
768
- function getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string {
769
- const parts: string[] = []
770
-
771
- for (let row = startRow; row <= endRow; row++) {
772
- const r = grid[row]
773
- if (!r) continue
774
-
775
- const colStart = row === startRow ? startCol : 0
776
- const colEnd = row === endRow ? endCol : cols
777
-
778
- let line = ""
779
- for (let col = colStart; col < colEnd; col++) {
780
- const cell = r[col]
781
- if (!cell) continue
782
- line += cell.char || " "
783
- }
784
- parts.push(line.replace(/\s+$/, ""))
785
- }
786
-
787
- return parts.join("\n")
788
- }
789
-
790
- function getMode(mode: string): boolean {
791
- switch (mode) {
792
- case "cursorVisible":
793
- return curVisible
794
- case "applicationCursor":
795
- return applicationCursor
796
- case "applicationKeypad":
797
- return applicationKeypad
798
- case "autoWrap":
799
- return autoWrap
800
- case "originMode":
801
- return originMode
802
- case "reverseVideo":
803
- return reverseVideo
804
- default:
805
- return false
806
- }
807
- }
808
-
809
- return {
810
- get cols() {
811
- return cols
812
- },
813
- get rows() {
814
- return rows
815
- },
816
- process,
817
- resize,
818
- reset: fullReset,
819
- getCell,
820
- getLine,
821
- getText,
822
- getTextRange,
823
- getCursorPosition: () => ({ x: curX, y: curY }),
824
- getCursorVisible: () => curVisible,
825
- getTitle: () => title,
826
- getMode,
827
- getScrollbackLength: () => scrollback.length,
828
- getViewportOffset: () => viewportOffset,
829
- scrollViewport: (delta: number) => {
830
- viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta))
831
- },
832
- }
833
- }