vterm.js 0.2.0 → 0.3.1
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 +23 -20
- package/package.json +2 -2
- package/src/screen.ts +376 -51
package/README.md
CHANGED
|
@@ -152,26 +152,29 @@ interface ScreenCell {
|
|
|
152
152
|
|
|
153
153
|
## vs vt100.js
|
|
154
154
|
|
|
155
|
-
| Feature
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
174
|
-
|
|
155
|
+
| Feature | vt100.js | vterm.js |
|
|
156
|
+
| ----------------------------------------- | -------- | -------- |
|
|
157
|
+
| Bold, underline (boolean), blink, inverse | Yes | Yes |
|
|
158
|
+
| Colors (8/256/truecolor) | No | Yes |
|
|
159
|
+
| Italic, faint, strikethrough, overline | No | Yes |
|
|
160
|
+
| Underline styles (curly, dotted, dashed) | No | Yes |
|
|
161
|
+
| Underline color | No | Yes |
|
|
162
|
+
| Wide characters (CJK, emoji) | No | Yes |
|
|
163
|
+
| Cursor shapes (DECSCUSR) | No | Yes |
|
|
164
|
+
| Insert/delete characters and lines | No | Yes |
|
|
165
|
+
| OSC 8 hyperlinks | No | Yes |
|
|
166
|
+
| DA1/DA2/DA3 responses | DA1 only | Yes |
|
|
167
|
+
| DSR/DECRPM responses | No | Yes |
|
|
168
|
+
| Mouse tracking | No | Yes |
|
|
169
|
+
| Focus tracking | No | Yes |
|
|
170
|
+
| Synchronized output | No | Yes |
|
|
171
|
+
| DEC Special Graphics | Yes | Yes |
|
|
172
|
+
| REP (repeat character) | No | Yes |
|
|
173
|
+
| DECSTR (soft reset) | No | Yes |
|
|
174
|
+
| DCS sequences | No | Yes |
|
|
175
|
+
| Package size | Smaller | Larger |
|
|
176
|
+
|
|
177
|
+
**Use vt100.js** for strict VT100 conformance — monochrome, no colors, no insert/delete. Fast and minimal.
|
|
175
178
|
|
|
176
179
|
**Use vterm.js** when you need everything — 100% coverage of the [terminfo.dev](https://terminfo.dev) feature matrix.
|
|
177
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vterm.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Modern terminal emulator — full VT/ECMA-48/xterm standards coverage. Pure TypeScript, zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"homepage": "https://github.com/beorn/vterm/tree/main/packages/vterm",
|
|
18
18
|
"bugs": "https://github.com/beorn/vterm/issues",
|
|
19
19
|
"license": "MIT",
|
|
20
|
-
"author": "
|
|
20
|
+
"author": "Bjørn Stabell",
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "https://github.com/beorn/vterm.git",
|
package/src/screen.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - Scrollback buffer with configurable limit
|
|
21
21
|
* - Wide character support (CJK, emoji ZWJ, regional indicators, VS-16)
|
|
22
22
|
* - OSC sequences (title, hyperlinks, clipboard, colors)
|
|
23
|
-
* - DCS sequences (
|
|
23
|
+
* - DCS sequences (XTVERSION, DECRQSS, XTGETTCAP, Sixel)
|
|
24
24
|
* - APC sequences (Kitty graphics protocol — parsed, query responses)
|
|
25
25
|
* - DA1/DA2/DA3 device attribute responses
|
|
26
26
|
* - DSR (device status report) responses
|
|
@@ -101,6 +101,9 @@ export interface Screen {
|
|
|
101
101
|
|
|
102
102
|
getTitle(): string
|
|
103
103
|
getMode(mode: string): boolean
|
|
104
|
+
getClipboard(): string
|
|
105
|
+
getCwd(): string
|
|
106
|
+
getNotifications(): string[]
|
|
104
107
|
|
|
105
108
|
getScrollbackLength(): number
|
|
106
109
|
getViewportOffset(): number
|
|
@@ -376,16 +379,40 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
376
379
|
let syncOutput = false
|
|
377
380
|
|
|
378
381
|
// Kitty keyboard protocol
|
|
382
|
+
// Headless design: we implement the full push/pop/query state machine so that
|
|
383
|
+
// applications can negotiate keyboard encoding modes. The flags are stored and
|
|
384
|
+
// queryable (CSI ? u responds correctly), and a host can read them to decide
|
|
385
|
+
// how to encode keypresses. The actual key encoding lives in the host layer
|
|
386
|
+
// (e.g. termless's encodeKeyToAnsi), not here — separation of concerns.
|
|
379
387
|
let kittyKeyboardFlags = 0
|
|
380
388
|
let kittyKeyboardStack: number[] = []
|
|
381
389
|
|
|
382
390
|
// Kitty graphics protocol
|
|
391
|
+
// Headless design: we parse the APC G protocol and respond to queries so that
|
|
392
|
+
// applications relying on the query→response handshake work correctly. We don't
|
|
393
|
+
// store image data because there's no pixel framebuffer — unlike sixel where we
|
|
394
|
+
// preserve the raw data for consumers, kitty graphics payloads are chunked and
|
|
395
|
+
// stateful, making storage without rendering impractical.
|
|
383
396
|
let hasKittyGraphics = false
|
|
384
397
|
|
|
385
398
|
// Scroll region (inclusive, 0-based)
|
|
386
399
|
let scrollTop = 0
|
|
387
400
|
let scrollBottom = rows - 1
|
|
388
401
|
|
|
402
|
+
// Left/right margin mode (DECLRMM, DECSET ?69)
|
|
403
|
+
let leftRightMarginMode = false
|
|
404
|
+
let leftMargin = 0
|
|
405
|
+
let rightMargin = cols - 1
|
|
406
|
+
|
|
407
|
+
// Color scheme reporting (mode 2031)
|
|
408
|
+
let colorSchemeReporting = false
|
|
409
|
+
|
|
410
|
+
// Text scale (OSC 66)
|
|
411
|
+
let textScale = 1
|
|
412
|
+
|
|
413
|
+
// Advanced clipboard (OSC 5522 — Kitty clipboard protocol)
|
|
414
|
+
let advancedClipboard = ""
|
|
415
|
+
|
|
389
416
|
// Viewport scroll offset
|
|
390
417
|
let viewportOffset = 0
|
|
391
418
|
|
|
@@ -395,10 +422,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
395
422
|
// Clipboard (OSC 52)
|
|
396
423
|
let clipboard = ""
|
|
397
424
|
|
|
398
|
-
//
|
|
425
|
+
// Current working directory (OSC 7)
|
|
426
|
+
let cwd = ""
|
|
427
|
+
|
|
428
|
+
// Notifications (OSC 9)
|
|
429
|
+
let notifications: string[] = []
|
|
430
|
+
|
|
431
|
+
// Semantic prompt zones (OSC 133 / OSC 633)
|
|
399
432
|
let semanticZones: SemanticZone[] = []
|
|
400
433
|
|
|
401
434
|
// Sixel graphics (DCS q)
|
|
435
|
+
// Headless design: we parse sixel data and store it for consumers (e.g. a GUI
|
|
436
|
+
// renderer could use getSixelImages() to display them). We don't decode pixels
|
|
437
|
+
// because there's no framebuffer to render into — but preserving the data means
|
|
438
|
+
// a host application gets full fidelity without re-parsing the stream.
|
|
402
439
|
let hasSixel = false
|
|
403
440
|
let sixelImages: SixelImage[] = []
|
|
404
441
|
|
|
@@ -483,28 +520,65 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
483
520
|
// ── Scrolling ──
|
|
484
521
|
|
|
485
522
|
function scrollUp(top: number, bottom: number): void {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
523
|
+
if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
|
|
524
|
+
// Scroll only within the left/right margin columns
|
|
525
|
+
const lm = leftMargin
|
|
526
|
+
const rm = rightMargin
|
|
527
|
+
for (let r = top; r < bottom; r++) {
|
|
528
|
+
const srcRow = grid[r + 1]!
|
|
529
|
+
const dstRow = grid[r]!
|
|
530
|
+
for (let c = lm; c <= rm && c < cols; c++) {
|
|
531
|
+
dstRow[c] = srcRow[c]!
|
|
532
|
+
}
|
|
491
533
|
}
|
|
534
|
+
// Clear the bottom row within margins
|
|
535
|
+
const bottomRow = grid[bottom]!
|
|
536
|
+
for (let c = lm; c <= rm && c < cols; c++) {
|
|
537
|
+
bottomRow[c] = EMPTY_CELL
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
// Full-width scroll
|
|
541
|
+
// Move top row to scrollback (only if main screen & top of screen)
|
|
542
|
+
if (grid === mainGrid && top === 0) {
|
|
543
|
+
scrollback.push(grid[0]!)
|
|
544
|
+
if (scrollback.length > scrollbackLimit * 2) {
|
|
545
|
+
scrollback.splice(0, scrollback.length - scrollbackLimit)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (let i = top; i < bottom; i++) {
|
|
549
|
+
grid[i] = grid[i + 1]!
|
|
550
|
+
softWrapped[i] = softWrapped[i + 1]!
|
|
551
|
+
}
|
|
552
|
+
grid[bottom] = makeRow(cols)
|
|
553
|
+
softWrapped[bottom] = false
|
|
492
554
|
}
|
|
493
|
-
for (let i = top; i < bottom; i++) {
|
|
494
|
-
grid[i] = grid[i + 1]!
|
|
495
|
-
softWrapped[i] = softWrapped[i + 1]!
|
|
496
|
-
}
|
|
497
|
-
grid[bottom] = makeRow(cols)
|
|
498
|
-
softWrapped[bottom] = false
|
|
499
555
|
}
|
|
500
556
|
|
|
501
557
|
function scrollDown(top: number, bottom: number): void {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
558
|
+
if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
|
|
559
|
+
// Scroll only within the left/right margin columns
|
|
560
|
+
const lm = leftMargin
|
|
561
|
+
const rm = rightMargin
|
|
562
|
+
for (let r = bottom; r > top; r--) {
|
|
563
|
+
const srcRow = grid[r - 1]!
|
|
564
|
+
const dstRow = grid[r]!
|
|
565
|
+
for (let c = lm; c <= rm && c < cols; c++) {
|
|
566
|
+
dstRow[c] = srcRow[c]!
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Clear the top row within margins
|
|
570
|
+
const topRow = grid[top]!
|
|
571
|
+
for (let c = lm; c <= rm && c < cols; c++) {
|
|
572
|
+
topRow[c] = EMPTY_CELL
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
for (let i = bottom; i > top; i--) {
|
|
576
|
+
grid[i] = grid[i - 1]!
|
|
577
|
+
softWrapped[i] = softWrapped[i - 1]!
|
|
578
|
+
}
|
|
579
|
+
grid[top] = makeRow(cols)
|
|
580
|
+
softWrapped[top] = false
|
|
505
581
|
}
|
|
506
|
-
grid[top] = makeRow(cols)
|
|
507
|
-
softWrapped[top] = false
|
|
508
582
|
}
|
|
509
583
|
|
|
510
584
|
// ── Character writing ──
|
|
@@ -647,30 +721,37 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
647
721
|
}
|
|
648
722
|
|
|
649
723
|
function writeCharCore(ch: string, wide: boolean): void {
|
|
724
|
+
// Guard: cannot write to a zero-dimension grid
|
|
725
|
+
if (cols <= 0 || rows <= 0) return
|
|
726
|
+
|
|
650
727
|
const charWidth = wide ? 2 : 1
|
|
728
|
+
const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : cols
|
|
729
|
+
const wrapReturn = leftRightMarginMode ? leftMargin : 0
|
|
651
730
|
|
|
652
|
-
// Handle autowrap at end of line
|
|
653
|
-
if (curX + charWidth >
|
|
731
|
+
// Handle autowrap at end of line (or right margin)
|
|
732
|
+
if (curX + charWidth > wrapBoundary) {
|
|
654
733
|
if (autoWrap) {
|
|
655
734
|
// Mark this row as soft-wrapped (auto-wrap caused the line break)
|
|
656
735
|
softWrapped[curY] = true
|
|
657
|
-
curX =
|
|
736
|
+
curX = wrapReturn
|
|
658
737
|
curY++
|
|
659
738
|
if (curY > scrollBottom) {
|
|
660
739
|
curY = scrollBottom
|
|
661
740
|
scrollUp(scrollTop, scrollBottom)
|
|
662
741
|
}
|
|
663
742
|
} else {
|
|
664
|
-
curX =
|
|
743
|
+
curX = wrapBoundary - charWidth
|
|
665
744
|
}
|
|
666
745
|
}
|
|
667
746
|
|
|
668
747
|
// Insert mode: shift existing characters right before writing
|
|
669
748
|
if (insertMode) {
|
|
670
749
|
const row = grid[curY]!
|
|
750
|
+
const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols
|
|
671
751
|
for (let i = 0; i < charWidth; i++) {
|
|
752
|
+
// Shift cells right within margin, dropping the cell at the right edge
|
|
753
|
+
row.splice(insertEnd - 1, 1)
|
|
672
754
|
row.splice(curX, 0, EMPTY_CELL)
|
|
673
|
-
row.pop()
|
|
674
755
|
}
|
|
675
756
|
}
|
|
676
757
|
|
|
@@ -792,9 +873,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
792
873
|
break
|
|
793
874
|
case "H": // CUP - Cursor Position
|
|
794
875
|
case "f": // HVP - same as CUP
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
876
|
+
if (originMode) {
|
|
877
|
+
// DECOM: positions are relative to scroll region
|
|
878
|
+
curY = scrollTop + (parts[0] ?? 1) - 1
|
|
879
|
+
curX = (parts[1] ?? 1) - 1
|
|
880
|
+
// Clamp to scroll region bounds
|
|
881
|
+
if (curY < scrollTop) curY = scrollTop
|
|
882
|
+
if (curY > scrollBottom) curY = scrollBottom
|
|
883
|
+
if (curX < 0) curX = 0
|
|
884
|
+
if (curX >= cols) curX = cols - 1
|
|
885
|
+
} else {
|
|
886
|
+
curY = (parts[0] ?? 1) - 1
|
|
887
|
+
curX = (parts[1] ?? 1) - 1
|
|
888
|
+
clampCursor()
|
|
889
|
+
}
|
|
798
890
|
break
|
|
799
891
|
case "J": // ED - Erase in Display
|
|
800
892
|
handleEraseDisplay(parts[0] ?? 0)
|
|
@@ -868,6 +960,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
868
960
|
case "c": // DA1 - Primary Device Attributes
|
|
869
961
|
if (onResponse) {
|
|
870
962
|
if (params === "" || params === "0") {
|
|
963
|
+
// VT200 (62) + sixel (4). We include sixel because we parse and
|
|
964
|
+
// preserve sixel data via getSixelImages() — applications checking
|
|
965
|
+
// DA1 for sixel support before sending image data will work correctly.
|
|
871
966
|
onResponse("\x1b[?62;4c")
|
|
872
967
|
}
|
|
873
968
|
}
|
|
@@ -882,9 +977,26 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
882
977
|
if (code === 4) insertMode = false
|
|
883
978
|
}
|
|
884
979
|
break
|
|
885
|
-
case "s": // SCP - Save Cursor Position
|
|
886
|
-
|
|
887
|
-
|
|
980
|
+
case "s": // SCP - Save Cursor Position / DECSLRM when left/right margin mode active
|
|
981
|
+
if (leftRightMarginMode && intermediates === "") {
|
|
982
|
+
// DECSLRM - Set Left and Right Margins (1-based params)
|
|
983
|
+
const pl = parts[0] ?? 0
|
|
984
|
+
const pr = parts[1] ?? 0
|
|
985
|
+
leftMargin = pl > 0 ? pl - 1 : 0
|
|
986
|
+
rightMargin = pr > 0 ? pr - 1 : cols - 1
|
|
987
|
+
if (leftMargin >= rightMargin) {
|
|
988
|
+
leftMargin = 0
|
|
989
|
+
rightMargin = cols - 1
|
|
990
|
+
}
|
|
991
|
+
if (leftMargin >= cols) leftMargin = 0
|
|
992
|
+
if (rightMargin >= cols) rightMargin = cols - 1
|
|
993
|
+
// Move cursor to home (top-left of margin area if origin mode, else absolute home)
|
|
994
|
+
curX = originMode ? leftMargin : 0
|
|
995
|
+
curY = originMode ? scrollTop : 0
|
|
996
|
+
} else {
|
|
997
|
+
savedCurX = curX
|
|
998
|
+
savedCurY = curY
|
|
999
|
+
}
|
|
888
1000
|
break
|
|
889
1001
|
case "u": // RCP - Restore Cursor Position
|
|
890
1002
|
curX = savedCurX
|
|
@@ -929,6 +1041,18 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
929
1041
|
}
|
|
930
1042
|
}
|
|
931
1043
|
|
|
1044
|
+
function handleCSIEq(params: string, _intermediates: string, finalByte: string): void {
|
|
1045
|
+
// CSI = sequences
|
|
1046
|
+
if (finalByte === "c") {
|
|
1047
|
+
// DA3 - Tertiary Device Attributes
|
|
1048
|
+
if (onResponse) {
|
|
1049
|
+
if (params === "" || params === "0") {
|
|
1050
|
+
onResponse("\x1bP!|00000000\x1b\\")
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
932
1056
|
function handleCSIPrivate(params: string, intermediates: string, finalByte: string): void {
|
|
933
1057
|
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
934
1058
|
|
|
@@ -983,6 +1107,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
983
1107
|
case 2026:
|
|
984
1108
|
value = syncOutput ? 1 : 2
|
|
985
1109
|
break
|
|
1110
|
+
case 69:
|
|
1111
|
+
value = leftRightMarginMode ? 1 : 2
|
|
1112
|
+
break
|
|
1113
|
+
case 2031:
|
|
1114
|
+
value = colorSchemeReporting ? 1 : 2
|
|
1115
|
+
break
|
|
986
1116
|
case 5:
|
|
987
1117
|
value = reverseVideo ? 1 : 2
|
|
988
1118
|
break
|
|
@@ -995,6 +1125,32 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
995
1125
|
return
|
|
996
1126
|
}
|
|
997
1127
|
|
|
1128
|
+
// Private DSR: CSI ? Ps n
|
|
1129
|
+
if (finalByte === "n") {
|
|
1130
|
+
if (onResponse) {
|
|
1131
|
+
const ps = parts[0] ?? 0
|
|
1132
|
+
if (ps === 6) {
|
|
1133
|
+
// DECXCPR — Extended Cursor Position Report
|
|
1134
|
+
onResponse(`\x1b[?${curY + 1};${curX + 1}n`)
|
|
1135
|
+
} else if (ps === 997) {
|
|
1136
|
+
// Color scheme report: 1 = dark, 2 = light
|
|
1137
|
+
onResponse("\x1b[?997;1n")
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// DECSED / DECSEL — selective erase (CSI ? J / CSI ? K)
|
|
1144
|
+
// We treat selective erase same as normal erase (no DECSCA protection tracking)
|
|
1145
|
+
if (finalByte === "J") {
|
|
1146
|
+
handleEraseDisplay(parts[0] ?? 0)
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
if (finalByte === "K") {
|
|
1150
|
+
handleEraseLine(parts[0] ?? 0)
|
|
1151
|
+
return
|
|
1152
|
+
}
|
|
1153
|
+
|
|
998
1154
|
const set = finalByte === "h"
|
|
999
1155
|
|
|
1000
1156
|
for (const code of parts) {
|
|
@@ -1032,17 +1188,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1032
1188
|
case 66: // DECNKM - Application Keypad
|
|
1033
1189
|
applicationKeypad = set
|
|
1034
1190
|
break
|
|
1191
|
+
case 9: // X10 mouse
|
|
1035
1192
|
case 1000: // Mouse tracking (basic)
|
|
1036
|
-
mouseTracking = set
|
|
1037
|
-
mouseTrackingMode = set ? 1000 : 0
|
|
1038
|
-
break
|
|
1039
1193
|
case 1002: // Mouse tracking (button events)
|
|
1040
|
-
mouseTracking = set
|
|
1041
|
-
mouseTrackingMode = set ? 1002 : 0
|
|
1042
|
-
break
|
|
1043
1194
|
case 1003: // Mouse tracking (all events)
|
|
1195
|
+
case 1015: // urxvt mouse encoding
|
|
1196
|
+
case 1016: // SGR pixel mouse
|
|
1044
1197
|
mouseTracking = set
|
|
1045
|
-
mouseTrackingMode = set ?
|
|
1198
|
+
mouseTrackingMode = set ? code : 0
|
|
1046
1199
|
break
|
|
1047
1200
|
case 1004: // Focus tracking
|
|
1048
1201
|
focusTracking = set
|
|
@@ -1070,12 +1223,23 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1070
1223
|
clampCursor()
|
|
1071
1224
|
}
|
|
1072
1225
|
break
|
|
1226
|
+
case 69: // DECLRMM - Left/Right Margin Mode
|
|
1227
|
+
leftRightMarginMode = set
|
|
1228
|
+
if (!set) {
|
|
1229
|
+
// Reset margins when mode is disabled
|
|
1230
|
+
leftMargin = 0
|
|
1231
|
+
rightMargin = cols - 1
|
|
1232
|
+
}
|
|
1233
|
+
break
|
|
1073
1234
|
case 2004: // Bracketed paste
|
|
1074
1235
|
bracketedPaste = set
|
|
1075
1236
|
break
|
|
1076
1237
|
case 2026: // Synchronized output
|
|
1077
1238
|
syncOutput = set
|
|
1078
1239
|
break
|
|
1240
|
+
case 2031: // Color scheme reporting
|
|
1241
|
+
colorSchemeReporting = set
|
|
1242
|
+
break
|
|
1079
1243
|
}
|
|
1080
1244
|
}
|
|
1081
1245
|
}
|
|
@@ -1109,15 +1273,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1109
1273
|
}
|
|
1110
1274
|
|
|
1111
1275
|
function handleEraseLine(mode: number): void {
|
|
1276
|
+
const eraseRight = leftRightMarginMode ? rightMargin : cols - 1
|
|
1277
|
+
const eraseLeft = leftRightMarginMode ? leftMargin : 0
|
|
1112
1278
|
switch (mode) {
|
|
1113
|
-
case 0: // Erase from cursor to end of line
|
|
1114
|
-
eraseCells(curY, curX, curY,
|
|
1279
|
+
case 0: // Erase from cursor to end of line (or right margin)
|
|
1280
|
+
eraseCells(curY, curX, curY, eraseRight)
|
|
1115
1281
|
break
|
|
1116
|
-
case 1: // Erase from start to cursor
|
|
1117
|
-
eraseCells(curY,
|
|
1282
|
+
case 1: // Erase from start (or left margin) to cursor
|
|
1283
|
+
eraseCells(curY, eraseLeft, curY, curX)
|
|
1118
1284
|
break
|
|
1119
|
-
case 2: // Erase entire line
|
|
1120
|
-
eraseCells(curY,
|
|
1285
|
+
case 2: // Erase entire line (within margins if active)
|
|
1286
|
+
eraseCells(curY, eraseLeft, curY, eraseRight)
|
|
1121
1287
|
break
|
|
1122
1288
|
}
|
|
1123
1289
|
}
|
|
@@ -1126,7 +1292,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1126
1292
|
const r = grid[row]
|
|
1127
1293
|
if (!r) return
|
|
1128
1294
|
for (let col = startCol; col <= endCol && col < cols; col++) {
|
|
1129
|
-
|
|
1295
|
+
const cell = emptyCell()
|
|
1296
|
+
// Fill erased cells with the current background color
|
|
1297
|
+
if (attrs.bg) {
|
|
1298
|
+
cell.bg = { ...attrs.bg }
|
|
1299
|
+
}
|
|
1300
|
+
r[col] = cell
|
|
1130
1301
|
}
|
|
1131
1302
|
}
|
|
1132
1303
|
|
|
@@ -1147,10 +1318,20 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1147
1318
|
function handleDeleteChars(count: number): void {
|
|
1148
1319
|
const row = grid[curY]
|
|
1149
1320
|
if (!row) return
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1321
|
+
if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
|
|
1322
|
+
// Delete within margin bounds: shift left, insert blanks at right margin
|
|
1323
|
+
for (let i = 0; i < count; i++) {
|
|
1324
|
+
if (curX <= rightMargin) {
|
|
1325
|
+
row.splice(curX, 1)
|
|
1326
|
+
row.splice(rightMargin, 0, emptyCell())
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
} else {
|
|
1330
|
+
for (let i = 0; i < count; i++) {
|
|
1331
|
+
if (curX < cols) {
|
|
1332
|
+
row.splice(curX, 1)
|
|
1333
|
+
row.push(emptyCell())
|
|
1334
|
+
}
|
|
1154
1335
|
}
|
|
1155
1336
|
}
|
|
1156
1337
|
}
|
|
@@ -1158,9 +1339,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1158
1339
|
function handleInsertChars(count: number): void {
|
|
1159
1340
|
const row = grid[curY]
|
|
1160
1341
|
if (!row) return
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1342
|
+
if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
|
|
1343
|
+
// Insert within margin bounds: shift right, drop chars at right margin
|
|
1344
|
+
for (let i = 0; i < count; i++) {
|
|
1345
|
+
row.splice(rightMargin, 1)
|
|
1346
|
+
row.splice(curX, 0, emptyCell())
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
for (let i = 0; i < count; i++) {
|
|
1350
|
+
row.splice(curX, 0, emptyCell())
|
|
1351
|
+
row.pop()
|
|
1352
|
+
}
|
|
1164
1353
|
}
|
|
1165
1354
|
}
|
|
1166
1355
|
|
|
@@ -1473,6 +1662,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1473
1662
|
}
|
|
1474
1663
|
case 1: // Set icon name (ignore)
|
|
1475
1664
|
break
|
|
1665
|
+
case 7: // Current working directory: OSC 7 ; file://host/path ST
|
|
1666
|
+
cwd = value
|
|
1667
|
+
break
|
|
1476
1668
|
case 8: {
|
|
1477
1669
|
// Hyperlink: OSC 8 ; params ; url ST
|
|
1478
1670
|
// Format: 8;params;url or 8;;url or 8;; (close)
|
|
@@ -1520,6 +1712,76 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1520
1712
|
}
|
|
1521
1713
|
break
|
|
1522
1714
|
}
|
|
1715
|
+
case 9: // Notifications: OSC 9 ; message ST (iTerm2/ConEmu convention)
|
|
1716
|
+
notifications.push(value)
|
|
1717
|
+
break
|
|
1718
|
+
case 633: {
|
|
1719
|
+
// VS Code shell integration (OSC 633 is a superset of OSC 133)
|
|
1720
|
+
// Maps to the same semantic zone model as FinalTerm markers.
|
|
1721
|
+
const marker = value.charAt(0)
|
|
1722
|
+
switch (marker) {
|
|
1723
|
+
case "A":
|
|
1724
|
+
semanticZones.push({ type: "prompt", startRow: curY, startCol: curX })
|
|
1725
|
+
break
|
|
1726
|
+
case "B":
|
|
1727
|
+
semanticZones.push({ type: "command", startRow: curY, startCol: curX })
|
|
1728
|
+
break
|
|
1729
|
+
case "C":
|
|
1730
|
+
semanticZones.push({ type: "output", startRow: curY, startCol: curX })
|
|
1731
|
+
break
|
|
1732
|
+
case "D":
|
|
1733
|
+
break
|
|
1734
|
+
}
|
|
1735
|
+
break
|
|
1736
|
+
}
|
|
1737
|
+
case 66: {
|
|
1738
|
+
// OSC 66 — Text Sizing
|
|
1739
|
+
if (value === "?" && onResponse) {
|
|
1740
|
+
// Query: respond with current text scale
|
|
1741
|
+
onResponse(`\x1b]66;s=${textScale}\x1b\\`)
|
|
1742
|
+
} else {
|
|
1743
|
+
// Parse key=value pairs (e.g., "s=2")
|
|
1744
|
+
const kvPairs = value.split(";")
|
|
1745
|
+
for (const pair of kvPairs) {
|
|
1746
|
+
const [k, v] = pair.split("=")
|
|
1747
|
+
if (k === "s" && v !== undefined) {
|
|
1748
|
+
const n = parseInt(v, 10)
|
|
1749
|
+
if (!isNaN(n) && n > 0) {
|
|
1750
|
+
textScale = n
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
break
|
|
1756
|
+
}
|
|
1757
|
+
case 5522: {
|
|
1758
|
+
// OSC 5522 — Advanced Clipboard (Kitty clipboard protocol)
|
|
1759
|
+
if (value === "?" && onResponse) {
|
|
1760
|
+
// Query: respond with stored clipboard data
|
|
1761
|
+
const encoded = btoa(advancedClipboard)
|
|
1762
|
+
onResponse(`\x1b]5522;${encoded}\x1b\\`)
|
|
1763
|
+
} else {
|
|
1764
|
+
// Store clipboard data (base64-encoded)
|
|
1765
|
+
try {
|
|
1766
|
+
advancedClipboard = atob(value)
|
|
1767
|
+
} catch {
|
|
1768
|
+
advancedClipboard = ""
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
break
|
|
1772
|
+
}
|
|
1773
|
+
case 1337: {
|
|
1774
|
+
// iTerm2 proprietary sequences
|
|
1775
|
+
if (value === "ReportCellSize" && onResponse) {
|
|
1776
|
+
// Report default 8x16 pixel cell size (standard monospace ratio)
|
|
1777
|
+
onResponse("\x1b]1337;ReportCellSize=16;8\x1b\\")
|
|
1778
|
+
} else if (value === "RequestCapabilities" && onResponse) {
|
|
1779
|
+
// Report empty capabilities (protocol supported, no iTerm2-specific features)
|
|
1780
|
+
onResponse("\x1b]1337;Capabilities=\x1b\\")
|
|
1781
|
+
}
|
|
1782
|
+
// Inline images (File=...) are silently consumed — no pixel framebuffer
|
|
1783
|
+
break
|
|
1784
|
+
}
|
|
1523
1785
|
}
|
|
1524
1786
|
}
|
|
1525
1787
|
|
|
@@ -1538,6 +1800,35 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1538
1800
|
return
|
|
1539
1801
|
}
|
|
1540
1802
|
|
|
1803
|
+
// DECRQSS: DCS $ q Pt ST → response DCS Ps $ r Pt ST
|
|
1804
|
+
if (data.startsWith("$q") && onResponse) {
|
|
1805
|
+
const pt = data.substring(2)
|
|
1806
|
+
if (pt === '"p') {
|
|
1807
|
+
// DECSCL - Conformance level: VT200 mode, 8-bit controls
|
|
1808
|
+
onResponse('\x1bP1$r62;1"p\x1b\\')
|
|
1809
|
+
} else {
|
|
1810
|
+
// Not recognized
|
|
1811
|
+
onResponse("\x1bP0$r\x1b\\")
|
|
1812
|
+
}
|
|
1813
|
+
return
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// XTGETTCAP: DCS + q hex ST → response DCS 1 + r hex = hexvalue ST
|
|
1817
|
+
if (data.startsWith("+q") && onResponse) {
|
|
1818
|
+
const hexName = data.substring(2)
|
|
1819
|
+
if (hexName === "544e") {
|
|
1820
|
+
// "TN" = terminal name → "vterm"
|
|
1821
|
+
const hexValue = Array.from(new TextEncoder().encode("vterm"))
|
|
1822
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
1823
|
+
.join("")
|
|
1824
|
+
onResponse(`\x1bP1+r544e=${hexValue}\x1b\\`)
|
|
1825
|
+
} else {
|
|
1826
|
+
// Unknown capability
|
|
1827
|
+
onResponse("\x1bP0+r\x1b\\")
|
|
1828
|
+
}
|
|
1829
|
+
return
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1541
1832
|
// Other DCS sequences are consumed and ignored
|
|
1542
1833
|
}
|
|
1543
1834
|
|
|
@@ -1587,6 +1878,11 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1587
1878
|
scrollTop = 0
|
|
1588
1879
|
scrollBottom = rows - 1
|
|
1589
1880
|
|
|
1881
|
+
// Reset left/right margins
|
|
1882
|
+
leftRightMarginMode = false
|
|
1883
|
+
leftMargin = 0
|
|
1884
|
+
rightMargin = cols - 1
|
|
1885
|
+
|
|
1590
1886
|
// Reset attributes
|
|
1591
1887
|
attrs = resetAttrs()
|
|
1592
1888
|
|
|
@@ -1635,9 +1931,17 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1635
1931
|
sixelImages = []
|
|
1636
1932
|
scrollTop = 0
|
|
1637
1933
|
scrollBottom = rows - 1
|
|
1934
|
+
leftRightMarginMode = false
|
|
1935
|
+
leftMargin = 0
|
|
1936
|
+
rightMargin = cols - 1
|
|
1937
|
+
colorSchemeReporting = false
|
|
1938
|
+
textScale = 1
|
|
1939
|
+
advancedClipboard = ""
|
|
1638
1940
|
viewportOffset = 0
|
|
1639
1941
|
charsetG0 = false
|
|
1640
1942
|
clipboard = ""
|
|
1943
|
+
cwd = ""
|
|
1944
|
+
notifications = []
|
|
1641
1945
|
lastChar = ""
|
|
1642
1946
|
pendingRegionalIndicator = null
|
|
1643
1947
|
afterZWJ = false
|
|
@@ -1777,6 +2081,14 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1777
2081
|
} else if (ch === ")") {
|
|
1778
2082
|
// Designate G1 character set (ignored, just consume next byte)
|
|
1779
2083
|
parserState = "escape_charset"
|
|
2084
|
+
} else if (ch === "=") {
|
|
2085
|
+
// DECKPAM - Application Keypad Mode
|
|
2086
|
+
applicationKeypad = true
|
|
2087
|
+
parserState = "ground"
|
|
2088
|
+
} else if (ch === ">") {
|
|
2089
|
+
// DECKPNM - Normal Keypad Mode
|
|
2090
|
+
applicationKeypad = false
|
|
2091
|
+
parserState = "ground"
|
|
1780
2092
|
} else if (ch === "_") {
|
|
1781
2093
|
// APC - Application Program Command
|
|
1782
2094
|
parserState = "apc"
|
|
@@ -1826,6 +2138,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1826
2138
|
handleCSIGt(paramPart.substring(1), intermediatePart, ch)
|
|
1827
2139
|
} else if (paramPart.startsWith("<")) {
|
|
1828
2140
|
handleCSILt(paramPart.substring(1), intermediatePart, ch)
|
|
2141
|
+
} else if (paramPart.startsWith("=")) {
|
|
2142
|
+
handleCSIEq(paramPart.substring(1), intermediatePart, ch)
|
|
1829
2143
|
} else {
|
|
1830
2144
|
handleCSI(paramPart, intermediatePart, ch)
|
|
1831
2145
|
}
|
|
@@ -2054,6 +2368,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
2054
2368
|
rows = newRows
|
|
2055
2369
|
scrollTop = 0
|
|
2056
2370
|
scrollBottom = rows - 1
|
|
2371
|
+
leftMargin = 0
|
|
2372
|
+
rightMargin = cols - 1
|
|
2057
2373
|
clampCursor()
|
|
2058
2374
|
}
|
|
2059
2375
|
|
|
@@ -2148,6 +2464,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
2148
2464
|
return syncOutput
|
|
2149
2465
|
case "sgrMouse":
|
|
2150
2466
|
return sgrMouse
|
|
2467
|
+
case "pixelMouse":
|
|
2468
|
+
return mouseTrackingMode === 1016
|
|
2469
|
+
case "leftRightMargin":
|
|
2470
|
+
return leftRightMarginMode
|
|
2471
|
+
case "colorSchemeReporting":
|
|
2472
|
+
return colorSchemeReporting
|
|
2151
2473
|
case "kittyKeyboard":
|
|
2152
2474
|
return kittyKeyboardFlags > 0
|
|
2153
2475
|
case "kittyGraphics":
|
|
@@ -2160,7 +2482,7 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
2160
2482
|
}
|
|
2161
2483
|
|
|
2162
2484
|
// Suppress unused variable warnings
|
|
2163
|
-
void [mouseTrackingMode]
|
|
2485
|
+
void [mouseTrackingMode, textScale, advancedClipboard]
|
|
2164
2486
|
|
|
2165
2487
|
return {
|
|
2166
2488
|
get cols() {
|
|
@@ -2182,6 +2504,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
2182
2504
|
getCursorBlinking: () => cursorBlinking,
|
|
2183
2505
|
getTitle: () => title,
|
|
2184
2506
|
getMode,
|
|
2507
|
+
getClipboard: () => clipboard,
|
|
2508
|
+
getCwd: () => cwd,
|
|
2509
|
+
getNotifications: () => [...notifications],
|
|
2185
2510
|
getScrollbackLength: () => scrollback.length,
|
|
2186
2511
|
getViewportOffset: () => viewportOffset,
|
|
2187
2512
|
scrollViewport: (delta: number) => {
|