vterm.js 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +106 -0
- package/dist/index.mjs +2380 -0
- package/package.json +13 -4
- package/src/index.ts +0 -10
- package/src/screen.ts +0 -2518
package/src/screen.ts
DELETED
|
@@ -1,2518 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vterm.js — Modern terminal emulator
|
|
3
|
-
*
|
|
4
|
-
* Full VT/ECMA-48/xterm standards coverage. Pure TypeScript, zero dependencies.
|
|
5
|
-
* Targets 100% of terminfo.dev's feature matrix:
|
|
6
|
-
*
|
|
7
|
-
* - All SGR attributes (bold, faint, italic, underline styles, overline, strikethrough, blink, hidden, inverse)
|
|
8
|
-
* - 16-color, 256-color, 24-bit truecolor (foreground, background, underline color)
|
|
9
|
-
* - Full cursor control (CUP, CUU/CUD/CUF/CUB, CPR, CHA, CNL, CPL, HVP, save/restore)
|
|
10
|
-
* - Cursor shape (DECSCUSR — block, underline, bar, blinking variants)
|
|
11
|
-
* - Erase operations (ED 0/1/2/3, EL 0/1/2, ECH)
|
|
12
|
-
* - Editing operations (ICH, DCH, IL, DL, REP)
|
|
13
|
-
* - Scroll regions (DECSTBM, SU, SD) with content preservation
|
|
14
|
-
* - DEC private modes (alt screen, auto-wrap, origin, insert, reverse video, bracketed paste)
|
|
15
|
-
* - Mouse tracking (X10, normal, button, any-event, SGR format)
|
|
16
|
-
* - Focus tracking (mode 1004)
|
|
17
|
-
* - Application cursor keys & keypad
|
|
18
|
-
* - Synchronized output (mode 2026)
|
|
19
|
-
* - Kitty keyboard protocol (CSI u / progressive enhancement)
|
|
20
|
-
* - Scrollback buffer with configurable limit
|
|
21
|
-
* - Wide character support (CJK, emoji ZWJ, regional indicators, VS-16)
|
|
22
|
-
* - OSC sequences (title, hyperlinks, clipboard, colors)
|
|
23
|
-
* - DCS sequences (XTVERSION, DECRQSS, XTGETTCAP, Sixel)
|
|
24
|
-
* - APC sequences (Kitty graphics protocol — parsed, query responses)
|
|
25
|
-
* - DA1/DA2/DA3 device attribute responses
|
|
26
|
-
* - DSR (device status report) responses
|
|
27
|
-
* - DECRPM (mode reporting)
|
|
28
|
-
* - Character sets (DEC Special Graphics, UTF-8)
|
|
29
|
-
* - Full C0/C1 control code handling
|
|
30
|
-
* - DECSTR soft terminal reset
|
|
31
|
-
*
|
|
32
|
-
* @see https://terminfo.dev for the feature matrix
|
|
33
|
-
* @see https://github.com/beorn/vterm for the monorepo
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
// ── Types ──────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
export interface CellColor {
|
|
39
|
-
r: number
|
|
40
|
-
g: number
|
|
41
|
-
b: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type UnderlineStyle = "none" | "single" | "double" | "curly" | "dotted" | "dashed"
|
|
45
|
-
|
|
46
|
-
export interface ScreenCell {
|
|
47
|
-
char: string
|
|
48
|
-
fg: CellColor | null
|
|
49
|
-
bg: CellColor | null
|
|
50
|
-
bold: boolean
|
|
51
|
-
faint: boolean
|
|
52
|
-
italic: boolean
|
|
53
|
-
underline: UnderlineStyle
|
|
54
|
-
underlineColor: CellColor | null
|
|
55
|
-
overline: boolean
|
|
56
|
-
strikethrough: boolean
|
|
57
|
-
inverse: boolean
|
|
58
|
-
hidden: boolean
|
|
59
|
-
blink: boolean
|
|
60
|
-
wide: boolean
|
|
61
|
-
url: string | null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface ScreenOptions {
|
|
65
|
-
cols?: number
|
|
66
|
-
rows?: number
|
|
67
|
-
scrollbackLimit?: number
|
|
68
|
-
/** Callback for DA1/DA2/DSR responses — write these back to the PTY */
|
|
69
|
-
onResponse?: (data: string) => void
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface SemanticZone {
|
|
73
|
-
type: "prompt" | "command" | "output"
|
|
74
|
-
startRow: number
|
|
75
|
-
startCol: number
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface SixelImage {
|
|
79
|
-
data: string
|
|
80
|
-
row: number // cursor row when sixel started
|
|
81
|
-
col: number // cursor col when sixel started
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface Screen {
|
|
85
|
-
readonly cols: number
|
|
86
|
-
readonly rows: number
|
|
87
|
-
|
|
88
|
-
process(data: Uint8Array): void
|
|
89
|
-
resize(cols: number, rows: number): void
|
|
90
|
-
reset(): void
|
|
91
|
-
|
|
92
|
-
getCell(row: number, col: number): ScreenCell
|
|
93
|
-
getLine(row: number): ScreenCell[]
|
|
94
|
-
getText(): string
|
|
95
|
-
getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string
|
|
96
|
-
|
|
97
|
-
getCursorPosition(): { x: number; y: number }
|
|
98
|
-
getCursorVisible(): boolean
|
|
99
|
-
getCursorShape(): "block" | "underline" | "bar"
|
|
100
|
-
getCursorBlinking(): boolean
|
|
101
|
-
|
|
102
|
-
getTitle(): string
|
|
103
|
-
getMode(mode: string): boolean
|
|
104
|
-
getClipboard(): string
|
|
105
|
-
getCwd(): string
|
|
106
|
-
getNotifications(): string[]
|
|
107
|
-
|
|
108
|
-
getScrollbackLength(): number
|
|
109
|
-
getViewportOffset(): number
|
|
110
|
-
scrollViewport(delta: number): void
|
|
111
|
-
|
|
112
|
-
getSemanticZones(): SemanticZone[]
|
|
113
|
-
getSixelImages(): SixelImage[]
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── Implementation ─────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
/** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
|
|
119
|
-
const EMPTY_CELL: ScreenCell = Object.freeze({
|
|
120
|
-
char: "",
|
|
121
|
-
fg: null,
|
|
122
|
-
bg: null,
|
|
123
|
-
bold: false,
|
|
124
|
-
faint: false,
|
|
125
|
-
italic: false,
|
|
126
|
-
underline: "none" as UnderlineStyle,
|
|
127
|
-
underlineColor: null,
|
|
128
|
-
overline: false,
|
|
129
|
-
strikethrough: false,
|
|
130
|
-
inverse: false,
|
|
131
|
-
hidden: false,
|
|
132
|
-
blink: false,
|
|
133
|
-
wide: false,
|
|
134
|
-
url: null,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
function emptyCell(): ScreenCell {
|
|
138
|
-
return { ...EMPTY_CELL }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ── ANSI 256-color palette ─────────────────────────────────────────────
|
|
142
|
-
|
|
143
|
-
const ANSI_16: readonly CellColor[] = [
|
|
144
|
-
{ r: 0x00, g: 0x00, b: 0x00 }, // 0 Black
|
|
145
|
-
{ r: 0x80, g: 0x00, b: 0x00 }, // 1 Red
|
|
146
|
-
{ r: 0x00, g: 0x80, b: 0x00 }, // 2 Green
|
|
147
|
-
{ r: 0x80, g: 0x80, b: 0x00 }, // 3 Yellow
|
|
148
|
-
{ r: 0x00, g: 0x00, b: 0x80 }, // 4 Blue
|
|
149
|
-
{ r: 0x80, g: 0x00, b: 0x80 }, // 5 Magenta
|
|
150
|
-
{ r: 0x00, g: 0x80, b: 0x80 }, // 6 Cyan
|
|
151
|
-
{ r: 0xc0, g: 0xc0, b: 0xc0 }, // 7 White
|
|
152
|
-
{ r: 0x80, g: 0x80, b: 0x80 }, // 8 Bright Black
|
|
153
|
-
{ r: 0xff, g: 0x00, b: 0x00 }, // 9 Bright Red
|
|
154
|
-
{ r: 0x00, g: 0xff, b: 0x00 }, // 10 Bright Green
|
|
155
|
-
{ r: 0xff, g: 0xff, b: 0x00 }, // 11 Bright Yellow
|
|
156
|
-
{ r: 0x00, g: 0x00, b: 0xff }, // 12 Bright Blue
|
|
157
|
-
{ r: 0xff, g: 0x00, b: 0xff }, // 13 Bright Magenta
|
|
158
|
-
{ r: 0x00, g: 0xff, b: 0xff }, // 14 Bright Cyan
|
|
159
|
-
{ r: 0xff, g: 0xff, b: 0xff }, // 15 Bright White
|
|
160
|
-
]
|
|
161
|
-
|
|
162
|
-
function buildPalette256(): CellColor[] {
|
|
163
|
-
const palette: CellColor[] = [...ANSI_16]
|
|
164
|
-
const levels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
|
|
165
|
-
for (let r = 0; r < 6; r++) {
|
|
166
|
-
for (let g = 0; g < 6; g++) {
|
|
167
|
-
for (let b = 0; b < 6; b++) {
|
|
168
|
-
palette.push({ r: levels[r]!, g: levels[g]!, b: levels[b]! })
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
for (let i = 0; i < 24; i++) {
|
|
173
|
-
const v = 8 + i * 10
|
|
174
|
-
palette.push({ r: v, g: v, b: v })
|
|
175
|
-
}
|
|
176
|
-
return palette
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const PALETTE_256 = buildPalette256()
|
|
180
|
-
|
|
181
|
-
// ── DEC Special Graphics character set ─────────────────────────────────
|
|
182
|
-
|
|
183
|
-
const DEC_SPECIAL_GRAPHICS: Record<string, string> = {
|
|
184
|
-
j: "\u2518", // ┘
|
|
185
|
-
k: "\u2510", // ┐
|
|
186
|
-
l: "\u250c", // ┌
|
|
187
|
-
m: "\u2514", // └
|
|
188
|
-
n: "\u253c", // ┼
|
|
189
|
-
q: "\u2500", // ─
|
|
190
|
-
t: "\u251c", // ├
|
|
191
|
-
u: "\u2524", // ┤
|
|
192
|
-
v: "\u2534", // ┴
|
|
193
|
-
w: "\u252c", // ┬
|
|
194
|
-
x: "\u2502", // │
|
|
195
|
-
a: "\u2592", // ▒
|
|
196
|
-
f: "\u00b0", // °
|
|
197
|
-
g: "\u00b1", // ±
|
|
198
|
-
"~": "\u00b7", // ·
|
|
199
|
-
y: "\u2264", // ≤
|
|
200
|
-
z: "\u2265", // ≥
|
|
201
|
-
"{": "\u03c0", // π
|
|
202
|
-
"|": "\u2260", // ≠
|
|
203
|
-
"}": "\u00a3", // £
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ── Unicode width & character classification ────────────────────────────
|
|
207
|
-
|
|
208
|
-
function isWide(codePoint: number): boolean {
|
|
209
|
-
return (
|
|
210
|
-
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
|
211
|
-
(codePoint >= 0x231a && codePoint <= 0x231b) || // Watch, Hourglass
|
|
212
|
-
(codePoint >= 0x2614 && codePoint <= 0x2615) || // Umbrella, Hot Beverage
|
|
213
|
-
(codePoint >= 0x2648 && codePoint <= 0x2653) || // Zodiac signs
|
|
214
|
-
codePoint === 0x267f || // Wheelchair
|
|
215
|
-
codePoint === 0x2693 || // Anchor
|
|
216
|
-
codePoint === 0x26a1 || // High Voltage
|
|
217
|
-
codePoint === 0x26ce || // Ophiuchus
|
|
218
|
-
codePoint === 0x26d4 || // No Entry
|
|
219
|
-
codePoint === 0x2705 || // Check Mark
|
|
220
|
-
codePoint === 0x2728 || // Sparkles
|
|
221
|
-
codePoint === 0x274c || // Cross Mark
|
|
222
|
-
codePoint === 0x274e || // Cross Mark variant
|
|
223
|
-
(codePoint >= 0x2753 && codePoint <= 0x2755) || // Question marks
|
|
224
|
-
(codePoint >= 0x2795 && codePoint <= 0x2797) || // Plus, Minus, Division
|
|
225
|
-
codePoint === 0x27b0 || // Curly Loop
|
|
226
|
-
codePoint === 0x27bf || // Double Curly Loop
|
|
227
|
-
(codePoint >= 0x2e80 && codePoint <= 0x303e) || // CJK Radicals
|
|
228
|
-
(codePoint >= 0x3041 && codePoint <= 0x33bf) || // Hiragana, Katakana, Bopomofo, etc.
|
|
229
|
-
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Unified Extension A
|
|
230
|
-
(codePoint >= 0x4e00 && codePoint <= 0xa4cf) || // CJK Unified Ideographs
|
|
231
|
-
(codePoint >= 0xa960 && codePoint <= 0xa97c) || // Hangul Jamo Extended-A
|
|
232
|
-
(codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
|
|
233
|
-
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
|
|
234
|
-
(codePoint >= 0xfe10 && codePoint <= 0xfe19) || // Vertical Forms
|
|
235
|
-
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) || // CJK Compatibility Forms
|
|
236
|
-
(codePoint >= 0xff01 && codePoint <= 0xff60) || // Fullwidth Forms
|
|
237
|
-
(codePoint >= 0xffe0 && codePoint <= 0xffe6) || // Fullwidth Signs
|
|
238
|
-
codePoint === 0x1f004 || // Mahjong Tile
|
|
239
|
-
codePoint === 0x1f0cf || // Playing Card
|
|
240
|
-
(codePoint >= 0x1f170 && codePoint <= 0x1f171) || // A/B buttons
|
|
241
|
-
(codePoint >= 0x1f17e && codePoint <= 0x1f17f) || // O/P buttons
|
|
242
|
-
codePoint === 0x1f18e || // AB button
|
|
243
|
-
(codePoint >= 0x1f191 && codePoint <= 0x1f19a) || // Squared symbols
|
|
244
|
-
(codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) || // Regional Indicators
|
|
245
|
-
(codePoint >= 0x1f200 && codePoint <= 0x1f202) || // Enclosed CJK
|
|
246
|
-
(codePoint >= 0x1f300 && codePoint <= 0x1f9ff) || // Misc Symbols/Emoticons
|
|
247
|
-
(codePoint >= 0x1fa00 && codePoint <= 0x1faff) || // Extended Symbols & Pictographs
|
|
248
|
-
(codePoint >= 0x20000 && codePoint <= 0x2fffd) || // CJK Extension B-F
|
|
249
|
-
(codePoint >= 0x30000 && codePoint <= 0x3fffd) // CJK Extension G+
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function isCombining(cp: number): boolean {
|
|
254
|
-
return (
|
|
255
|
-
(cp >= 0x0300 && cp <= 0x036f) || // Combining Diacritical Marks
|
|
256
|
-
(cp >= 0x0483 && cp <= 0x0489) || // Cyrillic combining
|
|
257
|
-
(cp >= 0x0591 && cp <= 0x05bd) || // Hebrew
|
|
258
|
-
cp === 0x05bf || // Hebrew
|
|
259
|
-
(cp >= 0x05c1 && cp <= 0x05c2) || // Hebrew
|
|
260
|
-
(cp >= 0x05c4 && cp <= 0x05c5) || // Hebrew
|
|
261
|
-
cp === 0x05c7 || // Hebrew
|
|
262
|
-
(cp >= 0x0610 && cp <= 0x061a) || // Arabic
|
|
263
|
-
(cp >= 0x064b && cp <= 0x065f) || // Arabic
|
|
264
|
-
cp === 0x0670 || // Arabic
|
|
265
|
-
(cp >= 0x06d6 && cp <= 0x06dc) || // Arabic
|
|
266
|
-
(cp >= 0x06df && cp <= 0x06e4) || // Arabic
|
|
267
|
-
(cp >= 0x06e7 && cp <= 0x06e8) || // Arabic
|
|
268
|
-
(cp >= 0x06ea && cp <= 0x06ed) || // Arabic
|
|
269
|
-
(cp >= 0x0730 && cp <= 0x074a) || // Syriac
|
|
270
|
-
(cp >= 0x0900 && cp <= 0x0903) || // Devanagari
|
|
271
|
-
(cp >= 0x093a && cp <= 0x094f) || // Devanagari
|
|
272
|
-
(cp >= 0x0951 && cp <= 0x0957) || // Devanagari
|
|
273
|
-
(cp >= 0x0962 && cp <= 0x0963) || // Devanagari
|
|
274
|
-
cp === 0x0e31 || // Thai
|
|
275
|
-
(cp >= 0x0e34 && cp <= 0x0e3a) || // Thai
|
|
276
|
-
(cp >= 0x0e47 && cp <= 0x0e4e) || // Thai
|
|
277
|
-
(cp >= 0x1ab0 && cp <= 0x1aff) || // Combining Diacritical Marks Extended
|
|
278
|
-
(cp >= 0x1dc0 && cp <= 0x1dff) || // Combining Diacritical Marks Supplement
|
|
279
|
-
(cp >= 0x20d0 && cp <= 0x20ff) || // Combining Diacritical Marks for Symbols
|
|
280
|
-
(cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors
|
|
281
|
-
(cp >= 0xe0100 && cp <= 0xe01ef) // Variation Selectors Supplement
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function isRegionalIndicator(cp: number): boolean {
|
|
286
|
-
return cp >= 0x1f1e6 && cp <= 0x1f1ff
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function isZWJ(cp: number): boolean {
|
|
290
|
-
return cp === 0x200d
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function isVS16(cp: number): boolean {
|
|
294
|
-
return cp === 0xfe0f
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function isEmojiModifier(cp: number): boolean {
|
|
298
|
-
return cp >= 0x1f3fb && cp <= 0x1f3ff // Skin tone modifiers
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ── Internal attrs interface ───────────────────────────────────────────
|
|
302
|
-
|
|
303
|
-
interface Attrs {
|
|
304
|
-
fg: CellColor | null
|
|
305
|
-
bg: CellColor | null
|
|
306
|
-
bold: boolean
|
|
307
|
-
faint: boolean
|
|
308
|
-
italic: boolean
|
|
309
|
-
underline: UnderlineStyle
|
|
310
|
-
underlineColor: CellColor | null
|
|
311
|
-
overline: boolean
|
|
312
|
-
strikethrough: boolean
|
|
313
|
-
inverse: boolean
|
|
314
|
-
hidden: boolean
|
|
315
|
-
blink: boolean
|
|
316
|
-
url: string | null
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ── Screen factory ─────────────────────────────────────────────────────
|
|
320
|
-
|
|
321
|
-
export function createScreen(options: ScreenOptions = {}): Screen {
|
|
322
|
-
let cols = options.cols ?? 80
|
|
323
|
-
let rows = options.rows ?? 24
|
|
324
|
-
const scrollbackLimit = options.scrollbackLimit ?? 1000
|
|
325
|
-
const onResponse = options.onResponse
|
|
326
|
-
|
|
327
|
-
// Main and alternate screen buffers
|
|
328
|
-
let mainGrid: ScreenCell[][] = makeGrid(cols, rows)
|
|
329
|
-
let altGrid: ScreenCell[][] = makeGrid(cols, rows)
|
|
330
|
-
let grid = mainGrid
|
|
331
|
-
let scrollback: ScreenCell[][] = []
|
|
332
|
-
|
|
333
|
-
// Cursor
|
|
334
|
-
let curX = 0
|
|
335
|
-
let curY = 0
|
|
336
|
-
let curVisible = true
|
|
337
|
-
let cursorShape: "block" | "underline" | "bar" = "block"
|
|
338
|
-
let cursorBlinking = true
|
|
339
|
-
|
|
340
|
-
// DECSC/DECRC saved state
|
|
341
|
-
interface SavedState {
|
|
342
|
-
curX: number
|
|
343
|
-
curY: number
|
|
344
|
-
attrs: Attrs
|
|
345
|
-
originMode: boolean
|
|
346
|
-
autoWrap: boolean
|
|
347
|
-
charsetG0: boolean // true = DEC Special Graphics
|
|
348
|
-
}
|
|
349
|
-
let savedState: SavedState = {
|
|
350
|
-
curX: 0,
|
|
351
|
-
curY: 0,
|
|
352
|
-
attrs: resetAttrs(),
|
|
353
|
-
originMode: false,
|
|
354
|
-
autoWrap: true,
|
|
355
|
-
charsetG0: false,
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Saved cursor for alt screen (separate from DECSC)
|
|
359
|
-
let savedCurX = 0
|
|
360
|
-
let savedCurY = 0
|
|
361
|
-
|
|
362
|
-
// Current drawing attributes
|
|
363
|
-
let attrs: Attrs = resetAttrs()
|
|
364
|
-
|
|
365
|
-
// Terminal state
|
|
366
|
-
let title = ""
|
|
367
|
-
let useAltScreen = false
|
|
368
|
-
let bracketedPaste = false
|
|
369
|
-
let applicationCursor = false
|
|
370
|
-
let applicationKeypad = false
|
|
371
|
-
let autoWrap = true
|
|
372
|
-
let mouseTracking = false
|
|
373
|
-
let mouseTrackingMode = 0 // 1000, 1002, 1003
|
|
374
|
-
let sgrMouse = false
|
|
375
|
-
let focusTracking = false
|
|
376
|
-
let originMode = false
|
|
377
|
-
let insertMode = false
|
|
378
|
-
let reverseVideo = false
|
|
379
|
-
let syncOutput = false
|
|
380
|
-
|
|
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.
|
|
387
|
-
let kittyKeyboardFlags = 0
|
|
388
|
-
let kittyKeyboardStack: number[] = []
|
|
389
|
-
|
|
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.
|
|
396
|
-
let hasKittyGraphics = false
|
|
397
|
-
|
|
398
|
-
// Scroll region (inclusive, 0-based)
|
|
399
|
-
let scrollTop = 0
|
|
400
|
-
let scrollBottom = rows - 1
|
|
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
|
-
|
|
416
|
-
// Viewport scroll offset
|
|
417
|
-
let viewportOffset = 0
|
|
418
|
-
|
|
419
|
-
// Character set: true = DEC Special Graphics (G0)
|
|
420
|
-
let charsetG0 = false
|
|
421
|
-
|
|
422
|
-
// Clipboard (OSC 52)
|
|
423
|
-
let clipboard = ""
|
|
424
|
-
|
|
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)
|
|
432
|
-
let semanticZones: SemanticZone[] = []
|
|
433
|
-
|
|
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.
|
|
439
|
-
let hasSixel = false
|
|
440
|
-
let sixelImages: SixelImage[] = []
|
|
441
|
-
|
|
442
|
-
// Soft-wrap tracking: true if the line break at end of this row was caused by auto-wrap
|
|
443
|
-
let mainSoftWrapped: boolean[] = new Array(rows).fill(false)
|
|
444
|
-
let altSoftWrapped: boolean[] = new Array(rows).fill(false)
|
|
445
|
-
let softWrapped = mainSoftWrapped
|
|
446
|
-
|
|
447
|
-
// Last printed character for REP
|
|
448
|
-
let lastChar = ""
|
|
449
|
-
|
|
450
|
-
// Unicode sequence state
|
|
451
|
-
let pendingRegionalIndicator: string | null = null // First RI waiting for pair
|
|
452
|
-
let afterZWJ = false // Next character should join with previous cell
|
|
453
|
-
|
|
454
|
-
// Parser state
|
|
455
|
-
let parserState:
|
|
456
|
-
| "ground"
|
|
457
|
-
| "escape"
|
|
458
|
-
| "escape_charset"
|
|
459
|
-
| "csi"
|
|
460
|
-
| "osc"
|
|
461
|
-
| "dcs"
|
|
462
|
-
| "dcs_passthrough"
|
|
463
|
-
| "osc_st"
|
|
464
|
-
| "dcs_st"
|
|
465
|
-
| "apc"
|
|
466
|
-
| "apc_st" = "ground"
|
|
467
|
-
let escBuf = ""
|
|
468
|
-
let oscBuf = ""
|
|
469
|
-
let dcsBuf = ""
|
|
470
|
-
let dcsStartRow = 0
|
|
471
|
-
let dcsStartCol = 0
|
|
472
|
-
let apcBuf = ""
|
|
473
|
-
|
|
474
|
-
// Decoder for incoming bytes
|
|
475
|
-
const decoder = new TextDecoder()
|
|
476
|
-
|
|
477
|
-
// ── Grid helpers ──
|
|
478
|
-
|
|
479
|
-
function makeGrid(c: number, r: number): ScreenCell[][] {
|
|
480
|
-
const g: ScreenCell[][] = []
|
|
481
|
-
for (let row = 0; row < r; row++) {
|
|
482
|
-
g.push(makeRow(c))
|
|
483
|
-
}
|
|
484
|
-
return g
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function makeRow(c: number): ScreenCell[] {
|
|
488
|
-
const row: ScreenCell[] = []
|
|
489
|
-
for (let col = 0; col < c; col++) {
|
|
490
|
-
row.push(EMPTY_CELL)
|
|
491
|
-
}
|
|
492
|
-
return row
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function resetAttrs(): Attrs {
|
|
496
|
-
return {
|
|
497
|
-
fg: null,
|
|
498
|
-
bg: null,
|
|
499
|
-
bold: false,
|
|
500
|
-
faint: false,
|
|
501
|
-
italic: false,
|
|
502
|
-
underline: "none",
|
|
503
|
-
underlineColor: null,
|
|
504
|
-
overline: false,
|
|
505
|
-
strikethrough: false,
|
|
506
|
-
inverse: false,
|
|
507
|
-
hidden: false,
|
|
508
|
-
blink: false,
|
|
509
|
-
url: null,
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function clampCursor(): void {
|
|
514
|
-
if (curX < 0) curX = 0
|
|
515
|
-
if (curX >= cols) curX = cols - 1
|
|
516
|
-
if (curY < 0) curY = 0
|
|
517
|
-
if (curY >= rows) curY = rows - 1
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// ── Scrolling ──
|
|
521
|
-
|
|
522
|
-
function scrollUp(top: number, bottom: number): void {
|
|
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
|
-
}
|
|
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
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function scrollDown(top: number, bottom: number): void {
|
|
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
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ── Character writing ──
|
|
585
|
-
|
|
586
|
-
/** Find the previous non-spacer cell (the cell before curX, skipping wide-char spacers) */
|
|
587
|
-
function getPrevCell(): { cell: ScreenCell; col: number } | null {
|
|
588
|
-
if (curX === 0 && curY === 0) return null
|
|
589
|
-
let prevCol = curX - 1
|
|
590
|
-
let prevRow = curY
|
|
591
|
-
if (prevCol < 0) {
|
|
592
|
-
prevRow--
|
|
593
|
-
if (prevRow < 0) return null
|
|
594
|
-
prevCol = cols - 1
|
|
595
|
-
}
|
|
596
|
-
const row = grid[prevRow]!
|
|
597
|
-
let cell = row[prevCol]!
|
|
598
|
-
// If we landed on a spacer (empty char after a wide character), go back one more
|
|
599
|
-
if (cell !== EMPTY_CELL && cell.char === "" && prevCol > 0) {
|
|
600
|
-
prevCol--
|
|
601
|
-
cell = row[prevCol]!
|
|
602
|
-
}
|
|
603
|
-
if (cell === EMPTY_CELL) return null
|
|
604
|
-
return { cell, col: prevCol }
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/** Widen a cell to 2 columns, adding a spacer cell after it */
|
|
608
|
-
function widenCell(row: ScreenCell[], col: number, cell: ScreenCell): void {
|
|
609
|
-
cell.wide = true
|
|
610
|
-
if (col + 1 < cols) {
|
|
611
|
-
let spacer = row[col + 1]!
|
|
612
|
-
if (spacer === EMPTY_CELL) {
|
|
613
|
-
spacer = { ...EMPTY_CELL }
|
|
614
|
-
row[col + 1] = spacer
|
|
615
|
-
}
|
|
616
|
-
spacer.char = ""
|
|
617
|
-
spacer.fg = null
|
|
618
|
-
spacer.bg = null
|
|
619
|
-
spacer.bold = false
|
|
620
|
-
spacer.faint = false
|
|
621
|
-
spacer.italic = false
|
|
622
|
-
spacer.underline = "none"
|
|
623
|
-
spacer.underlineColor = null
|
|
624
|
-
spacer.overline = false
|
|
625
|
-
spacer.strikethrough = false
|
|
626
|
-
spacer.inverse = false
|
|
627
|
-
spacer.hidden = false
|
|
628
|
-
spacer.blink = false
|
|
629
|
-
spacer.wide = false
|
|
630
|
-
spacer.url = null
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function writeChar(ch: string): void {
|
|
635
|
-
// Apply DEC Special Graphics character mapping
|
|
636
|
-
if (charsetG0 && ch.length === 1) {
|
|
637
|
-
const mapped = DEC_SPECIAL_GRAPHICS[ch]
|
|
638
|
-
if (mapped) ch = mapped
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const codePoint = ch.codePointAt(0) ?? 0
|
|
642
|
-
|
|
643
|
-
// ── VS-16 (U+FE0F): widen previous character to emoji presentation ──
|
|
644
|
-
if (isVS16(codePoint)) {
|
|
645
|
-
const prev = getPrevCell()
|
|
646
|
-
if (prev && !prev.cell.wide) {
|
|
647
|
-
prev.cell.char += ch
|
|
648
|
-
const row = grid[curY === 0 && curX === 0 ? 0 : curY]!
|
|
649
|
-
widenCell(row, prev.col, prev.cell)
|
|
650
|
-
// Advance cursor past the spacer
|
|
651
|
-
curX = prev.col + 2
|
|
652
|
-
if (curX >= cols) curX = cols - 1
|
|
653
|
-
}
|
|
654
|
-
return
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// ── Combining characters: append to previous cell, zero width ──
|
|
658
|
-
if (isCombining(codePoint) && !isVS16(codePoint)) {
|
|
659
|
-
const prev = getPrevCell()
|
|
660
|
-
if (prev) {
|
|
661
|
-
prev.cell.char += ch
|
|
662
|
-
}
|
|
663
|
-
return
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// ── Emoji modifier (skin tone): append to previous cell, zero width ──
|
|
667
|
-
if (isEmojiModifier(codePoint)) {
|
|
668
|
-
const prev = getPrevCell()
|
|
669
|
-
if (prev) {
|
|
670
|
-
prev.cell.char += ch
|
|
671
|
-
}
|
|
672
|
-
return
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ── ZWJ (U+200D): append to previous cell, flag for next char ──
|
|
676
|
-
if (isZWJ(codePoint)) {
|
|
677
|
-
const prev = getPrevCell()
|
|
678
|
-
if (prev) {
|
|
679
|
-
prev.cell.char += ch
|
|
680
|
-
afterZWJ = true
|
|
681
|
-
}
|
|
682
|
-
return
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// ── After ZWJ: append this character to the previous cell ──
|
|
686
|
-
if (afterZWJ) {
|
|
687
|
-
afterZWJ = false
|
|
688
|
-
const prev = getPrevCell()
|
|
689
|
-
if (prev) {
|
|
690
|
-
prev.cell.char += ch
|
|
691
|
-
// The ZWJ sequence stays in the same wide cell
|
|
692
|
-
return
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// ── Regional Indicators: pair into flag emoji ──
|
|
697
|
-
if (isRegionalIndicator(codePoint)) {
|
|
698
|
-
if (pendingRegionalIndicator !== null) {
|
|
699
|
-
// Second RI: combine with first to form a flag, render as wide
|
|
700
|
-
const flag = pendingRegionalIndicator + ch
|
|
701
|
-
pendingRegionalIndicator = null
|
|
702
|
-
// Write the combined flag as a wide character
|
|
703
|
-
writeCharCore(flag, true)
|
|
704
|
-
return
|
|
705
|
-
} else {
|
|
706
|
-
// First RI: store and wait for second
|
|
707
|
-
pendingRegionalIndicator = ch
|
|
708
|
-
return
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Flush any pending regional indicator that wasn't paired
|
|
713
|
-
if (pendingRegionalIndicator !== null) {
|
|
714
|
-
const ri = pendingRegionalIndicator
|
|
715
|
-
pendingRegionalIndicator = null
|
|
716
|
-
writeCharCore(ri, true)
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const wide = isWide(codePoint)
|
|
720
|
-
writeCharCore(ch, wide)
|
|
721
|
-
}
|
|
722
|
-
|
|
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
|
-
|
|
727
|
-
const charWidth = wide ? 2 : 1
|
|
728
|
-
const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : cols
|
|
729
|
-
const wrapReturn = leftRightMarginMode ? leftMargin : 0
|
|
730
|
-
|
|
731
|
-
// Handle autowrap at end of line (or right margin)
|
|
732
|
-
if (curX + charWidth > wrapBoundary) {
|
|
733
|
-
if (autoWrap) {
|
|
734
|
-
// Mark this row as soft-wrapped (auto-wrap caused the line break)
|
|
735
|
-
softWrapped[curY] = true
|
|
736
|
-
curX = wrapReturn
|
|
737
|
-
curY++
|
|
738
|
-
if (curY > scrollBottom) {
|
|
739
|
-
curY = scrollBottom
|
|
740
|
-
scrollUp(scrollTop, scrollBottom)
|
|
741
|
-
}
|
|
742
|
-
} else {
|
|
743
|
-
curX = wrapBoundary - charWidth
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Insert mode: shift existing characters right before writing
|
|
748
|
-
if (insertMode) {
|
|
749
|
-
const row = grid[curY]!
|
|
750
|
-
const insertEnd = leftRightMarginMode ? rightMargin + 1 : cols
|
|
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)
|
|
754
|
-
row.splice(curX, 0, EMPTY_CELL)
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// Copy-on-write: if cell is the shared EMPTY_CELL sentinel, create a fresh object
|
|
759
|
-
const row = grid[curY]!
|
|
760
|
-
let cell = row[curX]!
|
|
761
|
-
if (cell === EMPTY_CELL) {
|
|
762
|
-
cell = { ...EMPTY_CELL }
|
|
763
|
-
row[curX] = cell
|
|
764
|
-
}
|
|
765
|
-
cell.char = ch
|
|
766
|
-
cell.fg = attrs.fg ? { ...attrs.fg } : null
|
|
767
|
-
cell.bg = attrs.bg ? { ...attrs.bg } : null
|
|
768
|
-
cell.bold = attrs.bold
|
|
769
|
-
cell.faint = attrs.faint
|
|
770
|
-
cell.italic = attrs.italic
|
|
771
|
-
cell.underline = attrs.underline
|
|
772
|
-
cell.underlineColor = attrs.underlineColor ? { ...attrs.underlineColor } : null
|
|
773
|
-
cell.overline = attrs.overline
|
|
774
|
-
cell.strikethrough = attrs.strikethrough
|
|
775
|
-
cell.inverse = attrs.inverse
|
|
776
|
-
cell.hidden = attrs.hidden
|
|
777
|
-
cell.blink = attrs.blink
|
|
778
|
-
cell.wide = wide
|
|
779
|
-
cell.url = attrs.url
|
|
780
|
-
|
|
781
|
-
if (wide) {
|
|
782
|
-
widenCell(row, curX, cell)
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
curX += charWidth
|
|
786
|
-
lastChar = ch
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ── CSI handler ──
|
|
790
|
-
|
|
791
|
-
function handleCSI(params: string, intermediates: string, finalByte: string): void {
|
|
792
|
-
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
793
|
-
|
|
794
|
-
// CSI with intermediates
|
|
795
|
-
if (intermediates === " ") {
|
|
796
|
-
if (finalByte === "q") {
|
|
797
|
-
// DECSCUSR - Set Cursor Shape
|
|
798
|
-
const ps = parts[0] ?? 0
|
|
799
|
-
switch (ps) {
|
|
800
|
-
case 0:
|
|
801
|
-
case 1:
|
|
802
|
-
cursorShape = "block"
|
|
803
|
-
cursorBlinking = true
|
|
804
|
-
break
|
|
805
|
-
case 2:
|
|
806
|
-
cursorShape = "block"
|
|
807
|
-
cursorBlinking = false
|
|
808
|
-
break
|
|
809
|
-
case 3:
|
|
810
|
-
cursorShape = "underline"
|
|
811
|
-
cursorBlinking = true
|
|
812
|
-
break
|
|
813
|
-
case 4:
|
|
814
|
-
cursorShape = "underline"
|
|
815
|
-
cursorBlinking = false
|
|
816
|
-
break
|
|
817
|
-
case 5:
|
|
818
|
-
cursorShape = "bar"
|
|
819
|
-
cursorBlinking = true
|
|
820
|
-
break
|
|
821
|
-
case 6:
|
|
822
|
-
cursorShape = "bar"
|
|
823
|
-
cursorBlinking = false
|
|
824
|
-
break
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (intermediates === "!") {
|
|
831
|
-
if (finalByte === "p") {
|
|
832
|
-
// DECSTR - Soft Terminal Reset
|
|
833
|
-
softReset()
|
|
834
|
-
}
|
|
835
|
-
return
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (intermediates === "$") {
|
|
839
|
-
// Mode reporting is handled in CSI private
|
|
840
|
-
return
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
switch (finalByte) {
|
|
844
|
-
case "A": // CUU - Cursor Up
|
|
845
|
-
curY -= Math.max(parts[0] ?? 1, 1)
|
|
846
|
-
clampCursor()
|
|
847
|
-
break
|
|
848
|
-
case "B": // CUD - Cursor Down
|
|
849
|
-
curY += Math.max(parts[0] ?? 1, 1)
|
|
850
|
-
clampCursor()
|
|
851
|
-
break
|
|
852
|
-
case "C": // CUF - Cursor Forward
|
|
853
|
-
curX += Math.max(parts[0] ?? 1, 1)
|
|
854
|
-
clampCursor()
|
|
855
|
-
break
|
|
856
|
-
case "D": // CUB - Cursor Back
|
|
857
|
-
curX -= Math.max(parts[0] ?? 1, 1)
|
|
858
|
-
clampCursor()
|
|
859
|
-
break
|
|
860
|
-
case "E": // CNL - Cursor Next Line
|
|
861
|
-
curY += Math.max(parts[0] ?? 1, 1)
|
|
862
|
-
curX = 0
|
|
863
|
-
clampCursor()
|
|
864
|
-
break
|
|
865
|
-
case "F": // CPL - Cursor Previous Line
|
|
866
|
-
curY -= Math.max(parts[0] ?? 1, 1)
|
|
867
|
-
curX = 0
|
|
868
|
-
clampCursor()
|
|
869
|
-
break
|
|
870
|
-
case "G": // CHA - Cursor Horizontal Absolute
|
|
871
|
-
curX = (parts[0] ?? 1) - 1
|
|
872
|
-
clampCursor()
|
|
873
|
-
break
|
|
874
|
-
case "H": // CUP - Cursor Position
|
|
875
|
-
case "f": // HVP - same as CUP
|
|
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
|
-
}
|
|
890
|
-
break
|
|
891
|
-
case "J": // ED - Erase in Display
|
|
892
|
-
handleEraseDisplay(parts[0] ?? 0)
|
|
893
|
-
break
|
|
894
|
-
case "K": // EL - Erase in Line
|
|
895
|
-
handleEraseLine(parts[0] ?? 0)
|
|
896
|
-
break
|
|
897
|
-
case "L": // IL - Insert Lines
|
|
898
|
-
handleInsertLines(Math.max(parts[0] ?? 1, 1))
|
|
899
|
-
break
|
|
900
|
-
case "M": // DL - Delete Lines
|
|
901
|
-
handleDeleteLines(Math.max(parts[0] ?? 1, 1))
|
|
902
|
-
break
|
|
903
|
-
case "P": // DCH - Delete Characters
|
|
904
|
-
handleDeleteChars(Math.max(parts[0] ?? 1, 1))
|
|
905
|
-
break
|
|
906
|
-
case "@": // ICH - Insert Characters
|
|
907
|
-
handleInsertChars(Math.max(parts[0] ?? 1, 1))
|
|
908
|
-
break
|
|
909
|
-
case "X": // ECH - Erase Characters
|
|
910
|
-
handleEraseChars(Math.max(parts[0] ?? 1, 1))
|
|
911
|
-
break
|
|
912
|
-
case "S": // SU - Scroll Up
|
|
913
|
-
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
|
|
914
|
-
scrollUp(scrollTop, scrollBottom)
|
|
915
|
-
}
|
|
916
|
-
break
|
|
917
|
-
case "T": // SD - Scroll Down
|
|
918
|
-
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
|
|
919
|
-
scrollDown(scrollTop, scrollBottom)
|
|
920
|
-
}
|
|
921
|
-
break
|
|
922
|
-
case "b": // REP - Repeat preceding character
|
|
923
|
-
if (lastChar) {
|
|
924
|
-
const count = Math.max(parts[0] ?? 1, 1)
|
|
925
|
-
for (let i = 0; i < count; i++) {
|
|
926
|
-
writeChar(lastChar)
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
break
|
|
930
|
-
case "d": // VPA - Line Position Absolute
|
|
931
|
-
curY = (parts[0] ?? 1) - 1
|
|
932
|
-
clampCursor()
|
|
933
|
-
break
|
|
934
|
-
case "m": // SGR - Select Graphic Rendition
|
|
935
|
-
handleSGR(params)
|
|
936
|
-
break
|
|
937
|
-
case "r": // DECSTBM - Set Scrolling Region
|
|
938
|
-
scrollTop = (parts[0] ?? 1) - 1
|
|
939
|
-
scrollBottom = (parts[1] ?? rows) - 1
|
|
940
|
-
if (scrollTop < 0) scrollTop = 0
|
|
941
|
-
if (scrollBottom >= rows) scrollBottom = rows - 1
|
|
942
|
-
if (scrollTop > scrollBottom) {
|
|
943
|
-
scrollTop = 0
|
|
944
|
-
scrollBottom = rows - 1
|
|
945
|
-
}
|
|
946
|
-
curX = 0
|
|
947
|
-
curY = originMode ? scrollTop : 0
|
|
948
|
-
break
|
|
949
|
-
case "n": // DSR - Device Status Report
|
|
950
|
-
if (onResponse) {
|
|
951
|
-
if (parts[0] === 5) {
|
|
952
|
-
// Status report - OK
|
|
953
|
-
onResponse("\x1b[0n")
|
|
954
|
-
} else if (parts[0] === 6) {
|
|
955
|
-
// Cursor position report
|
|
956
|
-
onResponse(`\x1b[${curY + 1};${curX + 1}R`)
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
break
|
|
960
|
-
case "c": // DA1 - Primary Device Attributes
|
|
961
|
-
if (onResponse) {
|
|
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.
|
|
966
|
-
onResponse("\x1b[?62;4c")
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
break
|
|
970
|
-
case "h": // SM - Set Mode (non-private)
|
|
971
|
-
for (const code of parts) {
|
|
972
|
-
if (code === 4) insertMode = true // IRM - Insert/Replace Mode
|
|
973
|
-
}
|
|
974
|
-
break
|
|
975
|
-
case "l": // RM - Reset Mode (non-private)
|
|
976
|
-
for (const code of parts) {
|
|
977
|
-
if (code === 4) insertMode = false
|
|
978
|
-
}
|
|
979
|
-
break
|
|
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
|
-
}
|
|
1000
|
-
break
|
|
1001
|
-
case "u": // RCP - Restore Cursor Position
|
|
1002
|
-
curX = savedCurX
|
|
1003
|
-
curY = savedCurY
|
|
1004
|
-
clampCursor()
|
|
1005
|
-
break
|
|
1006
|
-
default:
|
|
1007
|
-
break
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function handleCSIGt(params: string, _intermediates: string, finalByte: string): void {
|
|
1012
|
-
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
1013
|
-
|
|
1014
|
-
// CSI > sequences
|
|
1015
|
-
if (finalByte === "c") {
|
|
1016
|
-
// DA2 - Secondary Device Attributes
|
|
1017
|
-
if (onResponse) {
|
|
1018
|
-
if (params === "" || params === "0") {
|
|
1019
|
-
onResponse("\x1b[>1;100;0c")
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
} else if (finalByte === "q") {
|
|
1023
|
-
// XTVERSION
|
|
1024
|
-
if (onResponse) {
|
|
1025
|
-
if (params === "" || params === "0") {
|
|
1026
|
-
onResponse("\x1bP>|vterm.js 0.1.0\x1b\\")
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
} else if (finalByte === "u") {
|
|
1030
|
-
// CSI > flags u — Push keyboard mode (Kitty keyboard protocol)
|
|
1031
|
-
kittyKeyboardStack.push(kittyKeyboardFlags)
|
|
1032
|
-
kittyKeyboardFlags = parts[0] ?? 0
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function handleCSILt(_params: string, _intermediates: string, finalByte: string): void {
|
|
1037
|
-
// CSI < sequences
|
|
1038
|
-
if (finalByte === "u") {
|
|
1039
|
-
// CSI < u — Pop keyboard mode (Kitty keyboard protocol)
|
|
1040
|
-
kittyKeyboardFlags = kittyKeyboardStack.pop() ?? 0
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
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
|
-
|
|
1056
|
-
function handleCSIPrivate(params: string, intermediates: string, finalByte: string): void {
|
|
1057
|
-
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
1058
|
-
|
|
1059
|
-
// CSI ? u — Query keyboard mode (Kitty keyboard protocol)
|
|
1060
|
-
if (finalByte === "u") {
|
|
1061
|
-
if (onResponse) {
|
|
1062
|
-
onResponse(`\x1b[?${kittyKeyboardFlags}u`)
|
|
1063
|
-
}
|
|
1064
|
-
return
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// DECRPM - Mode reporting: CSI ? Pd $ p
|
|
1068
|
-
if (intermediates === "$" && finalByte === "p") {
|
|
1069
|
-
if (onResponse) {
|
|
1070
|
-
const mode = parts[0] ?? 0
|
|
1071
|
-
let value = 0 // 0 = not recognized
|
|
1072
|
-
switch (mode) {
|
|
1073
|
-
case 1:
|
|
1074
|
-
value = applicationCursor ? 1 : 2
|
|
1075
|
-
break
|
|
1076
|
-
case 6:
|
|
1077
|
-
value = originMode ? 1 : 2
|
|
1078
|
-
break
|
|
1079
|
-
case 7:
|
|
1080
|
-
value = autoWrap ? 1 : 2
|
|
1081
|
-
break
|
|
1082
|
-
case 25:
|
|
1083
|
-
value = curVisible ? 1 : 2
|
|
1084
|
-
break
|
|
1085
|
-
case 47:
|
|
1086
|
-
case 1047:
|
|
1087
|
-
case 1049:
|
|
1088
|
-
value = useAltScreen ? 1 : 2
|
|
1089
|
-
break
|
|
1090
|
-
case 66:
|
|
1091
|
-
value = applicationKeypad ? 1 : 2
|
|
1092
|
-
break
|
|
1093
|
-
case 1000:
|
|
1094
|
-
case 1002:
|
|
1095
|
-
case 1003:
|
|
1096
|
-
value = mouseTracking ? 1 : 2
|
|
1097
|
-
break
|
|
1098
|
-
case 1004:
|
|
1099
|
-
value = focusTracking ? 1 : 2
|
|
1100
|
-
break
|
|
1101
|
-
case 1006:
|
|
1102
|
-
value = sgrMouse ? 1 : 2
|
|
1103
|
-
break
|
|
1104
|
-
case 2004:
|
|
1105
|
-
value = bracketedPaste ? 1 : 2
|
|
1106
|
-
break
|
|
1107
|
-
case 2026:
|
|
1108
|
-
value = syncOutput ? 1 : 2
|
|
1109
|
-
break
|
|
1110
|
-
case 69:
|
|
1111
|
-
value = leftRightMarginMode ? 1 : 2
|
|
1112
|
-
break
|
|
1113
|
-
case 2031:
|
|
1114
|
-
value = colorSchemeReporting ? 1 : 2
|
|
1115
|
-
break
|
|
1116
|
-
case 5:
|
|
1117
|
-
value = reverseVideo ? 1 : 2
|
|
1118
|
-
break
|
|
1119
|
-
case 4:
|
|
1120
|
-
value = insertMode ? 1 : 2
|
|
1121
|
-
break
|
|
1122
|
-
}
|
|
1123
|
-
onResponse(`\x1b[?${mode};${value}$y`)
|
|
1124
|
-
}
|
|
1125
|
-
return
|
|
1126
|
-
}
|
|
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
|
-
|
|
1154
|
-
const set = finalByte === "h"
|
|
1155
|
-
|
|
1156
|
-
for (const code of parts) {
|
|
1157
|
-
switch (code) {
|
|
1158
|
-
case 1: // DECCKM - Application Cursor
|
|
1159
|
-
applicationCursor = set
|
|
1160
|
-
break
|
|
1161
|
-
case 4: // IRM - Insert Mode (via DEC private)
|
|
1162
|
-
insertMode = set
|
|
1163
|
-
break
|
|
1164
|
-
case 5: // DECSCNM - Reverse Video
|
|
1165
|
-
reverseVideo = set
|
|
1166
|
-
break
|
|
1167
|
-
case 6: // DECOM - Origin Mode
|
|
1168
|
-
originMode = set
|
|
1169
|
-
break
|
|
1170
|
-
case 7: // DECAWM - Autowrap Mode
|
|
1171
|
-
autoWrap = set
|
|
1172
|
-
break
|
|
1173
|
-
case 25: // DECTCEM - Cursor Visible
|
|
1174
|
-
curVisible = set
|
|
1175
|
-
break
|
|
1176
|
-
case 47: // Alternate screen buffer (old)
|
|
1177
|
-
case 1047: // Alternate screen buffer
|
|
1178
|
-
if (set && !useAltScreen) {
|
|
1179
|
-
useAltScreen = true
|
|
1180
|
-
grid = altGrid
|
|
1181
|
-
softWrapped = altSoftWrapped
|
|
1182
|
-
} else if (!set && useAltScreen) {
|
|
1183
|
-
useAltScreen = false
|
|
1184
|
-
grid = mainGrid
|
|
1185
|
-
softWrapped = mainSoftWrapped
|
|
1186
|
-
}
|
|
1187
|
-
break
|
|
1188
|
-
case 66: // DECNKM - Application Keypad
|
|
1189
|
-
applicationKeypad = set
|
|
1190
|
-
break
|
|
1191
|
-
case 9: // X10 mouse
|
|
1192
|
-
case 1000: // Mouse tracking (basic)
|
|
1193
|
-
case 1002: // Mouse tracking (button events)
|
|
1194
|
-
case 1003: // Mouse tracking (all events)
|
|
1195
|
-
case 1015: // urxvt mouse encoding
|
|
1196
|
-
case 1016: // SGR pixel mouse
|
|
1197
|
-
mouseTracking = set
|
|
1198
|
-
mouseTrackingMode = set ? code : 0
|
|
1199
|
-
break
|
|
1200
|
-
case 1004: // Focus tracking
|
|
1201
|
-
focusTracking = set
|
|
1202
|
-
break
|
|
1203
|
-
case 1006: // SGR mouse mode
|
|
1204
|
-
sgrMouse = set
|
|
1205
|
-
break
|
|
1206
|
-
case 1049: // Alternate screen buffer + save/restore cursor
|
|
1207
|
-
if (set && !useAltScreen) {
|
|
1208
|
-
savedCurX = curX
|
|
1209
|
-
savedCurY = curY
|
|
1210
|
-
useAltScreen = true
|
|
1211
|
-
altGrid = makeGrid(cols, rows)
|
|
1212
|
-
altSoftWrapped = new Array(rows).fill(false)
|
|
1213
|
-
grid = altGrid
|
|
1214
|
-
softWrapped = altSoftWrapped
|
|
1215
|
-
curX = 0
|
|
1216
|
-
curY = 0
|
|
1217
|
-
} else if (!set && useAltScreen) {
|
|
1218
|
-
useAltScreen = false
|
|
1219
|
-
grid = mainGrid
|
|
1220
|
-
softWrapped = mainSoftWrapped
|
|
1221
|
-
curX = savedCurX
|
|
1222
|
-
curY = savedCurY
|
|
1223
|
-
clampCursor()
|
|
1224
|
-
}
|
|
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
|
|
1234
|
-
case 2004: // Bracketed paste
|
|
1235
|
-
bracketedPaste = set
|
|
1236
|
-
break
|
|
1237
|
-
case 2026: // Synchronized output
|
|
1238
|
-
syncOutput = set
|
|
1239
|
-
break
|
|
1240
|
-
case 2031: // Color scheme reporting
|
|
1241
|
-
colorSchemeReporting = set
|
|
1242
|
-
break
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// ── Erase operations ──
|
|
1248
|
-
|
|
1249
|
-
function handleEraseDisplay(mode: number): void {
|
|
1250
|
-
switch (mode) {
|
|
1251
|
-
case 0: // Erase from cursor to end
|
|
1252
|
-
eraseCells(curY, curX, curY, cols - 1)
|
|
1253
|
-
for (let row = curY + 1; row < rows; row++) {
|
|
1254
|
-
eraseCells(row, 0, row, cols - 1)
|
|
1255
|
-
}
|
|
1256
|
-
break
|
|
1257
|
-
case 1: // Erase from start to cursor
|
|
1258
|
-
for (let row = 0; row < curY; row++) {
|
|
1259
|
-
eraseCells(row, 0, row, cols - 1)
|
|
1260
|
-
}
|
|
1261
|
-
eraseCells(curY, 0, curY, curX)
|
|
1262
|
-
break
|
|
1263
|
-
case 2: // Erase entire display
|
|
1264
|
-
case 3: // Erase entire display + scrollback
|
|
1265
|
-
for (let row = 0; row < rows; row++) {
|
|
1266
|
-
eraseCells(row, 0, row, cols - 1)
|
|
1267
|
-
}
|
|
1268
|
-
if (mode === 3) {
|
|
1269
|
-
scrollback.length = 0
|
|
1270
|
-
}
|
|
1271
|
-
break
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
function handleEraseLine(mode: number): void {
|
|
1276
|
-
const eraseRight = leftRightMarginMode ? rightMargin : cols - 1
|
|
1277
|
-
const eraseLeft = leftRightMarginMode ? leftMargin : 0
|
|
1278
|
-
switch (mode) {
|
|
1279
|
-
case 0: // Erase from cursor to end of line (or right margin)
|
|
1280
|
-
eraseCells(curY, curX, curY, eraseRight)
|
|
1281
|
-
break
|
|
1282
|
-
case 1: // Erase from start (or left margin) to cursor
|
|
1283
|
-
eraseCells(curY, eraseLeft, curY, curX)
|
|
1284
|
-
break
|
|
1285
|
-
case 2: // Erase entire line (within margins if active)
|
|
1286
|
-
eraseCells(curY, eraseLeft, curY, eraseRight)
|
|
1287
|
-
break
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function eraseCells(row: number, startCol: number, _endRow: number, endCol: number): void {
|
|
1292
|
-
const r = grid[row]
|
|
1293
|
-
if (!r) return
|
|
1294
|
-
for (let col = startCol; col <= endCol && col < cols; col++) {
|
|
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
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
function handleInsertLines(count: number): void {
|
|
1305
|
-
if (curY < scrollTop || curY > scrollBottom) return
|
|
1306
|
-
for (let i = 0; i < count; i++) {
|
|
1307
|
-
scrollDown(curY, scrollBottom)
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
function handleDeleteLines(count: number): void {
|
|
1312
|
-
if (curY < scrollTop || curY > scrollBottom) return
|
|
1313
|
-
for (let i = 0; i < count; i++) {
|
|
1314
|
-
scrollUp(curY, scrollBottom)
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function handleDeleteChars(count: number): void {
|
|
1319
|
-
const row = grid[curY]
|
|
1320
|
-
if (!row) return
|
|
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
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
function handleInsertChars(count: number): void {
|
|
1340
|
-
const row = grid[curY]
|
|
1341
|
-
if (!row) return
|
|
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
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
function handleEraseChars(count: number): void {
|
|
1357
|
-
const row = grid[curY]
|
|
1358
|
-
if (!row) return
|
|
1359
|
-
for (let i = 0; i < count && curX + i < cols; i++) {
|
|
1360
|
-
row[curX + i] = emptyCell()
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// ── SGR (Select Graphic Rendition) ──
|
|
1365
|
-
|
|
1366
|
-
function handleSGR(rawParams: string): void {
|
|
1367
|
-
const segments = rawParams.split(";")
|
|
1368
|
-
const params: number[] = []
|
|
1369
|
-
const subParams = new Map<number, number[]>()
|
|
1370
|
-
for (const seg of segments) {
|
|
1371
|
-
if (seg.includes(":")) {
|
|
1372
|
-
const subs = seg.split(":").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
1373
|
-
subParams.set(params.length, subs)
|
|
1374
|
-
params.push(subs[0]!)
|
|
1375
|
-
} else {
|
|
1376
|
-
params.push(seg === "" ? 0 : parseInt(seg, 10))
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
|
|
1381
|
-
attrs = resetAttrs()
|
|
1382
|
-
return
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
let i = 0
|
|
1386
|
-
while (i < params.length) {
|
|
1387
|
-
const code = params[i]!
|
|
1388
|
-
switch (code) {
|
|
1389
|
-
case 0:
|
|
1390
|
-
attrs = resetAttrs()
|
|
1391
|
-
break
|
|
1392
|
-
case 1:
|
|
1393
|
-
attrs.bold = true
|
|
1394
|
-
break
|
|
1395
|
-
case 2:
|
|
1396
|
-
attrs.faint = true
|
|
1397
|
-
break
|
|
1398
|
-
case 3:
|
|
1399
|
-
attrs.italic = true
|
|
1400
|
-
break
|
|
1401
|
-
case 4: {
|
|
1402
|
-
// SGR 4 with optional sub-parameter: 4:0=none, 4:1=single, 4:3=curly, etc.
|
|
1403
|
-
const subs = subParams.get(i)
|
|
1404
|
-
if (subs && subs.length > 1) {
|
|
1405
|
-
const sub = subs[1]!
|
|
1406
|
-
switch (sub) {
|
|
1407
|
-
case 0:
|
|
1408
|
-
attrs.underline = "none"
|
|
1409
|
-
break
|
|
1410
|
-
case 1:
|
|
1411
|
-
attrs.underline = "single"
|
|
1412
|
-
break
|
|
1413
|
-
case 2:
|
|
1414
|
-
attrs.underline = "double"
|
|
1415
|
-
break
|
|
1416
|
-
case 3:
|
|
1417
|
-
attrs.underline = "curly"
|
|
1418
|
-
break
|
|
1419
|
-
case 4:
|
|
1420
|
-
attrs.underline = "dotted"
|
|
1421
|
-
break
|
|
1422
|
-
case 5:
|
|
1423
|
-
attrs.underline = "dashed"
|
|
1424
|
-
break
|
|
1425
|
-
default:
|
|
1426
|
-
attrs.underline = "single"
|
|
1427
|
-
break
|
|
1428
|
-
}
|
|
1429
|
-
} else {
|
|
1430
|
-
attrs.underline = "single"
|
|
1431
|
-
}
|
|
1432
|
-
break
|
|
1433
|
-
}
|
|
1434
|
-
case 5: // Slow blink
|
|
1435
|
-
attrs.blink = true
|
|
1436
|
-
break
|
|
1437
|
-
case 6: // Rapid blink (treat same as blink)
|
|
1438
|
-
attrs.blink = true
|
|
1439
|
-
break
|
|
1440
|
-
case 7:
|
|
1441
|
-
attrs.inverse = true
|
|
1442
|
-
break
|
|
1443
|
-
case 8: // Hidden/conceal
|
|
1444
|
-
attrs.hidden = true
|
|
1445
|
-
break
|
|
1446
|
-
case 9:
|
|
1447
|
-
attrs.strikethrough = true
|
|
1448
|
-
break
|
|
1449
|
-
case 21: // Double underline
|
|
1450
|
-
attrs.underline = "double"
|
|
1451
|
-
break
|
|
1452
|
-
case 22: // Normal intensity (neither bold nor faint)
|
|
1453
|
-
attrs.bold = false
|
|
1454
|
-
attrs.faint = false
|
|
1455
|
-
break
|
|
1456
|
-
case 23:
|
|
1457
|
-
attrs.italic = false
|
|
1458
|
-
break
|
|
1459
|
-
case 24:
|
|
1460
|
-
attrs.underline = "none"
|
|
1461
|
-
break
|
|
1462
|
-
case 25: // Blink off
|
|
1463
|
-
attrs.blink = false
|
|
1464
|
-
break
|
|
1465
|
-
case 27:
|
|
1466
|
-
attrs.inverse = false
|
|
1467
|
-
break
|
|
1468
|
-
case 28: // Reveal (turn off hidden/conceal)
|
|
1469
|
-
attrs.hidden = false
|
|
1470
|
-
break
|
|
1471
|
-
case 29:
|
|
1472
|
-
attrs.strikethrough = false
|
|
1473
|
-
break
|
|
1474
|
-
// Foreground colors 30-37
|
|
1475
|
-
case 30:
|
|
1476
|
-
case 31:
|
|
1477
|
-
case 32:
|
|
1478
|
-
case 33:
|
|
1479
|
-
case 34:
|
|
1480
|
-
case 35:
|
|
1481
|
-
case 36:
|
|
1482
|
-
case 37:
|
|
1483
|
-
attrs.fg = { ...PALETTE_256[code - 30]! }
|
|
1484
|
-
break
|
|
1485
|
-
case 38: {
|
|
1486
|
-
// Extended foreground: 38;5;N (256) or 38;2;R;G;B (truecolor)
|
|
1487
|
-
// Also handle colon form: 38:5:N or 38:2:R:G:B
|
|
1488
|
-
const subs = subParams.get(i)
|
|
1489
|
-
if (subs && subs.length >= 3) {
|
|
1490
|
-
const result = parseExtendedColorFromSubs(subs)
|
|
1491
|
-
if (result) attrs.fg = result
|
|
1492
|
-
} else {
|
|
1493
|
-
const result = parseExtendedColor(params, i)
|
|
1494
|
-
if (result) {
|
|
1495
|
-
attrs.fg = result.color
|
|
1496
|
-
i = result.nextIndex - 1
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
break
|
|
1500
|
-
}
|
|
1501
|
-
case 39: // Default foreground
|
|
1502
|
-
attrs.fg = null
|
|
1503
|
-
break
|
|
1504
|
-
// Background colors 40-47
|
|
1505
|
-
case 40:
|
|
1506
|
-
case 41:
|
|
1507
|
-
case 42:
|
|
1508
|
-
case 43:
|
|
1509
|
-
case 44:
|
|
1510
|
-
case 45:
|
|
1511
|
-
case 46:
|
|
1512
|
-
case 47:
|
|
1513
|
-
attrs.bg = { ...PALETTE_256[code - 40]! }
|
|
1514
|
-
break
|
|
1515
|
-
case 48: {
|
|
1516
|
-
// Extended background: 48;5;N (256) or 48;2;R;G;B (truecolor)
|
|
1517
|
-
const subs = subParams.get(i)
|
|
1518
|
-
if (subs && subs.length >= 3) {
|
|
1519
|
-
const result = parseExtendedColorFromSubs(subs)
|
|
1520
|
-
if (result) attrs.bg = result
|
|
1521
|
-
} else {
|
|
1522
|
-
const result = parseExtendedColor(params, i)
|
|
1523
|
-
if (result) {
|
|
1524
|
-
attrs.bg = result.color
|
|
1525
|
-
i = result.nextIndex - 1
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
break
|
|
1529
|
-
}
|
|
1530
|
-
case 49: // Default background
|
|
1531
|
-
attrs.bg = null
|
|
1532
|
-
break
|
|
1533
|
-
case 53: // Overline
|
|
1534
|
-
attrs.overline = true
|
|
1535
|
-
break
|
|
1536
|
-
case 55: // Overline off
|
|
1537
|
-
attrs.overline = false
|
|
1538
|
-
break
|
|
1539
|
-
case 58: {
|
|
1540
|
-
// Underline color: 58;5;N (256) or 58;2;R;G;B (truecolor)
|
|
1541
|
-
// Also handle colon form: 58:5:N or 58:2:R:G:B
|
|
1542
|
-
const subs = subParams.get(i)
|
|
1543
|
-
if (subs && subs.length >= 3) {
|
|
1544
|
-
const result = parseExtendedColorFromSubs(subs)
|
|
1545
|
-
if (result) attrs.underlineColor = result
|
|
1546
|
-
} else {
|
|
1547
|
-
const result = parseExtendedColor(params, i)
|
|
1548
|
-
if (result) {
|
|
1549
|
-
attrs.underlineColor = result.color
|
|
1550
|
-
i = result.nextIndex - 1
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
break
|
|
1554
|
-
}
|
|
1555
|
-
case 59: // Default underline color
|
|
1556
|
-
attrs.underlineColor = null
|
|
1557
|
-
break
|
|
1558
|
-
// Bright foreground 90-97
|
|
1559
|
-
case 90:
|
|
1560
|
-
case 91:
|
|
1561
|
-
case 92:
|
|
1562
|
-
case 93:
|
|
1563
|
-
case 94:
|
|
1564
|
-
case 95:
|
|
1565
|
-
case 96:
|
|
1566
|
-
case 97:
|
|
1567
|
-
attrs.fg = { ...PALETTE_256[code - 90 + 8]! }
|
|
1568
|
-
break
|
|
1569
|
-
// Bright background 100-107
|
|
1570
|
-
case 100:
|
|
1571
|
-
case 101:
|
|
1572
|
-
case 102:
|
|
1573
|
-
case 103:
|
|
1574
|
-
case 104:
|
|
1575
|
-
case 105:
|
|
1576
|
-
case 106:
|
|
1577
|
-
case 107:
|
|
1578
|
-
attrs.bg = { ...PALETTE_256[code - 100 + 8]! }
|
|
1579
|
-
break
|
|
1580
|
-
}
|
|
1581
|
-
i++
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function parseExtendedColor(params: number[], startIndex: number): { color: CellColor; nextIndex: number } | null {
|
|
1586
|
-
if (startIndex + 1 >= params.length) return null
|
|
1587
|
-
|
|
1588
|
-
const type = params[startIndex + 1]
|
|
1589
|
-
if (type === 5 && startIndex + 2 < params.length) {
|
|
1590
|
-
const idx = params[startIndex + 2]!
|
|
1591
|
-
const color = PALETTE_256[idx] ?? { r: 0, g: 0, b: 0 }
|
|
1592
|
-
return { color: { ...color }, nextIndex: startIndex + 3 }
|
|
1593
|
-
} else if (type === 2 && startIndex + 4 < params.length) {
|
|
1594
|
-
return {
|
|
1595
|
-
color: {
|
|
1596
|
-
r: params[startIndex + 2]!,
|
|
1597
|
-
g: params[startIndex + 3]!,
|
|
1598
|
-
b: params[startIndex + 4]!,
|
|
1599
|
-
},
|
|
1600
|
-
nextIndex: startIndex + 5,
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
return null
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
/** Parse extended color from colon sub-parameters (e.g., 38:2:R:G:B or 38:5:N) */
|
|
1607
|
-
function parseExtendedColorFromSubs(subs: number[]): CellColor | null {
|
|
1608
|
-
if (subs.length < 3) return null
|
|
1609
|
-
const type = subs[1]
|
|
1610
|
-
if (type === 5 && subs.length >= 3) {
|
|
1611
|
-
const idx = subs[2]!
|
|
1612
|
-
const color = PALETTE_256[idx] ?? { r: 0, g: 0, b: 0 }
|
|
1613
|
-
return { ...color }
|
|
1614
|
-
} else if (type === 2) {
|
|
1615
|
-
// Can be 38:2:R:G:B or 38:2:colorspace:R:G:B
|
|
1616
|
-
if (subs.length >= 5) {
|
|
1617
|
-
// 38:2:R:G:B (no colorspace) or 38:2:cs:R:G:B
|
|
1618
|
-
// If subs.length >= 6, assume colorspace variant
|
|
1619
|
-
if (subs.length >= 6) {
|
|
1620
|
-
return { r: subs[3]!, g: subs[4]!, b: subs[5]! }
|
|
1621
|
-
}
|
|
1622
|
-
return { r: subs[2]!, g: subs[3]!, b: subs[4]! }
|
|
1623
|
-
} else if (subs.length >= 4) {
|
|
1624
|
-
return { r: subs[2]!, g: subs[3]!, b: 0 }
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
return null
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// ── OSC handler ──
|
|
1631
|
-
|
|
1632
|
-
function handleOSC(oscString: string): void {
|
|
1633
|
-
const semicolonIdx = oscString.indexOf(";")
|
|
1634
|
-
if (semicolonIdx === -1) return
|
|
1635
|
-
|
|
1636
|
-
const code = parseInt(oscString.substring(0, semicolonIdx), 10)
|
|
1637
|
-
const value = oscString.substring(semicolonIdx + 1)
|
|
1638
|
-
|
|
1639
|
-
switch (code) {
|
|
1640
|
-
case 0: // Set icon name and window title
|
|
1641
|
-
case 2: // Set window title
|
|
1642
|
-
title = value
|
|
1643
|
-
break
|
|
1644
|
-
case 133: {
|
|
1645
|
-
// Semantic prompt markers (FinalTerm / shell integration)
|
|
1646
|
-
// Value format: "X" or "X;params" where X is A/B/C/D
|
|
1647
|
-
const marker = value.charAt(0)
|
|
1648
|
-
switch (marker) {
|
|
1649
|
-
case "A": // Start of prompt
|
|
1650
|
-
semanticZones.push({ type: "prompt", startRow: curY, startCol: curX })
|
|
1651
|
-
break
|
|
1652
|
-
case "B": // End of prompt / start of command
|
|
1653
|
-
semanticZones.push({ type: "command", startRow: curY, startCol: curX })
|
|
1654
|
-
break
|
|
1655
|
-
case "C": // End of command / start of output
|
|
1656
|
-
semanticZones.push({ type: "output", startRow: curY, startCol: curX })
|
|
1657
|
-
break
|
|
1658
|
-
case "D": // End of output (exit code in params, ignored for storage)
|
|
1659
|
-
break
|
|
1660
|
-
}
|
|
1661
|
-
break
|
|
1662
|
-
}
|
|
1663
|
-
case 1: // Set icon name (ignore)
|
|
1664
|
-
break
|
|
1665
|
-
case 7: // Current working directory: OSC 7 ; file://host/path ST
|
|
1666
|
-
cwd = value
|
|
1667
|
-
break
|
|
1668
|
-
case 8: {
|
|
1669
|
-
// Hyperlink: OSC 8 ; params ; url ST
|
|
1670
|
-
// Format: 8;params;url or 8;;url or 8;; (close)
|
|
1671
|
-
const secondSemicolon = value.indexOf(";")
|
|
1672
|
-
if (secondSemicolon !== -1) {
|
|
1673
|
-
const url = value.substring(secondSemicolon + 1)
|
|
1674
|
-
attrs.url = url || null
|
|
1675
|
-
}
|
|
1676
|
-
break
|
|
1677
|
-
}
|
|
1678
|
-
case 10: // Foreground color query
|
|
1679
|
-
if (value === "?" && onResponse) {
|
|
1680
|
-
// Default foreground (white-ish)
|
|
1681
|
-
onResponse("\x1b]10;rgb:ffff/ffff/ffff\x1b\\")
|
|
1682
|
-
}
|
|
1683
|
-
break
|
|
1684
|
-
case 11: // Background color query
|
|
1685
|
-
if (value === "?" && onResponse) {
|
|
1686
|
-
// Default background (black)
|
|
1687
|
-
onResponse("\x1b]11;rgb:0000/0000/0000\x1b\\")
|
|
1688
|
-
}
|
|
1689
|
-
break
|
|
1690
|
-
case 52: {
|
|
1691
|
-
// Clipboard: OSC 52 ; selection ; base64-data ST
|
|
1692
|
-
const clipSemi = value.indexOf(";")
|
|
1693
|
-
if (clipSemi !== -1) {
|
|
1694
|
-
const data = value.substring(clipSemi + 1)
|
|
1695
|
-
if (data === "?") {
|
|
1696
|
-
// Query clipboard
|
|
1697
|
-
if (onResponse) {
|
|
1698
|
-
const encoded = btoa(clipboard)
|
|
1699
|
-
onResponse(`\x1b]52;c;${encoded}\x1b\\`)
|
|
1700
|
-
}
|
|
1701
|
-
} else {
|
|
1702
|
-
// Set clipboard
|
|
1703
|
-
try {
|
|
1704
|
-
clipboard = atob(data)
|
|
1705
|
-
} catch {
|
|
1706
|
-
clipboard = ""
|
|
1707
|
-
}
|
|
1708
|
-
if (onResponse) {
|
|
1709
|
-
onResponse(`\x1b]52;c;${data}\x1b\\`)
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
break
|
|
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
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// ── DCS handler ──
|
|
1789
|
-
|
|
1790
|
-
function handleDCS(data: string): void {
|
|
1791
|
-
// Sixel graphics: DCS [Ps;Ps;Ps] q [sixel-data]
|
|
1792
|
-
const match = data.match(/^(\d*(?:;\d*)*)q(.*)$/s)
|
|
1793
|
-
if (match) {
|
|
1794
|
-
hasSixel = true
|
|
1795
|
-
sixelImages.push({
|
|
1796
|
-
data: match[2]!,
|
|
1797
|
-
row: dcsStartRow,
|
|
1798
|
-
col: dcsStartCol,
|
|
1799
|
-
})
|
|
1800
|
-
return
|
|
1801
|
-
}
|
|
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
|
-
|
|
1832
|
-
// Other DCS sequences are consumed and ignored
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
// ── APC handler ──
|
|
1836
|
-
|
|
1837
|
-
function handleAPC(data: string): void {
|
|
1838
|
-
if (!data.startsWith("G")) return
|
|
1839
|
-
|
|
1840
|
-
hasKittyGraphics = true
|
|
1841
|
-
|
|
1842
|
-
// Parse key=value pairs
|
|
1843
|
-
const semicolonIdx = data.indexOf(";")
|
|
1844
|
-
const kvPart = semicolonIdx >= 0 ? data.substring(1, semicolonIdx) : data.substring(1)
|
|
1845
|
-
|
|
1846
|
-
const params: Record<string, string> = {}
|
|
1847
|
-
for (const pair of kvPart.split(",")) {
|
|
1848
|
-
const eqIdx = pair.indexOf("=")
|
|
1849
|
-
if (eqIdx >= 0) {
|
|
1850
|
-
params[pair.substring(0, eqIdx)] = pair.substring(eqIdx + 1)
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
// Handle query action
|
|
1855
|
-
if (params.a === "q" && onResponse) {
|
|
1856
|
-
// Respond: OK
|
|
1857
|
-
onResponse(`\x1b_Gi=${params.i ?? "0"};OK\x1b\\`)
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
// ── Soft reset (DECSTR) ──
|
|
1862
|
-
|
|
1863
|
-
function softReset(): void {
|
|
1864
|
-
// Reset modes to defaults
|
|
1865
|
-
insertMode = false
|
|
1866
|
-
originMode = false
|
|
1867
|
-
autoWrap = true
|
|
1868
|
-
curVisible = true
|
|
1869
|
-
kittyKeyboardFlags = 0
|
|
1870
|
-
kittyKeyboardStack = []
|
|
1871
|
-
cursorShape = "block"
|
|
1872
|
-
cursorBlinking = true
|
|
1873
|
-
applicationCursor = false
|
|
1874
|
-
applicationKeypad = false
|
|
1875
|
-
reverseVideo = false
|
|
1876
|
-
|
|
1877
|
-
// Reset scroll region
|
|
1878
|
-
scrollTop = 0
|
|
1879
|
-
scrollBottom = rows - 1
|
|
1880
|
-
|
|
1881
|
-
// Reset left/right margins
|
|
1882
|
-
leftRightMarginMode = false
|
|
1883
|
-
leftMargin = 0
|
|
1884
|
-
rightMargin = cols - 1
|
|
1885
|
-
|
|
1886
|
-
// Reset attributes
|
|
1887
|
-
attrs = resetAttrs()
|
|
1888
|
-
|
|
1889
|
-
// Reset character set
|
|
1890
|
-
charsetG0 = false
|
|
1891
|
-
|
|
1892
|
-
// Reset cursor to home
|
|
1893
|
-
curX = 0
|
|
1894
|
-
curY = 0
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
// ── Full reset ──
|
|
1898
|
-
|
|
1899
|
-
function fullReset(): void {
|
|
1900
|
-
mainGrid = makeGrid(cols, rows)
|
|
1901
|
-
altGrid = makeGrid(cols, rows)
|
|
1902
|
-
grid = mainGrid
|
|
1903
|
-
scrollback = []
|
|
1904
|
-
curX = 0
|
|
1905
|
-
curY = 0
|
|
1906
|
-
curVisible = true
|
|
1907
|
-
cursorShape = "block"
|
|
1908
|
-
cursorBlinking = true
|
|
1909
|
-
savedCurX = 0
|
|
1910
|
-
savedCurY = 0
|
|
1911
|
-
savedState = { curX: 0, curY: 0, attrs: resetAttrs(), originMode: false, autoWrap: true, charsetG0: false }
|
|
1912
|
-
attrs = resetAttrs()
|
|
1913
|
-
title = ""
|
|
1914
|
-
useAltScreen = false
|
|
1915
|
-
bracketedPaste = false
|
|
1916
|
-
applicationCursor = false
|
|
1917
|
-
applicationKeypad = false
|
|
1918
|
-
autoWrap = true
|
|
1919
|
-
mouseTracking = false
|
|
1920
|
-
mouseTrackingMode = 0
|
|
1921
|
-
sgrMouse = false
|
|
1922
|
-
focusTracking = false
|
|
1923
|
-
originMode = false
|
|
1924
|
-
insertMode = false
|
|
1925
|
-
reverseVideo = false
|
|
1926
|
-
syncOutput = false
|
|
1927
|
-
kittyKeyboardFlags = 0
|
|
1928
|
-
kittyKeyboardStack = []
|
|
1929
|
-
hasKittyGraphics = false
|
|
1930
|
-
hasSixel = false
|
|
1931
|
-
sixelImages = []
|
|
1932
|
-
scrollTop = 0
|
|
1933
|
-
scrollBottom = rows - 1
|
|
1934
|
-
leftRightMarginMode = false
|
|
1935
|
-
leftMargin = 0
|
|
1936
|
-
rightMargin = cols - 1
|
|
1937
|
-
colorSchemeReporting = false
|
|
1938
|
-
textScale = 1
|
|
1939
|
-
advancedClipboard = ""
|
|
1940
|
-
viewportOffset = 0
|
|
1941
|
-
charsetG0 = false
|
|
1942
|
-
clipboard = ""
|
|
1943
|
-
cwd = ""
|
|
1944
|
-
notifications = []
|
|
1945
|
-
lastChar = ""
|
|
1946
|
-
pendingRegionalIndicator = null
|
|
1947
|
-
afterZWJ = false
|
|
1948
|
-
parserState = "ground"
|
|
1949
|
-
escBuf = ""
|
|
1950
|
-
oscBuf = ""
|
|
1951
|
-
apcBuf = ""
|
|
1952
|
-
semanticZones = []
|
|
1953
|
-
mainSoftWrapped = new Array(rows).fill(false)
|
|
1954
|
-
altSoftWrapped = new Array(rows).fill(false)
|
|
1955
|
-
softWrapped = mainSoftWrapped
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// ── Main parser ──
|
|
1959
|
-
|
|
1960
|
-
function process(data: Uint8Array): void {
|
|
1961
|
-
const text = decoder.decode(data, { stream: true })
|
|
1962
|
-
|
|
1963
|
-
for (let i = 0; i < text.length; i++) {
|
|
1964
|
-
const ch = text[i]!
|
|
1965
|
-
const code = text.charCodeAt(i)
|
|
1966
|
-
|
|
1967
|
-
switch (parserState) {
|
|
1968
|
-
case "ground":
|
|
1969
|
-
if (code === 0x1b) {
|
|
1970
|
-
parserState = "escape"
|
|
1971
|
-
escBuf = ""
|
|
1972
|
-
} else if (code === 0x07) {
|
|
1973
|
-
// BEL — ignore
|
|
1974
|
-
} else if (code === 0x08) {
|
|
1975
|
-
// BS - Backspace
|
|
1976
|
-
if (curX > 0) curX--
|
|
1977
|
-
} else if (code === 0x09) {
|
|
1978
|
-
// TAB
|
|
1979
|
-
curX = Math.min((Math.floor(curX / 8) + 1) * 8, cols - 1)
|
|
1980
|
-
} else if (code === 0x0a || code === 0x0b || code === 0x0c) {
|
|
1981
|
-
// LF, VT, FF — linefeed (hard break — clear any soft-wrap flag)
|
|
1982
|
-
softWrapped[curY] = false
|
|
1983
|
-
curY++
|
|
1984
|
-
if (curY > scrollBottom) {
|
|
1985
|
-
curY = scrollBottom
|
|
1986
|
-
scrollUp(scrollTop, scrollBottom)
|
|
1987
|
-
}
|
|
1988
|
-
} else if (code === 0x0d) {
|
|
1989
|
-
// CR - Carriage Return
|
|
1990
|
-
curX = 0
|
|
1991
|
-
} else if (code >= 0x20) {
|
|
1992
|
-
// Handle surrogate pairs for characters > U+FFFF
|
|
1993
|
-
let char = ch
|
|
1994
|
-
if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
|
|
1995
|
-
const nextCode = text.charCodeAt(i + 1)
|
|
1996
|
-
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
|
1997
|
-
char = ch + text[i + 1]!
|
|
1998
|
-
i++
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
writeChar(char)
|
|
2002
|
-
}
|
|
2003
|
-
break
|
|
2004
|
-
|
|
2005
|
-
case "escape":
|
|
2006
|
-
if (ch === "[") {
|
|
2007
|
-
parserState = "csi"
|
|
2008
|
-
escBuf = ""
|
|
2009
|
-
} else if (ch === "]") {
|
|
2010
|
-
parserState = "osc"
|
|
2011
|
-
oscBuf = ""
|
|
2012
|
-
} else if (ch === "P") {
|
|
2013
|
-
parserState = "dcs"
|
|
2014
|
-
escBuf = ""
|
|
2015
|
-
dcsBuf = ""
|
|
2016
|
-
dcsStartRow = curY
|
|
2017
|
-
dcsStartCol = curX
|
|
2018
|
-
} else if (ch === "c") {
|
|
2019
|
-
// RIS - Reset to Initial State
|
|
2020
|
-
fullReset()
|
|
2021
|
-
parserState = "ground"
|
|
2022
|
-
} else if (ch === "D") {
|
|
2023
|
-
// IND - Index (move cursor down, scroll if needed)
|
|
2024
|
-
curY++
|
|
2025
|
-
if (curY > scrollBottom) {
|
|
2026
|
-
curY = scrollBottom
|
|
2027
|
-
scrollUp(scrollTop, scrollBottom)
|
|
2028
|
-
}
|
|
2029
|
-
parserState = "ground"
|
|
2030
|
-
} else if (ch === "M") {
|
|
2031
|
-
// RI - Reverse Index (move cursor up, scroll if needed)
|
|
2032
|
-
curY--
|
|
2033
|
-
if (curY < scrollTop) {
|
|
2034
|
-
curY = scrollTop
|
|
2035
|
-
scrollDown(scrollTop, scrollBottom)
|
|
2036
|
-
}
|
|
2037
|
-
parserState = "ground"
|
|
2038
|
-
} else if (ch === "7") {
|
|
2039
|
-
// DECSC - Save Cursor + attributes + modes
|
|
2040
|
-
savedState = {
|
|
2041
|
-
curX,
|
|
2042
|
-
curY,
|
|
2043
|
-
attrs: {
|
|
2044
|
-
...attrs,
|
|
2045
|
-
fg: attrs.fg ? { ...attrs.fg } : null,
|
|
2046
|
-
bg: attrs.bg ? { ...attrs.bg } : null,
|
|
2047
|
-
underlineColor: attrs.underlineColor ? { ...attrs.underlineColor } : null,
|
|
2048
|
-
},
|
|
2049
|
-
originMode,
|
|
2050
|
-
autoWrap,
|
|
2051
|
-
charsetG0,
|
|
2052
|
-
}
|
|
2053
|
-
parserState = "ground"
|
|
2054
|
-
} else if (ch === "8") {
|
|
2055
|
-
// DECRC - Restore Cursor + attributes + modes
|
|
2056
|
-
curX = savedState.curX
|
|
2057
|
-
curY = savedState.curY
|
|
2058
|
-
attrs = {
|
|
2059
|
-
...savedState.attrs,
|
|
2060
|
-
fg: savedState.attrs.fg ? { ...savedState.attrs.fg } : null,
|
|
2061
|
-
bg: savedState.attrs.bg ? { ...savedState.attrs.bg } : null,
|
|
2062
|
-
underlineColor: savedState.attrs.underlineColor ? { ...savedState.attrs.underlineColor } : null,
|
|
2063
|
-
}
|
|
2064
|
-
originMode = savedState.originMode
|
|
2065
|
-
autoWrap = savedState.autoWrap
|
|
2066
|
-
charsetG0 = savedState.charsetG0
|
|
2067
|
-
clampCursor()
|
|
2068
|
-
parserState = "ground"
|
|
2069
|
-
} else if (ch === "E") {
|
|
2070
|
-
// NEL - Next Line
|
|
2071
|
-
curX = 0
|
|
2072
|
-
curY++
|
|
2073
|
-
if (curY > scrollBottom) {
|
|
2074
|
-
curY = scrollBottom
|
|
2075
|
-
scrollUp(scrollTop, scrollBottom)
|
|
2076
|
-
}
|
|
2077
|
-
parserState = "ground"
|
|
2078
|
-
} else if (ch === "(") {
|
|
2079
|
-
// Designate G0 character set
|
|
2080
|
-
parserState = "escape_charset"
|
|
2081
|
-
} else if (ch === ")") {
|
|
2082
|
-
// Designate G1 character set (ignored, just consume next byte)
|
|
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"
|
|
2092
|
-
} else if (ch === "_") {
|
|
2093
|
-
// APC - Application Program Command
|
|
2094
|
-
parserState = "apc"
|
|
2095
|
-
apcBuf = ""
|
|
2096
|
-
} else {
|
|
2097
|
-
// Unknown escape — return to ground
|
|
2098
|
-
parserState = "ground"
|
|
2099
|
-
}
|
|
2100
|
-
break
|
|
2101
|
-
|
|
2102
|
-
case "escape_charset":
|
|
2103
|
-
// Character set designation: ESC ( 0 = DEC Special Graphics, ESC ( B = ASCII
|
|
2104
|
-
if (ch === "0") {
|
|
2105
|
-
charsetG0 = true
|
|
2106
|
-
} else {
|
|
2107
|
-
charsetG0 = false // B = ASCII, or any other
|
|
2108
|
-
}
|
|
2109
|
-
parserState = "ground"
|
|
2110
|
-
break
|
|
2111
|
-
|
|
2112
|
-
case "csi": {
|
|
2113
|
-
if (code >= 0x40 && code <= 0x7e) {
|
|
2114
|
-
// Final byte — dispatch CSI
|
|
2115
|
-
// Extract intermediates (characters 0x20-0x2F between params and final)
|
|
2116
|
-
let paramPart = escBuf
|
|
2117
|
-
let intermediatePart = ""
|
|
2118
|
-
|
|
2119
|
-
// Check for intermediates at the end of escBuf
|
|
2120
|
-
// Intermediates are 0x20-0x2F: space ! " # $ % & ' ( ) * + , - . /
|
|
2121
|
-
let j = paramPart.length - 1
|
|
2122
|
-
while (j >= 0) {
|
|
2123
|
-
const c = paramPart.charCodeAt(j)
|
|
2124
|
-
if (c >= 0x20 && c <= 0x2f) {
|
|
2125
|
-
j--
|
|
2126
|
-
} else {
|
|
2127
|
-
break
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
if (j < paramPart.length - 1) {
|
|
2131
|
-
intermediatePart = paramPart.substring(j + 1)
|
|
2132
|
-
paramPart = paramPart.substring(0, j + 1)
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
if (paramPart.startsWith("?")) {
|
|
2136
|
-
handleCSIPrivate(paramPart.substring(1), intermediatePart, ch)
|
|
2137
|
-
} else if (paramPart.startsWith(">")) {
|
|
2138
|
-
handleCSIGt(paramPart.substring(1), intermediatePart, ch)
|
|
2139
|
-
} else if (paramPart.startsWith("<")) {
|
|
2140
|
-
handleCSILt(paramPart.substring(1), intermediatePart, ch)
|
|
2141
|
-
} else if (paramPart.startsWith("=")) {
|
|
2142
|
-
handleCSIEq(paramPart.substring(1), intermediatePart, ch)
|
|
2143
|
-
} else {
|
|
2144
|
-
handleCSI(paramPart, intermediatePart, ch)
|
|
2145
|
-
}
|
|
2146
|
-
parserState = "ground"
|
|
2147
|
-
} else if (escBuf.length >= 256) {
|
|
2148
|
-
parserState = "ground"
|
|
2149
|
-
} else {
|
|
2150
|
-
escBuf += ch
|
|
2151
|
-
}
|
|
2152
|
-
break
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
case "osc":
|
|
2156
|
-
if (code === 0x07) {
|
|
2157
|
-
// BEL terminates OSC
|
|
2158
|
-
handleOSC(oscBuf)
|
|
2159
|
-
parserState = "ground"
|
|
2160
|
-
} else if (code === 0x1b) {
|
|
2161
|
-
// ESC might be start of ST (\x1b\\)
|
|
2162
|
-
parserState = "osc_st"
|
|
2163
|
-
} else if (oscBuf.length >= 4096) {
|
|
2164
|
-
parserState = "ground"
|
|
2165
|
-
} else {
|
|
2166
|
-
oscBuf += ch
|
|
2167
|
-
}
|
|
2168
|
-
break
|
|
2169
|
-
|
|
2170
|
-
case "osc_st":
|
|
2171
|
-
if (ch === "\\") {
|
|
2172
|
-
// ST (String Terminator) — end of OSC
|
|
2173
|
-
handleOSC(oscBuf)
|
|
2174
|
-
}
|
|
2175
|
-
parserState = "ground"
|
|
2176
|
-
break
|
|
2177
|
-
|
|
2178
|
-
case "dcs":
|
|
2179
|
-
// Accumulate DCS data until ST (ESC \) or BEL
|
|
2180
|
-
if (code === 0x1b) {
|
|
2181
|
-
parserState = "dcs_st"
|
|
2182
|
-
} else if (code === 0x07) {
|
|
2183
|
-
// BEL terminates DCS
|
|
2184
|
-
handleDCS(dcsBuf)
|
|
2185
|
-
parserState = "ground"
|
|
2186
|
-
} else {
|
|
2187
|
-
dcsBuf += ch
|
|
2188
|
-
}
|
|
2189
|
-
break
|
|
2190
|
-
|
|
2191
|
-
case "dcs_st":
|
|
2192
|
-
// Expecting backslash to complete ST
|
|
2193
|
-
if (ch === "\\") {
|
|
2194
|
-
// ST (String Terminator) — end of DCS
|
|
2195
|
-
handleDCS(dcsBuf)
|
|
2196
|
-
}
|
|
2197
|
-
parserState = "ground"
|
|
2198
|
-
break
|
|
2199
|
-
|
|
2200
|
-
case "dcs_passthrough":
|
|
2201
|
-
// Consume until ST
|
|
2202
|
-
if (code === 0x1b) {
|
|
2203
|
-
parserState = "dcs_st"
|
|
2204
|
-
}
|
|
2205
|
-
break
|
|
2206
|
-
|
|
2207
|
-
case "apc":
|
|
2208
|
-
if (code === 0x1b) {
|
|
2209
|
-
parserState = "apc_st"
|
|
2210
|
-
} else if (code === 0x07) {
|
|
2211
|
-
// BEL terminates APC
|
|
2212
|
-
handleAPC(apcBuf)
|
|
2213
|
-
parserState = "ground"
|
|
2214
|
-
} else {
|
|
2215
|
-
apcBuf += ch
|
|
2216
|
-
}
|
|
2217
|
-
break
|
|
2218
|
-
|
|
2219
|
-
case "apc_st":
|
|
2220
|
-
if (ch === "\\") {
|
|
2221
|
-
// ST (String Terminator) — end of APC
|
|
2222
|
-
handleAPC(apcBuf)
|
|
2223
|
-
}
|
|
2224
|
-
parserState = "ground"
|
|
2225
|
-
break
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
// ── Resize ──
|
|
2231
|
-
|
|
2232
|
-
/**
|
|
2233
|
-
* Reconstruct logical lines from a grid, joining rows that were soft-wrapped.
|
|
2234
|
-
* Returns an array of logical lines, each being an array of ScreenCells (may be longer than cols).
|
|
2235
|
-
*/
|
|
2236
|
-
function getLogicalLines(srcGrid: ScreenCell[][], srcSoftWrapped: boolean[], srcRows: number): ScreenCell[][] {
|
|
2237
|
-
const logical: ScreenCell[][] = []
|
|
2238
|
-
let currentLine: ScreenCell[] = []
|
|
2239
|
-
|
|
2240
|
-
for (let r = 0; r < srcRows; r++) {
|
|
2241
|
-
const row = srcGrid[r]
|
|
2242
|
-
if (!row) continue
|
|
2243
|
-
// Append this row's cells to the current logical line
|
|
2244
|
-
for (let c = 0; c < row.length; c++) {
|
|
2245
|
-
currentLine.push(row[c]!)
|
|
2246
|
-
}
|
|
2247
|
-
if (srcSoftWrapped[r]) {
|
|
2248
|
-
// This row was soft-wrapped — continue accumulating into same logical line
|
|
2249
|
-
continue
|
|
2250
|
-
}
|
|
2251
|
-
// Hard break (or last row): finalize this logical line
|
|
2252
|
-
logical.push(currentLine)
|
|
2253
|
-
currentLine = []
|
|
2254
|
-
}
|
|
2255
|
-
// If there's a dangling line (shouldn't happen, but be safe)
|
|
2256
|
-
if (currentLine.length > 0) {
|
|
2257
|
-
logical.push(currentLine)
|
|
2258
|
-
}
|
|
2259
|
-
return logical
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
/**
|
|
2263
|
-
* Re-wrap logical lines to a new column width, producing grid rows and soft-wrap flags.
|
|
2264
|
-
*/
|
|
2265
|
-
function rewrapLines(logicalLines: ScreenCell[][], newCols: number): { rows: ScreenCell[][]; wrapped: boolean[] } {
|
|
2266
|
-
const outRows: ScreenCell[][] = []
|
|
2267
|
-
const outWrapped: boolean[] = []
|
|
2268
|
-
|
|
2269
|
-
for (const line of logicalLines) {
|
|
2270
|
-
// Trim trailing empty cells from logical line
|
|
2271
|
-
let lineLen = line.length
|
|
2272
|
-
while (lineLen > 0) {
|
|
2273
|
-
const cell = line[lineLen - 1]!
|
|
2274
|
-
if (cell === EMPTY_CELL || (cell.char === "" && !cell.wide)) {
|
|
2275
|
-
lineLen--
|
|
2276
|
-
} else {
|
|
2277
|
-
break
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
if (lineLen === 0) {
|
|
2282
|
-
// Empty logical line — produce one empty row
|
|
2283
|
-
outRows.push(makeRow(newCols))
|
|
2284
|
-
outWrapped.push(false)
|
|
2285
|
-
continue
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// Wrap the logical line content into rows of newCols width
|
|
2289
|
-
let pos = 0
|
|
2290
|
-
while (pos < lineLen) {
|
|
2291
|
-
const row = makeRow(newCols)
|
|
2292
|
-
let col = 0
|
|
2293
|
-
while (col < newCols && pos < lineLen) {
|
|
2294
|
-
const cell = line[pos]!
|
|
2295
|
-
if (cell.wide && col + 2 > newCols) {
|
|
2296
|
-
// Wide char doesn't fit — leave rest of row empty, wrap to next
|
|
2297
|
-
break
|
|
2298
|
-
}
|
|
2299
|
-
row[col] = cell === EMPTY_CELL ? EMPTY_CELL : { ...cell }
|
|
2300
|
-
col++
|
|
2301
|
-
pos++
|
|
2302
|
-
// If cell was wide, the next cell in the logical line is the spacer
|
|
2303
|
-
// which we already advanced past via pos++
|
|
2304
|
-
}
|
|
2305
|
-
const moreContent = pos < lineLen
|
|
2306
|
-
outRows.push(row)
|
|
2307
|
-
outWrapped.push(moreContent) // soft-wrapped if there's more content to come
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
return { rows: outRows, wrapped: outWrapped }
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
/**
|
|
2315
|
-
* Trim trailing empty rows from reflowed result, so they don't push content off the top
|
|
2316
|
-
* when we take the last newRows rows.
|
|
2317
|
-
*/
|
|
2318
|
-
function trimTrailingEmptyRows(result: { rows: ScreenCell[][]; wrapped: boolean[] }): void {
|
|
2319
|
-
while (result.rows.length > 1) {
|
|
2320
|
-
const lastRow = result.rows[result.rows.length - 1]!
|
|
2321
|
-
const isEmpty = lastRow.every((cell) => cell === EMPTY_CELL || (cell.char === "" && !cell.wide))
|
|
2322
|
-
if (isEmpty && !result.wrapped[result.rows.length - 2]) {
|
|
2323
|
-
// The row before wasn't soft-wrapped and this row is empty — trim it
|
|
2324
|
-
result.rows.pop()
|
|
2325
|
-
result.wrapped.pop()
|
|
2326
|
-
} else {
|
|
2327
|
-
break
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
function resize(newCols: number, newRows: number): void {
|
|
2333
|
-
// Reflow main grid
|
|
2334
|
-
const mainLogical = getLogicalLines(mainGrid, mainSoftWrapped, rows)
|
|
2335
|
-
const mainResult = rewrapLines(mainLogical, newCols)
|
|
2336
|
-
trimTrailingEmptyRows(mainResult)
|
|
2337
|
-
|
|
2338
|
-
// Reflow alt grid (usually not reflowed, but do it for consistency)
|
|
2339
|
-
const altLogical = getLogicalLines(altGrid, altSoftWrapped, rows)
|
|
2340
|
-
const altResult = rewrapLines(altLogical, newCols)
|
|
2341
|
-
trimTrailingEmptyRows(altResult)
|
|
2342
|
-
|
|
2343
|
-
// Build new grids: if reflowed content fits, place at top; if it overflows, take the last newRows
|
|
2344
|
-
const newMain = makeGrid(newCols, newRows)
|
|
2345
|
-
const newMainWrapped: boolean[] = new Array(newRows).fill(false)
|
|
2346
|
-
const mainStartRow = Math.max(0, mainResult.rows.length - newRows)
|
|
2347
|
-
for (let r = 0; r < newRows && mainStartRow + r < mainResult.rows.length; r++) {
|
|
2348
|
-
newMain[r] = mainResult.rows[mainStartRow + r]!
|
|
2349
|
-
newMainWrapped[r] = mainResult.wrapped[mainStartRow + r]!
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
// Build new alt grid
|
|
2353
|
-
const newAlt = makeGrid(newCols, newRows)
|
|
2354
|
-
const newAltWrapped: boolean[] = new Array(newRows).fill(false)
|
|
2355
|
-
const altStartRow = Math.max(0, altResult.rows.length - newRows)
|
|
2356
|
-
for (let r = 0; r < newRows && altStartRow + r < altResult.rows.length; r++) {
|
|
2357
|
-
newAlt[r] = altResult.rows[altStartRow + r]!
|
|
2358
|
-
newAltWrapped[r] = altResult.wrapped[altStartRow + r]!
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
mainGrid = newMain
|
|
2362
|
-
altGrid = newAlt
|
|
2363
|
-
mainSoftWrapped = newMainWrapped
|
|
2364
|
-
altSoftWrapped = newAltWrapped
|
|
2365
|
-
grid = useAltScreen ? altGrid : mainGrid
|
|
2366
|
-
softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped
|
|
2367
|
-
cols = newCols
|
|
2368
|
-
rows = newRows
|
|
2369
|
-
scrollTop = 0
|
|
2370
|
-
scrollBottom = rows - 1
|
|
2371
|
-
leftMargin = 0
|
|
2372
|
-
rightMargin = cols - 1
|
|
2373
|
-
clampCursor()
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
// ── Accessors ──
|
|
2377
|
-
|
|
2378
|
-
function getCell(row: number, col: number): ScreenCell {
|
|
2379
|
-
const r = grid[row]
|
|
2380
|
-
if (!r || col >= cols) return emptyCell()
|
|
2381
|
-
return { ...r[col]! }
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
function getLine(row: number): ScreenCell[] {
|
|
2385
|
-
const r = grid[row]
|
|
2386
|
-
if (!r) return makeRow(cols)
|
|
2387
|
-
return r.map((cell) => ({ ...cell }))
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
function getText(): string {
|
|
2391
|
-
const lines: string[] = []
|
|
2392
|
-
for (let r = 0; r < rows; r++) {
|
|
2393
|
-
lines.push(rowToString(grid[r]!))
|
|
2394
|
-
}
|
|
2395
|
-
return lines.join("\n")
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
function rowToString(row: ScreenCell[]): string {
|
|
2399
|
-
let line = ""
|
|
2400
|
-
for (let i = 0; i < row.length; i++) {
|
|
2401
|
-
const cell = row[i]!
|
|
2402
|
-
if (cell.wide) {
|
|
2403
|
-
line += cell.char
|
|
2404
|
-
} else if (cell.char === "") {
|
|
2405
|
-
if (i > 0 && row[i - 1]?.wide) {
|
|
2406
|
-
continue
|
|
2407
|
-
}
|
|
2408
|
-
line += " "
|
|
2409
|
-
} else {
|
|
2410
|
-
line += cell.char
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
return line.replace(/\s+$/, "")
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
function getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string {
|
|
2417
|
-
const parts: string[] = []
|
|
2418
|
-
|
|
2419
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
2420
|
-
const r = grid[row]
|
|
2421
|
-
if (!r) continue
|
|
2422
|
-
|
|
2423
|
-
const colStart = row === startRow ? startCol : 0
|
|
2424
|
-
const colEnd = row === endRow ? endCol : cols
|
|
2425
|
-
|
|
2426
|
-
let line = ""
|
|
2427
|
-
for (let col = colStart; col < colEnd; col++) {
|
|
2428
|
-
const cell = r[col]
|
|
2429
|
-
if (!cell) continue
|
|
2430
|
-
if (cell.char === "" && col > 0 && r[col - 1]?.wide) continue
|
|
2431
|
-
line += cell.char || " "
|
|
2432
|
-
}
|
|
2433
|
-
parts.push(line.replace(/\s+$/, ""))
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
return parts.join("\n")
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
function getMode(mode: string): boolean {
|
|
2440
|
-
switch (mode) {
|
|
2441
|
-
case "altScreen":
|
|
2442
|
-
return useAltScreen
|
|
2443
|
-
case "cursorVisible":
|
|
2444
|
-
return curVisible
|
|
2445
|
-
case "bracketedPaste":
|
|
2446
|
-
return bracketedPaste
|
|
2447
|
-
case "applicationCursor":
|
|
2448
|
-
return applicationCursor
|
|
2449
|
-
case "applicationKeypad":
|
|
2450
|
-
return applicationKeypad
|
|
2451
|
-
case "autoWrap":
|
|
2452
|
-
return autoWrap
|
|
2453
|
-
case "mouseTracking":
|
|
2454
|
-
return mouseTracking
|
|
2455
|
-
case "focusTracking":
|
|
2456
|
-
return focusTracking
|
|
2457
|
-
case "originMode":
|
|
2458
|
-
return originMode
|
|
2459
|
-
case "insertMode":
|
|
2460
|
-
return insertMode
|
|
2461
|
-
case "reverseVideo":
|
|
2462
|
-
return reverseVideo
|
|
2463
|
-
case "syncOutput":
|
|
2464
|
-
return syncOutput
|
|
2465
|
-
case "sgrMouse":
|
|
2466
|
-
return sgrMouse
|
|
2467
|
-
case "pixelMouse":
|
|
2468
|
-
return mouseTrackingMode === 1016
|
|
2469
|
-
case "leftRightMargin":
|
|
2470
|
-
return leftRightMarginMode
|
|
2471
|
-
case "colorSchemeReporting":
|
|
2472
|
-
return colorSchemeReporting
|
|
2473
|
-
case "kittyKeyboard":
|
|
2474
|
-
return kittyKeyboardFlags > 0
|
|
2475
|
-
case "kittyGraphics":
|
|
2476
|
-
return hasKittyGraphics
|
|
2477
|
-
case "sixel":
|
|
2478
|
-
return hasSixel
|
|
2479
|
-
default:
|
|
2480
|
-
return false
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
// Suppress unused variable warnings
|
|
2485
|
-
void [mouseTrackingMode, textScale, advancedClipboard]
|
|
2486
|
-
|
|
2487
|
-
return {
|
|
2488
|
-
get cols() {
|
|
2489
|
-
return cols
|
|
2490
|
-
},
|
|
2491
|
-
get rows() {
|
|
2492
|
-
return rows
|
|
2493
|
-
},
|
|
2494
|
-
process,
|
|
2495
|
-
resize,
|
|
2496
|
-
reset: fullReset,
|
|
2497
|
-
getCell,
|
|
2498
|
-
getLine,
|
|
2499
|
-
getText,
|
|
2500
|
-
getTextRange,
|
|
2501
|
-
getCursorPosition: () => ({ x: curX, y: curY }),
|
|
2502
|
-
getCursorVisible: () => curVisible,
|
|
2503
|
-
getCursorShape: () => cursorShape,
|
|
2504
|
-
getCursorBlinking: () => cursorBlinking,
|
|
2505
|
-
getTitle: () => title,
|
|
2506
|
-
getMode,
|
|
2507
|
-
getClipboard: () => clipboard,
|
|
2508
|
-
getCwd: () => cwd,
|
|
2509
|
-
getNotifications: () => [...notifications],
|
|
2510
|
-
getScrollbackLength: () => scrollback.length,
|
|
2511
|
-
getViewportOffset: () => viewportOffset,
|
|
2512
|
-
scrollViewport: (delta: number) => {
|
|
2513
|
-
viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta))
|
|
2514
|
-
},
|
|
2515
|
-
getSemanticZones: () => semanticZones.map((z) => ({ ...z })),
|
|
2516
|
-
getSixelImages: () => sixelImages.map((img) => ({ ...img })),
|
|
2517
|
-
}
|
|
2518
|
-
}
|