vterm.js 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -0
- package/package.json +1 -1
- package/src/screen.ts +1499 -101
package/src/screen.ts
CHANGED
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
* - Scrollback buffer with configurable limit
|
|
20
20
|
* - Wide character support (CJK, emoji ZWJ, regional indicators, VS-16)
|
|
21
21
|
* - OSC sequences (title, hyperlinks, clipboard, colors)
|
|
22
|
-
* - DCS sequences (
|
|
22
|
+
* - DCS sequences (consumed and ignored, XTVERSION response)
|
|
23
23
|
* - DA1/DA2/DA3 device attribute responses
|
|
24
24
|
* - DSR (device status report) responses
|
|
25
25
|
* - DECRPM (mode reporting)
|
|
26
26
|
* - Character sets (DEC Special Graphics, UTF-8)
|
|
27
27
|
* - Full C0/C1 control code handling
|
|
28
|
-
* -
|
|
28
|
+
* - DECSTR soft terminal reset
|
|
29
29
|
*
|
|
30
30
|
* @see https://terminfo.dev for the feature matrix
|
|
31
31
|
* @see https://github.com/beorn/vterm for the monorepo
|
|
@@ -95,6 +95,7 @@ export interface Screen {
|
|
|
95
95
|
|
|
96
96
|
// ── Implementation ─────────────────────────────────────────────────────
|
|
97
97
|
|
|
98
|
+
/** Frozen sentinel for unwritten cells — never mutate, copy-on-write in writeChar(). */
|
|
98
99
|
const EMPTY_CELL: ScreenCell = Object.freeze({
|
|
99
100
|
char: "",
|
|
100
101
|
fg: null,
|
|
@@ -117,174 +118,1571 @@ function emptyCell(): ScreenCell {
|
|
|
117
118
|
return { ...EMPTY_CELL }
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
// ── ANSI 256-color palette ─────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
const ANSI_16: readonly CellColor[] = [
|
|
124
|
+
{ r: 0x00, g: 0x00, b: 0x00 }, // 0 Black
|
|
125
|
+
{ r: 0x80, g: 0x00, b: 0x00 }, // 1 Red
|
|
126
|
+
{ r: 0x00, g: 0x80, b: 0x00 }, // 2 Green
|
|
127
|
+
{ r: 0x80, g: 0x80, b: 0x00 }, // 3 Yellow
|
|
128
|
+
{ r: 0x00, g: 0x00, b: 0x80 }, // 4 Blue
|
|
129
|
+
{ r: 0x80, g: 0x00, b: 0x80 }, // 5 Magenta
|
|
130
|
+
{ r: 0x00, g: 0x80, b: 0x80 }, // 6 Cyan
|
|
131
|
+
{ r: 0xc0, g: 0xc0, b: 0xc0 }, // 7 White
|
|
132
|
+
{ r: 0x80, g: 0x80, b: 0x80 }, // 8 Bright Black
|
|
133
|
+
{ r: 0xff, g: 0x00, b: 0x00 }, // 9 Bright Red
|
|
134
|
+
{ r: 0x00, g: 0xff, b: 0x00 }, // 10 Bright Green
|
|
135
|
+
{ r: 0xff, g: 0xff, b: 0x00 }, // 11 Bright Yellow
|
|
136
|
+
{ r: 0x00, g: 0x00, b: 0xff }, // 12 Bright Blue
|
|
137
|
+
{ r: 0xff, g: 0x00, b: 0xff }, // 13 Bright Magenta
|
|
138
|
+
{ r: 0x00, g: 0xff, b: 0xff }, // 14 Bright Cyan
|
|
139
|
+
{ r: 0xff, g: 0xff, b: 0xff }, // 15 Bright White
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
function buildPalette256(): CellColor[] {
|
|
143
|
+
const palette: CellColor[] = [...ANSI_16]
|
|
144
|
+
const levels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
|
|
145
|
+
for (let r = 0; r < 6; r++) {
|
|
146
|
+
for (let g = 0; g < 6; g++) {
|
|
147
|
+
for (let b = 0; b < 6; b++) {
|
|
148
|
+
palette.push({ r: levels[r]!, g: levels[g]!, b: levels[b]! })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (let i = 0; i < 24; i++) {
|
|
153
|
+
const v = 8 + i * 10
|
|
154
|
+
palette.push({ r: v, g: v, b: v })
|
|
155
|
+
}
|
|
156
|
+
return palette
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const PALETTE_256 = buildPalette256()
|
|
160
|
+
|
|
161
|
+
// ── DEC Special Graphics character set ─────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const DEC_SPECIAL_GRAPHICS: Record<string, string> = {
|
|
164
|
+
j: "\u2518", // ┘
|
|
165
|
+
k: "\u2510", // ┐
|
|
166
|
+
l: "\u250c", // ┌
|
|
167
|
+
m: "\u2514", // └
|
|
168
|
+
n: "\u253c", // ┼
|
|
169
|
+
q: "\u2500", // ─
|
|
170
|
+
t: "\u251c", // ├
|
|
171
|
+
u: "\u2524", // ┤
|
|
172
|
+
v: "\u2534", // ┴
|
|
173
|
+
w: "\u252c", // ┬
|
|
174
|
+
x: "\u2502", // │
|
|
175
|
+
a: "\u2592", // ▒
|
|
176
|
+
f: "\u00b0", // °
|
|
177
|
+
g: "\u00b1", // ±
|
|
178
|
+
"~": "\u00b7", // ·
|
|
179
|
+
y: "\u2264", // ≤
|
|
180
|
+
z: "\u2265", // ≥
|
|
181
|
+
"{": "\u03c0", // π
|
|
182
|
+
"|": "\u2260", // ≠
|
|
183
|
+
"}": "\u00a3", // £
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Unicode width (simplified — CJK detection) ─────────────────────────
|
|
187
|
+
|
|
188
|
+
function isWide(codePoint: number): boolean {
|
|
189
|
+
return (
|
|
190
|
+
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
|
191
|
+
(codePoint >= 0x2e80 && codePoint <= 0x303e) || // CJK Radicals
|
|
192
|
+
(codePoint >= 0x3041 && codePoint <= 0x33bf) || // Hiragana, Katakana, Bopomofo, etc.
|
|
193
|
+
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Unified Extension A
|
|
194
|
+
(codePoint >= 0x4e00 && codePoint <= 0xa4cf) || // CJK Unified Ideographs
|
|
195
|
+
(codePoint >= 0xa960 && codePoint <= 0xa97c) || // Hangul Jamo Extended-A
|
|
196
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
|
|
197
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
|
|
198
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) || // Vertical Forms
|
|
199
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) || // CJK Compatibility Forms
|
|
200
|
+
(codePoint >= 0xff01 && codePoint <= 0xff60) || // Fullwidth Forms
|
|
201
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) || // Fullwidth Signs
|
|
202
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f9ff) || // Misc Symbols/Emoticons
|
|
203
|
+
(codePoint >= 0x1fa00 && codePoint <= 0x1faff) || // Extended Symbols & Pictographs
|
|
204
|
+
(codePoint >= 0x20000 && codePoint <= 0x2fffd) || // CJK Extension B-F
|
|
205
|
+
(codePoint >= 0x30000 && codePoint <= 0x3fffd) // CJK Extension G+
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Internal attrs interface ───────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
interface Attrs {
|
|
212
|
+
fg: CellColor | null
|
|
213
|
+
bg: CellColor | null
|
|
214
|
+
bold: boolean
|
|
215
|
+
faint: boolean
|
|
216
|
+
italic: boolean
|
|
217
|
+
underline: UnderlineStyle
|
|
218
|
+
underlineColor: CellColor | null
|
|
219
|
+
overline: boolean
|
|
220
|
+
strikethrough: boolean
|
|
221
|
+
inverse: boolean
|
|
222
|
+
hidden: boolean
|
|
223
|
+
blink: boolean
|
|
224
|
+
url: string | null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Screen factory ─────────────────────────────────────────────────────
|
|
228
|
+
|
|
120
229
|
export function createScreen(options: ScreenOptions = {}): Screen {
|
|
121
230
|
let cols = options.cols ?? 80
|
|
122
231
|
let rows = options.rows ?? 24
|
|
123
232
|
const scrollbackLimit = options.scrollbackLimit ?? 1000
|
|
124
233
|
const onResponse = options.onResponse
|
|
125
234
|
|
|
126
|
-
//
|
|
127
|
-
let
|
|
128
|
-
let altGrid: ScreenCell[][] =
|
|
235
|
+
// Main and alternate screen buffers
|
|
236
|
+
let mainGrid: ScreenCell[][] = makeGrid(cols, rows)
|
|
237
|
+
let altGrid: ScreenCell[][] = makeGrid(cols, rows)
|
|
238
|
+
let grid = mainGrid
|
|
129
239
|
let scrollback: ScreenCell[][] = []
|
|
130
240
|
|
|
131
|
-
// Cursor
|
|
132
|
-
let
|
|
133
|
-
let
|
|
134
|
-
let
|
|
241
|
+
// Cursor
|
|
242
|
+
let curX = 0
|
|
243
|
+
let curY = 0
|
|
244
|
+
let curVisible = true
|
|
135
245
|
let cursorShape: "block" | "underline" | "bar" = "block"
|
|
136
246
|
let cursorBlinking = true
|
|
137
|
-
let savedCursor = { x: 0, y: 0, attrs: emptyCell() }
|
|
138
247
|
|
|
139
|
-
//
|
|
140
|
-
|
|
248
|
+
// DECSC/DECRC saved state
|
|
249
|
+
interface SavedState {
|
|
250
|
+
curX: number
|
|
251
|
+
curY: number
|
|
252
|
+
attrs: Attrs
|
|
253
|
+
originMode: boolean
|
|
254
|
+
autoWrap: boolean
|
|
255
|
+
charsetG0: boolean // true = DEC Special Graphics
|
|
256
|
+
}
|
|
257
|
+
let savedState: SavedState = {
|
|
258
|
+
curX: 0,
|
|
259
|
+
curY: 0,
|
|
260
|
+
attrs: resetAttrs(),
|
|
261
|
+
originMode: false,
|
|
262
|
+
autoWrap: true,
|
|
263
|
+
charsetG0: false,
|
|
264
|
+
}
|
|
141
265
|
|
|
142
|
-
//
|
|
143
|
-
let
|
|
144
|
-
let
|
|
145
|
-
|
|
146
|
-
|
|
266
|
+
// Saved cursor for alt screen (separate from DECSC)
|
|
267
|
+
let savedCurX = 0
|
|
268
|
+
let savedCurY = 0
|
|
269
|
+
|
|
270
|
+
// Current drawing attributes
|
|
271
|
+
let attrs: Attrs = resetAttrs()
|
|
272
|
+
|
|
273
|
+
// Terminal state
|
|
274
|
+
let title = ""
|
|
275
|
+
let useAltScreen = false
|
|
147
276
|
let bracketedPaste = false
|
|
148
277
|
let applicationCursor = false
|
|
149
278
|
let applicationKeypad = false
|
|
279
|
+
let autoWrap = true
|
|
150
280
|
let mouseTracking = false
|
|
281
|
+
let mouseTrackingMode = 0 // 1000, 1002, 1003
|
|
282
|
+
let sgrMouse = false
|
|
151
283
|
let focusTracking = false
|
|
284
|
+
let originMode = false
|
|
285
|
+
let insertMode = false
|
|
152
286
|
let reverseVideo = false
|
|
153
287
|
let syncOutput = false
|
|
154
288
|
|
|
155
|
-
// Scroll region
|
|
289
|
+
// Scroll region (inclusive, 0-based)
|
|
156
290
|
let scrollTop = 0
|
|
157
291
|
let scrollBottom = rows - 1
|
|
158
292
|
|
|
159
|
-
//
|
|
160
|
-
let title = ""
|
|
161
|
-
|
|
162
|
-
// Viewport
|
|
293
|
+
// Viewport scroll offset
|
|
163
294
|
let viewportOffset = 0
|
|
164
295
|
|
|
296
|
+
// Character set: true = DEC Special Graphics (G0)
|
|
297
|
+
let charsetG0 = false
|
|
298
|
+
|
|
299
|
+
// Clipboard (OSC 52)
|
|
300
|
+
let clipboard = ""
|
|
301
|
+
|
|
302
|
+
// Last printed character for REP
|
|
303
|
+
let lastChar = ""
|
|
304
|
+
|
|
165
305
|
// Parser state
|
|
166
|
-
let
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
306
|
+
let parserState:
|
|
307
|
+
| "ground"
|
|
308
|
+
| "escape"
|
|
309
|
+
| "escape_charset"
|
|
310
|
+
| "csi"
|
|
311
|
+
| "osc"
|
|
312
|
+
| "dcs"
|
|
313
|
+
| "dcs_passthrough"
|
|
314
|
+
| "osc_st"
|
|
315
|
+
| "dcs_st" = "ground"
|
|
316
|
+
let escBuf = ""
|
|
317
|
+
let oscBuf = ""
|
|
171
318
|
|
|
172
|
-
//
|
|
173
|
-
|
|
319
|
+
// Decoder for incoming bytes
|
|
320
|
+
const decoder = new TextDecoder()
|
|
174
321
|
|
|
175
|
-
|
|
322
|
+
// ── Grid helpers ──
|
|
323
|
+
|
|
324
|
+
function makeGrid(c: number, r: number): ScreenCell[][] {
|
|
176
325
|
const g: ScreenCell[][] = []
|
|
177
|
-
for (let
|
|
178
|
-
g.push(
|
|
326
|
+
for (let row = 0; row < r; row++) {
|
|
327
|
+
g.push(makeRow(c))
|
|
179
328
|
}
|
|
180
329
|
return g
|
|
181
330
|
}
|
|
182
331
|
|
|
183
|
-
function
|
|
332
|
+
function makeRow(c: number): ScreenCell[] {
|
|
184
333
|
const row: ScreenCell[] = []
|
|
185
|
-
for (let
|
|
186
|
-
row.push(
|
|
334
|
+
for (let col = 0; col < c; col++) {
|
|
335
|
+
row.push(EMPTY_CELL)
|
|
187
336
|
}
|
|
188
337
|
return row
|
|
189
338
|
}
|
|
190
339
|
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
340
|
+
function resetAttrs(): Attrs {
|
|
341
|
+
return {
|
|
342
|
+
fg: null,
|
|
343
|
+
bg: null,
|
|
344
|
+
bold: false,
|
|
345
|
+
faint: false,
|
|
346
|
+
italic: false,
|
|
347
|
+
underline: "none",
|
|
348
|
+
underlineColor: null,
|
|
349
|
+
overline: false,
|
|
350
|
+
strikethrough: false,
|
|
351
|
+
inverse: false,
|
|
352
|
+
hidden: false,
|
|
353
|
+
blink: false,
|
|
354
|
+
url: null,
|
|
195
355
|
}
|
|
196
356
|
}
|
|
197
357
|
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
358
|
+
function clampCursor(): void {
|
|
359
|
+
if (curX < 0) curX = 0
|
|
360
|
+
if (curX >= cols) curX = cols - 1
|
|
361
|
+
if (curY < 0) curY = 0
|
|
362
|
+
if (curY >= rows) curY = rows - 1
|
|
201
363
|
}
|
|
202
364
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
grid
|
|
365
|
+
// ── Scrolling ──
|
|
366
|
+
|
|
367
|
+
function scrollUp(top: number, bottom: number): void {
|
|
368
|
+
// Move top row to scrollback (only if main screen & top of screen)
|
|
369
|
+
if (grid === mainGrid && top === 0) {
|
|
370
|
+
scrollback.push(grid[0]!)
|
|
371
|
+
if (scrollback.length > scrollbackLimit * 2) {
|
|
372
|
+
scrollback.splice(0, scrollback.length - scrollbackLimit)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (let i = top; i < bottom; i++) {
|
|
376
|
+
grid[i] = grid[i + 1]!
|
|
377
|
+
}
|
|
378
|
+
grid[bottom] = makeRow(cols)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function scrollDown(top: number, bottom: number): void {
|
|
382
|
+
for (let i = bottom; i > top; i--) {
|
|
383
|
+
grid[i] = grid[i - 1]!
|
|
384
|
+
}
|
|
385
|
+
grid[top] = makeRow(cols)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Character writing ──
|
|
389
|
+
|
|
390
|
+
function writeChar(ch: string): void {
|
|
391
|
+
// Apply DEC Special Graphics character mapping
|
|
392
|
+
if (charsetG0 && ch.length === 1) {
|
|
393
|
+
const mapped = DEC_SPECIAL_GRAPHICS[ch]
|
|
394
|
+
if (mapped) ch = mapped
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const codePoint = ch.codePointAt(0) ?? 0
|
|
398
|
+
const wide = isWide(codePoint)
|
|
399
|
+
const charWidth = wide ? 2 : 1
|
|
400
|
+
|
|
401
|
+
// Handle autowrap at end of line
|
|
402
|
+
if (curX + charWidth > cols) {
|
|
403
|
+
if (autoWrap) {
|
|
404
|
+
curX = 0
|
|
405
|
+
curY++
|
|
406
|
+
if (curY > scrollBottom) {
|
|
407
|
+
curY = scrollBottom
|
|
408
|
+
scrollUp(scrollTop, scrollBottom)
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
curX = cols - charWidth
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Insert mode: shift existing characters right before writing
|
|
416
|
+
if (insertMode) {
|
|
417
|
+
const row = grid[curY]!
|
|
418
|
+
for (let i = 0; i < charWidth; i++) {
|
|
419
|
+
row.splice(curX, 0, EMPTY_CELL)
|
|
420
|
+
row.pop()
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Copy-on-write: if cell is the shared EMPTY_CELL sentinel, create a fresh object
|
|
425
|
+
const row = grid[curY]!
|
|
426
|
+
let cell = row[curX]!
|
|
427
|
+
if (cell === EMPTY_CELL) {
|
|
428
|
+
cell = { ...EMPTY_CELL }
|
|
429
|
+
row[curX] = cell
|
|
430
|
+
}
|
|
431
|
+
cell.char = ch
|
|
432
|
+
cell.fg = attrs.fg ? { ...attrs.fg } : null
|
|
433
|
+
cell.bg = attrs.bg ? { ...attrs.bg } : null
|
|
434
|
+
cell.bold = attrs.bold
|
|
435
|
+
cell.faint = attrs.faint
|
|
436
|
+
cell.italic = attrs.italic
|
|
437
|
+
cell.underline = attrs.underline
|
|
438
|
+
cell.underlineColor = attrs.underlineColor ? { ...attrs.underlineColor } : null
|
|
439
|
+
cell.overline = attrs.overline
|
|
440
|
+
cell.strikethrough = attrs.strikethrough
|
|
441
|
+
cell.inverse = attrs.inverse
|
|
442
|
+
cell.hidden = attrs.hidden
|
|
443
|
+
cell.blink = attrs.blink
|
|
444
|
+
cell.wide = wide
|
|
445
|
+
cell.url = attrs.url
|
|
446
|
+
|
|
447
|
+
if (wide && curX + 1 < cols) {
|
|
448
|
+
let spacer = row[curX + 1]!
|
|
449
|
+
if (spacer === EMPTY_CELL) {
|
|
450
|
+
spacer = { ...EMPTY_CELL }
|
|
451
|
+
row[curX + 1] = spacer
|
|
452
|
+
}
|
|
453
|
+
spacer.char = ""
|
|
454
|
+
spacer.fg = null
|
|
455
|
+
spacer.bg = null
|
|
456
|
+
spacer.bold = false
|
|
457
|
+
spacer.faint = false
|
|
458
|
+
spacer.italic = false
|
|
459
|
+
spacer.underline = "none"
|
|
460
|
+
spacer.underlineColor = null
|
|
461
|
+
spacer.overline = false
|
|
462
|
+
spacer.strikethrough = false
|
|
463
|
+
spacer.inverse = false
|
|
464
|
+
spacer.hidden = false
|
|
465
|
+
spacer.blink = false
|
|
466
|
+
spacer.wide = false
|
|
467
|
+
spacer.url = null
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
curX += charWidth
|
|
471
|
+
lastChar = ch
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── CSI handler ──
|
|
475
|
+
|
|
476
|
+
function handleCSI(params: string, intermediates: string, finalByte: string): void {
|
|
477
|
+
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
478
|
+
|
|
479
|
+
// CSI with intermediates
|
|
480
|
+
if (intermediates === " ") {
|
|
481
|
+
if (finalByte === "q") {
|
|
482
|
+
// DECSCUSR - Set Cursor Shape
|
|
483
|
+
const ps = parts[0] ?? 0
|
|
484
|
+
switch (ps) {
|
|
485
|
+
case 0:
|
|
486
|
+
case 1:
|
|
487
|
+
cursorShape = "block"
|
|
488
|
+
cursorBlinking = true
|
|
489
|
+
break
|
|
490
|
+
case 2:
|
|
491
|
+
cursorShape = "block"
|
|
492
|
+
cursorBlinking = false
|
|
493
|
+
break
|
|
494
|
+
case 3:
|
|
495
|
+
cursorShape = "underline"
|
|
496
|
+
cursorBlinking = true
|
|
497
|
+
break
|
|
498
|
+
case 4:
|
|
499
|
+
cursorShape = "underline"
|
|
500
|
+
cursorBlinking = false
|
|
501
|
+
break
|
|
502
|
+
case 5:
|
|
503
|
+
cursorShape = "bar"
|
|
504
|
+
cursorBlinking = true
|
|
505
|
+
break
|
|
506
|
+
case 6:
|
|
507
|
+
cursorShape = "bar"
|
|
508
|
+
cursorBlinking = false
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (intermediates === "!") {
|
|
516
|
+
if (finalByte === "p") {
|
|
517
|
+
// DECSTR - Soft Terminal Reset
|
|
518
|
+
softReset()
|
|
519
|
+
}
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (intermediates === "$") {
|
|
524
|
+
// Mode reporting is handled in CSI private
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
switch (finalByte) {
|
|
529
|
+
case "A": // CUU - Cursor Up
|
|
530
|
+
curY -= Math.max(parts[0] ?? 1, 1)
|
|
531
|
+
clampCursor()
|
|
532
|
+
break
|
|
533
|
+
case "B": // CUD - Cursor Down
|
|
534
|
+
curY += Math.max(parts[0] ?? 1, 1)
|
|
535
|
+
clampCursor()
|
|
536
|
+
break
|
|
537
|
+
case "C": // CUF - Cursor Forward
|
|
538
|
+
curX += Math.max(parts[0] ?? 1, 1)
|
|
539
|
+
clampCursor()
|
|
540
|
+
break
|
|
541
|
+
case "D": // CUB - Cursor Back
|
|
542
|
+
curX -= Math.max(parts[0] ?? 1, 1)
|
|
543
|
+
clampCursor()
|
|
544
|
+
break
|
|
545
|
+
case "E": // CNL - Cursor Next Line
|
|
546
|
+
curY += Math.max(parts[0] ?? 1, 1)
|
|
547
|
+
curX = 0
|
|
548
|
+
clampCursor()
|
|
549
|
+
break
|
|
550
|
+
case "F": // CPL - Cursor Previous Line
|
|
551
|
+
curY -= Math.max(parts[0] ?? 1, 1)
|
|
552
|
+
curX = 0
|
|
553
|
+
clampCursor()
|
|
554
|
+
break
|
|
555
|
+
case "G": // CHA - Cursor Horizontal Absolute
|
|
556
|
+
curX = (parts[0] ?? 1) - 1
|
|
557
|
+
clampCursor()
|
|
558
|
+
break
|
|
559
|
+
case "H": // CUP - Cursor Position
|
|
560
|
+
case "f": // HVP - same as CUP
|
|
561
|
+
curY = (parts[0] ?? 1) - 1
|
|
562
|
+
curX = (parts[1] ?? 1) - 1
|
|
563
|
+
clampCursor()
|
|
564
|
+
break
|
|
565
|
+
case "J": // ED - Erase in Display
|
|
566
|
+
handleEraseDisplay(parts[0] ?? 0)
|
|
567
|
+
break
|
|
568
|
+
case "K": // EL - Erase in Line
|
|
569
|
+
handleEraseLine(parts[0] ?? 0)
|
|
570
|
+
break
|
|
571
|
+
case "L": // IL - Insert Lines
|
|
572
|
+
handleInsertLines(Math.max(parts[0] ?? 1, 1))
|
|
573
|
+
break
|
|
574
|
+
case "M": // DL - Delete Lines
|
|
575
|
+
handleDeleteLines(Math.max(parts[0] ?? 1, 1))
|
|
576
|
+
break
|
|
577
|
+
case "P": // DCH - Delete Characters
|
|
578
|
+
handleDeleteChars(Math.max(parts[0] ?? 1, 1))
|
|
579
|
+
break
|
|
580
|
+
case "@": // ICH - Insert Characters
|
|
581
|
+
handleInsertChars(Math.max(parts[0] ?? 1, 1))
|
|
582
|
+
break
|
|
583
|
+
case "X": // ECH - Erase Characters
|
|
584
|
+
handleEraseChars(Math.max(parts[0] ?? 1, 1))
|
|
585
|
+
break
|
|
586
|
+
case "S": // SU - Scroll Up
|
|
587
|
+
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
|
|
588
|
+
scrollUp(scrollTop, scrollBottom)
|
|
589
|
+
}
|
|
590
|
+
break
|
|
591
|
+
case "T": // SD - Scroll Down
|
|
592
|
+
for (let i = 0; i < Math.max(parts[0] ?? 1, 1); i++) {
|
|
593
|
+
scrollDown(scrollTop, scrollBottom)
|
|
594
|
+
}
|
|
595
|
+
break
|
|
596
|
+
case "b": // REP - Repeat preceding character
|
|
597
|
+
if (lastChar) {
|
|
598
|
+
const count = Math.max(parts[0] ?? 1, 1)
|
|
599
|
+
for (let i = 0; i < count; i++) {
|
|
600
|
+
writeChar(lastChar)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
break
|
|
604
|
+
case "d": // VPA - Line Position Absolute
|
|
605
|
+
curY = (parts[0] ?? 1) - 1
|
|
606
|
+
clampCursor()
|
|
607
|
+
break
|
|
608
|
+
case "m": // SGR - Select Graphic Rendition
|
|
609
|
+
handleSGR(params)
|
|
610
|
+
break
|
|
611
|
+
case "r": // DECSTBM - Set Scrolling Region
|
|
612
|
+
scrollTop = (parts[0] ?? 1) - 1
|
|
613
|
+
scrollBottom = (parts[1] ?? rows) - 1
|
|
614
|
+
if (scrollTop < 0) scrollTop = 0
|
|
615
|
+
if (scrollBottom >= rows) scrollBottom = rows - 1
|
|
616
|
+
if (scrollTop > scrollBottom) {
|
|
617
|
+
scrollTop = 0
|
|
618
|
+
scrollBottom = rows - 1
|
|
619
|
+
}
|
|
620
|
+
curX = 0
|
|
621
|
+
curY = originMode ? scrollTop : 0
|
|
622
|
+
break
|
|
623
|
+
case "n": // DSR - Device Status Report
|
|
624
|
+
if (onResponse) {
|
|
625
|
+
if (parts[0] === 5) {
|
|
626
|
+
// Status report - OK
|
|
627
|
+
onResponse("\x1b[0n")
|
|
628
|
+
} else if (parts[0] === 6) {
|
|
629
|
+
// Cursor position report
|
|
630
|
+
onResponse(`\x1b[${curY + 1};${curX + 1}R`)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
break
|
|
634
|
+
case "c": // DA1 - Primary Device Attributes
|
|
635
|
+
if (onResponse) {
|
|
636
|
+
if (params === "" || params === "0") {
|
|
637
|
+
onResponse("\x1b[?62;4c")
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
break
|
|
641
|
+
case "s": // SCP - Save Cursor Position
|
|
642
|
+
savedCurX = curX
|
|
643
|
+
savedCurY = curY
|
|
644
|
+
break
|
|
645
|
+
case "u": // RCP - Restore Cursor Position
|
|
646
|
+
curX = savedCurX
|
|
647
|
+
curY = savedCurY
|
|
648
|
+
clampCursor()
|
|
649
|
+
break
|
|
650
|
+
default:
|
|
651
|
+
break
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function handleCSIGt(params: string, _intermediates: string, finalByte: string): void {
|
|
656
|
+
// CSI > sequences
|
|
657
|
+
if (finalByte === "c") {
|
|
658
|
+
// DA2 - Secondary Device Attributes
|
|
659
|
+
if (onResponse) {
|
|
660
|
+
if (params === "" || params === "0") {
|
|
661
|
+
onResponse("\x1b[>1;100;0c")
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} else if (finalByte === "q") {
|
|
665
|
+
// XTVERSION
|
|
666
|
+
if (onResponse) {
|
|
667
|
+
if (params === "" || params === "0") {
|
|
668
|
+
onResponse("\x1bP>|vterm.js 0.1.0\x1b\\")
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function handleCSIPrivate(params: string, intermediates: string, finalByte: string): void {
|
|
675
|
+
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
676
|
+
|
|
677
|
+
// DECRPM - Mode reporting: CSI ? Pd $ p
|
|
678
|
+
if (intermediates === "$" && finalByte === "p") {
|
|
679
|
+
if (onResponse) {
|
|
680
|
+
const mode = parts[0] ?? 0
|
|
681
|
+
let value = 0 // 0 = not recognized
|
|
682
|
+
switch (mode) {
|
|
683
|
+
case 1:
|
|
684
|
+
value = applicationCursor ? 1 : 2
|
|
685
|
+
break
|
|
686
|
+
case 6:
|
|
687
|
+
value = originMode ? 1 : 2
|
|
688
|
+
break
|
|
689
|
+
case 7:
|
|
690
|
+
value = autoWrap ? 1 : 2
|
|
691
|
+
break
|
|
692
|
+
case 25:
|
|
693
|
+
value = curVisible ? 1 : 2
|
|
694
|
+
break
|
|
695
|
+
case 47:
|
|
696
|
+
case 1047:
|
|
697
|
+
case 1049:
|
|
698
|
+
value = useAltScreen ? 1 : 2
|
|
699
|
+
break
|
|
700
|
+
case 66:
|
|
701
|
+
value = applicationKeypad ? 1 : 2
|
|
702
|
+
break
|
|
703
|
+
case 1000:
|
|
704
|
+
case 1002:
|
|
705
|
+
case 1003:
|
|
706
|
+
value = mouseTracking ? 1 : 2
|
|
707
|
+
break
|
|
708
|
+
case 1004:
|
|
709
|
+
value = focusTracking ? 1 : 2
|
|
710
|
+
break
|
|
711
|
+
case 1006:
|
|
712
|
+
value = sgrMouse ? 1 : 2
|
|
713
|
+
break
|
|
714
|
+
case 2004:
|
|
715
|
+
value = bracketedPaste ? 1 : 2
|
|
716
|
+
break
|
|
717
|
+
case 2026:
|
|
718
|
+
value = syncOutput ? 1 : 2
|
|
719
|
+
break
|
|
720
|
+
case 5:
|
|
721
|
+
value = reverseVideo ? 1 : 2
|
|
722
|
+
break
|
|
723
|
+
case 4:
|
|
724
|
+
value = insertMode ? 1 : 2
|
|
725
|
+
break
|
|
726
|
+
}
|
|
727
|
+
onResponse(`\x1b[?${mode};${value}$y`)
|
|
728
|
+
}
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const set = finalByte === "h"
|
|
733
|
+
|
|
734
|
+
for (const code of parts) {
|
|
735
|
+
switch (code) {
|
|
736
|
+
case 1: // DECCKM - Application Cursor
|
|
737
|
+
applicationCursor = set
|
|
738
|
+
break
|
|
739
|
+
case 4: // IRM - Insert Mode (via DEC private)
|
|
740
|
+
insertMode = set
|
|
741
|
+
break
|
|
742
|
+
case 5: // DECSCNM - Reverse Video
|
|
743
|
+
reverseVideo = set
|
|
744
|
+
break
|
|
745
|
+
case 6: // DECOM - Origin Mode
|
|
746
|
+
originMode = set
|
|
747
|
+
break
|
|
748
|
+
case 7: // DECAWM - Autowrap Mode
|
|
749
|
+
autoWrap = set
|
|
750
|
+
break
|
|
751
|
+
case 25: // DECTCEM - Cursor Visible
|
|
752
|
+
curVisible = set
|
|
753
|
+
break
|
|
754
|
+
case 47: // Alternate screen buffer (old)
|
|
755
|
+
case 1047: // Alternate screen buffer
|
|
756
|
+
if (set && !useAltScreen) {
|
|
757
|
+
useAltScreen = true
|
|
758
|
+
grid = altGrid
|
|
759
|
+
} else if (!set && useAltScreen) {
|
|
760
|
+
useAltScreen = false
|
|
761
|
+
grid = mainGrid
|
|
762
|
+
}
|
|
763
|
+
break
|
|
764
|
+
case 66: // DECNKM - Application Keypad
|
|
765
|
+
applicationKeypad = set
|
|
766
|
+
break
|
|
767
|
+
case 1000: // Mouse tracking (basic)
|
|
768
|
+
mouseTracking = set
|
|
769
|
+
mouseTrackingMode = set ? 1000 : 0
|
|
770
|
+
break
|
|
771
|
+
case 1002: // Mouse tracking (button events)
|
|
772
|
+
mouseTracking = set
|
|
773
|
+
mouseTrackingMode = set ? 1002 : 0
|
|
774
|
+
break
|
|
775
|
+
case 1003: // Mouse tracking (all events)
|
|
776
|
+
mouseTracking = set
|
|
777
|
+
mouseTrackingMode = set ? 1003 : 0
|
|
778
|
+
break
|
|
779
|
+
case 1004: // Focus tracking
|
|
780
|
+
focusTracking = set
|
|
781
|
+
break
|
|
782
|
+
case 1006: // SGR mouse mode
|
|
783
|
+
sgrMouse = set
|
|
784
|
+
break
|
|
785
|
+
case 1049: // Alternate screen buffer + save/restore cursor
|
|
786
|
+
if (set && !useAltScreen) {
|
|
787
|
+
savedCurX = curX
|
|
788
|
+
savedCurY = curY
|
|
789
|
+
useAltScreen = true
|
|
790
|
+
altGrid = makeGrid(cols, rows)
|
|
791
|
+
grid = altGrid
|
|
792
|
+
curX = 0
|
|
793
|
+
curY = 0
|
|
794
|
+
} else if (!set && useAltScreen) {
|
|
795
|
+
useAltScreen = false
|
|
796
|
+
grid = mainGrid
|
|
797
|
+
curX = savedCurX
|
|
798
|
+
curY = savedCurY
|
|
799
|
+
clampCursor()
|
|
800
|
+
}
|
|
801
|
+
break
|
|
802
|
+
case 2004: // Bracketed paste
|
|
803
|
+
bracketedPaste = set
|
|
804
|
+
break
|
|
805
|
+
case 2026: // Synchronized output
|
|
806
|
+
syncOutput = set
|
|
807
|
+
break
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ── Erase operations ──
|
|
813
|
+
|
|
814
|
+
function handleEraseDisplay(mode: number): void {
|
|
815
|
+
switch (mode) {
|
|
816
|
+
case 0: // Erase from cursor to end
|
|
817
|
+
eraseCells(curY, curX, curY, cols - 1)
|
|
818
|
+
for (let row = curY + 1; row < rows; row++) {
|
|
819
|
+
eraseCells(row, 0, row, cols - 1)
|
|
820
|
+
}
|
|
821
|
+
break
|
|
822
|
+
case 1: // Erase from start to cursor
|
|
823
|
+
for (let row = 0; row < curY; row++) {
|
|
824
|
+
eraseCells(row, 0, row, cols - 1)
|
|
825
|
+
}
|
|
826
|
+
eraseCells(curY, 0, curY, curX)
|
|
827
|
+
break
|
|
828
|
+
case 2: // Erase entire display
|
|
829
|
+
case 3: // Erase entire display + scrollback
|
|
830
|
+
for (let row = 0; row < rows; row++) {
|
|
831
|
+
eraseCells(row, 0, row, cols - 1)
|
|
832
|
+
}
|
|
833
|
+
if (mode === 3) {
|
|
834
|
+
scrollback.length = 0
|
|
835
|
+
}
|
|
836
|
+
break
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function handleEraseLine(mode: number): void {
|
|
841
|
+
switch (mode) {
|
|
842
|
+
case 0: // Erase from cursor to end of line
|
|
843
|
+
eraseCells(curY, curX, curY, cols - 1)
|
|
844
|
+
break
|
|
845
|
+
case 1: // Erase from start to cursor
|
|
846
|
+
eraseCells(curY, 0, curY, curX)
|
|
847
|
+
break
|
|
848
|
+
case 2: // Erase entire line
|
|
849
|
+
eraseCells(curY, 0, curY, cols - 1)
|
|
850
|
+
break
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function eraseCells(row: number, startCol: number, _endRow: number, endCol: number): void {
|
|
855
|
+
const r = grid[row]
|
|
856
|
+
if (!r) return
|
|
857
|
+
for (let col = startCol; col <= endCol && col < cols; col++) {
|
|
858
|
+
r[col] = emptyCell()
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function handleInsertLines(count: number): void {
|
|
863
|
+
if (curY < scrollTop || curY > scrollBottom) return
|
|
864
|
+
for (let i = 0; i < count; i++) {
|
|
865
|
+
scrollDown(curY, scrollBottom)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function handleDeleteLines(count: number): void {
|
|
870
|
+
if (curY < scrollTop || curY > scrollBottom) return
|
|
871
|
+
for (let i = 0; i < count; i++) {
|
|
872
|
+
scrollUp(curY, scrollBottom)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function handleDeleteChars(count: number): void {
|
|
877
|
+
const row = grid[curY]
|
|
878
|
+
if (!row) return
|
|
879
|
+
for (let i = 0; i < count; i++) {
|
|
880
|
+
if (curX < cols) {
|
|
881
|
+
row.splice(curX, 1)
|
|
882
|
+
row.push(emptyCell())
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function handleInsertChars(count: number): void {
|
|
888
|
+
const row = grid[curY]
|
|
889
|
+
if (!row) return
|
|
890
|
+
for (let i = 0; i < count; i++) {
|
|
891
|
+
row.splice(curX, 0, emptyCell())
|
|
892
|
+
row.pop()
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function handleEraseChars(count: number): void {
|
|
897
|
+
const row = grid[curY]
|
|
898
|
+
if (!row) return
|
|
899
|
+
for (let i = 0; i < count && curX + i < cols; i++) {
|
|
900
|
+
row[curX + i] = emptyCell()
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ── SGR (Select Graphic Rendition) ──
|
|
905
|
+
|
|
906
|
+
function handleSGR(rawParams: string): void {
|
|
907
|
+
const segments = rawParams.split(";")
|
|
908
|
+
const params: number[] = []
|
|
909
|
+
const subParams = new Map<number, number[]>()
|
|
910
|
+
for (const seg of segments) {
|
|
911
|
+
if (seg.includes(":")) {
|
|
912
|
+
const subs = seg.split(":").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
913
|
+
subParams.set(params.length, subs)
|
|
914
|
+
params.push(subs[0]!)
|
|
915
|
+
} else {
|
|
916
|
+
params.push(seg === "" ? 0 : parseInt(seg, 10))
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (params.length === 0 || (params.length === 1 && params[0] === 0)) {
|
|
921
|
+
attrs = resetAttrs()
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
let i = 0
|
|
926
|
+
while (i < params.length) {
|
|
927
|
+
const code = params[i]!
|
|
928
|
+
switch (code) {
|
|
929
|
+
case 0:
|
|
930
|
+
attrs = resetAttrs()
|
|
931
|
+
break
|
|
932
|
+
case 1:
|
|
933
|
+
attrs.bold = true
|
|
934
|
+
break
|
|
935
|
+
case 2:
|
|
936
|
+
attrs.faint = true
|
|
937
|
+
break
|
|
938
|
+
case 3:
|
|
939
|
+
attrs.italic = true
|
|
940
|
+
break
|
|
941
|
+
case 4: {
|
|
942
|
+
// SGR 4 with optional sub-parameter: 4:0=none, 4:1=single, 4:3=curly, etc.
|
|
943
|
+
const subs = subParams.get(i)
|
|
944
|
+
if (subs && subs.length > 1) {
|
|
945
|
+
const sub = subs[1]!
|
|
946
|
+
switch (sub) {
|
|
947
|
+
case 0:
|
|
948
|
+
attrs.underline = "none"
|
|
949
|
+
break
|
|
950
|
+
case 1:
|
|
951
|
+
attrs.underline = "single"
|
|
952
|
+
break
|
|
953
|
+
case 2:
|
|
954
|
+
attrs.underline = "double"
|
|
955
|
+
break
|
|
956
|
+
case 3:
|
|
957
|
+
attrs.underline = "curly"
|
|
958
|
+
break
|
|
959
|
+
case 4:
|
|
960
|
+
attrs.underline = "dotted"
|
|
961
|
+
break
|
|
962
|
+
case 5:
|
|
963
|
+
attrs.underline = "dashed"
|
|
964
|
+
break
|
|
965
|
+
default:
|
|
966
|
+
attrs.underline = "single"
|
|
967
|
+
break
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
attrs.underline = "single"
|
|
971
|
+
}
|
|
972
|
+
break
|
|
973
|
+
}
|
|
974
|
+
case 5: // Slow blink
|
|
975
|
+
attrs.blink = true
|
|
976
|
+
break
|
|
977
|
+
case 6: // Rapid blink (treat same as blink)
|
|
978
|
+
attrs.blink = true
|
|
979
|
+
break
|
|
980
|
+
case 7:
|
|
981
|
+
attrs.inverse = true
|
|
982
|
+
break
|
|
983
|
+
case 8: // Hidden/conceal
|
|
984
|
+
attrs.hidden = true
|
|
985
|
+
break
|
|
986
|
+
case 9:
|
|
987
|
+
attrs.strikethrough = true
|
|
988
|
+
break
|
|
989
|
+
case 21: // Double underline
|
|
990
|
+
attrs.underline = "double"
|
|
991
|
+
break
|
|
992
|
+
case 22: // Normal intensity (neither bold nor faint)
|
|
993
|
+
attrs.bold = false
|
|
994
|
+
attrs.faint = false
|
|
995
|
+
break
|
|
996
|
+
case 23:
|
|
997
|
+
attrs.italic = false
|
|
998
|
+
break
|
|
999
|
+
case 24:
|
|
1000
|
+
attrs.underline = "none"
|
|
1001
|
+
break
|
|
1002
|
+
case 25: // Blink off
|
|
1003
|
+
attrs.blink = false
|
|
1004
|
+
break
|
|
1005
|
+
case 27:
|
|
1006
|
+
attrs.inverse = false
|
|
1007
|
+
break
|
|
1008
|
+
case 28: // Reveal (turn off hidden/conceal)
|
|
1009
|
+
attrs.hidden = false
|
|
1010
|
+
break
|
|
1011
|
+
case 29:
|
|
1012
|
+
attrs.strikethrough = false
|
|
1013
|
+
break
|
|
1014
|
+
// Foreground colors 30-37
|
|
1015
|
+
case 30:
|
|
1016
|
+
case 31:
|
|
1017
|
+
case 32:
|
|
1018
|
+
case 33:
|
|
1019
|
+
case 34:
|
|
1020
|
+
case 35:
|
|
1021
|
+
case 36:
|
|
1022
|
+
case 37:
|
|
1023
|
+
attrs.fg = { ...PALETTE_256[code - 30]! }
|
|
1024
|
+
break
|
|
1025
|
+
case 38: {
|
|
1026
|
+
// Extended foreground: 38;5;N (256) or 38;2;R;G;B (truecolor)
|
|
1027
|
+
// Also handle colon form: 38:5:N or 38:2:R:G:B
|
|
1028
|
+
const subs = subParams.get(i)
|
|
1029
|
+
if (subs && subs.length >= 3) {
|
|
1030
|
+
const result = parseExtendedColorFromSubs(subs)
|
|
1031
|
+
if (result) attrs.fg = result
|
|
1032
|
+
} else {
|
|
1033
|
+
const result = parseExtendedColor(params, i)
|
|
1034
|
+
if (result) {
|
|
1035
|
+
attrs.fg = result.color
|
|
1036
|
+
i = result.nextIndex - 1
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
break
|
|
1040
|
+
}
|
|
1041
|
+
case 39: // Default foreground
|
|
1042
|
+
attrs.fg = null
|
|
1043
|
+
break
|
|
1044
|
+
// Background colors 40-47
|
|
1045
|
+
case 40:
|
|
1046
|
+
case 41:
|
|
1047
|
+
case 42:
|
|
1048
|
+
case 43:
|
|
1049
|
+
case 44:
|
|
1050
|
+
case 45:
|
|
1051
|
+
case 46:
|
|
1052
|
+
case 47:
|
|
1053
|
+
attrs.bg = { ...PALETTE_256[code - 40]! }
|
|
1054
|
+
break
|
|
1055
|
+
case 48: {
|
|
1056
|
+
// Extended background: 48;5;N (256) or 48;2;R;G;B (truecolor)
|
|
1057
|
+
const subs = subParams.get(i)
|
|
1058
|
+
if (subs && subs.length >= 3) {
|
|
1059
|
+
const result = parseExtendedColorFromSubs(subs)
|
|
1060
|
+
if (result) attrs.bg = result
|
|
1061
|
+
} else {
|
|
1062
|
+
const result = parseExtendedColor(params, i)
|
|
1063
|
+
if (result) {
|
|
1064
|
+
attrs.bg = result.color
|
|
1065
|
+
i = result.nextIndex - 1
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
break
|
|
1069
|
+
}
|
|
1070
|
+
case 49: // Default background
|
|
1071
|
+
attrs.bg = null
|
|
1072
|
+
break
|
|
1073
|
+
case 53: // Overline
|
|
1074
|
+
attrs.overline = true
|
|
1075
|
+
break
|
|
1076
|
+
case 55: // Overline off
|
|
1077
|
+
attrs.overline = false
|
|
1078
|
+
break
|
|
1079
|
+
case 58: {
|
|
1080
|
+
// Underline color: 58;5;N (256) or 58;2;R;G;B (truecolor)
|
|
1081
|
+
// Also handle colon form: 58:5:N or 58:2:R:G:B
|
|
1082
|
+
const subs = subParams.get(i)
|
|
1083
|
+
if (subs && subs.length >= 3) {
|
|
1084
|
+
const result = parseExtendedColorFromSubs(subs)
|
|
1085
|
+
if (result) attrs.underlineColor = result
|
|
1086
|
+
} else {
|
|
1087
|
+
const result = parseExtendedColor(params, i)
|
|
1088
|
+
if (result) {
|
|
1089
|
+
attrs.underlineColor = result.color
|
|
1090
|
+
i = result.nextIndex - 1
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
break
|
|
1094
|
+
}
|
|
1095
|
+
case 59: // Default underline color
|
|
1096
|
+
attrs.underlineColor = null
|
|
1097
|
+
break
|
|
1098
|
+
// Bright foreground 90-97
|
|
1099
|
+
case 90:
|
|
1100
|
+
case 91:
|
|
1101
|
+
case 92:
|
|
1102
|
+
case 93:
|
|
1103
|
+
case 94:
|
|
1104
|
+
case 95:
|
|
1105
|
+
case 96:
|
|
1106
|
+
case 97:
|
|
1107
|
+
attrs.fg = { ...PALETTE_256[code - 90 + 8]! }
|
|
1108
|
+
break
|
|
1109
|
+
// Bright background 100-107
|
|
1110
|
+
case 100:
|
|
1111
|
+
case 101:
|
|
1112
|
+
case 102:
|
|
1113
|
+
case 103:
|
|
1114
|
+
case 104:
|
|
1115
|
+
case 105:
|
|
1116
|
+
case 106:
|
|
1117
|
+
case 107:
|
|
1118
|
+
attrs.bg = { ...PALETTE_256[code - 100 + 8]! }
|
|
1119
|
+
break
|
|
1120
|
+
}
|
|
1121
|
+
i++
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function parseExtendedColor(params: number[], startIndex: number): { color: CellColor; nextIndex: number } | null {
|
|
1126
|
+
if (startIndex + 1 >= params.length) return null
|
|
1127
|
+
|
|
1128
|
+
const type = params[startIndex + 1]
|
|
1129
|
+
if (type === 5 && startIndex + 2 < params.length) {
|
|
1130
|
+
const idx = params[startIndex + 2]!
|
|
1131
|
+
const color = PALETTE_256[idx] ?? { r: 0, g: 0, b: 0 }
|
|
1132
|
+
return { color: { ...color }, nextIndex: startIndex + 3 }
|
|
1133
|
+
} else if (type === 2 && startIndex + 4 < params.length) {
|
|
1134
|
+
return {
|
|
1135
|
+
color: {
|
|
1136
|
+
r: params[startIndex + 2]!,
|
|
1137
|
+
g: params[startIndex + 3]!,
|
|
1138
|
+
b: params[startIndex + 4]!,
|
|
1139
|
+
},
|
|
1140
|
+
nextIndex: startIndex + 5,
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return null
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** Parse extended color from colon sub-parameters (e.g., 38:2:R:G:B or 38:5:N) */
|
|
1147
|
+
function parseExtendedColorFromSubs(subs: number[]): CellColor | null {
|
|
1148
|
+
if (subs.length < 3) return null
|
|
1149
|
+
const type = subs[1]
|
|
1150
|
+
if (type === 5 && subs.length >= 3) {
|
|
1151
|
+
const idx = subs[2]!
|
|
1152
|
+
const color = PALETTE_256[idx] ?? { r: 0, g: 0, b: 0 }
|
|
1153
|
+
return { ...color }
|
|
1154
|
+
} else if (type === 2) {
|
|
1155
|
+
// Can be 38:2:R:G:B or 38:2:colorspace:R:G:B
|
|
1156
|
+
if (subs.length >= 5) {
|
|
1157
|
+
// 38:2:R:G:B (no colorspace) or 38:2:cs:R:G:B
|
|
1158
|
+
// If subs.length >= 6, assume colorspace variant
|
|
1159
|
+
if (subs.length >= 6) {
|
|
1160
|
+
return { r: subs[3]!, g: subs[4]!, b: subs[5]! }
|
|
1161
|
+
}
|
|
1162
|
+
return { r: subs[2]!, g: subs[3]!, b: subs[4]! }
|
|
1163
|
+
} else if (subs.length >= 4) {
|
|
1164
|
+
return { r: subs[2]!, g: subs[3]!, b: 0 }
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return null
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// ── OSC handler ──
|
|
1171
|
+
|
|
1172
|
+
function handleOSC(oscString: string): void {
|
|
1173
|
+
const semicolonIdx = oscString.indexOf(";")
|
|
1174
|
+
if (semicolonIdx === -1) return
|
|
1175
|
+
|
|
1176
|
+
const code = parseInt(oscString.substring(0, semicolonIdx), 10)
|
|
1177
|
+
const value = oscString.substring(semicolonIdx + 1)
|
|
1178
|
+
|
|
1179
|
+
switch (code) {
|
|
1180
|
+
case 0: // Set icon name and window title
|
|
1181
|
+
case 2: // Set window title
|
|
1182
|
+
title = value
|
|
1183
|
+
break
|
|
1184
|
+
case 1: // Set icon name (ignore)
|
|
1185
|
+
break
|
|
1186
|
+
case 8: {
|
|
1187
|
+
// Hyperlink: OSC 8 ; params ; url ST
|
|
1188
|
+
// Format: 8;params;url or 8;;url or 8;; (close)
|
|
1189
|
+
const secondSemicolon = value.indexOf(";")
|
|
1190
|
+
if (secondSemicolon !== -1) {
|
|
1191
|
+
const url = value.substring(secondSemicolon + 1)
|
|
1192
|
+
attrs.url = url || null
|
|
1193
|
+
}
|
|
1194
|
+
break
|
|
1195
|
+
}
|
|
1196
|
+
case 10: // Foreground color query
|
|
1197
|
+
if (value === "?" && onResponse) {
|
|
1198
|
+
// Default foreground (white-ish)
|
|
1199
|
+
onResponse("\x1b]10;rgb:ffff/ffff/ffff\x1b\\")
|
|
1200
|
+
}
|
|
1201
|
+
break
|
|
1202
|
+
case 11: // Background color query
|
|
1203
|
+
if (value === "?" && onResponse) {
|
|
1204
|
+
// Default background (black)
|
|
1205
|
+
onResponse("\x1b]11;rgb:0000/0000/0000\x1b\\")
|
|
1206
|
+
}
|
|
1207
|
+
break
|
|
1208
|
+
case 52: {
|
|
1209
|
+
// Clipboard: OSC 52 ; selection ; base64-data ST
|
|
1210
|
+
const clipSemi = value.indexOf(";")
|
|
1211
|
+
if (clipSemi !== -1) {
|
|
1212
|
+
const data = value.substring(clipSemi + 1)
|
|
1213
|
+
if (data === "?") {
|
|
1214
|
+
// Query clipboard
|
|
1215
|
+
if (onResponse) {
|
|
1216
|
+
const encoded = btoa(clipboard)
|
|
1217
|
+
onResponse(`\x1b]52;c;${encoded}\x1b\\`)
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
// Set clipboard
|
|
1221
|
+
try {
|
|
1222
|
+
clipboard = atob(data)
|
|
1223
|
+
} catch {
|
|
1224
|
+
clipboard = ""
|
|
1225
|
+
}
|
|
1226
|
+
if (onResponse) {
|
|
1227
|
+
onResponse(`\x1b]52;c;${data}\x1b\\`)
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
break
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// ── Soft reset (DECSTR) ──
|
|
1237
|
+
|
|
1238
|
+
function softReset(): void {
|
|
1239
|
+
// Reset modes to defaults
|
|
1240
|
+
insertMode = false
|
|
1241
|
+
originMode = false
|
|
1242
|
+
autoWrap = true
|
|
1243
|
+
curVisible = true
|
|
1244
|
+
cursorShape = "block"
|
|
1245
|
+
cursorBlinking = true
|
|
1246
|
+
applicationCursor = false
|
|
1247
|
+
applicationKeypad = false
|
|
1248
|
+
reverseVideo = false
|
|
1249
|
+
|
|
1250
|
+
// Reset scroll region
|
|
208
1251
|
scrollTop = 0
|
|
209
1252
|
scrollBottom = rows - 1
|
|
1253
|
+
|
|
1254
|
+
// Reset attributes
|
|
1255
|
+
attrs = resetAttrs()
|
|
1256
|
+
|
|
1257
|
+
// Reset character set
|
|
1258
|
+
charsetG0 = false
|
|
1259
|
+
|
|
1260
|
+
// Reset cursor to home
|
|
1261
|
+
curX = 0
|
|
1262
|
+
curY = 0
|
|
210
1263
|
}
|
|
211
1264
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1265
|
+
// ── Full reset ──
|
|
1266
|
+
|
|
1267
|
+
function fullReset(): void {
|
|
1268
|
+
mainGrid = makeGrid(cols, rows)
|
|
1269
|
+
altGrid = makeGrid(cols, rows)
|
|
1270
|
+
grid = mainGrid
|
|
215
1271
|
scrollback = []
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
1272
|
+
curX = 0
|
|
1273
|
+
curY = 0
|
|
1274
|
+
curVisible = true
|
|
219
1275
|
cursorShape = "block"
|
|
220
1276
|
cursorBlinking = true
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
1277
|
+
savedCurX = 0
|
|
1278
|
+
savedCurY = 0
|
|
1279
|
+
savedState = { curX: 0, curY: 0, attrs: resetAttrs(), originMode: false, autoWrap: true, charsetG0: false }
|
|
1280
|
+
attrs = resetAttrs()
|
|
1281
|
+
title = ""
|
|
1282
|
+
useAltScreen = false
|
|
226
1283
|
bracketedPaste = false
|
|
227
1284
|
applicationCursor = false
|
|
228
1285
|
applicationKeypad = false
|
|
1286
|
+
autoWrap = true
|
|
229
1287
|
mouseTracking = false
|
|
1288
|
+
mouseTrackingMode = 0
|
|
1289
|
+
sgrMouse = false
|
|
230
1290
|
focusTracking = false
|
|
1291
|
+
originMode = false
|
|
1292
|
+
insertMode = false
|
|
231
1293
|
reverseVideo = false
|
|
232
1294
|
syncOutput = false
|
|
233
1295
|
scrollTop = 0
|
|
234
1296
|
scrollBottom = rows - 1
|
|
235
|
-
title = ""
|
|
236
1297
|
viewportOffset = 0
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1298
|
+
charsetG0 = false
|
|
1299
|
+
clipboard = ""
|
|
1300
|
+
lastChar = ""
|
|
1301
|
+
parserState = "ground"
|
|
1302
|
+
escBuf = ""
|
|
1303
|
+
oscBuf = ""
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// ── Main parser ──
|
|
1307
|
+
|
|
1308
|
+
function process(data: Uint8Array): void {
|
|
1309
|
+
const text = decoder.decode(data, { stream: true })
|
|
1310
|
+
|
|
1311
|
+
for (let i = 0; i < text.length; i++) {
|
|
1312
|
+
const ch = text[i]!
|
|
1313
|
+
const code = text.charCodeAt(i)
|
|
1314
|
+
|
|
1315
|
+
switch (parserState) {
|
|
1316
|
+
case "ground":
|
|
1317
|
+
if (code === 0x1b) {
|
|
1318
|
+
parserState = "escape"
|
|
1319
|
+
escBuf = ""
|
|
1320
|
+
} else if (code === 0x07) {
|
|
1321
|
+
// BEL — ignore
|
|
1322
|
+
} else if (code === 0x08) {
|
|
1323
|
+
// BS - Backspace
|
|
1324
|
+
if (curX > 0) curX--
|
|
1325
|
+
} else if (code === 0x09) {
|
|
1326
|
+
// TAB
|
|
1327
|
+
curX = Math.min((Math.floor(curX / 8) + 1) * 8, cols - 1)
|
|
1328
|
+
} else if (code === 0x0a || code === 0x0b || code === 0x0c) {
|
|
1329
|
+
// LF, VT, FF — linefeed
|
|
1330
|
+
curY++
|
|
1331
|
+
if (curY > scrollBottom) {
|
|
1332
|
+
curY = scrollBottom
|
|
1333
|
+
scrollUp(scrollTop, scrollBottom)
|
|
1334
|
+
}
|
|
1335
|
+
} else if (code === 0x0d) {
|
|
1336
|
+
// CR - Carriage Return
|
|
1337
|
+
curX = 0
|
|
1338
|
+
} else if (code >= 0x20) {
|
|
1339
|
+
// Handle surrogate pairs for characters > U+FFFF
|
|
1340
|
+
let char = ch
|
|
1341
|
+
if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
|
|
1342
|
+
const nextCode = text.charCodeAt(i + 1)
|
|
1343
|
+
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
|
1344
|
+
char = ch + text[i + 1]!
|
|
1345
|
+
i++
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
writeChar(char)
|
|
1349
|
+
}
|
|
1350
|
+
break
|
|
1351
|
+
|
|
1352
|
+
case "escape":
|
|
1353
|
+
if (ch === "[") {
|
|
1354
|
+
parserState = "csi"
|
|
1355
|
+
escBuf = ""
|
|
1356
|
+
} else if (ch === "]") {
|
|
1357
|
+
parserState = "osc"
|
|
1358
|
+
oscBuf = ""
|
|
1359
|
+
} else if (ch === "P") {
|
|
1360
|
+
parserState = "dcs"
|
|
1361
|
+
escBuf = ""
|
|
1362
|
+
} else if (ch === "c") {
|
|
1363
|
+
// RIS - Reset to Initial State
|
|
1364
|
+
fullReset()
|
|
1365
|
+
parserState = "ground"
|
|
1366
|
+
} else if (ch === "D") {
|
|
1367
|
+
// IND - Index (move cursor down, scroll if needed)
|
|
1368
|
+
curY++
|
|
1369
|
+
if (curY > scrollBottom) {
|
|
1370
|
+
curY = scrollBottom
|
|
1371
|
+
scrollUp(scrollTop, scrollBottom)
|
|
1372
|
+
}
|
|
1373
|
+
parserState = "ground"
|
|
1374
|
+
} else if (ch === "M") {
|
|
1375
|
+
// RI - Reverse Index (move cursor up, scroll if needed)
|
|
1376
|
+
curY--
|
|
1377
|
+
if (curY < scrollTop) {
|
|
1378
|
+
curY = scrollTop
|
|
1379
|
+
scrollDown(scrollTop, scrollBottom)
|
|
1380
|
+
}
|
|
1381
|
+
parserState = "ground"
|
|
1382
|
+
} else if (ch === "7") {
|
|
1383
|
+
// DECSC - Save Cursor + attributes + modes
|
|
1384
|
+
savedState = {
|
|
1385
|
+
curX,
|
|
1386
|
+
curY,
|
|
1387
|
+
attrs: {
|
|
1388
|
+
...attrs,
|
|
1389
|
+
fg: attrs.fg ? { ...attrs.fg } : null,
|
|
1390
|
+
bg: attrs.bg ? { ...attrs.bg } : null,
|
|
1391
|
+
underlineColor: attrs.underlineColor ? { ...attrs.underlineColor } : null,
|
|
1392
|
+
},
|
|
1393
|
+
originMode,
|
|
1394
|
+
autoWrap,
|
|
1395
|
+
charsetG0,
|
|
1396
|
+
}
|
|
1397
|
+
parserState = "ground"
|
|
1398
|
+
} else if (ch === "8") {
|
|
1399
|
+
// DECRC - Restore Cursor + attributes + modes
|
|
1400
|
+
curX = savedState.curX
|
|
1401
|
+
curY = savedState.curY
|
|
1402
|
+
attrs = {
|
|
1403
|
+
...savedState.attrs,
|
|
1404
|
+
fg: savedState.attrs.fg ? { ...savedState.attrs.fg } : null,
|
|
1405
|
+
bg: savedState.attrs.bg ? { ...savedState.attrs.bg } : null,
|
|
1406
|
+
underlineColor: savedState.attrs.underlineColor ? { ...savedState.attrs.underlineColor } : null,
|
|
1407
|
+
}
|
|
1408
|
+
originMode = savedState.originMode
|
|
1409
|
+
autoWrap = savedState.autoWrap
|
|
1410
|
+
charsetG0 = savedState.charsetG0
|
|
1411
|
+
clampCursor()
|
|
1412
|
+
parserState = "ground"
|
|
1413
|
+
} else if (ch === "E") {
|
|
1414
|
+
// NEL - Next Line
|
|
1415
|
+
curX = 0
|
|
1416
|
+
curY++
|
|
1417
|
+
if (curY > scrollBottom) {
|
|
1418
|
+
curY = scrollBottom
|
|
1419
|
+
scrollUp(scrollTop, scrollBottom)
|
|
1420
|
+
}
|
|
1421
|
+
parserState = "ground"
|
|
1422
|
+
} else if (ch === "(") {
|
|
1423
|
+
// Designate G0 character set
|
|
1424
|
+
parserState = "escape_charset"
|
|
1425
|
+
} else if (ch === ")") {
|
|
1426
|
+
// Designate G1 character set (ignored, just consume next byte)
|
|
1427
|
+
parserState = "escape_charset"
|
|
1428
|
+
} else {
|
|
1429
|
+
// Unknown escape — return to ground
|
|
1430
|
+
parserState = "ground"
|
|
1431
|
+
}
|
|
1432
|
+
break
|
|
1433
|
+
|
|
1434
|
+
case "escape_charset":
|
|
1435
|
+
// Character set designation: ESC ( 0 = DEC Special Graphics, ESC ( B = ASCII
|
|
1436
|
+
if (ch === "0") {
|
|
1437
|
+
charsetG0 = true
|
|
1438
|
+
} else {
|
|
1439
|
+
charsetG0 = false // B = ASCII, or any other
|
|
1440
|
+
}
|
|
1441
|
+
parserState = "ground"
|
|
1442
|
+
break
|
|
1443
|
+
|
|
1444
|
+
case "csi": {
|
|
1445
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
1446
|
+
// Final byte — dispatch CSI
|
|
1447
|
+
// Extract intermediates (characters 0x20-0x2F between params and final)
|
|
1448
|
+
let paramPart = escBuf
|
|
1449
|
+
let intermediatePart = ""
|
|
1450
|
+
|
|
1451
|
+
// Check for intermediates at the end of escBuf
|
|
1452
|
+
// Intermediates are 0x20-0x2F: space ! " # $ % & ' ( ) * + , - . /
|
|
1453
|
+
let j = paramPart.length - 1
|
|
1454
|
+
while (j >= 0) {
|
|
1455
|
+
const c = paramPart.charCodeAt(j)
|
|
1456
|
+
if (c >= 0x20 && c <= 0x2f) {
|
|
1457
|
+
j--
|
|
1458
|
+
} else {
|
|
1459
|
+
break
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (j < paramPart.length - 1) {
|
|
1463
|
+
intermediatePart = paramPart.substring(j + 1)
|
|
1464
|
+
paramPart = paramPart.substring(0, j + 1)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (paramPart.startsWith("?")) {
|
|
1468
|
+
handleCSIPrivate(paramPart.substring(1), intermediatePart, ch)
|
|
1469
|
+
} else if (paramPart.startsWith(">")) {
|
|
1470
|
+
handleCSIGt(paramPart.substring(1), intermediatePart, ch)
|
|
1471
|
+
} else {
|
|
1472
|
+
handleCSI(paramPart, intermediatePart, ch)
|
|
1473
|
+
}
|
|
1474
|
+
parserState = "ground"
|
|
1475
|
+
} else if (escBuf.length >= 256) {
|
|
1476
|
+
parserState = "ground"
|
|
1477
|
+
} else {
|
|
1478
|
+
escBuf += ch
|
|
1479
|
+
}
|
|
1480
|
+
break
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
case "osc":
|
|
1484
|
+
if (code === 0x07) {
|
|
1485
|
+
// BEL terminates OSC
|
|
1486
|
+
handleOSC(oscBuf)
|
|
1487
|
+
parserState = "ground"
|
|
1488
|
+
} else if (code === 0x1b) {
|
|
1489
|
+
// ESC might be start of ST (\x1b\\)
|
|
1490
|
+
parserState = "osc_st"
|
|
1491
|
+
} else if (oscBuf.length >= 4096) {
|
|
1492
|
+
parserState = "ground"
|
|
1493
|
+
} else {
|
|
1494
|
+
oscBuf += ch
|
|
1495
|
+
}
|
|
1496
|
+
break
|
|
1497
|
+
|
|
1498
|
+
case "osc_st":
|
|
1499
|
+
if (ch === "\\") {
|
|
1500
|
+
// ST (String Terminator) — end of OSC
|
|
1501
|
+
handleOSC(oscBuf)
|
|
1502
|
+
}
|
|
1503
|
+
parserState = "ground"
|
|
1504
|
+
break
|
|
1505
|
+
|
|
1506
|
+
case "dcs":
|
|
1507
|
+
// Consume until ST
|
|
1508
|
+
if (code === 0x1b) {
|
|
1509
|
+
parserState = "dcs_st"
|
|
1510
|
+
} else if (code === 0x07) {
|
|
1511
|
+
// Some implementations use BEL to terminate DCS
|
|
1512
|
+
parserState = "ground"
|
|
1513
|
+
}
|
|
1514
|
+
break
|
|
1515
|
+
|
|
1516
|
+
case "dcs_st":
|
|
1517
|
+
// Expecting backslash to complete ST
|
|
1518
|
+
if (ch === "\\") {
|
|
1519
|
+
// DCS complete
|
|
1520
|
+
}
|
|
1521
|
+
parserState = "ground"
|
|
1522
|
+
break
|
|
1523
|
+
|
|
1524
|
+
case "dcs_passthrough":
|
|
1525
|
+
// Consume until ST
|
|
1526
|
+
if (code === 0x1b) {
|
|
1527
|
+
parserState = "dcs_st"
|
|
1528
|
+
}
|
|
1529
|
+
break
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// ── Resize ──
|
|
1535
|
+
|
|
1536
|
+
function resize(newCols: number, newRows: number): void {
|
|
1537
|
+
const newMain = makeGrid(newCols, newRows)
|
|
1538
|
+
const newAlt = makeGrid(newCols, newRows)
|
|
1539
|
+
|
|
1540
|
+
copyGrid(mainGrid, newMain, Math.min(cols, newCols), Math.min(rows, newRows))
|
|
1541
|
+
copyGrid(altGrid, newAlt, Math.min(cols, newCols), Math.min(rows, newRows))
|
|
1542
|
+
|
|
1543
|
+
mainGrid = newMain
|
|
1544
|
+
altGrid = newAlt
|
|
1545
|
+
grid = useAltScreen ? altGrid : mainGrid
|
|
1546
|
+
cols = newCols
|
|
1547
|
+
rows = newRows
|
|
1548
|
+
scrollTop = 0
|
|
1549
|
+
scrollBottom = rows - 1
|
|
1550
|
+
clampCursor()
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function copyGrid(src: ScreenCell[][], dst: ScreenCell[][], copyCols: number, copyRows: number): void {
|
|
1554
|
+
for (let row = 0; row < copyRows; row++) {
|
|
1555
|
+
for (let col = 0; col < copyCols; col++) {
|
|
1556
|
+
const srcCell = src[row]?.[col]
|
|
1557
|
+
if (srcCell) {
|
|
1558
|
+
dst[row]![col] = { ...srcCell }
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// ── Accessors ──
|
|
1565
|
+
|
|
1566
|
+
function getCell(row: number, col: number): ScreenCell {
|
|
1567
|
+
const r = grid[row]
|
|
1568
|
+
if (!r || col >= cols) return emptyCell()
|
|
1569
|
+
return { ...r[col]! }
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function getLine(row: number): ScreenCell[] {
|
|
1573
|
+
const r = grid[row]
|
|
1574
|
+
if (!r) return makeRow(cols)
|
|
1575
|
+
return r.map((cell) => ({ ...cell }))
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function getText(): string {
|
|
1579
|
+
const lines: string[] = []
|
|
1580
|
+
for (let r = 0; r < rows; r++) {
|
|
1581
|
+
lines.push(rowToString(grid[r]!))
|
|
1582
|
+
}
|
|
1583
|
+
return lines.join("\n")
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function rowToString(row: ScreenCell[]): string {
|
|
1587
|
+
let line = ""
|
|
1588
|
+
for (let i = 0; i < row.length; i++) {
|
|
1589
|
+
const cell = row[i]!
|
|
1590
|
+
if (cell.wide) {
|
|
1591
|
+
line += cell.char
|
|
1592
|
+
} else if (cell.char === "") {
|
|
1593
|
+
if (i > 0 && row[i - 1]?.wide) {
|
|
1594
|
+
continue
|
|
1595
|
+
}
|
|
1596
|
+
line += " "
|
|
1597
|
+
} else {
|
|
1598
|
+
line += cell.char
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return line.replace(/\s+$/, "")
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function getTextRange(startRow: number, startCol: number, endRow: number, endCol: number): string {
|
|
1605
|
+
const parts: string[] = []
|
|
1606
|
+
|
|
1607
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
1608
|
+
const r = grid[row]
|
|
1609
|
+
if (!r) continue
|
|
1610
|
+
|
|
1611
|
+
const colStart = row === startRow ? startCol : 0
|
|
1612
|
+
const colEnd = row === endRow ? endCol : cols
|
|
1613
|
+
|
|
1614
|
+
let line = ""
|
|
1615
|
+
for (let col = colStart; col < colEnd; col++) {
|
|
1616
|
+
const cell = r[col]
|
|
1617
|
+
if (!cell) continue
|
|
1618
|
+
if (cell.char === "" && col > 0 && r[col - 1]?.wide) continue
|
|
1619
|
+
line += cell.char || " "
|
|
1620
|
+
}
|
|
1621
|
+
parts.push(line.replace(/\s+$/, ""))
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return parts.join("\n")
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function getMode(mode: string): boolean {
|
|
1628
|
+
switch (mode) {
|
|
1629
|
+
case "altScreen":
|
|
1630
|
+
return useAltScreen
|
|
1631
|
+
case "cursorVisible":
|
|
1632
|
+
return curVisible
|
|
1633
|
+
case "bracketedPaste":
|
|
1634
|
+
return bracketedPaste
|
|
1635
|
+
case "applicationCursor":
|
|
1636
|
+
return applicationCursor
|
|
1637
|
+
case "applicationKeypad":
|
|
1638
|
+
return applicationKeypad
|
|
1639
|
+
case "autoWrap":
|
|
1640
|
+
return autoWrap
|
|
1641
|
+
case "mouseTracking":
|
|
1642
|
+
return mouseTracking
|
|
1643
|
+
case "focusTracking":
|
|
1644
|
+
return focusTracking
|
|
1645
|
+
case "originMode":
|
|
1646
|
+
return originMode
|
|
1647
|
+
case "insertMode":
|
|
1648
|
+
return insertMode
|
|
1649
|
+
case "reverseVideo":
|
|
1650
|
+
return reverseVideo
|
|
1651
|
+
case "syncOutput":
|
|
1652
|
+
return syncOutput
|
|
1653
|
+
case "sgrMouse":
|
|
1654
|
+
return sgrMouse
|
|
1655
|
+
default:
|
|
1656
|
+
return false
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Suppress unused variable warnings
|
|
1661
|
+
void [mouseTrackingMode]
|
|
252
1662
|
|
|
253
1663
|
return {
|
|
254
|
-
get cols() {
|
|
255
|
-
|
|
1664
|
+
get cols() {
|
|
1665
|
+
return cols
|
|
1666
|
+
},
|
|
1667
|
+
get rows() {
|
|
1668
|
+
return rows
|
|
1669
|
+
},
|
|
256
1670
|
process,
|
|
257
1671
|
resize,
|
|
258
|
-
reset,
|
|
259
|
-
getCell
|
|
260
|
-
getLine
|
|
261
|
-
getText
|
|
262
|
-
getTextRange
|
|
263
|
-
getCursorPosition: () => ({ x:
|
|
264
|
-
getCursorVisible: () =>
|
|
1672
|
+
reset: fullReset,
|
|
1673
|
+
getCell,
|
|
1674
|
+
getLine,
|
|
1675
|
+
getText,
|
|
1676
|
+
getTextRange,
|
|
1677
|
+
getCursorPosition: () => ({ x: curX, y: curY }),
|
|
1678
|
+
getCursorVisible: () => curVisible,
|
|
265
1679
|
getCursorShape: () => cursorShape,
|
|
266
1680
|
getCursorBlinking: () => cursorBlinking,
|
|
267
1681
|
getTitle: () => title,
|
|
268
|
-
getMode
|
|
269
|
-
switch (mode) {
|
|
270
|
-
case "altScreen": return altScreen
|
|
271
|
-
case "autoWrap": return autoWrap
|
|
272
|
-
case "originMode": return originMode
|
|
273
|
-
case "insertMode": return insertMode
|
|
274
|
-
case "bracketedPaste": return bracketedPaste
|
|
275
|
-
case "applicationCursor": return applicationCursor
|
|
276
|
-
case "applicationKeypad": return applicationKeypad
|
|
277
|
-
case "mouseTracking": return mouseTracking
|
|
278
|
-
case "focusTracking": return focusTracking
|
|
279
|
-
case "reverseVideo": return reverseVideo
|
|
280
|
-
case "syncOutput": return syncOutput
|
|
281
|
-
case "cursorVisible": return cursorVisible
|
|
282
|
-
default: return false
|
|
283
|
-
}
|
|
284
|
-
},
|
|
1682
|
+
getMode,
|
|
285
1683
|
getScrollbackLength: () => scrollback.length,
|
|
286
1684
|
getViewportOffset: () => viewportOffset,
|
|
287
|
-
scrollViewport: (delta) => {
|
|
1685
|
+
scrollViewport: (delta: number) => {
|
|
288
1686
|
viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta))
|
|
289
1687
|
},
|
|
290
1688
|
}
|