@next_term/core 0.0.1-next.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.
@@ -0,0 +1,1952 @@
1
+ import { CELL_SIZE, DEFAULT_CELL_W0, DEFAULT_CELL_W1 } from "../cell-grid.js";
2
+ import { Action, State, TABLE } from "./states.js";
3
+ // Attribute bit positions in the attrs byte (word 1, bits 8-15)
4
+ const ATTR_BOLD = 0x01;
5
+ const ATTR_DIM = 0x10;
6
+ const ATTR_ITALIC = 0x02;
7
+ const ATTR_UNDERLINE = 0x04;
8
+ const ATTR_STRIKETHROUGH = 0x08;
9
+ // bits 4-5: underline style (reserved)
10
+ const ATTR_INVERSE = 0x40;
11
+ const ATTR_HIDDEN = 0x20;
12
+ export class VTParser {
13
+ state = State.GROUND;
14
+ bufferSet;
15
+ // CSI parameter collection — pre-allocated typed array avoids GC/hidden-class overhead
16
+ static MAX_PARAMS = 16;
17
+ params = new Int32Array(VTParser.MAX_PARAMS);
18
+ paramCount = 0;
19
+ currentParam = 0;
20
+ hasParam = false;
21
+ // Stores only the last intermediate byte (0x20-0x2F). All sequences we
22
+ // implement use at most one intermediate: ' ' (DECSCUSR), '!' (DECSTR),
23
+ // '#' (DECALN), '(' / ')' (charset). The ISO 2022 spec defines rare
24
+ // two-intermediate sequences (e.g. ESC $ ( F for 94x94 charsets) where
25
+ // the first intermediate would be lost — acceptable since we don't
26
+ // implement ISO 2022 multi-byte charset designation.
27
+ intermediatesByte = 0;
28
+ prefixByte = 0; // single byte: '?' = 0x3f, '>' = 0x3e, '=' = 0x3d, 0 = none
29
+ // SGR state
30
+ fgIndex = 7; // default foreground (white)
31
+ bgIndex = 0; // default background (black)
32
+ attrs = 0;
33
+ fgIsRGB = false;
34
+ bgIsRGB = false;
35
+ fgRGB = 0;
36
+ bgRGB = 0;
37
+ // OSC string collection — pre-allocated typed array avoids GC/hidden-class overhead
38
+ static MAX_OSC_LENGTH = 4096;
39
+ oscParts = new Uint8Array(VTParser.MAX_OSC_LENGTH);
40
+ oscLength = 0;
41
+ // DCS string collection — pre-allocated typed array avoids GC/hidden-class overhead
42
+ static MAX_DCS_LENGTH = 4096;
43
+ dcsParts = new Uint8Array(VTParser.MAX_DCS_LENGTH);
44
+ dcsLength = 0;
45
+ dcsFinal = 0;
46
+ // DCS tmux passthrough collection — bypasses state machine for inner sequences
47
+ // tmuxSeeking: true after HOOK with dcsFinal='t' while we verify "mux;" prefix
48
+ // inTmuxDcs: true once "mux;" confirmed; raw bytes collected into tmuxParts
49
+ // tmuxPrevWasEsc: tracks pending ESC for \x1b\x1b unescaping and outer-ST detection
50
+ static MAX_TMUX_LENGTH = 8192;
51
+ tmuxParts = new Uint8Array(VTParser.MAX_TMUX_LENGTH);
52
+ tmuxLength = 0;
53
+ tmuxSeeking = false;
54
+ inTmuxDcs = false;
55
+ tmuxPrevWasEsc = false;
56
+ tmuxDepth = 0; // recursion guard for dispatchTmux
57
+ // UTF-8 decoding state
58
+ utf8Bytes = 0;
59
+ utf8Codepoint = 0;
60
+ // Terminal mode flags
61
+ lineFeedMode = false; // LNM: when set, LF also does CR
62
+ autoWrapMode = true; // DECAWM: when enabled (default), chars wrap at right margin
63
+ originMode = false; // DECOM: when set, cursor position relative to scroll region
64
+ // Mode flags exposed to the input handler
65
+ applicationCursorKeys = false; // DECCKM (mode 1)
66
+ applicationKeypad = false; // DECKPAM / DECKPNM
67
+ bracketedPasteMode = false; // mode 2004
68
+ syncedOutput = false; // mode 2026 — synchronized output (render gating)
69
+ mouseProtocol = "none"; // modes 9, 1000, 1002, 1003
70
+ mouseEncoding = "default"; // mode 1006
71
+ sendFocusEvents = false; // mode 1004
72
+ // Kitty keyboard protocol flags (CSI = / > / < / ? u)
73
+ kittyFlags = 0; // current flags bitfield
74
+ kittyFlagsStack = []; // push/pop stack
75
+ // REP — last printed codepoint for CSI b
76
+ lastPrintedCodepoint = 0;
77
+ // Response buffer for DSR, DA, etc.
78
+ responseBuffer = [];
79
+ // Title stack for window manipulation
80
+ titleStack = [];
81
+ // Title change callback
82
+ onTitleChange = null;
83
+ // OSC 52 clipboard callback: (selection: string, data: string | null) => void
84
+ // data is null for queries (?), base64 string for writes.
85
+ onOsc52 = null;
86
+ // OSC 4 color palette callback: (index: number, spec: string | null) => void
87
+ // spec is null for queries (?), color string (e.g. "rgb:ff/00/00") for sets.
88
+ onOsc4 = null;
89
+ // OSC 7 current working directory callback: (uri: string) => void
90
+ onOsc7 = null;
91
+ // OSC 10 foreground color callback: (spec: string | null) => void
92
+ // spec is null for queries (?), color string for sets.
93
+ onOsc10 = null;
94
+ // OSC 11 background color callback: (spec: string | null) => void
95
+ onOsc11 = null;
96
+ // OSC 12 cursor color callback: (spec: string | null) => void
97
+ onOsc12 = null;
98
+ // OSC 104 reset color palette callback: (index: number) => void
99
+ // index is -1 for "reset all", or 0-255 for a specific palette entry.
100
+ onOsc104 = null;
101
+ // OSC 8 hyperlink callback: (params: string, uri: string) => void
102
+ // params is the optional colon-separated key=value metadata (may be "").
103
+ // uri is the hyperlink target (empty string closes the link).
104
+ onOsc8 = null;
105
+ // OSC 133 shell integration callback: (type: string, payload: string) => void
106
+ // type is the event letter: "A" (prompt start), "B" (command start),
107
+ // "C" (command output start), "D" (command end), "E" (command text), "P" (property), etc.
108
+ // payload is the string after the type letter and its optional semicolon separator
109
+ // (e.g. exit code string for "D", "k=cwd;v=/path" for "P", empty string for A/B/C).
110
+ onOsc133 = null;
111
+ // Synchronized output mode 2026 callback: (active: boolean) => void
112
+ // Called when mode 2026 is activated (true) or deactivated (false).
113
+ onSyncOutput = null;
114
+ // DCS handler callback: (finalByte, params, intermediate, data) => void
115
+ // finalByte: the final byte that triggered HOOK (0x40-0x7E, identifies the DCS command).
116
+ // params: numeric params collected before the intermediate/final (CSI-style; may be empty).
117
+ // intermediate: the single intermediate byte collected (0x20-0x2F), or 0 if none.
118
+ // data: the passthrough data collected between HOOK and ST, as a string.
119
+ onDcs = null;
120
+ // DCS tmux passthrough callback: (innerSeq: string) => void
121
+ // Called when a tmux-wrapped DCS sequence (ESC P tmux; ... ESC \) is received,
122
+ // with the decoded inner escape sequence string (ESCs already unescaped).
123
+ // The inner sequence is also processed through the parser automatically.
124
+ onDcsTmux = null;
125
+ // Kitty keyboard flags change callback: (flags: number) => void
126
+ onKittyFlags = null;
127
+ constructor(bufferSet) {
128
+ this.bufferSet = bufferSet;
129
+ }
130
+ /** Register a callback for title changes (OSC 0/1/2). */
131
+ setTitleChangeCallback(cb) {
132
+ this.onTitleChange = cb;
133
+ }
134
+ /** Register a callback for OSC 52 clipboard sequences.
135
+ * `selection` is the clipboard selection string (e.g. "c" for clipboard).
136
+ * `data` is the base64-encoded payload, or null for a query request ("?").
137
+ */
138
+ setOsc52Callback(cb) {
139
+ this.onOsc52 = cb;
140
+ }
141
+ /** Register a callback for OSC 4 color palette sequences.
142
+ * Called once per index;spec pair in the sequence.
143
+ * `index` is the palette index (0-255).
144
+ * `spec` is the color specification string (e.g. "rgb:ff/00/00"), or null for a query ("?").
145
+ */
146
+ setOsc4Callback(cb) {
147
+ this.onOsc4 = cb;
148
+ }
149
+ /** Register a callback for OSC 7 current working directory sequences.
150
+ * Called with the URI payload (e.g. "file:///hostname/path").
151
+ */
152
+ setOsc7Callback(cb) {
153
+ this.onOsc7 = cb;
154
+ }
155
+ /** Register a callback for OSC 10 foreground color sequences.
156
+ * `spec` is the color specification string (e.g. "rgb:ffff/ffff/ffff"), or null for a query ("?").
157
+ */
158
+ setOsc10Callback(cb) {
159
+ this.onOsc10 = cb;
160
+ }
161
+ /** Register a callback for OSC 11 background color sequences.
162
+ * `spec` is the color specification string (e.g. "rgb:0000/0000/0000"), or null for a query ("?").
163
+ */
164
+ setOsc11Callback(cb) {
165
+ this.onOsc11 = cb;
166
+ }
167
+ /** Register a callback for OSC 12 cursor color sequences.
168
+ * `spec` is the color specification string, or null for a query ("?").
169
+ */
170
+ setOsc12Callback(cb) {
171
+ this.onOsc12 = cb;
172
+ }
173
+ /** Register a callback for OSC 104 reset color palette sequences.
174
+ * Called once per index to reset; `index` is 0-255 for a specific palette
175
+ * entry, or -1 when no index is given (reset the entire palette).
176
+ */
177
+ setOsc104Callback(cb) {
178
+ this.onOsc104 = cb;
179
+ }
180
+ /** Register a callback for OSC 8 hyperlink sequences.
181
+ * `params` is the optional colon-separated key=value metadata string (may be "").
182
+ * `uri` is the hyperlink target URI (empty string closes the active link).
183
+ */
184
+ setOsc8Callback(cb) {
185
+ this.onOsc8 = cb;
186
+ }
187
+ /** Register a callback for OSC 133 shell integration (semantic prompt) sequences.
188
+ * `type` is the event letter: "A" (prompt start), "B" (command start),
189
+ * "C" (command output start), "D" (command end), "E" (command text), "P" (property), etc.
190
+ * `payload` is the string after the type letter and its optional semicolon separator
191
+ * (both `133;<type>;<payload>` and `133;<type><payload>` forms are accepted; empty string
192
+ * for A/B/C; exit-code digits for D; command text for E; key=value for P).
193
+ */
194
+ setOsc133Callback(cb) {
195
+ this.onOsc133 = cb;
196
+ }
197
+ /** Register a callback for synchronized output mode 2026 changes.
198
+ * Called with `true` when DECSET ?2026h activates the mode and `false`
199
+ * when DECRST ?2026l deactivates it. The web layer uses this to pause
200
+ * and resume the render loop (frame buffering).
201
+ */
202
+ setSyncOutputCallback(cb) {
203
+ this.onSyncOutput = cb;
204
+ }
205
+ /** Register a callback for DCS (Device Control String) sequences.
206
+ * Called when a DCS sequence is fully received (terminated by ST or C1 ST).
207
+ * `finalByte` identifies the DCS command (0x40–0x7E, e.g. 0x70 for 'p').
208
+ * `params` are the numeric params collected before the intermediate/final byte.
209
+ * `intermediate` is the single intermediate byte (0x20–0x2F), or 0 if none.
210
+ * `data` is the passthrough string collected between the final byte and ST.
211
+ */
212
+ setDcsCallback(cb) {
213
+ this.onDcs = cb;
214
+ }
215
+ /** Register a callback for DCS tmux passthrough sequences.
216
+ * Called when the outer DCS tmux wrapper (ESC P tmux; ... ESC \) is received.
217
+ * `innerSeq` is the decoded inner escape sequence string (with doubled ESCs unescaped).
218
+ * The inner sequence is also automatically processed through the parser.
219
+ */
220
+ setDcsTmuxCallback(cb) {
221
+ this.onDcsTmux = cb;
222
+ }
223
+ /** Register a callback fired when kitty keyboard flags change (CSI =/>/</? u). */
224
+ setKittyFlagsCallback(cb) {
225
+ this.onKittyFlags = cb;
226
+ }
227
+ get cursor() {
228
+ return this.bufferSet.active.cursor;
229
+ }
230
+ get cols() {
231
+ return this.bufferSet.cols;
232
+ }
233
+ get rows() {
234
+ return this.bufferSet.rows;
235
+ }
236
+ get buf() {
237
+ return this.bufferSet.active;
238
+ }
239
+ get grid() {
240
+ return this.bufferSet.active.grid;
241
+ }
242
+ /** Check if there are pending responses to read. */
243
+ hasResponse() {
244
+ return this.responseBuffer.length > 0;
245
+ }
246
+ /** Read the next response from the response buffer. */
247
+ readResponse() {
248
+ return this.responseBuffer.shift() ?? null;
249
+ }
250
+ /** Process raw bytes from the PTY. */
251
+ write(data) {
252
+ // Cache hot state in locals for the duration of the write call
253
+ let state = this.state;
254
+ let utf8Bytes = this.utf8Bytes;
255
+ let utf8Codepoint = this.utf8Codepoint;
256
+ const len = data.length;
257
+ for (let i = 0; i < len; i++) {
258
+ const byte = data[i];
259
+ // DCS tmux passthrough: once inTmuxDcs is set (after confirming "mux;" prefix),
260
+ // we collect all bytes raw (bypassing the VT state machine) and only stop on
261
+ // the outer ESC \ terminator. Each \x1b\x1b pair in the data decodes to one \x1b;
262
+ // a lone \x1b followed by \ is the outer string terminator.
263
+ if (this.inTmuxDcs) {
264
+ if (byte === 0x1b) {
265
+ if (this.tmuxPrevWasEsc) {
266
+ // \x1b\x1b → decoded as single ESC in inner sequence
267
+ if (this.tmuxLength < VTParser.MAX_TMUX_LENGTH) {
268
+ this.tmuxParts[this.tmuxLength++] = 0x1b;
269
+ }
270
+ this.tmuxPrevWasEsc = false;
271
+ }
272
+ else {
273
+ this.tmuxPrevWasEsc = true;
274
+ }
275
+ continue;
276
+ }
277
+ if (this.tmuxPrevWasEsc) {
278
+ this.tmuxPrevWasEsc = false;
279
+ if (byte === 0x5c) {
280
+ // \x1b\ → outer ST, end of tmux DCS passthrough
281
+ this.inTmuxDcs = false;
282
+ state = State.GROUND;
283
+ this.dispatchTmux();
284
+ continue;
285
+ }
286
+ // ESC followed by a non-backslash byte: emit ESC then the byte
287
+ if (this.tmuxLength < VTParser.MAX_TMUX_LENGTH) {
288
+ this.tmuxParts[this.tmuxLength++] = 0x1b;
289
+ }
290
+ if (this.tmuxLength < VTParser.MAX_TMUX_LENGTH) {
291
+ this.tmuxParts[this.tmuxLength++] = byte;
292
+ }
293
+ continue;
294
+ }
295
+ // Regular passthrough byte
296
+ if (this.tmuxLength < VTParser.MAX_TMUX_LENGTH) {
297
+ this.tmuxParts[this.tmuxLength++] = byte;
298
+ }
299
+ continue;
300
+ }
301
+ // ESC sequence fast-path: peek at the next byte to handle common
302
+ // multi-byte escape sequences without per-byte table lookups.
303
+ // ESC is an "anywhere" transition so this is valid from all states.
304
+ if (byte === 0x1b && i + 1 < len) {
305
+ const next = data[i + 1];
306
+ // ESC \ — String Terminator. Dispatch OSC/DCS directly.
307
+ if (next === 0x5c) {
308
+ if (state === State.OSC_STRING) {
309
+ this.oscDispatch();
310
+ }
311
+ else if (state === State.DCS_PASSTHROUGH) {
312
+ this.dcsDispatch();
313
+ }
314
+ // Clear params/intermediates so no state leaks from aborted sequences
315
+ this.clear();
316
+ state = State.GROUND;
317
+ i++;
318
+ continue;
319
+ }
320
+ // ESC [ — CSI entry, with optional fast dispatch
321
+ if (next === 0x5b) {
322
+ this.clear();
323
+ if (i + 2 < len) {
324
+ const fb = data[i + 2];
325
+ // Parameterless CSI: ESC [ <final>
326
+ if (fb >= 0x40 && fb <= 0x7e) {
327
+ this.csiDispatch(fb);
328
+ state = State.GROUND;
329
+ i += 2;
330
+ continue;
331
+ }
332
+ // CSI with private prefix: ESC [ <prefix> <final>
333
+ if (fb >= 0x3c && fb <= 0x3f && i + 3 < len) {
334
+ const fb2 = data[i + 3];
335
+ if (fb2 >= 0x40 && fb2 <= 0x7e) {
336
+ this.prefixByte = fb;
337
+ this.csiDispatch(fb2);
338
+ state = State.GROUND;
339
+ i += 3;
340
+ continue;
341
+ }
342
+ }
343
+ // CSI with parameters: ESC [ <digits/semicolons> <final>
344
+ // Parse all params inline to avoid state machine round-trip.
345
+ // NOTE: param parsing logic is duplicated in the Action.PARAM read-ahead
346
+ // below — keep both in sync when changing param handling (e.g. subparams).
347
+ if (fb >= 0x30 && fb <= 0x39) {
348
+ let j = i + 2;
349
+ let currentParam = 0;
350
+ let hasParam = false;
351
+ let paramCount = 0;
352
+ while (j < len) {
353
+ const b = data[j];
354
+ if (b >= 0x30 && b <= 0x39) {
355
+ if (currentParam <= 99999)
356
+ currentParam = currentParam * 10 + (b - 0x30);
357
+ hasParam = true;
358
+ j++;
359
+ }
360
+ else if (b === 0x3b) {
361
+ if (paramCount < VTParser.MAX_PARAMS)
362
+ this.params[paramCount++] = hasParam ? currentParam : 0;
363
+ currentParam = 0;
364
+ hasParam = false;
365
+ j++;
366
+ }
367
+ else {
368
+ break;
369
+ }
370
+ }
371
+ if (j < len) {
372
+ const finalByte = data[j];
373
+ if (finalByte >= 0x40 && finalByte <= 0x7e) {
374
+ if (hasParam || paramCount > 0) {
375
+ if (paramCount < VTParser.MAX_PARAMS)
376
+ this.params[paramCount++] = hasParam ? currentParam : 0;
377
+ }
378
+ this.csiDispatch(finalByte, paramCount);
379
+ state = State.GROUND;
380
+ i = j;
381
+ continue;
382
+ }
383
+ }
384
+ // Incomplete or non-final byte — save parsed state, let state machine handle
385
+ this.currentParam = currentParam;
386
+ this.hasParam = hasParam;
387
+ this.paramCount = paramCount;
388
+ state = State.CSI_PARAM;
389
+ i = j - 1; // main loop i++ will process data[j]
390
+ continue;
391
+ }
392
+ }
393
+ state = State.CSI_ENTRY;
394
+ i++;
395
+ continue;
396
+ }
397
+ // ESC ] — OSC string start
398
+ if (next === 0x5d) {
399
+ this.clear();
400
+ this.oscLength = 0;
401
+ state = State.OSC_STRING;
402
+ i++;
403
+ continue;
404
+ }
405
+ // ESC P — DCS entry
406
+ if (next === 0x50) {
407
+ this.clear();
408
+ state = State.DCS_ENTRY;
409
+ i++;
410
+ continue;
411
+ }
412
+ // ESC + intermediate (0x20-0x2F) + final (0x30-0x7E) — 3-byte ESC
413
+ if (next >= 0x20 && next <= 0x2f && i + 2 < len) {
414
+ const fb = data[i + 2];
415
+ if (fb >= 0x30 && fb <= 0x7e) {
416
+ this.clear();
417
+ this.intermediatesByte = next;
418
+ this.escDispatch(fb);
419
+ state = State.GROUND;
420
+ i += 2;
421
+ continue;
422
+ }
423
+ }
424
+ // ESC + final byte — 2-byte ESC dispatch (D, E, M, 7, 8, =, >, c, H, etc.)
425
+ // Excludes bytes already handled: 0x50(P), 0x58(SOS), 0x5B([), 0x5C(\),
426
+ // 0x5D(]), 0x5E(PM), 0x5F(APC), and intermediates 0x20-0x2F.
427
+ if ((next >= 0x30 && next <= 0x4f) ||
428
+ (next >= 0x51 && next <= 0x57) ||
429
+ next === 0x59 ||
430
+ next === 0x5a ||
431
+ (next >= 0x60 && next <= 0x7e)) {
432
+ this.clear();
433
+ this.escDispatch(next);
434
+ state = State.GROUND;
435
+ i++;
436
+ continue;
437
+ }
438
+ }
439
+ // UTF-8 continuation handling in GROUND state (split across writes)
440
+ if (state === State.GROUND && utf8Bytes > 0) {
441
+ if ((byte & 0xc0) === 0x80) {
442
+ utf8Codepoint = (utf8Codepoint << 6) | (byte & 0x3f);
443
+ utf8Bytes--;
444
+ if (utf8Bytes === 0) {
445
+ this.printCodepoint(utf8Codepoint);
446
+ }
447
+ continue;
448
+ }
449
+ // Invalid continuation - reset and process byte normally
450
+ utf8Bytes = 0;
451
+ }
452
+ // Combined printable batch: handles both ASCII (0x20-0x7E) and UTF-8
453
+ // (0xC0-0xF7) in a single tight loop. Pre-computes cell template values
454
+ // and caches grid state to avoid per-character method call overhead.
455
+ // NOTE: utf8Bytes is guaranteed 0 here when state === GROUND
456
+ // (the continuation handler above either continues or resets it).
457
+ if (state === State.GROUND &&
458
+ ((byte >= 0x20 && byte <= 0x7e) || (byte >= 0xc0 && byte <= 0xf7))) {
459
+ const buf = this.bufferSet.active;
460
+ const cursor = buf.cursor;
461
+ const grid = buf.grid;
462
+ const gridData = grid.data;
463
+ const gridCols = this.cols;
464
+ const fgVal = this.fgIsRGB ? this.fgRGB & 0xff : this.fgIndex;
465
+ const word0Base = (this.fgIsRGB ? 1 << 21 : 0) | (this.bgIsRGB ? 1 << 22 : 0) | ((fgVal & 0xff) << 23);
466
+ const word1 = ((this.bgIsRGB ? this.bgRGB & 0xff : this.bgIndex) & 0xff) | ((this.attrs & 0xff) << 8);
467
+ let cachedRow = cursor.row;
468
+ let cachedRowStart = grid.rowStart(cachedRow);
469
+ let cellIdx = cachedRowStart + cursor.col * CELL_SIZE;
470
+ const lastCol = gridCols - 1;
471
+ let lastCp = 0;
472
+ let j = i;
473
+ while (j < len) {
474
+ const b = data[j];
475
+ let cp;
476
+ // ASCII (single byte) — most common, check first
477
+ if (b >= 0x20 && b <= 0x7e) {
478
+ cp = b;
479
+ }
480
+ // UTF-8 2-byte (0xC0-0xDF)
481
+ else if (b >= 0xc0 && b < 0xe0) {
482
+ if (j + 1 >= len || (data[j + 1] & 0xc0) !== 0x80)
483
+ break;
484
+ cp = ((b & 0x1f) << 6) | (data[j + 1] & 0x3f);
485
+ j += 1;
486
+ }
487
+ // UTF-8 3-byte (0xE0-0xEF)
488
+ else if (b >= 0xe0 && b < 0xf0) {
489
+ if (j + 2 >= len || (data[j + 1] & 0xc0) !== 0x80 || (data[j + 2] & 0xc0) !== 0x80)
490
+ break;
491
+ cp = ((b & 0x0f) << 12) | ((data[j + 1] & 0x3f) << 6) | (data[j + 2] & 0x3f);
492
+ j += 2;
493
+ }
494
+ // UTF-8 4-byte (0xF0-0xF7)
495
+ else if (b >= 0xf0 && b <= 0xf7) {
496
+ if (j + 3 >= len ||
497
+ (data[j + 1] & 0xc0) !== 0x80 ||
498
+ (data[j + 2] & 0xc0) !== 0x80 ||
499
+ (data[j + 3] & 0xc0) !== 0x80)
500
+ break;
501
+ cp =
502
+ ((b & 0x07) << 18) |
503
+ ((data[j + 1] & 0x3f) << 12) |
504
+ ((data[j + 2] & 0x3f) << 6) |
505
+ (data[j + 3] & 0x3f);
506
+ j += 3;
507
+ }
508
+ // Non-printable byte — exit batch
509
+ else {
510
+ break;
511
+ }
512
+ // Inline cell write — duplicates printCodepoint() for throughput.
513
+ // If you change wrap/cell-write/cursor logic here, update printCodepoint() too.
514
+ if (cursor.wrapPending) {
515
+ cursor.wrapPending = false;
516
+ if (this.autoWrapMode) {
517
+ grid.markDirty(cachedRow);
518
+ cursor.col = 0;
519
+ cursor.row++;
520
+ if (cursor.row > buf.scrollBottom) {
521
+ cursor.row = buf.scrollBottom;
522
+ this._scrollUpFull();
523
+ }
524
+ cachedRow = cursor.row;
525
+ cachedRowStart = grid.rowStart(cachedRow);
526
+ cellIdx = cachedRowStart;
527
+ }
528
+ }
529
+ gridData[cellIdx] = (cp & 0x1fffff) | word0Base;
530
+ gridData[cellIdx + 1] = word1;
531
+ if (this.fgIsRGB)
532
+ grid.rgbColors[cursor.col] = this.fgRGB;
533
+ if (this.bgIsRGB)
534
+ grid.rgbColors[256 + cursor.col] = this.bgRGB;
535
+ if (cursor.col >= lastCol) {
536
+ cursor.wrapPending = true;
537
+ }
538
+ else {
539
+ cursor.col++;
540
+ cellIdx += CELL_SIZE;
541
+ }
542
+ lastCp = cp;
543
+ j++;
544
+ }
545
+ if (j > i)
546
+ grid.markDirty(cachedRow);
547
+ if (lastCp > 0)
548
+ this.lastPrintedCodepoint = lastCp;
549
+ // Handle incomplete UTF-8 at end of buffer
550
+ if (j < len && data[j] >= 0xc0 && data[j] <= 0xf7) {
551
+ const b = data[j];
552
+ if (b < 0xe0) {
553
+ utf8Bytes = 1;
554
+ utf8Codepoint = b & 0x1f;
555
+ }
556
+ else if (b < 0xf0) {
557
+ utf8Bytes = 2;
558
+ utf8Codepoint = b & 0x0f;
559
+ }
560
+ else {
561
+ utf8Bytes = 3;
562
+ utf8Codepoint = b & 0x07;
563
+ }
564
+ i = j;
565
+ continue;
566
+ }
567
+ i = j - 1;
568
+ continue;
569
+ }
570
+ const packed = TABLE[state * 256 + byte];
571
+ const action = packed >>> 4;
572
+ state = packed & 0x0f;
573
+ // Inlined action dispatch — eliminates performAction() call overhead
574
+ // and adds read-ahead loops for PARAM, OSC_PUT, and DCS PUT.
575
+ switch (action) {
576
+ case Action.PRINT:
577
+ this.printCodepoint(byte);
578
+ break;
579
+ case Action.EXECUTE:
580
+ this.execute(byte);
581
+ break;
582
+ case Action.COLLECT:
583
+ if (byte === 0x3f || byte === 0x3e || byte === 0x3d || byte === 0x3c) {
584
+ this.prefixByte = byte;
585
+ }
586
+ else {
587
+ this.intermediatesByte = byte;
588
+ }
589
+ break;
590
+ case Action.PARAM: {
591
+ // Read-ahead: consume all consecutive param bytes (digits 0x30-0x39
592
+ // and semicolons 0x3B) in a tight loop. For a typical CSI like
593
+ // \x1b[38;2;128;64;32m this reduces 10 table lookups + 10 function
594
+ // calls to 1 table lookup + 1 tight loop.
595
+ // NOTE: param parsing logic is duplicated in the ESC [ read-ahead
596
+ // above — keep both in sync when changing param handling (e.g. subparams).
597
+ let j = i;
598
+ do {
599
+ const b = data[j];
600
+ if (b === 0x3b) {
601
+ if (this.paramCount < VTParser.MAX_PARAMS) {
602
+ this.params[this.paramCount++] = this.hasParam ? this.currentParam : 0;
603
+ }
604
+ this.currentParam = 0;
605
+ this.hasParam = false;
606
+ }
607
+ else {
608
+ if (this.currentParam <= 99999) {
609
+ this.currentParam = this.currentParam * 10 + (b - 0x30);
610
+ }
611
+ this.hasParam = true;
612
+ }
613
+ } while (++j < len && ((data[j] >= 0x30 && data[j] <= 0x39) || data[j] === 0x3b));
614
+ // Peek: if next byte is a CSI final (0x40-0x7E), dispatch directly
615
+ // without returning to the table for one more lookup.
616
+ if (state === State.CSI_PARAM && j < len) {
617
+ const fb = data[j];
618
+ if (fb >= 0x40 && fb <= 0x7e) {
619
+ this.csiDispatch(fb);
620
+ state = State.GROUND;
621
+ i = j;
622
+ break;
623
+ }
624
+ }
625
+ i = j - 1;
626
+ break;
627
+ }
628
+ case Action.ESC_DISPATCH:
629
+ this.escDispatch(byte);
630
+ break;
631
+ case Action.CSI_DISPATCH:
632
+ this.csiDispatch(byte);
633
+ break;
634
+ case Action.PUT:
635
+ // DCS passthrough read-ahead — collect data bytes into the DCS buffer.
636
+ // Cap at MAX_DCS_LENGTH to prevent unbounded growth on malformed input.
637
+ if (this.dcsLength < VTParser.MAX_DCS_LENGTH) {
638
+ this.dcsParts[this.dcsLength++] = byte;
639
+ }
640
+ while (i + 1 < len && data[i + 1] >= 0x20 && data[i + 1] <= 0x7e) {
641
+ i++;
642
+ if (this.dcsLength < VTParser.MAX_DCS_LENGTH) {
643
+ this.dcsParts[this.dcsLength++] = data[i];
644
+ }
645
+ }
646
+ // Check for tmux prefix once we have at least 4 bytes ("mux;")
647
+ if (this.tmuxSeeking && this.dcsLength >= 4) {
648
+ this.tmuxSeeking = false;
649
+ if (this.dcsParts[0] === 0x6d && // 'm'
650
+ this.dcsParts[1] === 0x75 && // 'u'
651
+ this.dcsParts[2] === 0x78 && // 'x'
652
+ this.dcsParts[3] === 0x3b // ';'
653
+ ) {
654
+ this.inTmuxDcs = true;
655
+ this.tmuxLength = 0;
656
+ this.tmuxPrevWasEsc = false;
657
+ }
658
+ }
659
+ break;
660
+ case Action.OSC_START:
661
+ this.oscLength = 0;
662
+ break;
663
+ case Action.OSC_PUT: {
664
+ // Read-ahead: consume all consecutive OSC content bytes (0x20-0x7E)
665
+ // Cap at MAX_OSC_LENGTH to prevent unbounded growth on malformed input.
666
+ if (this.oscLength < VTParser.MAX_OSC_LENGTH) {
667
+ this.oscParts[this.oscLength++] = byte;
668
+ }
669
+ while (i + 1 < len && data[i + 1] >= 0x20 && data[i + 1] <= 0x7e) {
670
+ i++;
671
+ if (this.oscLength < VTParser.MAX_OSC_LENGTH) {
672
+ this.oscParts[this.oscLength++] = data[i];
673
+ }
674
+ }
675
+ break;
676
+ }
677
+ case Action.OSC_END:
678
+ this.oscDispatch();
679
+ break;
680
+ case Action.CLEAR:
681
+ this.clear();
682
+ break;
683
+ case Action.HOOK:
684
+ // DCS HOOK: the final byte has been received; snapshot params and
685
+ // record the final byte, then reset the data buffer for collection.
686
+ this.finalizeParams();
687
+ this.dcsFinal = byte;
688
+ this.dcsLength = 0;
689
+ // Start tmux-seeking if final byte is 't' (0x74) — we'll verify the
690
+ // "mux;" prefix as PUT bytes arrive.
691
+ this.tmuxSeeking = byte === 0x74;
692
+ this.inTmuxDcs = false;
693
+ this.tmuxLength = 0;
694
+ this.tmuxPrevWasEsc = false;
695
+ break;
696
+ case Action.UNHOOK:
697
+ // DCS UNHOOK: ST (C1 0x9c) terminated the DCS sequence.
698
+ this.dcsDispatch();
699
+ break;
700
+ // NONE, IGNORE — no-op
701
+ }
702
+ }
703
+ // Write back locals
704
+ this.state = state;
705
+ this.utf8Bytes = utf8Bytes;
706
+ this.utf8Codepoint = utf8Codepoint;
707
+ }
708
+ // Called from: UTF-8 split-write continuation, Action.PRINT fallback, CSI REP.
709
+ // The combined batch in write() inlines this logic for throughput — keep in sync.
710
+ printCodepoint(cp) {
711
+ const buf = this.bufferSet.active;
712
+ const cursor = buf.cursor;
713
+ const grid = buf.grid;
714
+ // Resolve pending wrap from a previous print at the last column
715
+ if (cursor.wrapPending) {
716
+ cursor.wrapPending = false;
717
+ if (this.autoWrapMode) {
718
+ cursor.col = 0;
719
+ cursor.row++;
720
+ if (cursor.row > buf.scrollBottom) {
721
+ cursor.row = buf.scrollBottom;
722
+ this._scrollUpFull();
723
+ }
724
+ }
725
+ }
726
+ // Safety clamp (should not be needed in normal operation)
727
+ if (cursor.col >= this.cols) {
728
+ cursor.col = this.cols - 1;
729
+ }
730
+ // Inline cell write — avoids setCell() function call overhead.
731
+ const idx = grid.rowStart(cursor.row) + cursor.col * CELL_SIZE;
732
+ const fgVal = this.fgIsRGB ? this.fgRGB & 0xff : this.fgIndex;
733
+ grid.data[idx] =
734
+ (cp & 0x1fffff) |
735
+ (this.fgIsRGB ? 1 << 21 : 0) |
736
+ (this.bgIsRGB ? 1 << 22 : 0) |
737
+ ((fgVal & 0xff) << 23);
738
+ grid.data[idx + 1] =
739
+ ((this.bgIsRGB ? this.bgRGB & 0xff : this.bgIndex) & 0xff) | ((this.attrs & 0xff) << 8);
740
+ grid.markDirty(cursor.row);
741
+ // Store full RGB values in the rgbColors lookup if using RGB
742
+ if (this.fgIsRGB) {
743
+ grid.rgbColors[cursor.col] = this.fgRGB;
744
+ }
745
+ if (this.bgIsRGB) {
746
+ grid.rgbColors[256 + cursor.col] = this.bgRGB;
747
+ }
748
+ this.lastPrintedCodepoint = cp;
749
+ // Advance cursor; if at last column, defer wrap
750
+ if (cursor.col >= this.cols - 1) {
751
+ cursor.wrapPending = true;
752
+ }
753
+ else {
754
+ cursor.col++;
755
+ }
756
+ }
757
+ execute(byte) {
758
+ const cursor = this.buf.cursor;
759
+ cursor.wrapPending = false;
760
+ switch (byte) {
761
+ case 0x07: // BEL
762
+ break;
763
+ case 0x08: // BS
764
+ if (cursor.col > 0)
765
+ cursor.col--;
766
+ break;
767
+ case 0x09: // HT (tab)
768
+ cursor.col = this.buf.nextTabStop(cursor.col);
769
+ break;
770
+ case 0x0a: // LF
771
+ case 0x0b: // VT
772
+ case 0x0c: // FF
773
+ if (this.lineFeedMode) {
774
+ cursor.col = 0;
775
+ }
776
+ this.linefeed();
777
+ break;
778
+ case 0x0d: // CR
779
+ cursor.col = 0;
780
+ break;
781
+ case 0x0e: // SO (shift out)
782
+ case 0x0f: // SI (shift in)
783
+ // Character set switching - not implemented
784
+ break;
785
+ }
786
+ }
787
+ linefeed() {
788
+ const cursor = this.buf.cursor;
789
+ if (cursor.row === this.buf.scrollBottom) {
790
+ this._scrollUpFull();
791
+ }
792
+ else if (cursor.row < this.rows - 1) {
793
+ cursor.row++;
794
+ }
795
+ }
796
+ /**
797
+ * Fast-path scroll up: combines scrollUpWithHistory + scrollUp for the
798
+ * common full-screen case (rotateUp instead of copyWithin). Falls back
799
+ * to scrollUpWithHistory for partial scroll regions.
800
+ */
801
+ _scrollUpFull() {
802
+ const buf = this.bufferSet.active;
803
+ if (buf.scrollTop === 0 && buf.scrollBottom === this.rows - 1) {
804
+ const grid = buf.grid;
805
+ if (this.bufferSet.maxScrollback > 0 && buf === this.bufferSet.normal) {
806
+ this.bufferSet.pushScrollback(grid.copyRow(0));
807
+ }
808
+ grid.rotateUp();
809
+ grid.clearRowRaw(buf.scrollBottom);
810
+ grid.markDirtyRange(buf.scrollTop, buf.scrollBottom);
811
+ }
812
+ else {
813
+ this.bufferSet.scrollUpWithHistory();
814
+ }
815
+ }
816
+ clear() {
817
+ this.paramCount = 0;
818
+ this.currentParam = 0;
819
+ this.hasParam = false;
820
+ this.intermediatesByte = 0;
821
+ this.prefixByte = 0;
822
+ }
823
+ finalizeParams() {
824
+ if (this.hasParam || this.paramCount > 0) {
825
+ if (this.paramCount < VTParser.MAX_PARAMS) {
826
+ this.params[this.paramCount++] = this.hasParam ? this.currentParam : 0;
827
+ }
828
+ }
829
+ return this.paramCount;
830
+ }
831
+ csiDispatch(finalByte, preFinalized) {
832
+ const paramCount = preFinalized ?? this.finalizeParams();
833
+ const p0 = paramCount > 0 ? this.params[0] : 0;
834
+ const p1 = paramCount > 1 ? this.params[1] : 0;
835
+ const buf = this.bufferSet.active;
836
+ if (this.prefixByte === 0x3f) {
837
+ // '?' — private modes
838
+ this.csiPrivate(finalByte, paramCount);
839
+ return;
840
+ }
841
+ if (this.prefixByte === 0x3e) {
842
+ // '>' — Secondary Device Attributes and Kitty push-flags
843
+ if (finalByte === 0x63 /* 'c' */ && (p0 || 0) === 0) {
844
+ const response = new TextEncoder().encode("\x1b[>0;277;0c");
845
+ this.responseBuffer.push(response);
846
+ }
847
+ else if (finalByte === 0x75 /* 'u' */) {
848
+ // CSI > flags u — push current flags onto stack, then set new flags
849
+ this.kittyFlagsStack.push(this.kittyFlags);
850
+ if (this.kittyFlagsStack.length > 99)
851
+ this.kittyFlagsStack.shift();
852
+ if (p0 !== 0) {
853
+ this.kittyFlags = p0;
854
+ this.onKittyFlags?.(this.kittyFlags);
855
+ }
856
+ }
857
+ return;
858
+ }
859
+ if (this.prefixByte === 0x3c) {
860
+ // '<' — Kitty pop-flags
861
+ if (finalByte === 0x75 /* 'u' */) {
862
+ // CSI < n u — pop n entries from the stack (default 1)
863
+ const n = p0 || 1;
864
+ for (let i = 0; i < n; i++) {
865
+ if (this.kittyFlagsStack.length > 0) {
866
+ this.kittyFlags = this.kittyFlagsStack.pop() ?? 0;
867
+ }
868
+ }
869
+ this.onKittyFlags?.(this.kittyFlags);
870
+ }
871
+ return;
872
+ }
873
+ if (this.prefixByte === 0x3d) {
874
+ // '=' — Kitty set-flags
875
+ if (finalByte === 0x75 /* 'u' */) {
876
+ // CSI = flags ; mode u — set keyboard flags
877
+ // mode: 1=set (default), 2=or, 3=and, 4=xor
878
+ const mode = p1 || 1;
879
+ switch (mode) {
880
+ case 1:
881
+ this.kittyFlags = p0;
882
+ break;
883
+ case 2:
884
+ this.kittyFlags |= p0;
885
+ break;
886
+ case 3:
887
+ this.kittyFlags &= p0;
888
+ break;
889
+ case 4:
890
+ this.kittyFlags ^= p0;
891
+ break;
892
+ }
893
+ this.onKittyFlags?.(this.kittyFlags);
894
+ }
895
+ return;
896
+ }
897
+ // Handle intermediates for special sequences
898
+ if (this.intermediatesByte === 0x20) {
899
+ // ' '
900
+ if (finalByte === 0x71 /* 'q' */) {
901
+ // DECSCUSR - Set Cursor Style
902
+ this.setCursorStyle(p0 || 0);
903
+ }
904
+ return;
905
+ }
906
+ if (this.intermediatesByte === 0x21) {
907
+ // '!'
908
+ if (finalByte === 0x70 /* 'p' */) {
909
+ // DECSTR - Soft Terminal Reset
910
+ this.softReset();
911
+ }
912
+ return;
913
+ }
914
+ switch (finalByte) {
915
+ case 0x41: // 'A' - CUU - Cursor Up
916
+ this.cursorUp(p0 || 1);
917
+ break;
918
+ case 0x42: // 'B' - CUD - Cursor Down
919
+ this.cursorDown(p0 || 1);
920
+ break;
921
+ case 0x43: // 'C' - CUF - Cursor Forward
922
+ this.cursorForward(p0 || 1);
923
+ break;
924
+ case 0x44: // 'D' - CUB - Cursor Backward
925
+ this.cursorBackward(p0 || 1);
926
+ break;
927
+ case 0x45: // 'E' - CNL - Cursor Next Line
928
+ buf.cursor.col = 0;
929
+ this.cursorDown(p0 || 1);
930
+ break;
931
+ case 0x46: // 'F' - CPL - Cursor Previous Line
932
+ buf.cursor.col = 0;
933
+ this.cursorUp(p0 || 1);
934
+ break;
935
+ case 0x47: // 'G' - CHA - Cursor Horizontal Absolute
936
+ buf.cursor.wrapPending = false;
937
+ buf.cursor.col = Math.min((p0 || 1) - 1, this.cols - 1);
938
+ break;
939
+ case 0x48: // 'H' - CUP - Cursor Position
940
+ case 0x66: // 'f' - HVP - Horizontal Vertical Position
941
+ this.cursorPosition(p0 || 1, p1 || 1);
942
+ break;
943
+ case 0x49: // 'I' - CHT - Cursor Forward Tab
944
+ buf.cursor.wrapPending = false;
945
+ for (let t = 0; t < (p0 || 1); t++) {
946
+ buf.cursor.col = buf.nextTabStop(buf.cursor.col);
947
+ }
948
+ break;
949
+ case 0x4a: // 'J' - ED - Erase in Display
950
+ buf.cursor.wrapPending = false;
951
+ this.eraseInDisplay(p0 || 0);
952
+ break;
953
+ case 0x4b: // 'K' - EL - Erase in Line
954
+ buf.cursor.wrapPending = false;
955
+ this.eraseInLine(p0 || 0);
956
+ break;
957
+ case 0x4c: // 'L' - IL - Insert Lines
958
+ buf.cursor.wrapPending = false;
959
+ this.insertLines(p0 || 1);
960
+ break;
961
+ case 0x4d: // 'M' - DL - Delete Lines
962
+ buf.cursor.wrapPending = false;
963
+ this.deleteLines(p0 || 1);
964
+ break;
965
+ case 0x50: // 'P' - DCH - Delete Characters
966
+ buf.cursor.wrapPending = false;
967
+ this.deleteChars(p0 || 1);
968
+ break;
969
+ case 0x53: // 'S' - SU - Scroll Up
970
+ for (let i = 0; i < (p0 || 1); i++) {
971
+ buf.scrollUp();
972
+ }
973
+ break;
974
+ case 0x54: // 'T' - SD - Scroll Down
975
+ for (let i = 0; i < (p0 || 1); i++) {
976
+ buf.scrollDown();
977
+ }
978
+ break;
979
+ case 0x5a: // 'Z' - CBT - Cursor Backward Tab
980
+ buf.cursor.wrapPending = false;
981
+ for (let t = 0; t < (p0 || 1); t++) {
982
+ buf.cursor.col = buf.prevTabStop(buf.cursor.col);
983
+ }
984
+ break;
985
+ case 0x60: // '`' - HPA - Horizontal Position Absolute
986
+ buf.cursor.wrapPending = false;
987
+ buf.cursor.col = Math.min((p0 || 1) - 1, this.cols - 1);
988
+ break;
989
+ case 0x61: // 'a' - HPR - Horizontal Position Relative
990
+ this.cursorForward(p0 || 1);
991
+ break;
992
+ case 0x62: // 'b' - REP - Repeat Preceding Character
993
+ if (this.lastPrintedCodepoint > 0) {
994
+ // Clamp to one full screen to prevent DoS from large repeat counts
995
+ const count = Math.min(p0 || 1, this.cols * this.rows);
996
+ for (let i = 0; i < count; i++) {
997
+ this.printCodepoint(this.lastPrintedCodepoint);
998
+ }
999
+ }
1000
+ break;
1001
+ case 0x63: // 'c' - DA - Primary Device Attributes
1002
+ if ((p0 || 0) === 0) {
1003
+ this.reportDeviceAttributes();
1004
+ }
1005
+ break;
1006
+ case 0x64: // 'd' - VPA - Line Position Absolute
1007
+ buf.cursor.wrapPending = false;
1008
+ buf.cursor.row = Math.min(Math.max((p0 || 1) - 1, 0), this.rows - 1);
1009
+ break;
1010
+ case 0x65: // 'e' - VPR - Vertical Position Relative
1011
+ this.cursorDown(p0 || 1);
1012
+ break;
1013
+ case 0x68: // 'h' - SM - Set Mode
1014
+ this.setMode(paramCount);
1015
+ break;
1016
+ case 0x6c: // 'l' - RM - Reset Mode
1017
+ this.resetMode(paramCount);
1018
+ break;
1019
+ case 0x6d: // 'm' - SGR - Select Graphic Rendition
1020
+ this.sgr(paramCount);
1021
+ break;
1022
+ case 0x6e: // 'n' - DSR - Device Status Report
1023
+ if (p0 === 6) {
1024
+ this.reportCursorPosition();
1025
+ }
1026
+ break;
1027
+ case 0x72: // 'r' - DECSTBM - Set Top and Bottom Margins
1028
+ this.setScrollRegion(p0 || 1, p1 || this.rows);
1029
+ break;
1030
+ case 0x73: // 's' - SCP - Save Cursor Position
1031
+ buf.saveCursor();
1032
+ break;
1033
+ case 0x74: // 't' - Window manipulation
1034
+ this.windowManipulation(paramCount);
1035
+ break;
1036
+ case 0x75: // 'u' - RCP - Restore Cursor Position
1037
+ buf.restoreCursor();
1038
+ break;
1039
+ case 0x40: // '@' - ICH - Insert Characters
1040
+ buf.cursor.wrapPending = false;
1041
+ this.insertChars(p0 || 1);
1042
+ break;
1043
+ case 0x58: // 'X' - ECH - Erase Characters
1044
+ buf.cursor.wrapPending = false;
1045
+ this.eraseChars(p0 || 1);
1046
+ break;
1047
+ case 0x67: // 'g' - TBC - Tab Clear
1048
+ if ((p0 || 0) === 0) {
1049
+ buf.tabStops.delete(buf.cursor.col);
1050
+ }
1051
+ else if (p0 === 3) {
1052
+ buf.tabStops.clear();
1053
+ }
1054
+ break;
1055
+ }
1056
+ }
1057
+ csiPrivate(finalByte, paramCount) {
1058
+ switch (finalByte) {
1059
+ case 0x68: // 'h' - DECSET
1060
+ for (let i = 0; i < paramCount; i++) {
1061
+ this.decset(this.params[i], true);
1062
+ }
1063
+ break;
1064
+ case 0x6c: // 'l' - DECRST
1065
+ for (let i = 0; i < paramCount; i++) {
1066
+ this.decset(this.params[i], false);
1067
+ }
1068
+ break;
1069
+ case 0x6e: // 'n' - DECDSR
1070
+ if (this.params[0] === 6) {
1071
+ this.reportCursorPosition();
1072
+ }
1073
+ break;
1074
+ case 0x75: // 'u' - Kitty keyboard flags query
1075
+ // CSI ? u — respond with current flags: \x1b[?{flags}u
1076
+ {
1077
+ const flagStr = `\x1b[?${this.kittyFlags}u`;
1078
+ this.responseBuffer.push(new TextEncoder().encode(flagStr));
1079
+ }
1080
+ break;
1081
+ }
1082
+ }
1083
+ decset(mode, on) {
1084
+ switch (mode) {
1085
+ case 1: // DECCKM - Application Cursor Keys
1086
+ this.applicationCursorKeys = on;
1087
+ break;
1088
+ case 6: // DECOM - Origin Mode
1089
+ this.originMode = on;
1090
+ // When origin mode changes, cursor goes to home
1091
+ if (on) {
1092
+ this.buf.cursor.row = this.buf.scrollTop;
1093
+ }
1094
+ else {
1095
+ this.buf.cursor.row = 0;
1096
+ }
1097
+ this.buf.cursor.col = 0;
1098
+ break;
1099
+ case 7: // DECAWM - Auto-wrap mode
1100
+ this.autoWrapMode = on;
1101
+ break;
1102
+ case 9: // X10 mouse reporting
1103
+ this.mouseProtocol = on ? "x10" : "none";
1104
+ break;
1105
+ case 12: // Cursor blink (att610)
1106
+ // Tracked but cosmetic — cursor blink handled in renderer
1107
+ break;
1108
+ case 25: // DECTCEM - cursor visibility
1109
+ this.buf.cursor.visible = on;
1110
+ break;
1111
+ case 47: // Alternate screen buffer (no save/restore)
1112
+ case 1047:
1113
+ if (on) {
1114
+ this.bufferSet.activateAlternate();
1115
+ }
1116
+ else {
1117
+ this.bufferSet.activateNormal();
1118
+ }
1119
+ break;
1120
+ case 1000: // VT200 mouse (click only)
1121
+ this.mouseProtocol = on ? "vt200" : "none";
1122
+ break;
1123
+ case 1002: // Button event tracking (click + drag)
1124
+ this.mouseProtocol = on ? "drag" : "none";
1125
+ break;
1126
+ case 1003: // Any event tracking (all motion)
1127
+ this.mouseProtocol = on ? "any" : "none";
1128
+ break;
1129
+ case 1004: // Send focus events
1130
+ this.sendFocusEvents = on;
1131
+ break;
1132
+ case 1006: // SGR mouse encoding
1133
+ this.mouseEncoding = on ? "sgr" : "default";
1134
+ break;
1135
+ case 1048: // Save/restore cursor (standalone)
1136
+ if (on) {
1137
+ this.buf.saveCursor();
1138
+ }
1139
+ else {
1140
+ this.buf.restoreCursor();
1141
+ }
1142
+ break;
1143
+ case 1049: // Alternate screen buffer with save/restore cursor
1144
+ if (on) {
1145
+ this.bufferSet.normal.saveCursor();
1146
+ this.bufferSet.activateAlternate();
1147
+ }
1148
+ else {
1149
+ this.bufferSet.activateNormal();
1150
+ this.bufferSet.normal.restoreCursor();
1151
+ }
1152
+ break;
1153
+ case 2004: // Bracketed paste mode
1154
+ this.bracketedPasteMode = on;
1155
+ break;
1156
+ case 2026: // Synchronized output — render gating
1157
+ this.syncedOutput = on;
1158
+ this.onSyncOutput?.(on);
1159
+ break;
1160
+ }
1161
+ }
1162
+ /** SM - Set Mode (non-private) */
1163
+ setMode(paramCount) {
1164
+ for (let i = 0; i < paramCount; i++) {
1165
+ if (this.params[i] === 20) {
1166
+ this.lineFeedMode = true;
1167
+ }
1168
+ }
1169
+ }
1170
+ /** RM - Reset Mode (non-private) */
1171
+ resetMode(paramCount) {
1172
+ for (let i = 0; i < paramCount; i++) {
1173
+ if (this.params[i] === 20) {
1174
+ this.lineFeedMode = false;
1175
+ }
1176
+ }
1177
+ }
1178
+ escDispatch(byte) {
1179
+ const buf = this.bufferSet.active;
1180
+ // Handle ESC # sequences (intermediates contain '#')
1181
+ if (this.intermediatesByte === 0x23) {
1182
+ if (byte === 0x38) {
1183
+ // '8' — DECALN: fill screen with 'E'
1184
+ const grid = buf.grid;
1185
+ for (let r = 0; r < this.rows; r++) {
1186
+ for (let c = 0; c < this.cols; c++) {
1187
+ grid.setCell(r, c, 0x45, 7, 0, 0);
1188
+ }
1189
+ }
1190
+ }
1191
+ return;
1192
+ }
1193
+ // Handle ESC ( / ESC ) for character set designation
1194
+ if (this.intermediatesByte === 0x28 || this.intermediatesByte === 0x29) {
1195
+ return;
1196
+ }
1197
+ switch (byte) {
1198
+ case 0x37: // '7' - DECSC - Save Cursor
1199
+ buf.saveCursor();
1200
+ break;
1201
+ case 0x38: // '8' - DECRC - Restore Cursor
1202
+ buf.restoreCursor();
1203
+ break;
1204
+ case 0x3d: // '=' - DECKPAM - Application Keypad Mode
1205
+ this.applicationKeypad = true;
1206
+ break;
1207
+ case 0x3e: // '>' - DECKPNM - Normal Keypad Mode
1208
+ this.applicationKeypad = false;
1209
+ break;
1210
+ case 0x44: // 'D' - IND - Index
1211
+ buf.cursor.wrapPending = false;
1212
+ this.linefeed();
1213
+ break;
1214
+ case 0x45: // 'E' - NEL - Next Line
1215
+ buf.cursor.wrapPending = false;
1216
+ buf.cursor.col = 0;
1217
+ this.linefeed();
1218
+ break;
1219
+ case 0x4d: // 'M' - RI - Reverse Index
1220
+ buf.cursor.wrapPending = false;
1221
+ if (buf.cursor.row === buf.scrollTop) {
1222
+ buf.scrollDown();
1223
+ }
1224
+ else if (buf.cursor.row > 0) {
1225
+ buf.cursor.row--;
1226
+ }
1227
+ break;
1228
+ case 0x63: // 'c' - RIS - Full Reset
1229
+ this.fullReset();
1230
+ break;
1231
+ case 0x48: // 'H' - HTS - Horizontal Tab Set
1232
+ buf.tabStops.add(buf.cursor.col);
1233
+ break;
1234
+ }
1235
+ }
1236
+ // ---- DCS dispatch ----
1237
+ dcsDispatch() {
1238
+ // Reset tmux-seeking state on any DCS dispatch (sequence ended)
1239
+ this.tmuxSeeking = false;
1240
+ this.inTmuxDcs = false;
1241
+ if (!this.onDcs)
1242
+ return;
1243
+ // Build the data string from collected passthrough bytes
1244
+ let data = "";
1245
+ for (let i = 0; i < this.dcsLength; i++) {
1246
+ data += String.fromCharCode(this.dcsParts[i]);
1247
+ }
1248
+ // Build the params snapshot (already finalized by finalizeParams() in HOOK)
1249
+ const params = [];
1250
+ for (let i = 0; i < this.paramCount; i++) {
1251
+ params.push(this.params[i]);
1252
+ }
1253
+ this.onDcs(this.dcsFinal, params, this.intermediatesByte, data);
1254
+ }
1255
+ // ---- DCS tmux passthrough dispatch ----
1256
+ dispatchTmux() {
1257
+ if (this.tmuxDepth > 0)
1258
+ return; // guard against recursive tmux passthrough
1259
+ const len = this.tmuxLength;
1260
+ if (len === 0)
1261
+ return;
1262
+ // Build the inner sequence string for the callback
1263
+ let inner = "";
1264
+ for (let i = 0; i < len; i++) {
1265
+ inner += String.fromCharCode(this.tmuxParts[i]);
1266
+ }
1267
+ if (this.onDcsTmux) {
1268
+ this.onDcsTmux(inner);
1269
+ }
1270
+ // Re-process the decoded inner sequence through the parser
1271
+ this.tmuxDepth++;
1272
+ this.write(this.tmuxParts.subarray(0, len));
1273
+ this.tmuxDepth--;
1274
+ this.tmuxLength = 0;
1275
+ }
1276
+ // ---- OSC dispatch ----
1277
+ oscDispatch() {
1278
+ // Find ';' separator in the numeric parts buffer
1279
+ let semiIdx = -1;
1280
+ for (let i = 0; i < this.oscLength; i++) {
1281
+ if (this.oscParts[i] === 0x3b) {
1282
+ semiIdx = i;
1283
+ break;
1284
+ }
1285
+ }
1286
+ // OSC 104 with no arguments (no semicolon) = reset the entire palette.
1287
+ if (semiIdx === -1) {
1288
+ if (this.onOsc104) {
1289
+ let c = 0;
1290
+ for (let i = 0; i < this.oscLength; i++) {
1291
+ c = c * 10 + (this.oscParts[i] - 0x30);
1292
+ }
1293
+ if (c === 104)
1294
+ this.onOsc104(-1);
1295
+ }
1296
+ return;
1297
+ }
1298
+ // Parse the code (digits before semicolon)
1299
+ let code = 0;
1300
+ for (let i = 0; i < semiIdx; i++) {
1301
+ code = code * 10 + (this.oscParts[i] - 0x30);
1302
+ }
1303
+ switch (code) {
1304
+ case 0: // Set icon name + window title
1305
+ case 1: // Set icon name
1306
+ case 2: // Set window title
1307
+ if (this.onTitleChange) {
1308
+ // Build string only when callback exists
1309
+ let data = "";
1310
+ for (let i = semiIdx + 1; i < this.oscLength; i++) {
1311
+ const ch = this.oscParts[i];
1312
+ // Strip control characters (C0/C1)
1313
+ if (ch >= 0x20 && ch < 0x7f) {
1314
+ data += String.fromCharCode(ch);
1315
+ }
1316
+ }
1317
+ this.onTitleChange(data);
1318
+ }
1319
+ break;
1320
+ case 4: // OSC 4 — set/query color palette entries
1321
+ // Format: 4;<index>;<spec>[;<index>;<spec>...] (pairs separated by ';')
1322
+ // spec is a color string (e.g. "rgb:ff/00/00", "#rrggbb") or "?" for a query.
1323
+ if (this.onOsc4) {
1324
+ // Walk pairs starting at semiIdx+1
1325
+ let pos = semiIdx + 1;
1326
+ while (pos < this.oscLength) {
1327
+ // Parse index (digits until ';')
1328
+ let nextSemi = -1;
1329
+ for (let i = pos; i < this.oscLength; i++) {
1330
+ if (this.oscParts[i] === 0x3b) {
1331
+ nextSemi = i;
1332
+ break;
1333
+ }
1334
+ }
1335
+ if (nextSemi === -1)
1336
+ break; // malformed: no spec separator
1337
+ let colorIdx = 0;
1338
+ for (let i = pos; i < nextSemi; i++) {
1339
+ colorIdx = colorIdx * 10 + (this.oscParts[i] - 0x30);
1340
+ }
1341
+ // Find end of spec (next ';' or end of OSC)
1342
+ let specEnd = this.oscLength;
1343
+ for (let i = nextSemi + 1; i < this.oscLength; i++) {
1344
+ if (this.oscParts[i] === 0x3b) {
1345
+ specEnd = i;
1346
+ break;
1347
+ }
1348
+ }
1349
+ // Build spec string
1350
+ const specLen = specEnd - nextSemi - 1;
1351
+ let spec;
1352
+ if (specLen === 1 && this.oscParts[nextSemi + 1] === 0x3f) {
1353
+ spec = null; // query
1354
+ }
1355
+ else {
1356
+ let s = "";
1357
+ for (let i = nextSemi + 1; i < specEnd; i++) {
1358
+ s += String.fromCharCode(this.oscParts[i]);
1359
+ }
1360
+ spec = s;
1361
+ }
1362
+ this.onOsc4(colorIdx, spec);
1363
+ pos = specEnd + 1; // advance past spec and trailing ';'
1364
+ }
1365
+ }
1366
+ break;
1367
+ case 7: // OSC 7 — current working directory (URI after semicolon)
1368
+ if (this.onOsc7) {
1369
+ let uri = "";
1370
+ for (let i = semiIdx + 1; i < this.oscLength; i++) {
1371
+ uri += String.fromCharCode(this.oscParts[i]);
1372
+ }
1373
+ this.onOsc7(uri);
1374
+ }
1375
+ break;
1376
+ case 52: // OSC 52 clipboard read/write
1377
+ if (this.onOsc52) {
1378
+ // Format: 52;<selection>;<base64-data> or 52;<selection>;?
1379
+ // Find second semicolon after semiIdx
1380
+ let semi2 = -1;
1381
+ for (let i = semiIdx + 1; i < this.oscLength; i++) {
1382
+ if (this.oscParts[i] === 0x3b) {
1383
+ semi2 = i;
1384
+ break;
1385
+ }
1386
+ }
1387
+ if (semi2 === -1)
1388
+ break;
1389
+ // Build selection string (bytes between first and second semicolons)
1390
+ let selection = "";
1391
+ for (let i = semiIdx + 1; i < semi2; i++) {
1392
+ selection += String.fromCharCode(this.oscParts[i]);
1393
+ }
1394
+ // Build data string (bytes after second semicolon)
1395
+ // A single "?" byte means query; otherwise it's base64 payload
1396
+ const payloadLen = this.oscLength - semi2 - 1;
1397
+ let osc52data = null;
1398
+ if (payloadLen === 1 && this.oscParts[semi2 + 1] === 0x3f) {
1399
+ // Query request
1400
+ osc52data = null;
1401
+ }
1402
+ else {
1403
+ let payload = "";
1404
+ for (let i = semi2 + 1; i < this.oscLength; i++) {
1405
+ payload += String.fromCharCode(this.oscParts[i]);
1406
+ }
1407
+ osc52data = payload;
1408
+ }
1409
+ this.onOsc52(selection, osc52data);
1410
+ }
1411
+ break;
1412
+ case 10: // OSC 10 — foreground color query/set
1413
+ case 11: // OSC 11 — background color query/set
1414
+ case 12: {
1415
+ // OSC 12 — cursor color query/set
1416
+ // Format: 10;<spec> or 10;? (query)
1417
+ const cb = code === 10 ? this.onOsc10 : code === 11 ? this.onOsc11 : this.onOsc12;
1418
+ if (cb) {
1419
+ const payloadStart = semiIdx + 1;
1420
+ const payloadLen = this.oscLength - payloadStart;
1421
+ let dynSpec;
1422
+ if (payloadLen === 1 && this.oscParts[payloadStart] === 0x3f) {
1423
+ dynSpec = null; // query
1424
+ }
1425
+ else {
1426
+ let s = "";
1427
+ for (let i = payloadStart; i < this.oscLength; i++) {
1428
+ s += String.fromCharCode(this.oscParts[i]);
1429
+ }
1430
+ dynSpec = s;
1431
+ }
1432
+ cb(dynSpec);
1433
+ }
1434
+ break;
1435
+ }
1436
+ // Other OSC codes can be added later
1437
+ case 8: // OSC 8 — hyperlinks
1438
+ // Format: 8;<params>;<uri>
1439
+ // <params> is optional colon-separated key=value metadata (may be "").
1440
+ // <uri> is the hyperlink target (empty string closes the link).
1441
+ if (this.onOsc8) {
1442
+ // Find the second semicolon (separating params from uri)
1443
+ let semi2 = -1;
1444
+ for (let i = semiIdx + 1; i < this.oscLength; i++) {
1445
+ if (this.oscParts[i] === 0x3b) {
1446
+ semi2 = i;
1447
+ break;
1448
+ }
1449
+ }
1450
+ if (semi2 === -1)
1451
+ break; // malformed: no URI separator
1452
+ // Build params string
1453
+ let osc8params = "";
1454
+ for (let i = semiIdx + 1; i < semi2; i++) {
1455
+ osc8params += String.fromCharCode(this.oscParts[i]);
1456
+ }
1457
+ // Build URI string
1458
+ let osc8uri = "";
1459
+ for (let i = semi2 + 1; i < this.oscLength; i++) {
1460
+ osc8uri += String.fromCharCode(this.oscParts[i]);
1461
+ }
1462
+ this.onOsc8(osc8params, osc8uri);
1463
+ }
1464
+ break;
1465
+ case 104: // OSC 104 — reset color palette entry/entries
1466
+ // Format: 104;<c1>;<c2>;... where c1..cN are 0-255 palette indices.
1467
+ // (The no-argument form is handled above in the semiIdx === -1 path.)
1468
+ if (this.onOsc104) {
1469
+ let pos = semiIdx + 1;
1470
+ while (pos < this.oscLength) {
1471
+ // Parse next decimal integer up to ';' or end
1472
+ let val = 0;
1473
+ let end = pos;
1474
+ while (end < this.oscLength && this.oscParts[end] !== 0x3b) {
1475
+ val = val * 10 + (this.oscParts[end] - 0x30);
1476
+ end++;
1477
+ }
1478
+ if (end > pos)
1479
+ this.onOsc104(val);
1480
+ pos = end + 1; // skip past the ';'
1481
+ }
1482
+ }
1483
+ break;
1484
+ case 133: // OSC 133 — shell integration / semantic prompts (FinalTerm protocol)
1485
+ // Format: 133;<type>[;<payload>]
1486
+ // <type> is a single ASCII letter: A (prompt start), B (command start),
1487
+ // C (command output start), D (command end), E (command text), P (property), …
1488
+ // <payload> is everything after the type letter and its optional trailing semicolon.
1489
+ if (this.onOsc133) {
1490
+ // First byte after the code semicolon is the type letter.
1491
+ const typeStart = semiIdx + 1;
1492
+ if (typeStart >= this.oscLength)
1493
+ break; // malformed: no type letter
1494
+ const typeLetter = String.fromCharCode(this.oscParts[typeStart]);
1495
+ // Payload is everything after an optional semicolon following the type letter.
1496
+ let osc133payload = "";
1497
+ if (typeStart + 1 < this.oscLength) {
1498
+ // Skip the separating semicolon if present.
1499
+ const payloadStart = this.oscParts[typeStart + 1] === 0x3b ? typeStart + 2 : typeStart + 1;
1500
+ for (let i = payloadStart; i < this.oscLength; i++) {
1501
+ osc133payload += String.fromCharCode(this.oscParts[i]);
1502
+ }
1503
+ }
1504
+ this.onOsc133(typeLetter, osc133payload);
1505
+ }
1506
+ break;
1507
+ }
1508
+ }
1509
+ // ---- Cursor movement ----
1510
+ cursorUp(n) {
1511
+ this.buf.cursor.wrapPending = false;
1512
+ this.buf.cursor.row = Math.max(this.buf.cursor.row - n, this.buf.scrollTop);
1513
+ }
1514
+ cursorDown(n) {
1515
+ this.buf.cursor.wrapPending = false;
1516
+ this.buf.cursor.row = Math.min(this.buf.cursor.row + n, this.buf.scrollBottom);
1517
+ }
1518
+ cursorForward(n) {
1519
+ this.buf.cursor.wrapPending = false;
1520
+ this.buf.cursor.col = Math.min(this.buf.cursor.col + n, this.cols - 1);
1521
+ }
1522
+ cursorBackward(n) {
1523
+ this.buf.cursor.wrapPending = false;
1524
+ this.buf.cursor.col = Math.max(this.buf.cursor.col - n, 0);
1525
+ }
1526
+ cursorPosition(row, col) {
1527
+ this.buf.cursor.wrapPending = false;
1528
+ if (this.originMode) {
1529
+ // In origin mode, coordinates are relative to scroll region
1530
+ this.buf.cursor.row = Math.min(Math.max(row - 1 + this.buf.scrollTop, this.buf.scrollTop), this.buf.scrollBottom);
1531
+ }
1532
+ else {
1533
+ this.buf.cursor.row = Math.min(Math.max(row - 1, 0), this.rows - 1);
1534
+ }
1535
+ this.buf.cursor.col = Math.min(Math.max(col - 1, 0), this.cols - 1);
1536
+ }
1537
+ // ---- Cursor style (DECSCUSR) ----
1538
+ setCursorStyle(ps) {
1539
+ switch (ps) {
1540
+ case 0:
1541
+ case 1: // blinking block
1542
+ case 2: // steady block
1543
+ this.buf.cursor.style = "block";
1544
+ break;
1545
+ case 3: // blinking underline
1546
+ case 4: // steady underline
1547
+ this.buf.cursor.style = "underline";
1548
+ break;
1549
+ case 5: // blinking bar
1550
+ case 6: // steady bar
1551
+ this.buf.cursor.style = "bar";
1552
+ break;
1553
+ }
1554
+ }
1555
+ // ---- Device Status Report (DSR) ----
1556
+ reportCursorPosition() {
1557
+ const row = this.buf.cursor.row + 1;
1558
+ const col = this.buf.cursor.col + 1;
1559
+ const response = new TextEncoder().encode(`\x1b[${row};${col}R`);
1560
+ this.responseBuffer.push(response);
1561
+ }
1562
+ // ---- Primary Device Attributes (DA) ----
1563
+ reportDeviceAttributes() {
1564
+ const response = new TextEncoder().encode("\x1b[?1;2c");
1565
+ this.responseBuffer.push(response);
1566
+ }
1567
+ // ---- Window manipulation ----
1568
+ windowManipulation(paramCount) {
1569
+ const ps = paramCount > 0 ? this.params[0] : 0;
1570
+ switch (ps) {
1571
+ case 22: // Push title to stack
1572
+ this.titleStack.push("");
1573
+ break;
1574
+ case 23: // Pop title from stack
1575
+ this.titleStack.pop();
1576
+ break;
1577
+ }
1578
+ }
1579
+ // ---- Erase ----
1580
+ eraseInDisplay(mode) {
1581
+ const cursor = this.buf.cursor;
1582
+ const grid = this.grid;
1583
+ switch (mode) {
1584
+ case 0: // from cursor to end
1585
+ this.eraseCells(cursor.row, cursor.col, cursor.row, this.cols - 1);
1586
+ for (let r = cursor.row + 1; r < this.rows; r++) {
1587
+ grid.clearRowRaw(r);
1588
+ }
1589
+ if (cursor.row + 1 < this.rows) {
1590
+ grid.markDirtyRange(cursor.row + 1, this.rows - 1);
1591
+ }
1592
+ break;
1593
+ case 1: // from beginning to cursor
1594
+ for (let r = 0; r < cursor.row; r++) {
1595
+ grid.clearRowRaw(r);
1596
+ }
1597
+ if (cursor.row > 0) {
1598
+ grid.markDirtyRange(0, cursor.row - 1);
1599
+ }
1600
+ this.eraseCells(cursor.row, 0, cursor.row, cursor.col);
1601
+ break;
1602
+ case 2: // entire display
1603
+ case 3: // entire display + scrollback
1604
+ for (let r = 0; r < this.rows; r++) {
1605
+ grid.clearRowRaw(r);
1606
+ }
1607
+ grid.markDirtyRange(0, this.rows - 1);
1608
+ break;
1609
+ }
1610
+ }
1611
+ eraseInLine(mode) {
1612
+ const cursor = this.buf.cursor;
1613
+ switch (mode) {
1614
+ case 0: // from cursor to end of line
1615
+ this.eraseCells(cursor.row, cursor.col, cursor.row, this.cols - 1);
1616
+ break;
1617
+ case 1: // from beginning of line to cursor
1618
+ this.eraseCells(cursor.row, 0, cursor.row, cursor.col);
1619
+ break;
1620
+ case 2: // entire line
1621
+ this.grid.clearRow(cursor.row);
1622
+ break;
1623
+ }
1624
+ }
1625
+ eraseCells(row, startCol, _endRow, endCol) {
1626
+ // Inline cell writes to avoid per-cell setCell + markDirty overhead
1627
+ const grid = this.grid;
1628
+ const rowBase = grid.rowStart(row);
1629
+ const start = Math.max(startCol, 0);
1630
+ const end = Math.min(endCol, this.cols - 1);
1631
+ for (let c = start; c <= end; c++) {
1632
+ const idx = rowBase + c * CELL_SIZE;
1633
+ grid.data[idx] = DEFAULT_CELL_W0;
1634
+ grid.data[idx + 1] = DEFAULT_CELL_W1;
1635
+ }
1636
+ grid.markDirty(row);
1637
+ }
1638
+ eraseChars(n) {
1639
+ const cursor = this.buf.cursor;
1640
+ const grid = this.grid;
1641
+ const rowBase = grid.rowStart(cursor.row);
1642
+ const end = Math.min(cursor.col + n, this.cols);
1643
+ for (let c = cursor.col; c < end; c++) {
1644
+ const idx = rowBase + c * CELL_SIZE;
1645
+ grid.data[idx] = DEFAULT_CELL_W0;
1646
+ grid.data[idx + 1] = DEFAULT_CELL_W1;
1647
+ }
1648
+ grid.markDirty(cursor.row);
1649
+ }
1650
+ // ---- Insert / Delete ----
1651
+ insertLines(n) {
1652
+ const cursor = this.buf.cursor;
1653
+ if (cursor.row < this.buf.scrollTop || cursor.row > this.buf.scrollBottom)
1654
+ return;
1655
+ const grid = this.grid;
1656
+ const rowSize = this.cols * CELL_SIZE;
1657
+ // Clamp n to available rows and shift in a single O(H) pass
1658
+ n = Math.min(n, this.buf.scrollBottom - cursor.row + 1);
1659
+ // Shift rows down by n: copy each row to its final position in one pass
1660
+ for (let r = this.buf.scrollBottom; r >= cursor.row + n; r--) {
1661
+ const src = grid.rowStart(r - n);
1662
+ const dst = grid.rowStart(r);
1663
+ grid.data.copyWithin(dst, src, src + rowSize);
1664
+ }
1665
+ // Clear the n inserted rows
1666
+ for (let r = cursor.row; r < cursor.row + n; r++) {
1667
+ grid.clearRowRaw(r);
1668
+ }
1669
+ grid.markDirtyRange(cursor.row, this.buf.scrollBottom);
1670
+ }
1671
+ deleteLines(n) {
1672
+ const cursor = this.buf.cursor;
1673
+ if (cursor.row < this.buf.scrollTop || cursor.row > this.buf.scrollBottom)
1674
+ return;
1675
+ const grid = this.grid;
1676
+ const rowSize = this.cols * CELL_SIZE;
1677
+ // Clamp n to available rows and shift in a single O(H) pass
1678
+ n = Math.min(n, this.buf.scrollBottom - cursor.row + 1);
1679
+ // Shift rows up by n: copy each row to its final position in one pass
1680
+ for (let r = cursor.row; r <= this.buf.scrollBottom - n; r++) {
1681
+ const src = grid.rowStart(r + n);
1682
+ const dst = grid.rowStart(r);
1683
+ grid.data.copyWithin(dst, src, src + rowSize);
1684
+ }
1685
+ // Clear the n vacated rows at the bottom
1686
+ for (let r = this.buf.scrollBottom - n + 1; r <= this.buf.scrollBottom; r++) {
1687
+ grid.clearRowRaw(r);
1688
+ }
1689
+ grid.markDirtyRange(cursor.row, this.buf.scrollBottom);
1690
+ }
1691
+ insertChars(n) {
1692
+ const cursor = this.buf.cursor;
1693
+ const row = cursor.row;
1694
+ // Clamp n to remaining space
1695
+ n = Math.min(n, this.cols - cursor.col);
1696
+ // Shift cells right using physical row offset
1697
+ const rowBase = this.grid.rowStart(row);
1698
+ for (let c = this.cols - 1; c >= cursor.col + n; c--) {
1699
+ const src = rowBase + (c - n) * CELL_SIZE;
1700
+ const dst = rowBase + c * CELL_SIZE;
1701
+ this.grid.data[dst] = this.grid.data[src];
1702
+ this.grid.data[dst + 1] = this.grid.data[src + 1];
1703
+ }
1704
+ // Clear inserted cells with default colors
1705
+ for (let i = 0; i < n; i++) {
1706
+ this.grid.setCell(row, cursor.col + i, 0x20, 7, 0, 0);
1707
+ }
1708
+ this.grid.markDirty(row);
1709
+ }
1710
+ deleteChars(n) {
1711
+ const cursor = this.buf.cursor;
1712
+ const row = cursor.row;
1713
+ // Clamp n to remaining space
1714
+ n = Math.min(n, this.cols - cursor.col);
1715
+ // Shift cells left using physical row offset
1716
+ const rowBase = this.grid.rowStart(row);
1717
+ for (let c = cursor.col; c < this.cols - n; c++) {
1718
+ const src = rowBase + (c + n) * CELL_SIZE;
1719
+ const dst = rowBase + c * CELL_SIZE;
1720
+ this.grid.data[dst] = this.grid.data[src];
1721
+ this.grid.data[dst + 1] = this.grid.data[src + 1];
1722
+ }
1723
+ // Clear vacated cells at end with default colors
1724
+ for (let c = this.cols - n; c < this.cols; c++) {
1725
+ this.grid.setCell(row, c, 0x20, 7, 0, 0);
1726
+ }
1727
+ this.grid.markDirty(row);
1728
+ }
1729
+ // ---- Scroll region ----
1730
+ setScrollRegion(top, bottom) {
1731
+ const t = Math.max(top - 1, 0);
1732
+ const b = Math.min(bottom - 1, this.rows - 1);
1733
+ if (t < b) {
1734
+ this.buf.scrollTop = t;
1735
+ this.buf.scrollBottom = b;
1736
+ // Cursor moves to home (respects DECOM)
1737
+ if (this.originMode) {
1738
+ this.buf.cursor.row = this.buf.scrollTop;
1739
+ }
1740
+ else {
1741
+ this.buf.cursor.row = 0;
1742
+ }
1743
+ this.buf.cursor.col = 0;
1744
+ this.buf.cursor.wrapPending = false;
1745
+ }
1746
+ }
1747
+ // ---- SGR ----
1748
+ sgr(paramCount) {
1749
+ if (paramCount === 0) {
1750
+ this.params[0] = 0;
1751
+ paramCount = 1;
1752
+ }
1753
+ for (let i = 0; i < paramCount; i++) {
1754
+ const p = this.params[i];
1755
+ switch (p) {
1756
+ case 0: // Reset
1757
+ this.attrs = 0;
1758
+ this.fgIndex = 7;
1759
+ this.bgIndex = 0;
1760
+ this.fgIsRGB = false;
1761
+ this.bgIsRGB = false;
1762
+ break;
1763
+ case 1:
1764
+ this.attrs |= ATTR_BOLD;
1765
+ break;
1766
+ case 2:
1767
+ this.attrs |= ATTR_DIM;
1768
+ break;
1769
+ case 3:
1770
+ this.attrs |= ATTR_ITALIC;
1771
+ break;
1772
+ case 4:
1773
+ this.attrs |= ATTR_UNDERLINE;
1774
+ break;
1775
+ case 7:
1776
+ this.attrs |= ATTR_INVERSE;
1777
+ break;
1778
+ case 8:
1779
+ this.attrs |= ATTR_HIDDEN;
1780
+ break;
1781
+ case 9:
1782
+ this.attrs |= ATTR_STRIKETHROUGH;
1783
+ break;
1784
+ case 22:
1785
+ this.attrs &= ~(ATTR_BOLD | ATTR_DIM);
1786
+ break;
1787
+ case 23:
1788
+ this.attrs &= ~ATTR_ITALIC;
1789
+ break;
1790
+ case 24:
1791
+ this.attrs &= ~ATTR_UNDERLINE;
1792
+ break;
1793
+ case 27:
1794
+ this.attrs &= ~ATTR_INVERSE;
1795
+ break;
1796
+ case 28:
1797
+ this.attrs &= ~ATTR_HIDDEN;
1798
+ break;
1799
+ case 29:
1800
+ this.attrs &= ~ATTR_STRIKETHROUGH;
1801
+ break;
1802
+ // Standard foreground colors (30-37)
1803
+ case 30:
1804
+ case 31:
1805
+ case 32:
1806
+ case 33:
1807
+ case 34:
1808
+ case 35:
1809
+ case 36:
1810
+ case 37:
1811
+ this.fgIndex = p - 30;
1812
+ this.fgIsRGB = false;
1813
+ break;
1814
+ // Extended foreground (38)
1815
+ case 38:
1816
+ i = this.parseSgrColor(paramCount, i, true);
1817
+ break;
1818
+ // Default foreground (39)
1819
+ case 39:
1820
+ this.fgIndex = 7;
1821
+ this.fgIsRGB = false;
1822
+ break;
1823
+ // Standard background colors (40-47)
1824
+ case 40:
1825
+ case 41:
1826
+ case 42:
1827
+ case 43:
1828
+ case 44:
1829
+ case 45:
1830
+ case 46:
1831
+ case 47:
1832
+ this.bgIndex = p - 40;
1833
+ this.bgIsRGB = false;
1834
+ break;
1835
+ // Extended background (48)
1836
+ case 48:
1837
+ i = this.parseSgrColor(paramCount, i, false);
1838
+ break;
1839
+ // Default background (49)
1840
+ case 49:
1841
+ this.bgIndex = 0;
1842
+ this.bgIsRGB = false;
1843
+ break;
1844
+ // Bright foreground colors (90-97)
1845
+ case 90:
1846
+ case 91:
1847
+ case 92:
1848
+ case 93:
1849
+ case 94:
1850
+ case 95:
1851
+ case 96:
1852
+ case 97:
1853
+ this.fgIndex = p - 90 + 8;
1854
+ this.fgIsRGB = false;
1855
+ break;
1856
+ // Bright background colors (100-107)
1857
+ case 100:
1858
+ case 101:
1859
+ case 102:
1860
+ case 103:
1861
+ case 104:
1862
+ case 105:
1863
+ case 106:
1864
+ case 107:
1865
+ this.bgIndex = p - 100 + 8;
1866
+ this.bgIsRGB = false;
1867
+ break;
1868
+ }
1869
+ }
1870
+ }
1871
+ /** Parse SGR 38/48 extended color. Returns updated index. */
1872
+ parseSgrColor(paramCount, i, isFg) {
1873
+ if (i + 1 >= paramCount)
1874
+ return i;
1875
+ const mode = this.params[i + 1];
1876
+ if (mode === 5) {
1877
+ // 256-color: 38;5;n or 48;5;n
1878
+ if (i + 2 < paramCount) {
1879
+ const colorIdx = this.params[i + 2];
1880
+ if (isFg) {
1881
+ this.fgIndex = colorIdx & 0xff;
1882
+ this.fgIsRGB = false;
1883
+ }
1884
+ else {
1885
+ this.bgIndex = colorIdx & 0xff;
1886
+ this.bgIsRGB = false;
1887
+ }
1888
+ return i + 2;
1889
+ }
1890
+ }
1891
+ else if (mode === 2) {
1892
+ // 24-bit RGB: 38;2;r;g;b or 48;2;r;g;b
1893
+ if (i + 4 < paramCount) {
1894
+ const r = this.params[i + 2] & 0xff;
1895
+ const g = this.params[i + 3] & 0xff;
1896
+ const b = this.params[i + 4] & 0xff;
1897
+ const rgb = (r << 16) | (g << 8) | b;
1898
+ if (isFg) {
1899
+ this.fgRGB = rgb;
1900
+ this.fgIsRGB = true;
1901
+ }
1902
+ else {
1903
+ this.bgRGB = rgb;
1904
+ this.bgIsRGB = true;
1905
+ }
1906
+ return i + 4;
1907
+ }
1908
+ }
1909
+ return i;
1910
+ }
1911
+ // ---- Soft reset (DECSTR) ----
1912
+ softReset() {
1913
+ // Reset terminal modes but preserve certain settings
1914
+ this.attrs = 0;
1915
+ this.fgIndex = 7;
1916
+ this.bgIndex = 0;
1917
+ this.fgIsRGB = false;
1918
+ this.bgIsRGB = false;
1919
+ this.lineFeedMode = false;
1920
+ this.autoWrapMode = true;
1921
+ this.originMode = false;
1922
+ this.applicationCursorKeys = false;
1923
+ this.applicationKeypad = false;
1924
+ this.bracketedPasteMode = false;
1925
+ this.syncedOutput = false;
1926
+ this.mouseProtocol = "none";
1927
+ this.mouseEncoding = "default";
1928
+ this.sendFocusEvents = false;
1929
+ this.buf.cursor.visible = true;
1930
+ this.buf.cursor.style = "block";
1931
+ this.buf.cursor.wrapPending = false;
1932
+ this.buf.scrollTop = 0;
1933
+ this.buf.scrollBottom = this.rows - 1;
1934
+ // Note: cursor position is NOT reset by soft reset
1935
+ // Note: screen content is NOT cleared by soft reset
1936
+ }
1937
+ fullReset() {
1938
+ this.state = State.GROUND;
1939
+ this.clear();
1940
+ this.lastPrintedCodepoint = 0;
1941
+ this.responseBuffer = [];
1942
+ this.titleStack = [];
1943
+ this.kittyFlags = 0;
1944
+ this.kittyFlagsStack.length = 0;
1945
+ this.bufferSet.activateNormal();
1946
+ this.softReset();
1947
+ this.buf.cursor.row = 0;
1948
+ this.buf.cursor.col = 0;
1949
+ this.grid.clear();
1950
+ }
1951
+ }
1952
+ //# sourceMappingURL=index.js.map