vterm.js 0.1.2 → 0.2.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/README.md +44 -44
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/screen.ts +545 -41
package/README.md
CHANGED
|
@@ -100,40 +100,40 @@ console.log(cell.underlineColor) // { r: 0, g: 150, b: 255 }
|
|
|
100
100
|
|
|
101
101
|
### `createVtermScreen(options)`
|
|
102
102
|
|
|
103
|
-
| Option
|
|
104
|
-
|
|
105
|
-
| `cols`
|
|
106
|
-
| `rows`
|
|
107
|
-
| `scrollbackLimit` | `number`
|
|
108
|
-
| `onResponse`
|
|
103
|
+
| Option | Type | Default | Description |
|
|
104
|
+
| ----------------- | ------------------------ | ------- | ---------------------------------- |
|
|
105
|
+
| `cols` | `number` | `80` | Terminal width |
|
|
106
|
+
| `rows` | `number` | `24` | Terminal height |
|
|
107
|
+
| `scrollbackLimit` | `number` | `1000` | Max scrollback lines |
|
|
108
|
+
| `onResponse` | `(data: string) => void` | — | Callback for DA1/DA2/DSR responses |
|
|
109
109
|
|
|
110
110
|
### Screen methods
|
|
111
111
|
|
|
112
|
-
| Method
|
|
113
|
-
|
|
114
|
-
| `process(data: Uint8Array)`
|
|
115
|
-
| `getText()`
|
|
116
|
-
| `getTextRange(sr, sc, er, ec)` | Get text in a range
|
|
117
|
-
| `getLine(row)`
|
|
118
|
-
| `getCell(row, col)`
|
|
119
|
-
| `getCursorPosition()`
|
|
120
|
-
| `getCursorVisible()`
|
|
121
|
-
| `getCursorShape()`
|
|
122
|
-
| `getCursorBlinking()`
|
|
123
|
-
| `getMode(mode)`
|
|
124
|
-
| `getTitle()`
|
|
125
|
-
| `getScrollbackLength()`
|
|
126
|
-
| `getViewportOffset()`
|
|
127
|
-
| `scrollViewport(delta)`
|
|
128
|
-
| `resize(cols, rows)`
|
|
129
|
-
| `reset()`
|
|
112
|
+
| Method | Description |
|
|
113
|
+
| ------------------------------ | ------------------------------------------------------ |
|
|
114
|
+
| `process(data: Uint8Array)` | Feed raw terminal data |
|
|
115
|
+
| `getText()` | Get all text (scrollback + screen) |
|
|
116
|
+
| `getTextRange(sr, sc, er, ec)` | Get text in a range |
|
|
117
|
+
| `getLine(row)` | Get cells for a row |
|
|
118
|
+
| `getCell(row, col)` | Get a single cell |
|
|
119
|
+
| `getCursorPosition()` | Get cursor `{ x, y }` |
|
|
120
|
+
| `getCursorVisible()` | Check cursor visibility |
|
|
121
|
+
| `getCursorShape()` | Get cursor shape: `"block"`, `"underline"`, or `"bar"` |
|
|
122
|
+
| `getCursorBlinking()` | Check if cursor is blinking |
|
|
123
|
+
| `getMode(mode)` | Check terminal mode |
|
|
124
|
+
| `getTitle()` | Get window title |
|
|
125
|
+
| `getScrollbackLength()` | Number of scrollback lines |
|
|
126
|
+
| `getViewportOffset()` | Current viewport scroll offset |
|
|
127
|
+
| `scrollViewport(delta)` | Scroll viewport |
|
|
128
|
+
| `resize(cols, rows)` | Resize terminal |
|
|
129
|
+
| `reset()` | Reset to initial state |
|
|
130
130
|
|
|
131
131
|
### Cell properties
|
|
132
132
|
|
|
133
133
|
```typescript
|
|
134
134
|
interface ScreenCell {
|
|
135
135
|
char: string
|
|
136
|
-
fg: CellColor | null
|
|
136
|
+
fg: CellColor | null // { r, g, b }
|
|
137
137
|
bg: CellColor | null
|
|
138
138
|
bold: boolean
|
|
139
139
|
faint: boolean
|
|
@@ -146,30 +146,30 @@ interface ScreenCell {
|
|
|
146
146
|
hidden: boolean
|
|
147
147
|
blink: boolean
|
|
148
148
|
wide: boolean
|
|
149
|
-
url: string | null
|
|
149
|
+
url: string | null // OSC 8 hyperlink
|
|
150
150
|
}
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
## vs vt100.js
|
|
154
154
|
|
|
155
|
-
| Feature
|
|
156
|
-
|
|
157
|
-
| SGR basics (bold, italic, underline, colors) | Yes
|
|
158
|
-
| Underline styles (curly, dotted, dashed)
|
|
159
|
-
| Underline color
|
|
160
|
-
| Blink, overline
|
|
161
|
-
| Cursor shapes (DECSCUSR)
|
|
162
|
-
| OSC 8 hyperlinks
|
|
163
|
-
| DA1/DA2/DA3 responses
|
|
164
|
-
| DSR/DECRPM responses
|
|
165
|
-
| Mouse tracking
|
|
166
|
-
| Focus tracking
|
|
167
|
-
| Synchronized output
|
|
168
|
-
| DEC Special Graphics
|
|
169
|
-
| REP (repeat character)
|
|
170
|
-
| DECSTR (soft reset)
|
|
171
|
-
| DCS sequences
|
|
172
|
-
| Package size
|
|
155
|
+
| Feature | vt100.js | vterm.js |
|
|
156
|
+
| -------------------------------------------- | -------- | -------- |
|
|
157
|
+
| SGR basics (bold, italic, underline, colors) | Yes | Yes |
|
|
158
|
+
| Underline styles (curly, dotted, dashed) | Yes | Yes |
|
|
159
|
+
| Underline color | No | Yes |
|
|
160
|
+
| Blink, overline | No | Yes |
|
|
161
|
+
| Cursor shapes (DECSCUSR) | No | Yes |
|
|
162
|
+
| OSC 8 hyperlinks | No | Yes |
|
|
163
|
+
| DA1/DA2/DA3 responses | No | Yes |
|
|
164
|
+
| DSR/DECRPM responses | No | Yes |
|
|
165
|
+
| Mouse tracking | No | Yes |
|
|
166
|
+
| Focus tracking | No | Yes |
|
|
167
|
+
| Synchronized output | No | Yes |
|
|
168
|
+
| DEC Special Graphics | No | Yes |
|
|
169
|
+
| REP (repeat character) | No | Yes |
|
|
170
|
+
| DECSTR (soft reset) | No | Yes |
|
|
171
|
+
| DCS sequences | No | Yes |
|
|
172
|
+
| Package size | Smaller | Larger |
|
|
173
173
|
|
|
174
174
|
**Use vt100.js** when you want fast and simple — it covers ~90% of real-world terminal usage.
|
|
175
175
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/screen.ts
CHANGED
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
* - Focus tracking (mode 1004)
|
|
17
17
|
* - Application cursor keys & keypad
|
|
18
18
|
* - Synchronized output (mode 2026)
|
|
19
|
+
* - Kitty keyboard protocol (CSI u / progressive enhancement)
|
|
19
20
|
* - Scrollback buffer with configurable limit
|
|
20
21
|
* - Wide character support (CJK, emoji ZWJ, regional indicators, VS-16)
|
|
21
22
|
* - OSC sequences (title, hyperlinks, clipboard, colors)
|
|
22
23
|
* - DCS sequences (consumed and ignored, XTVERSION response)
|
|
24
|
+
* - APC sequences (Kitty graphics protocol — parsed, query responses)
|
|
23
25
|
* - DA1/DA2/DA3 device attribute responses
|
|
24
26
|
* - DSR (device status report) responses
|
|
25
27
|
* - DECRPM (mode reporting)
|
|
@@ -67,6 +69,18 @@ export interface ScreenOptions {
|
|
|
67
69
|
onResponse?: (data: string) => void
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
export interface SemanticZone {
|
|
73
|
+
type: "prompt" | "command" | "output"
|
|
74
|
+
startRow: number
|
|
75
|
+
startCol: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SixelImage {
|
|
79
|
+
data: string
|
|
80
|
+
row: number // cursor row when sixel started
|
|
81
|
+
col: number // cursor col when sixel started
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
export interface Screen {
|
|
71
85
|
readonly cols: number
|
|
72
86
|
readonly rows: number
|
|
@@ -91,6 +105,9 @@ export interface Screen {
|
|
|
91
105
|
getScrollbackLength(): number
|
|
92
106
|
getViewportOffset(): number
|
|
93
107
|
scrollViewport(delta: number): void
|
|
108
|
+
|
|
109
|
+
getSemanticZones(): SemanticZone[]
|
|
110
|
+
getSixelImages(): SixelImage[]
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
// ── Implementation ─────────────────────────────────────────────────────
|
|
@@ -183,11 +200,27 @@ const DEC_SPECIAL_GRAPHICS: Record<string, string> = {
|
|
|
183
200
|
"}": "\u00a3", // £
|
|
184
201
|
}
|
|
185
202
|
|
|
186
|
-
// ── Unicode width
|
|
203
|
+
// ── Unicode width & character classification ────────────────────────────
|
|
187
204
|
|
|
188
205
|
function isWide(codePoint: number): boolean {
|
|
189
206
|
return (
|
|
190
207
|
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
|
208
|
+
(codePoint >= 0x231a && codePoint <= 0x231b) || // Watch, Hourglass
|
|
209
|
+
(codePoint >= 0x2614 && codePoint <= 0x2615) || // Umbrella, Hot Beverage
|
|
210
|
+
(codePoint >= 0x2648 && codePoint <= 0x2653) || // Zodiac signs
|
|
211
|
+
codePoint === 0x267f || // Wheelchair
|
|
212
|
+
codePoint === 0x2693 || // Anchor
|
|
213
|
+
codePoint === 0x26a1 || // High Voltage
|
|
214
|
+
codePoint === 0x26ce || // Ophiuchus
|
|
215
|
+
codePoint === 0x26d4 || // No Entry
|
|
216
|
+
codePoint === 0x2705 || // Check Mark
|
|
217
|
+
codePoint === 0x2728 || // Sparkles
|
|
218
|
+
codePoint === 0x274c || // Cross Mark
|
|
219
|
+
codePoint === 0x274e || // Cross Mark variant
|
|
220
|
+
(codePoint >= 0x2753 && codePoint <= 0x2755) || // Question marks
|
|
221
|
+
(codePoint >= 0x2795 && codePoint <= 0x2797) || // Plus, Minus, Division
|
|
222
|
+
codePoint === 0x27b0 || // Curly Loop
|
|
223
|
+
codePoint === 0x27bf || // Double Curly Loop
|
|
191
224
|
(codePoint >= 0x2e80 && codePoint <= 0x303e) || // CJK Radicals
|
|
192
225
|
(codePoint >= 0x3041 && codePoint <= 0x33bf) || // Hiragana, Katakana, Bopomofo, etc.
|
|
193
226
|
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Unified Extension A
|
|
@@ -199,6 +232,14 @@ function isWide(codePoint: number): boolean {
|
|
|
199
232
|
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) || // CJK Compatibility Forms
|
|
200
233
|
(codePoint >= 0xff01 && codePoint <= 0xff60) || // Fullwidth Forms
|
|
201
234
|
(codePoint >= 0xffe0 && codePoint <= 0xffe6) || // Fullwidth Signs
|
|
235
|
+
codePoint === 0x1f004 || // Mahjong Tile
|
|
236
|
+
codePoint === 0x1f0cf || // Playing Card
|
|
237
|
+
(codePoint >= 0x1f170 && codePoint <= 0x1f171) || // A/B buttons
|
|
238
|
+
(codePoint >= 0x1f17e && codePoint <= 0x1f17f) || // O/P buttons
|
|
239
|
+
codePoint === 0x1f18e || // AB button
|
|
240
|
+
(codePoint >= 0x1f191 && codePoint <= 0x1f19a) || // Squared symbols
|
|
241
|
+
(codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) || // Regional Indicators
|
|
242
|
+
(codePoint >= 0x1f200 && codePoint <= 0x1f202) || // Enclosed CJK
|
|
202
243
|
(codePoint >= 0x1f300 && codePoint <= 0x1f9ff) || // Misc Symbols/Emoticons
|
|
203
244
|
(codePoint >= 0x1fa00 && codePoint <= 0x1faff) || // Extended Symbols & Pictographs
|
|
204
245
|
(codePoint >= 0x20000 && codePoint <= 0x2fffd) || // CJK Extension B-F
|
|
@@ -206,6 +247,54 @@ function isWide(codePoint: number): boolean {
|
|
|
206
247
|
)
|
|
207
248
|
}
|
|
208
249
|
|
|
250
|
+
function isCombining(cp: number): boolean {
|
|
251
|
+
return (
|
|
252
|
+
(cp >= 0x0300 && cp <= 0x036f) || // Combining Diacritical Marks
|
|
253
|
+
(cp >= 0x0483 && cp <= 0x0489) || // Cyrillic combining
|
|
254
|
+
(cp >= 0x0591 && cp <= 0x05bd) || // Hebrew
|
|
255
|
+
cp === 0x05bf || // Hebrew
|
|
256
|
+
(cp >= 0x05c1 && cp <= 0x05c2) || // Hebrew
|
|
257
|
+
(cp >= 0x05c4 && cp <= 0x05c5) || // Hebrew
|
|
258
|
+
cp === 0x05c7 || // Hebrew
|
|
259
|
+
(cp >= 0x0610 && cp <= 0x061a) || // Arabic
|
|
260
|
+
(cp >= 0x064b && cp <= 0x065f) || // Arabic
|
|
261
|
+
cp === 0x0670 || // Arabic
|
|
262
|
+
(cp >= 0x06d6 && cp <= 0x06dc) || // Arabic
|
|
263
|
+
(cp >= 0x06df && cp <= 0x06e4) || // Arabic
|
|
264
|
+
(cp >= 0x06e7 && cp <= 0x06e8) || // Arabic
|
|
265
|
+
(cp >= 0x06ea && cp <= 0x06ed) || // Arabic
|
|
266
|
+
(cp >= 0x0730 && cp <= 0x074a) || // Syriac
|
|
267
|
+
(cp >= 0x0900 && cp <= 0x0903) || // Devanagari
|
|
268
|
+
(cp >= 0x093a && cp <= 0x094f) || // Devanagari
|
|
269
|
+
(cp >= 0x0951 && cp <= 0x0957) || // Devanagari
|
|
270
|
+
(cp >= 0x0962 && cp <= 0x0963) || // Devanagari
|
|
271
|
+
cp === 0x0e31 || // Thai
|
|
272
|
+
(cp >= 0x0e34 && cp <= 0x0e3a) || // Thai
|
|
273
|
+
(cp >= 0x0e47 && cp <= 0x0e4e) || // Thai
|
|
274
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) || // Combining Diacritical Marks Extended
|
|
275
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) || // Combining Diacritical Marks Supplement
|
|
276
|
+
(cp >= 0x20d0 && cp <= 0x20ff) || // Combining Diacritical Marks for Symbols
|
|
277
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) || // Variation Selectors
|
|
278
|
+
(cp >= 0xe0100 && cp <= 0xe01ef) // Variation Selectors Supplement
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isRegionalIndicator(cp: number): boolean {
|
|
283
|
+
return cp >= 0x1f1e6 && cp <= 0x1f1ff
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isZWJ(cp: number): boolean {
|
|
287
|
+
return cp === 0x200d
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isVS16(cp: number): boolean {
|
|
291
|
+
return cp === 0xfe0f
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isEmojiModifier(cp: number): boolean {
|
|
295
|
+
return cp >= 0x1f3fb && cp <= 0x1f3ff // Skin tone modifiers
|
|
296
|
+
}
|
|
297
|
+
|
|
209
298
|
// ── Internal attrs interface ───────────────────────────────────────────
|
|
210
299
|
|
|
211
300
|
interface Attrs {
|
|
@@ -286,6 +375,13 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
286
375
|
let reverseVideo = false
|
|
287
376
|
let syncOutput = false
|
|
288
377
|
|
|
378
|
+
// Kitty keyboard protocol
|
|
379
|
+
let kittyKeyboardFlags = 0
|
|
380
|
+
let kittyKeyboardStack: number[] = []
|
|
381
|
+
|
|
382
|
+
// Kitty graphics protocol
|
|
383
|
+
let hasKittyGraphics = false
|
|
384
|
+
|
|
289
385
|
// Scroll region (inclusive, 0-based)
|
|
290
386
|
let scrollTop = 0
|
|
291
387
|
let scrollBottom = rows - 1
|
|
@@ -299,9 +395,25 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
299
395
|
// Clipboard (OSC 52)
|
|
300
396
|
let clipboard = ""
|
|
301
397
|
|
|
398
|
+
// Semantic prompt zones (OSC 133)
|
|
399
|
+
let semanticZones: SemanticZone[] = []
|
|
400
|
+
|
|
401
|
+
// Sixel graphics (DCS q)
|
|
402
|
+
let hasSixel = false
|
|
403
|
+
let sixelImages: SixelImage[] = []
|
|
404
|
+
|
|
405
|
+
// Soft-wrap tracking: true if the line break at end of this row was caused by auto-wrap
|
|
406
|
+
let mainSoftWrapped: boolean[] = new Array(rows).fill(false)
|
|
407
|
+
let altSoftWrapped: boolean[] = new Array(rows).fill(false)
|
|
408
|
+
let softWrapped = mainSoftWrapped
|
|
409
|
+
|
|
302
410
|
// Last printed character for REP
|
|
303
411
|
let lastChar = ""
|
|
304
412
|
|
|
413
|
+
// Unicode sequence state
|
|
414
|
+
let pendingRegionalIndicator: string | null = null // First RI waiting for pair
|
|
415
|
+
let afterZWJ = false // Next character should join with previous cell
|
|
416
|
+
|
|
305
417
|
// Parser state
|
|
306
418
|
let parserState:
|
|
307
419
|
| "ground"
|
|
@@ -312,9 +424,15 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
312
424
|
| "dcs"
|
|
313
425
|
| "dcs_passthrough"
|
|
314
426
|
| "osc_st"
|
|
315
|
-
| "dcs_st"
|
|
427
|
+
| "dcs_st"
|
|
428
|
+
| "apc"
|
|
429
|
+
| "apc_st" = "ground"
|
|
316
430
|
let escBuf = ""
|
|
317
431
|
let oscBuf = ""
|
|
432
|
+
let dcsBuf = ""
|
|
433
|
+
let dcsStartRow = 0
|
|
434
|
+
let dcsStartCol = 0
|
|
435
|
+
let apcBuf = ""
|
|
318
436
|
|
|
319
437
|
// Decoder for incoming bytes
|
|
320
438
|
const decoder = new TextDecoder()
|
|
@@ -374,19 +492,71 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
374
492
|
}
|
|
375
493
|
for (let i = top; i < bottom; i++) {
|
|
376
494
|
grid[i] = grid[i + 1]!
|
|
495
|
+
softWrapped[i] = softWrapped[i + 1]!
|
|
377
496
|
}
|
|
378
497
|
grid[bottom] = makeRow(cols)
|
|
498
|
+
softWrapped[bottom] = false
|
|
379
499
|
}
|
|
380
500
|
|
|
381
501
|
function scrollDown(top: number, bottom: number): void {
|
|
382
502
|
for (let i = bottom; i > top; i--) {
|
|
383
503
|
grid[i] = grid[i - 1]!
|
|
504
|
+
softWrapped[i] = softWrapped[i - 1]!
|
|
384
505
|
}
|
|
385
506
|
grid[top] = makeRow(cols)
|
|
507
|
+
softWrapped[top] = false
|
|
386
508
|
}
|
|
387
509
|
|
|
388
510
|
// ── Character writing ──
|
|
389
511
|
|
|
512
|
+
/** Find the previous non-spacer cell (the cell before curX, skipping wide-char spacers) */
|
|
513
|
+
function getPrevCell(): { cell: ScreenCell; col: number } | null {
|
|
514
|
+
if (curX === 0 && curY === 0) return null
|
|
515
|
+
let prevCol = curX - 1
|
|
516
|
+
let prevRow = curY
|
|
517
|
+
if (prevCol < 0) {
|
|
518
|
+
prevRow--
|
|
519
|
+
if (prevRow < 0) return null
|
|
520
|
+
prevCol = cols - 1
|
|
521
|
+
}
|
|
522
|
+
const row = grid[prevRow]!
|
|
523
|
+
let cell = row[prevCol]!
|
|
524
|
+
// If we landed on a spacer (empty char after a wide character), go back one more
|
|
525
|
+
if (cell !== EMPTY_CELL && cell.char === "" && prevCol > 0) {
|
|
526
|
+
prevCol--
|
|
527
|
+
cell = row[prevCol]!
|
|
528
|
+
}
|
|
529
|
+
if (cell === EMPTY_CELL) return null
|
|
530
|
+
return { cell, col: prevCol }
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Widen a cell to 2 columns, adding a spacer cell after it */
|
|
534
|
+
function widenCell(row: ScreenCell[], col: number, cell: ScreenCell): void {
|
|
535
|
+
cell.wide = true
|
|
536
|
+
if (col + 1 < cols) {
|
|
537
|
+
let spacer = row[col + 1]!
|
|
538
|
+
if (spacer === EMPTY_CELL) {
|
|
539
|
+
spacer = { ...EMPTY_CELL }
|
|
540
|
+
row[col + 1] = spacer
|
|
541
|
+
}
|
|
542
|
+
spacer.char = ""
|
|
543
|
+
spacer.fg = null
|
|
544
|
+
spacer.bg = null
|
|
545
|
+
spacer.bold = false
|
|
546
|
+
spacer.faint = false
|
|
547
|
+
spacer.italic = false
|
|
548
|
+
spacer.underline = "none"
|
|
549
|
+
spacer.underlineColor = null
|
|
550
|
+
spacer.overline = false
|
|
551
|
+
spacer.strikethrough = false
|
|
552
|
+
spacer.inverse = false
|
|
553
|
+
spacer.hidden = false
|
|
554
|
+
spacer.blink = false
|
|
555
|
+
spacer.wide = false
|
|
556
|
+
spacer.url = null
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
390
560
|
function writeChar(ch: string): void {
|
|
391
561
|
// Apply DEC Special Graphics character mapping
|
|
392
562
|
if (charsetG0 && ch.length === 1) {
|
|
@@ -395,12 +565,95 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
395
565
|
}
|
|
396
566
|
|
|
397
567
|
const codePoint = ch.codePointAt(0) ?? 0
|
|
568
|
+
|
|
569
|
+
// ── VS-16 (U+FE0F): widen previous character to emoji presentation ──
|
|
570
|
+
if (isVS16(codePoint)) {
|
|
571
|
+
const prev = getPrevCell()
|
|
572
|
+
if (prev && !prev.cell.wide) {
|
|
573
|
+
prev.cell.char += ch
|
|
574
|
+
const row = grid[curY === 0 && curX === 0 ? 0 : curY]!
|
|
575
|
+
widenCell(row, prev.col, prev.cell)
|
|
576
|
+
// Advance cursor past the spacer
|
|
577
|
+
curX = prev.col + 2
|
|
578
|
+
if (curX >= cols) curX = cols - 1
|
|
579
|
+
}
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ── Combining characters: append to previous cell, zero width ──
|
|
584
|
+
if (isCombining(codePoint) && !isVS16(codePoint)) {
|
|
585
|
+
const prev = getPrevCell()
|
|
586
|
+
if (prev) {
|
|
587
|
+
prev.cell.char += ch
|
|
588
|
+
}
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── Emoji modifier (skin tone): append to previous cell, zero width ──
|
|
593
|
+
if (isEmojiModifier(codePoint)) {
|
|
594
|
+
const prev = getPrevCell()
|
|
595
|
+
if (prev) {
|
|
596
|
+
prev.cell.char += ch
|
|
597
|
+
}
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ── ZWJ (U+200D): append to previous cell, flag for next char ──
|
|
602
|
+
if (isZWJ(codePoint)) {
|
|
603
|
+
const prev = getPrevCell()
|
|
604
|
+
if (prev) {
|
|
605
|
+
prev.cell.char += ch
|
|
606
|
+
afterZWJ = true
|
|
607
|
+
}
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── After ZWJ: append this character to the previous cell ──
|
|
612
|
+
if (afterZWJ) {
|
|
613
|
+
afterZWJ = false
|
|
614
|
+
const prev = getPrevCell()
|
|
615
|
+
if (prev) {
|
|
616
|
+
prev.cell.char += ch
|
|
617
|
+
// The ZWJ sequence stays in the same wide cell
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ── Regional Indicators: pair into flag emoji ──
|
|
623
|
+
if (isRegionalIndicator(codePoint)) {
|
|
624
|
+
if (pendingRegionalIndicator !== null) {
|
|
625
|
+
// Second RI: combine with first to form a flag, render as wide
|
|
626
|
+
const flag = pendingRegionalIndicator + ch
|
|
627
|
+
pendingRegionalIndicator = null
|
|
628
|
+
// Write the combined flag as a wide character
|
|
629
|
+
writeCharCore(flag, true)
|
|
630
|
+
return
|
|
631
|
+
} else {
|
|
632
|
+
// First RI: store and wait for second
|
|
633
|
+
pendingRegionalIndicator = ch
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Flush any pending regional indicator that wasn't paired
|
|
639
|
+
if (pendingRegionalIndicator !== null) {
|
|
640
|
+
const ri = pendingRegionalIndicator
|
|
641
|
+
pendingRegionalIndicator = null
|
|
642
|
+
writeCharCore(ri, true)
|
|
643
|
+
}
|
|
644
|
+
|
|
398
645
|
const wide = isWide(codePoint)
|
|
646
|
+
writeCharCore(ch, wide)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function writeCharCore(ch: string, wide: boolean): void {
|
|
399
650
|
const charWidth = wide ? 2 : 1
|
|
400
651
|
|
|
401
652
|
// Handle autowrap at end of line
|
|
402
653
|
if (curX + charWidth > cols) {
|
|
403
654
|
if (autoWrap) {
|
|
655
|
+
// Mark this row as soft-wrapped (auto-wrap caused the line break)
|
|
656
|
+
softWrapped[curY] = true
|
|
404
657
|
curX = 0
|
|
405
658
|
curY++
|
|
406
659
|
if (curY > scrollBottom) {
|
|
@@ -444,27 +697,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
444
697
|
cell.wide = wide
|
|
445
698
|
cell.url = attrs.url
|
|
446
699
|
|
|
447
|
-
if (wide
|
|
448
|
-
|
|
449
|
-
if (spacer === EMPTY_CELL) {
|
|
450
|
-
spacer = { ...EMPTY_CELL }
|
|
451
|
-
row[curX + 1] = spacer
|
|
452
|
-
}
|
|
453
|
-
spacer.char = ""
|
|
454
|
-
spacer.fg = null
|
|
455
|
-
spacer.bg = null
|
|
456
|
-
spacer.bold = false
|
|
457
|
-
spacer.faint = false
|
|
458
|
-
spacer.italic = false
|
|
459
|
-
spacer.underline = "none"
|
|
460
|
-
spacer.underlineColor = null
|
|
461
|
-
spacer.overline = false
|
|
462
|
-
spacer.strikethrough = false
|
|
463
|
-
spacer.inverse = false
|
|
464
|
-
spacer.hidden = false
|
|
465
|
-
spacer.blink = false
|
|
466
|
-
spacer.wide = false
|
|
467
|
-
spacer.url = null
|
|
700
|
+
if (wide) {
|
|
701
|
+
widenCell(row, curX, cell)
|
|
468
702
|
}
|
|
469
703
|
|
|
470
704
|
curX += charWidth
|
|
@@ -638,6 +872,16 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
638
872
|
}
|
|
639
873
|
}
|
|
640
874
|
break
|
|
875
|
+
case "h": // SM - Set Mode (non-private)
|
|
876
|
+
for (const code of parts) {
|
|
877
|
+
if (code === 4) insertMode = true // IRM - Insert/Replace Mode
|
|
878
|
+
}
|
|
879
|
+
break
|
|
880
|
+
case "l": // RM - Reset Mode (non-private)
|
|
881
|
+
for (const code of parts) {
|
|
882
|
+
if (code === 4) insertMode = false
|
|
883
|
+
}
|
|
884
|
+
break
|
|
641
885
|
case "s": // SCP - Save Cursor Position
|
|
642
886
|
savedCurX = curX
|
|
643
887
|
savedCurY = curY
|
|
@@ -653,6 +897,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
653
897
|
}
|
|
654
898
|
|
|
655
899
|
function handleCSIGt(params: string, _intermediates: string, finalByte: string): void {
|
|
900
|
+
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
901
|
+
|
|
656
902
|
// CSI > sequences
|
|
657
903
|
if (finalByte === "c") {
|
|
658
904
|
// DA2 - Secondary Device Attributes
|
|
@@ -668,12 +914,32 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
668
914
|
onResponse("\x1bP>|vterm.js 0.1.0\x1b\\")
|
|
669
915
|
}
|
|
670
916
|
}
|
|
917
|
+
} else if (finalByte === "u") {
|
|
918
|
+
// CSI > flags u — Push keyboard mode (Kitty keyboard protocol)
|
|
919
|
+
kittyKeyboardStack.push(kittyKeyboardFlags)
|
|
920
|
+
kittyKeyboardFlags = parts[0] ?? 0
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function handleCSILt(_params: string, _intermediates: string, finalByte: string): void {
|
|
925
|
+
// CSI < sequences
|
|
926
|
+
if (finalByte === "u") {
|
|
927
|
+
// CSI < u — Pop keyboard mode (Kitty keyboard protocol)
|
|
928
|
+
kittyKeyboardFlags = kittyKeyboardStack.pop() ?? 0
|
|
671
929
|
}
|
|
672
930
|
}
|
|
673
931
|
|
|
674
932
|
function handleCSIPrivate(params: string, intermediates: string, finalByte: string): void {
|
|
675
933
|
const parts = params.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)))
|
|
676
934
|
|
|
935
|
+
// CSI ? u — Query keyboard mode (Kitty keyboard protocol)
|
|
936
|
+
if (finalByte === "u") {
|
|
937
|
+
if (onResponse) {
|
|
938
|
+
onResponse(`\x1b[?${kittyKeyboardFlags}u`)
|
|
939
|
+
}
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
|
|
677
943
|
// DECRPM - Mode reporting: CSI ? Pd $ p
|
|
678
944
|
if (intermediates === "$" && finalByte === "p") {
|
|
679
945
|
if (onResponse) {
|
|
@@ -756,9 +1022,11 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
756
1022
|
if (set && !useAltScreen) {
|
|
757
1023
|
useAltScreen = true
|
|
758
1024
|
grid = altGrid
|
|
1025
|
+
softWrapped = altSoftWrapped
|
|
759
1026
|
} else if (!set && useAltScreen) {
|
|
760
1027
|
useAltScreen = false
|
|
761
1028
|
grid = mainGrid
|
|
1029
|
+
softWrapped = mainSoftWrapped
|
|
762
1030
|
}
|
|
763
1031
|
break
|
|
764
1032
|
case 66: // DECNKM - Application Keypad
|
|
@@ -788,12 +1056,15 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
788
1056
|
savedCurY = curY
|
|
789
1057
|
useAltScreen = true
|
|
790
1058
|
altGrid = makeGrid(cols, rows)
|
|
1059
|
+
altSoftWrapped = new Array(rows).fill(false)
|
|
791
1060
|
grid = altGrid
|
|
1061
|
+
softWrapped = altSoftWrapped
|
|
792
1062
|
curX = 0
|
|
793
1063
|
curY = 0
|
|
794
1064
|
} else if (!set && useAltScreen) {
|
|
795
1065
|
useAltScreen = false
|
|
796
1066
|
grid = mainGrid
|
|
1067
|
+
softWrapped = mainSoftWrapped
|
|
797
1068
|
curX = savedCurX
|
|
798
1069
|
curY = savedCurY
|
|
799
1070
|
clampCursor()
|
|
@@ -1181,6 +1452,25 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1181
1452
|
case 2: // Set window title
|
|
1182
1453
|
title = value
|
|
1183
1454
|
break
|
|
1455
|
+
case 133: {
|
|
1456
|
+
// Semantic prompt markers (FinalTerm / shell integration)
|
|
1457
|
+
// Value format: "X" or "X;params" where X is A/B/C/D
|
|
1458
|
+
const marker = value.charAt(0)
|
|
1459
|
+
switch (marker) {
|
|
1460
|
+
case "A": // Start of prompt
|
|
1461
|
+
semanticZones.push({ type: "prompt", startRow: curY, startCol: curX })
|
|
1462
|
+
break
|
|
1463
|
+
case "B": // End of prompt / start of command
|
|
1464
|
+
semanticZones.push({ type: "command", startRow: curY, startCol: curX })
|
|
1465
|
+
break
|
|
1466
|
+
case "C": // End of command / start of output
|
|
1467
|
+
semanticZones.push({ type: "output", startRow: curY, startCol: curX })
|
|
1468
|
+
break
|
|
1469
|
+
case "D": // End of output (exit code in params, ignored for storage)
|
|
1470
|
+
break
|
|
1471
|
+
}
|
|
1472
|
+
break
|
|
1473
|
+
}
|
|
1184
1474
|
case 1: // Set icon name (ignore)
|
|
1185
1475
|
break
|
|
1186
1476
|
case 8: {
|
|
@@ -1233,6 +1523,50 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1233
1523
|
}
|
|
1234
1524
|
}
|
|
1235
1525
|
|
|
1526
|
+
// ── DCS handler ──
|
|
1527
|
+
|
|
1528
|
+
function handleDCS(data: string): void {
|
|
1529
|
+
// Sixel graphics: DCS [Ps;Ps;Ps] q [sixel-data]
|
|
1530
|
+
const match = data.match(/^(\d*(?:;\d*)*)q(.*)$/s)
|
|
1531
|
+
if (match) {
|
|
1532
|
+
hasSixel = true
|
|
1533
|
+
sixelImages.push({
|
|
1534
|
+
data: match[2]!,
|
|
1535
|
+
row: dcsStartRow,
|
|
1536
|
+
col: dcsStartCol,
|
|
1537
|
+
})
|
|
1538
|
+
return
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Other DCS sequences are consumed and ignored
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// ── APC handler ──
|
|
1545
|
+
|
|
1546
|
+
function handleAPC(data: string): void {
|
|
1547
|
+
if (!data.startsWith("G")) return
|
|
1548
|
+
|
|
1549
|
+
hasKittyGraphics = true
|
|
1550
|
+
|
|
1551
|
+
// Parse key=value pairs
|
|
1552
|
+
const semicolonIdx = data.indexOf(";")
|
|
1553
|
+
const kvPart = semicolonIdx >= 0 ? data.substring(1, semicolonIdx) : data.substring(1)
|
|
1554
|
+
|
|
1555
|
+
const params: Record<string, string> = {}
|
|
1556
|
+
for (const pair of kvPart.split(",")) {
|
|
1557
|
+
const eqIdx = pair.indexOf("=")
|
|
1558
|
+
if (eqIdx >= 0) {
|
|
1559
|
+
params[pair.substring(0, eqIdx)] = pair.substring(eqIdx + 1)
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Handle query action
|
|
1564
|
+
if (params.a === "q" && onResponse) {
|
|
1565
|
+
// Respond: OK
|
|
1566
|
+
onResponse(`\x1b_Gi=${params.i ?? "0"};OK\x1b\\`)
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1236
1570
|
// ── Soft reset (DECSTR) ──
|
|
1237
1571
|
|
|
1238
1572
|
function softReset(): void {
|
|
@@ -1241,6 +1575,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1241
1575
|
originMode = false
|
|
1242
1576
|
autoWrap = true
|
|
1243
1577
|
curVisible = true
|
|
1578
|
+
kittyKeyboardFlags = 0
|
|
1579
|
+
kittyKeyboardStack = []
|
|
1244
1580
|
cursorShape = "block"
|
|
1245
1581
|
cursorBlinking = true
|
|
1246
1582
|
applicationCursor = false
|
|
@@ -1292,15 +1628,27 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1292
1628
|
insertMode = false
|
|
1293
1629
|
reverseVideo = false
|
|
1294
1630
|
syncOutput = false
|
|
1631
|
+
kittyKeyboardFlags = 0
|
|
1632
|
+
kittyKeyboardStack = []
|
|
1633
|
+
hasKittyGraphics = false
|
|
1634
|
+
hasSixel = false
|
|
1635
|
+
sixelImages = []
|
|
1295
1636
|
scrollTop = 0
|
|
1296
1637
|
scrollBottom = rows - 1
|
|
1297
1638
|
viewportOffset = 0
|
|
1298
1639
|
charsetG0 = false
|
|
1299
1640
|
clipboard = ""
|
|
1300
1641
|
lastChar = ""
|
|
1642
|
+
pendingRegionalIndicator = null
|
|
1643
|
+
afterZWJ = false
|
|
1301
1644
|
parserState = "ground"
|
|
1302
1645
|
escBuf = ""
|
|
1303
1646
|
oscBuf = ""
|
|
1647
|
+
apcBuf = ""
|
|
1648
|
+
semanticZones = []
|
|
1649
|
+
mainSoftWrapped = new Array(rows).fill(false)
|
|
1650
|
+
altSoftWrapped = new Array(rows).fill(false)
|
|
1651
|
+
softWrapped = mainSoftWrapped
|
|
1304
1652
|
}
|
|
1305
1653
|
|
|
1306
1654
|
// ── Main parser ──
|
|
@@ -1326,7 +1674,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1326
1674
|
// TAB
|
|
1327
1675
|
curX = Math.min((Math.floor(curX / 8) + 1) * 8, cols - 1)
|
|
1328
1676
|
} else if (code === 0x0a || code === 0x0b || code === 0x0c) {
|
|
1329
|
-
// LF, VT, FF — linefeed
|
|
1677
|
+
// LF, VT, FF — linefeed (hard break — clear any soft-wrap flag)
|
|
1678
|
+
softWrapped[curY] = false
|
|
1330
1679
|
curY++
|
|
1331
1680
|
if (curY > scrollBottom) {
|
|
1332
1681
|
curY = scrollBottom
|
|
@@ -1359,6 +1708,9 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1359
1708
|
} else if (ch === "P") {
|
|
1360
1709
|
parserState = "dcs"
|
|
1361
1710
|
escBuf = ""
|
|
1711
|
+
dcsBuf = ""
|
|
1712
|
+
dcsStartRow = curY
|
|
1713
|
+
dcsStartCol = curX
|
|
1362
1714
|
} else if (ch === "c") {
|
|
1363
1715
|
// RIS - Reset to Initial State
|
|
1364
1716
|
fullReset()
|
|
@@ -1425,6 +1777,10 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1425
1777
|
} else if (ch === ")") {
|
|
1426
1778
|
// Designate G1 character set (ignored, just consume next byte)
|
|
1427
1779
|
parserState = "escape_charset"
|
|
1780
|
+
} else if (ch === "_") {
|
|
1781
|
+
// APC - Application Program Command
|
|
1782
|
+
parserState = "apc"
|
|
1783
|
+
apcBuf = ""
|
|
1428
1784
|
} else {
|
|
1429
1785
|
// Unknown escape — return to ground
|
|
1430
1786
|
parserState = "ground"
|
|
@@ -1468,6 +1824,8 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1468
1824
|
handleCSIPrivate(paramPart.substring(1), intermediatePart, ch)
|
|
1469
1825
|
} else if (paramPart.startsWith(">")) {
|
|
1470
1826
|
handleCSIGt(paramPart.substring(1), intermediatePart, ch)
|
|
1827
|
+
} else if (paramPart.startsWith("<")) {
|
|
1828
|
+
handleCSILt(paramPart.substring(1), intermediatePart, ch)
|
|
1471
1829
|
} else {
|
|
1472
1830
|
handleCSI(paramPart, intermediatePart, ch)
|
|
1473
1831
|
}
|
|
@@ -1504,19 +1862,23 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1504
1862
|
break
|
|
1505
1863
|
|
|
1506
1864
|
case "dcs":
|
|
1507
|
-
//
|
|
1865
|
+
// Accumulate DCS data until ST (ESC \) or BEL
|
|
1508
1866
|
if (code === 0x1b) {
|
|
1509
1867
|
parserState = "dcs_st"
|
|
1510
1868
|
} else if (code === 0x07) {
|
|
1511
|
-
//
|
|
1869
|
+
// BEL terminates DCS
|
|
1870
|
+
handleDCS(dcsBuf)
|
|
1512
1871
|
parserState = "ground"
|
|
1872
|
+
} else {
|
|
1873
|
+
dcsBuf += ch
|
|
1513
1874
|
}
|
|
1514
1875
|
break
|
|
1515
1876
|
|
|
1516
1877
|
case "dcs_st":
|
|
1517
1878
|
// Expecting backslash to complete ST
|
|
1518
1879
|
if (ch === "\\") {
|
|
1519
|
-
// DCS
|
|
1880
|
+
// ST (String Terminator) — end of DCS
|
|
1881
|
+
handleDCS(dcsBuf)
|
|
1520
1882
|
}
|
|
1521
1883
|
parserState = "ground"
|
|
1522
1884
|
break
|
|
@@ -1527,22 +1889,167 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1527
1889
|
parserState = "dcs_st"
|
|
1528
1890
|
}
|
|
1529
1891
|
break
|
|
1892
|
+
|
|
1893
|
+
case "apc":
|
|
1894
|
+
if (code === 0x1b) {
|
|
1895
|
+
parserState = "apc_st"
|
|
1896
|
+
} else if (code === 0x07) {
|
|
1897
|
+
// BEL terminates APC
|
|
1898
|
+
handleAPC(apcBuf)
|
|
1899
|
+
parserState = "ground"
|
|
1900
|
+
} else {
|
|
1901
|
+
apcBuf += ch
|
|
1902
|
+
}
|
|
1903
|
+
break
|
|
1904
|
+
|
|
1905
|
+
case "apc_st":
|
|
1906
|
+
if (ch === "\\") {
|
|
1907
|
+
// ST (String Terminator) — end of APC
|
|
1908
|
+
handleAPC(apcBuf)
|
|
1909
|
+
}
|
|
1910
|
+
parserState = "ground"
|
|
1911
|
+
break
|
|
1530
1912
|
}
|
|
1531
1913
|
}
|
|
1532
1914
|
}
|
|
1533
1915
|
|
|
1534
1916
|
// ── Resize ──
|
|
1535
1917
|
|
|
1918
|
+
/**
|
|
1919
|
+
* Reconstruct logical lines from a grid, joining rows that were soft-wrapped.
|
|
1920
|
+
* Returns an array of logical lines, each being an array of ScreenCells (may be longer than cols).
|
|
1921
|
+
*/
|
|
1922
|
+
function getLogicalLines(srcGrid: ScreenCell[][], srcSoftWrapped: boolean[], srcRows: number): ScreenCell[][] {
|
|
1923
|
+
const logical: ScreenCell[][] = []
|
|
1924
|
+
let currentLine: ScreenCell[] = []
|
|
1925
|
+
|
|
1926
|
+
for (let r = 0; r < srcRows; r++) {
|
|
1927
|
+
const row = srcGrid[r]
|
|
1928
|
+
if (!row) continue
|
|
1929
|
+
// Append this row's cells to the current logical line
|
|
1930
|
+
for (let c = 0; c < row.length; c++) {
|
|
1931
|
+
currentLine.push(row[c]!)
|
|
1932
|
+
}
|
|
1933
|
+
if (srcSoftWrapped[r]) {
|
|
1934
|
+
// This row was soft-wrapped — continue accumulating into same logical line
|
|
1935
|
+
continue
|
|
1936
|
+
}
|
|
1937
|
+
// Hard break (or last row): finalize this logical line
|
|
1938
|
+
logical.push(currentLine)
|
|
1939
|
+
currentLine = []
|
|
1940
|
+
}
|
|
1941
|
+
// If there's a dangling line (shouldn't happen, but be safe)
|
|
1942
|
+
if (currentLine.length > 0) {
|
|
1943
|
+
logical.push(currentLine)
|
|
1944
|
+
}
|
|
1945
|
+
return logical
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Re-wrap logical lines to a new column width, producing grid rows and soft-wrap flags.
|
|
1950
|
+
*/
|
|
1951
|
+
function rewrapLines(logicalLines: ScreenCell[][], newCols: number): { rows: ScreenCell[][]; wrapped: boolean[] } {
|
|
1952
|
+
const outRows: ScreenCell[][] = []
|
|
1953
|
+
const outWrapped: boolean[] = []
|
|
1954
|
+
|
|
1955
|
+
for (const line of logicalLines) {
|
|
1956
|
+
// Trim trailing empty cells from logical line
|
|
1957
|
+
let lineLen = line.length
|
|
1958
|
+
while (lineLen > 0) {
|
|
1959
|
+
const cell = line[lineLen - 1]!
|
|
1960
|
+
if (cell === EMPTY_CELL || (cell.char === "" && !cell.wide)) {
|
|
1961
|
+
lineLen--
|
|
1962
|
+
} else {
|
|
1963
|
+
break
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (lineLen === 0) {
|
|
1968
|
+
// Empty logical line — produce one empty row
|
|
1969
|
+
outRows.push(makeRow(newCols))
|
|
1970
|
+
outWrapped.push(false)
|
|
1971
|
+
continue
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Wrap the logical line content into rows of newCols width
|
|
1975
|
+
let pos = 0
|
|
1976
|
+
while (pos < lineLen) {
|
|
1977
|
+
const row = makeRow(newCols)
|
|
1978
|
+
let col = 0
|
|
1979
|
+
while (col < newCols && pos < lineLen) {
|
|
1980
|
+
const cell = line[pos]!
|
|
1981
|
+
if (cell.wide && col + 2 > newCols) {
|
|
1982
|
+
// Wide char doesn't fit — leave rest of row empty, wrap to next
|
|
1983
|
+
break
|
|
1984
|
+
}
|
|
1985
|
+
row[col] = cell === EMPTY_CELL ? EMPTY_CELL : { ...cell }
|
|
1986
|
+
col++
|
|
1987
|
+
pos++
|
|
1988
|
+
// If cell was wide, the next cell in the logical line is the spacer
|
|
1989
|
+
// which we already advanced past via pos++
|
|
1990
|
+
}
|
|
1991
|
+
const moreContent = pos < lineLen
|
|
1992
|
+
outRows.push(row)
|
|
1993
|
+
outWrapped.push(moreContent) // soft-wrapped if there's more content to come
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
return { rows: outRows, wrapped: outWrapped }
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* Trim trailing empty rows from reflowed result, so they don't push content off the top
|
|
2002
|
+
* when we take the last newRows rows.
|
|
2003
|
+
*/
|
|
2004
|
+
function trimTrailingEmptyRows(result: { rows: ScreenCell[][]; wrapped: boolean[] }): void {
|
|
2005
|
+
while (result.rows.length > 1) {
|
|
2006
|
+
const lastRow = result.rows[result.rows.length - 1]!
|
|
2007
|
+
const isEmpty = lastRow.every((cell) => cell === EMPTY_CELL || (cell.char === "" && !cell.wide))
|
|
2008
|
+
if (isEmpty && !result.wrapped[result.rows.length - 2]) {
|
|
2009
|
+
// The row before wasn't soft-wrapped and this row is empty — trim it
|
|
2010
|
+
result.rows.pop()
|
|
2011
|
+
result.wrapped.pop()
|
|
2012
|
+
} else {
|
|
2013
|
+
break
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1536
2018
|
function resize(newCols: number, newRows: number): void {
|
|
2019
|
+
// Reflow main grid
|
|
2020
|
+
const mainLogical = getLogicalLines(mainGrid, mainSoftWrapped, rows)
|
|
2021
|
+
const mainResult = rewrapLines(mainLogical, newCols)
|
|
2022
|
+
trimTrailingEmptyRows(mainResult)
|
|
2023
|
+
|
|
2024
|
+
// Reflow alt grid (usually not reflowed, but do it for consistency)
|
|
2025
|
+
const altLogical = getLogicalLines(altGrid, altSoftWrapped, rows)
|
|
2026
|
+
const altResult = rewrapLines(altLogical, newCols)
|
|
2027
|
+
trimTrailingEmptyRows(altResult)
|
|
2028
|
+
|
|
2029
|
+
// Build new grids: if reflowed content fits, place at top; if it overflows, take the last newRows
|
|
1537
2030
|
const newMain = makeGrid(newCols, newRows)
|
|
1538
|
-
const
|
|
2031
|
+
const newMainWrapped: boolean[] = new Array(newRows).fill(false)
|
|
2032
|
+
const mainStartRow = Math.max(0, mainResult.rows.length - newRows)
|
|
2033
|
+
for (let r = 0; r < newRows && mainStartRow + r < mainResult.rows.length; r++) {
|
|
2034
|
+
newMain[r] = mainResult.rows[mainStartRow + r]!
|
|
2035
|
+
newMainWrapped[r] = mainResult.wrapped[mainStartRow + r]!
|
|
2036
|
+
}
|
|
1539
2037
|
|
|
1540
|
-
|
|
1541
|
-
|
|
2038
|
+
// Build new alt grid
|
|
2039
|
+
const newAlt = makeGrid(newCols, newRows)
|
|
2040
|
+
const newAltWrapped: boolean[] = new Array(newRows).fill(false)
|
|
2041
|
+
const altStartRow = Math.max(0, altResult.rows.length - newRows)
|
|
2042
|
+
for (let r = 0; r < newRows && altStartRow + r < altResult.rows.length; r++) {
|
|
2043
|
+
newAlt[r] = altResult.rows[altStartRow + r]!
|
|
2044
|
+
newAltWrapped[r] = altResult.wrapped[altStartRow + r]!
|
|
2045
|
+
}
|
|
1542
2046
|
|
|
1543
2047
|
mainGrid = newMain
|
|
1544
2048
|
altGrid = newAlt
|
|
2049
|
+
mainSoftWrapped = newMainWrapped
|
|
2050
|
+
altSoftWrapped = newAltWrapped
|
|
1545
2051
|
grid = useAltScreen ? altGrid : mainGrid
|
|
2052
|
+
softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped
|
|
1546
2053
|
cols = newCols
|
|
1547
2054
|
rows = newRows
|
|
1548
2055
|
scrollTop = 0
|
|
@@ -1550,17 +2057,6 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1550
2057
|
clampCursor()
|
|
1551
2058
|
}
|
|
1552
2059
|
|
|
1553
|
-
function copyGrid(src: ScreenCell[][], dst: ScreenCell[][], copyCols: number, copyRows: number): void {
|
|
1554
|
-
for (let row = 0; row < copyRows; row++) {
|
|
1555
|
-
for (let col = 0; col < copyCols; col++) {
|
|
1556
|
-
const srcCell = src[row]?.[col]
|
|
1557
|
-
if (srcCell) {
|
|
1558
|
-
dst[row]![col] = { ...srcCell }
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
2060
|
// ── Accessors ──
|
|
1565
2061
|
|
|
1566
2062
|
function getCell(row: number, col: number): ScreenCell {
|
|
@@ -1652,6 +2148,12 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1652
2148
|
return syncOutput
|
|
1653
2149
|
case "sgrMouse":
|
|
1654
2150
|
return sgrMouse
|
|
2151
|
+
case "kittyKeyboard":
|
|
2152
|
+
return kittyKeyboardFlags > 0
|
|
2153
|
+
case "kittyGraphics":
|
|
2154
|
+
return hasKittyGraphics
|
|
2155
|
+
case "sixel":
|
|
2156
|
+
return hasSixel
|
|
1655
2157
|
default:
|
|
1656
2158
|
return false
|
|
1657
2159
|
}
|
|
@@ -1685,5 +2187,7 @@ export function createScreen(options: ScreenOptions = {}): Screen {
|
|
|
1685
2187
|
scrollViewport: (delta: number) => {
|
|
1686
2188
|
viewportOffset = Math.max(0, Math.min(scrollback.length, viewportOffset + delta))
|
|
1687
2189
|
},
|
|
2190
|
+
getSemanticZones: () => semanticZones.map((z) => ({ ...z })),
|
|
2191
|
+
getSixelImages: () => sixelImages.map((img) => ({ ...img })),
|
|
1688
2192
|
}
|
|
1689
2193
|
}
|