@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.
- package/dist/buffer.d.ts +46 -0
- package/dist/buffer.d.ts.map +1 -0
- package/dist/buffer.js +146 -0
- package/dist/buffer.js.map +1 -0
- package/dist/cell-grid.d.ts +102 -0
- package/dist/cell-grid.d.ts.map +1 -0
- package/dist/cell-grid.js +294 -0
- package/dist/cell-grid.js.map +1 -0
- package/dist/gesture-handler.d.ts +65 -0
- package/dist/gesture-handler.d.ts.map +1 -0
- package/dist/gesture-handler.js +186 -0
- package/dist/gesture-handler.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/index.d.ts +196 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +1952 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/states.d.ts +39 -0
- package/dist/parser/states.d.ts.map +1 -0
- package/dist/parser/states.js +240 -0
- package/dist/parser/states.js.map +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -0
|
@@ -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
|