vterm.js 0.6.1 → 0.7.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 +61 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +272 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -435,6 +435,67 @@ The **reactive** counterpart is `signals.damage$` (see [Signals](#signals--the-r
|
|
|
435
435
|
|
|
436
436
|
**Use vterm.js** when you need everything — 100% coverage of the [terminfo.dev](https://terminfo.dev) feature matrix.
|
|
437
437
|
|
|
438
|
+
## vterm.js and xterm.js
|
|
439
|
+
|
|
440
|
+
[xterm.js](https://www.npmjs.com/package/@xterm/xterm) is the standard for putting a terminal on a web page — DOM, canvas, and WebGL renderers, a large addon ecosystem, and the terminal inside VS Code. When the job is rendering a terminal to a human, reach for xterm.js. We do: [Termless](https://termless.dev)'s own `@termless/web-player`, which plays back recorded terminal sessions in the browser, is built directly on `@xterm/xterm`.
|
|
441
|
+
|
|
442
|
+
vterm.js is for the opposite case: nothing ever gets painted. It's headless-first — the terminal as a **readable data structure**, not a rendered surface. Zero dependencies, pure TypeScript, full VT/ECMA-48/xterm standards coverage, a binary snapshot codec for exact state capture, a reactive read plane (`screen.signals`), absolute-row scrollback addressing, and semantic-zone detection (OSC 133 / OSC 633 prompt/command/output). That combination is aimed at testing, session recording, audit trails, and AI-agent infrastructure — consumers that need to know exactly what a terminal contains without ever drawing it.
|
|
443
|
+
|
|
444
|
+
Because vterm.js makes no display claims, it's held to a stricter standard instead: engine conformance, graded publicly and continuously in [Termless](https://termless.dev)'s differential conformance corpus against xterm.js, Ghostty, and neovim's libvterm. Current per-engine results and every known gap live in the ratchet ledger (`known-gaps.json`) — it's two-way: an unlisted failure fails the build, and a listed gap that starts passing fails too. Full results are public at [termless.dev/advanced/conformance-corpus](https://termless.dev/advanced/conformance-corpus).
|
|
445
|
+
|
|
446
|
+
## Deliberate divergences
|
|
447
|
+
|
|
448
|
+
A handful of behaviours where vterm.js knowingly differs from neovim's
|
|
449
|
+
libvterm, the reference implementation the conformance corpus is mined from.
|
|
450
|
+
Each was ruled deliberately rather than left as a gap, so **100% conformance
|
|
451
|
+
for vterm.js means 126/128 plus these documented divergences** — not 128.
|
|
452
|
+
|
|
453
|
+
Each entry names the _override word_ that would reverse the ruling. If you need
|
|
454
|
+
the other behaviour, open an issue with that word and the reason; it is the
|
|
455
|
+
argument, not the preference, that would move it.
|
|
456
|
+
|
|
457
|
+
### Cursor follows reflow when a resize shrinks below it
|
|
458
|
+
|
|
459
|
+
Shrinking the terminal past the cursor's column carries the cursor with its
|
|
460
|
+
line through the rewrap. libvterm, xterm.js and vt100.js instead clamp it to
|
|
461
|
+
the last column of the row it was on.
|
|
462
|
+
|
|
463
|
+
Following the line is what a reflowing terminal does — the cursor stays with
|
|
464
|
+
the text a user was typing, rather than jumping to wherever that text used to
|
|
465
|
+
end. Clamping is the pre-reflow behaviour the other three inherit from a
|
|
466
|
+
shared ancestry, and three engines agreeing about their own history is not an
|
|
467
|
+
argument about which is correct. Both ends of our own record/replay path read
|
|
468
|
+
vterm.js, so there is no internal disagreement to resolve.
|
|
469
|
+
|
|
470
|
+
**Override word: `clamp`.**
|
|
471
|
+
|
|
472
|
+
### RIS clears the scrollback
|
|
473
|
+
|
|
474
|
+
A full reset (`ESC c`) discards scrollback along with the screen. libvterm
|
|
475
|
+
preserves it, so history banked before a reset can still be scrolled back to —
|
|
476
|
+
and can even be popped back onto the screen by a later resize.
|
|
477
|
+
|
|
478
|
+
Every other engine in the corpus — xterm.js, Ghostty and vt100.js — clears on
|
|
479
|
+
RIS, as does xterm itself. A reset that leaves history behind surprises the
|
|
480
|
+
common case, where `reset` is reached for precisely to get a clean terminal.
|
|
481
|
+
|
|
482
|
+
**Override word: `match-ris`.**
|
|
483
|
+
|
|
484
|
+
### Related: the deferred-wrap cursor
|
|
485
|
+
|
|
486
|
+
Not an excluded case, but the same family and worth knowing if you read the
|
|
487
|
+
cursor. When a glyph fills the last column with autowrap on, the terminal owes
|
|
488
|
+
a wrap it has not performed. vterm.js represents that as the cursor sitting at
|
|
489
|
+
`col == cols` — one past the last column — as xterm.js and vt100.js do.
|
|
490
|
+
libvterm and Ghostty instead keep the cursor _on_ the last column and carry a
|
|
491
|
+
separate pending-wrap flag. The corpus normalizes this rather than excluding
|
|
492
|
+
it, so those cases pass; consumers reading `cursor.col` should simply expect
|
|
493
|
+
the `col == cols` form.
|
|
494
|
+
|
|
495
|
+
These entries mirror the corpus-side table in the suite's own README and the
|
|
496
|
+
machine-readable reasons in `known-gaps.json`. A new ruling lands in all three
|
|
497
|
+
at once.
|
|
498
|
+
|
|
438
499
|
## See also
|
|
439
500
|
|
|
440
501
|
- [vt100.js](../vt100/) — VT100-era emulator (smaller, focused)
|
package/dist/index.d.mts
CHANGED
|
@@ -77,6 +77,8 @@ interface ScreenCell {
|
|
|
77
77
|
hidden: boolean;
|
|
78
78
|
blink: boolean;
|
|
79
79
|
wide: boolean;
|
|
80
|
+
/** DECSCA: a selective erase (DECSED/DECSEL) leaves this cell alone. */
|
|
81
|
+
protected: boolean;
|
|
80
82
|
url: string | null;
|
|
81
83
|
}
|
|
82
84
|
interface ScreenOptions {
|
|
@@ -118,6 +120,8 @@ interface ScreenAttrsSnapshot {
|
|
|
118
120
|
inverse: boolean;
|
|
119
121
|
hidden: boolean;
|
|
120
122
|
blink: boolean;
|
|
123
|
+
/** DECSCA — cells written under it resist selective erase. */
|
|
124
|
+
protected: boolean;
|
|
121
125
|
url: string | null;
|
|
122
126
|
}
|
|
123
127
|
interface ScreenColorStateSnapshot {
|
|
@@ -164,6 +168,26 @@ interface Snapshot {
|
|
|
164
168
|
* `restore()` treats missing as all-false (hard-wrapped).
|
|
165
169
|
*/
|
|
166
170
|
scrollbackSoftWrapped: boolean[];
|
|
171
|
+
/**
|
|
172
|
+
* DEC line attributes (DECDWL/DECDHL), SPARSE: only rows that are not normal
|
|
173
|
+
* appear, as `[rowIndex, attr]` pairs — 1 double-width, 2 double-height top,
|
|
174
|
+
* 3 double-height bottom.
|
|
175
|
+
*
|
|
176
|
+
* Sparse and header-resident on purpose. Double-width lines are a banner
|
|
177
|
+
* feature that almost no session emits, so a dense array per grid would be
|
|
178
|
+
* thousands of zeros; and the binary grid sections have no skippable framing,
|
|
179
|
+
* so a new array THERE would desynchronise an older reader rather than be
|
|
180
|
+
* ignored by it.
|
|
181
|
+
*
|
|
182
|
+
* Absent in snapshots taken before this field existed — `restore()` treats
|
|
183
|
+
* missing as all-normal. **Readers older than this field render DWL/DHL lines
|
|
184
|
+
* single-width**: a visual downgrade, never corruption.
|
|
185
|
+
*/
|
|
186
|
+
lineAttrs?: {
|
|
187
|
+
main: [number, number][];
|
|
188
|
+
alt: [number, number][];
|
|
189
|
+
scrollback: [number, number][];
|
|
190
|
+
};
|
|
167
191
|
cursor: {
|
|
168
192
|
x: number;
|
|
169
193
|
y: number;
|
|
@@ -193,6 +217,8 @@ interface Snapshot {
|
|
|
193
217
|
focusTracking: boolean;
|
|
194
218
|
origin: boolean;
|
|
195
219
|
insert: boolean;
|
|
220
|
+
/** LNM — LF also returns the carriage. Absent in older snapshots. */
|
|
221
|
+
newLine: boolean;
|
|
196
222
|
reverseVideo: boolean;
|
|
197
223
|
syncOutput: boolean;
|
|
198
224
|
kittyKeyboardFlags: number;
|
|
@@ -402,6 +428,8 @@ interface TerminalModes {
|
|
|
402
428
|
focusTracking: boolean;
|
|
403
429
|
originMode: boolean;
|
|
404
430
|
insertMode: boolean;
|
|
431
|
+
/** LNM — LF also returns the carriage (ECMA-48 mode 20). */
|
|
432
|
+
newLineMode: boolean;
|
|
405
433
|
reverseVideo: boolean;
|
|
406
434
|
syncOutput: boolean;
|
|
407
435
|
leftRightMargin: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const EMPTY_CELL = Object.freeze({
|
|
|
17
17
|
hidden: false,
|
|
18
18
|
blink: false,
|
|
19
19
|
wide: false,
|
|
20
|
+
protected: false,
|
|
20
21
|
url: null
|
|
21
22
|
});
|
|
22
23
|
function emptyCell() {
|
|
@@ -31,7 +32,7 @@ function emptyCell() {
|
|
|
31
32
|
* case), so the read path allocates nothing there.
|
|
32
33
|
*/
|
|
33
34
|
function stripColorIndex(c) {
|
|
34
|
-
if (c
|
|
35
|
+
if (c?.index === void 0) return c;
|
|
35
36
|
return {
|
|
36
37
|
r: c.r,
|
|
37
38
|
g: c.g,
|
|
@@ -62,6 +63,7 @@ const F_HAS_FG = 4096;
|
|
|
62
63
|
const F_HAS_BG = 8192;
|
|
63
64
|
const F_HAS_UL = 16384;
|
|
64
65
|
const F_HAS_URL = 32768;
|
|
66
|
+
const F_PROTECTED$1 = 65536;
|
|
65
67
|
const UL_STYLES = [
|
|
66
68
|
"none",
|
|
67
69
|
"single",
|
|
@@ -147,6 +149,7 @@ function makePackedRow(width) {
|
|
|
147
149
|
if (a.inverse) m |= F_INVERSE$1;
|
|
148
150
|
if (a.hidden) m |= F_HIDDEN$1;
|
|
149
151
|
if (a.blink) m |= F_BLINK$1;
|
|
152
|
+
if (a.protected) m |= F_PROTECTED$1;
|
|
150
153
|
if (wide) m |= F_WIDE$1;
|
|
151
154
|
return m | underlineToBits(a.underline);
|
|
152
155
|
}
|
|
@@ -234,6 +237,7 @@ function makePackedRow(width) {
|
|
|
234
237
|
inverse: (m & F_INVERSE$1) !== 0,
|
|
235
238
|
hidden: (m & F_HIDDEN$1) !== 0,
|
|
236
239
|
blink: (m & F_BLINK$1) !== 0,
|
|
240
|
+
protected: (m & F_PROTECTED$1) !== 0,
|
|
237
241
|
wide: (m & F_WIDE$1) !== 0,
|
|
238
242
|
url: m & F_HAS_URL ? urlMap.get(col) ?? null : null
|
|
239
243
|
};
|
|
@@ -497,6 +501,7 @@ function createScreen(options = {}) {
|
|
|
497
501
|
let focusTracking = false;
|
|
498
502
|
let originMode = false;
|
|
499
503
|
let insertMode = false;
|
|
504
|
+
let newLineMode = false;
|
|
500
505
|
let reverseVideo = false;
|
|
501
506
|
let syncOutput = false;
|
|
502
507
|
let kittyKeyboardFlags = 0;
|
|
@@ -778,7 +783,68 @@ function createScreen(options = {}) {
|
|
|
778
783
|
let mainSoftWrapped = new Array(rows).fill(false);
|
|
779
784
|
let altSoftWrapped = new Array(rows).fill(false);
|
|
780
785
|
let softWrapped = mainSoftWrapped;
|
|
786
|
+
let mainLineAttrs = new Array(rows).fill(0);
|
|
787
|
+
let altLineAttrs = new Array(rows).fill(0);
|
|
788
|
+
let lineAttrs = mainLineAttrs;
|
|
781
789
|
let scrollbackSoftWrapped = [];
|
|
790
|
+
let scrollbackLineAttrs = [];
|
|
791
|
+
/**
|
|
792
|
+
* Drop every banked scrollback row AND its soft-wrap flag.
|
|
793
|
+
*
|
|
794
|
+
* These two arrays are one data structure wearing two names, and nothing in
|
|
795
|
+
* the type system says so. `ESC [ 3 J` (erase display + scrollback) once
|
|
796
|
+
* emptied the rows and left the flags, which diverged the pair permanently
|
|
797
|
+
* by however many rows were banked at that instant. Every later push
|
|
798
|
+
* extended both and preserved the gap, so the snapshot codec refused to
|
|
799
|
+
* write a frame — `soft-wrap length 3580 != rowCount 2103` — and the seat
|
|
800
|
+
* kept running with its recording silently ended
|
|
801
|
+
* (@i/5-agent-loop/vterm-codec-ends-recording).
|
|
802
|
+
*
|
|
803
|
+
* `ESC [ 3 J` is not exotic: `clear` emits it on a modern terminal. Every
|
|
804
|
+
* clear goes through here so the pairing is structural rather than
|
|
805
|
+
* remembered at each call site.
|
|
806
|
+
*/
|
|
807
|
+
/** Dense per-row attributes -> sparse `[row, attr]` pairs, skipping normal rows. */
|
|
808
|
+
function toSparseAttrs(dense) {
|
|
809
|
+
const out = [];
|
|
810
|
+
for (let r = 0; r < dense.length; r++) {
|
|
811
|
+
const a = dense[r] ?? 0;
|
|
812
|
+
if (a !== 0) out.push([r, a]);
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
816
|
+
/** Sparse pairs -> a dense array of `length`; out-of-range rows are dropped. */
|
|
817
|
+
function fromSparseAttrs(sparse, length) {
|
|
818
|
+
const dense = new Array(length).fill(0);
|
|
819
|
+
for (const pair of sparse ?? []) {
|
|
820
|
+
const [r, a] = pair;
|
|
821
|
+
if (Number.isInteger(r) && r >= 0 && r < length) dense[r] = a;
|
|
822
|
+
}
|
|
823
|
+
return dense;
|
|
824
|
+
}
|
|
825
|
+
function clearScrollback() {
|
|
826
|
+
scrollback.length = 0;
|
|
827
|
+
scrollbackSoftWrapped.length = 0;
|
|
828
|
+
scrollbackLineAttrs.length = 0;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Bank one row into scrollback, trimming to the limit. Returns how many rows
|
|
832
|
+
* were trimmed off the front so a caller tracking absolute rows can rebase.
|
|
833
|
+
*
|
|
834
|
+
* Every scrollback mutation goes through a helper that moves BOTH arrays —
|
|
835
|
+
* see `clearScrollback` for what one unpaired mutation cost.
|
|
836
|
+
*/
|
|
837
|
+
function bankScrollbackRow(row, wrapped, lineAttr) {
|
|
838
|
+
scrollback.push(row);
|
|
839
|
+
scrollbackSoftWrapped.push(wrapped);
|
|
840
|
+
scrollbackLineAttrs.push(lineAttr);
|
|
841
|
+
if (scrollback.length <= scrollbackLimit * 2) return 0;
|
|
842
|
+
const over = scrollback.length - scrollbackLimit;
|
|
843
|
+
scrollback.splice(0, over);
|
|
844
|
+
scrollbackSoftWrapped.splice(0, over);
|
|
845
|
+
scrollbackLineAttrs.splice(0, over);
|
|
846
|
+
return over;
|
|
847
|
+
}
|
|
782
848
|
let lastChar = "";
|
|
783
849
|
let pendingRegionalIndicator = null;
|
|
784
850
|
let afterZWJ = false;
|
|
@@ -912,6 +978,16 @@ function createScreen(options = {}) {
|
|
|
912
978
|
fn(cells);
|
|
913
979
|
row.replaceAllFromCells(cells);
|
|
914
980
|
}
|
|
981
|
+
/**
|
|
982
|
+
* SGR 0 resets the pen but NOT DECSCA: character protection is independent
|
|
983
|
+
* of SGR, and is cleared only by DECSTR/RIS. Using the full `resetAttrs()`
|
|
984
|
+
* here would let any `ESC [ 0 m` silently unprotect subsequent writes.
|
|
985
|
+
*/
|
|
986
|
+
function resetSgrAttrs() {
|
|
987
|
+
const next = resetAttrs();
|
|
988
|
+
next.protected = attrs.protected;
|
|
989
|
+
return next;
|
|
990
|
+
}
|
|
915
991
|
function resetAttrs() {
|
|
916
992
|
return {
|
|
917
993
|
fg: null,
|
|
@@ -926,14 +1002,49 @@ function createScreen(options = {}) {
|
|
|
926
1002
|
inverse: false,
|
|
927
1003
|
hidden: false,
|
|
928
1004
|
blink: false,
|
|
929
|
-
url: null
|
|
1005
|
+
url: null,
|
|
1006
|
+
protected: false
|
|
930
1007
|
};
|
|
931
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Usable columns on a row. A DEC double-width line (DECDWL/DECDHL) renders
|
|
1011
|
+
* each cell twice as wide, so the row addresses and wraps at half the
|
|
1012
|
+
* terminal's columns — an 80-column screen wraps a double-width line at 40.
|
|
1013
|
+
*/
|
|
1014
|
+
function rowCols(row) {
|
|
1015
|
+
return (lineAttrs[row] ?? 0) === 0 ? cols : cols >> 1;
|
|
1016
|
+
}
|
|
932
1017
|
function clampCursor() {
|
|
933
1018
|
if (curX < 0) curX = 0;
|
|
934
|
-
if (curX >= cols) curX = cols - 1;
|
|
935
1019
|
if (curY < 0) curY = 0;
|
|
936
1020
|
if (curY >= rows) curY = rows - 1;
|
|
1021
|
+
const usable = rowCols(curY);
|
|
1022
|
+
if (curX >= usable) curX = usable - 1;
|
|
1023
|
+
if (originMode) {
|
|
1024
|
+
if (curY < scrollTop) curY = scrollTop;
|
|
1025
|
+
if (curY > scrollBottom) curY = scrollBottom;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Move the cursor down one line, scrolling ONLY at the region's bottom edge.
|
|
1030
|
+
*
|
|
1031
|
+
* `curY++; if (curY > scrollBottom) { curY = scrollBottom; scrollUp() }`
|
|
1032
|
+
* looks equivalent and is not. From a row BELOW the scrolling region it
|
|
1033
|
+
* clamps the cursor BACKWARDS into the region and scrolls a region the
|
|
1034
|
+
* cursor was never in — with DECSTBM at 1;10, a linefeed from row 19 landed
|
|
1035
|
+
* on row 9 instead of row 20.
|
|
1036
|
+
*
|
|
1037
|
+
* DECSTBM confines SCROLLING, not movement: a cursor outside the region
|
|
1038
|
+
* simply walks the screen.
|
|
1039
|
+
*/
|
|
1040
|
+
function lineFeedDown() {
|
|
1041
|
+
if (curY === scrollBottom) scrollUp(scrollTop, scrollBottom);
|
|
1042
|
+
else if (curY < rows - 1) curY++;
|
|
1043
|
+
}
|
|
1044
|
+
/** RI's mirror: scroll only at the region's top edge, else walk up. */
|
|
1045
|
+
function reverseIndexUp() {
|
|
1046
|
+
if (curY === scrollTop) scrollDown(scrollTop, scrollBottom);
|
|
1047
|
+
else if (curY > 0) curY--;
|
|
937
1048
|
}
|
|
938
1049
|
function scrollUp(top, bottom) {
|
|
939
1050
|
if (leftRightMarginMode && (leftMargin > 0 || rightMargin < cols - 1)) {
|
|
@@ -950,13 +1061,10 @@ function createScreen(options = {}) {
|
|
|
950
1061
|
} else {
|
|
951
1062
|
const enteredScrollback = grid === mainGrid && top === 0;
|
|
952
1063
|
if (enteredScrollback) {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
markAbsoluteRowDirty(scrollback.length - 1);
|
|
956
|
-
if (
|
|
957
|
-
const over = scrollback.length - scrollbackLimit;
|
|
958
|
-
scrollback.splice(0, over);
|
|
959
|
-
scrollbackSoftWrapped.splice(0, over);
|
|
1064
|
+
const banked = grid[0];
|
|
1065
|
+
const over = banked === void 0 ? 0 : bankScrollbackRow(banked, softWrapped[0] ?? false, lineAttrs[0] ?? 0);
|
|
1066
|
+
markAbsoluteRowDirty(scrollback.length - 1 + over);
|
|
1067
|
+
if (over > 0) {
|
|
960
1068
|
if (!dirtyAll && dirtyRows.size > 0) {
|
|
961
1069
|
const shifted = /* @__PURE__ */ new Set();
|
|
962
1070
|
for (const r of dirtyRows) if (r >= over) shifted.add(r - over);
|
|
@@ -1108,7 +1216,7 @@ function createScreen(options = {}) {
|
|
|
1108
1216
|
function writeCharCore(ch, wide) {
|
|
1109
1217
|
if (cols <= 0 || rows <= 0) return;
|
|
1110
1218
|
const charWidth = wide ? 2 : 1;
|
|
1111
|
-
const wrapBoundary = leftRightMarginMode ? rightMargin + 1 :
|
|
1219
|
+
const wrapBoundary = leftRightMarginMode ? rightMargin + 1 : rowCols(curY);
|
|
1112
1220
|
const wrapReturn = leftRightMarginMode ? leftMargin : 0;
|
|
1113
1221
|
if (curX + charWidth > wrapBoundary) if (autoWrap) {
|
|
1114
1222
|
softWrapped[curY] = true;
|
|
@@ -1133,6 +1241,7 @@ function createScreen(options = {}) {
|
|
|
1133
1241
|
if (wide) widenCell(row, curX);
|
|
1134
1242
|
markScreenRowDirty(curY);
|
|
1135
1243
|
curX += charWidth;
|
|
1244
|
+
if (!autoWrap && curX >= wrapBoundary) curX = wrapBoundary - 1;
|
|
1136
1245
|
lastChar = ch;
|
|
1137
1246
|
}
|
|
1138
1247
|
function handleCSI(params, intermediates, finalByte) {
|
|
@@ -1169,6 +1278,10 @@ function createScreen(options = {}) {
|
|
|
1169
1278
|
else if (finalByte === "A") handleShiftRight(Math.max(parts[0] ?? 1, 1));
|
|
1170
1279
|
return;
|
|
1171
1280
|
}
|
|
1281
|
+
if (intermediates === "\"" && finalByte === "q") {
|
|
1282
|
+
attrs.protected = (parts[0] ?? 0) === 1;
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1172
1285
|
if (intermediates === "!") {
|
|
1173
1286
|
if (finalByte === "p") softReset();
|
|
1174
1287
|
return;
|
|
@@ -1278,18 +1391,22 @@ function createScreen(options = {}) {
|
|
|
1278
1391
|
}
|
|
1279
1392
|
switch (finalByte) {
|
|
1280
1393
|
case "A":
|
|
1394
|
+
case "k":
|
|
1281
1395
|
curY -= Math.max(parts[0] ?? 1, 1);
|
|
1282
1396
|
clampCursor();
|
|
1283
1397
|
break;
|
|
1284
1398
|
case "B":
|
|
1399
|
+
case "e":
|
|
1285
1400
|
curY += Math.max(parts[0] ?? 1, 1);
|
|
1286
1401
|
clampCursor();
|
|
1287
1402
|
break;
|
|
1288
1403
|
case "C":
|
|
1404
|
+
case "a":
|
|
1289
1405
|
curX += Math.max(parts[0] ?? 1, 1);
|
|
1290
1406
|
clampCursor();
|
|
1291
1407
|
break;
|
|
1292
1408
|
case "D":
|
|
1409
|
+
case "j":
|
|
1293
1410
|
curX -= Math.max(parts[0] ?? 1, 1);
|
|
1294
1411
|
clampCursor();
|
|
1295
1412
|
break;
|
|
@@ -1351,12 +1468,14 @@ function createScreen(options = {}) {
|
|
|
1351
1468
|
case "H":
|
|
1352
1469
|
case "f":
|
|
1353
1470
|
if (originMode) {
|
|
1471
|
+
const originX = leftRightMarginMode ? leftMargin : 0;
|
|
1472
|
+
const limitX = leftRightMarginMode ? rightMargin : cols - 1;
|
|
1354
1473
|
curY = scrollTop + (parts[0] ?? 1) - 1;
|
|
1355
|
-
curX = (parts[1] ?? 1) - 1;
|
|
1474
|
+
curX = originX + (parts[1] ?? 1) - 1;
|
|
1356
1475
|
if (curY < scrollTop) curY = scrollTop;
|
|
1357
1476
|
if (curY > scrollBottom) curY = scrollBottom;
|
|
1358
|
-
if (curX <
|
|
1359
|
-
if (curX
|
|
1477
|
+
if (curX < originX) curX = originX;
|
|
1478
|
+
if (curX > limitX) curX = limitX;
|
|
1360
1479
|
} else {
|
|
1361
1480
|
curY = (parts[0] ?? 1) - 1;
|
|
1362
1481
|
curX = (parts[1] ?? 1) - 1;
|
|
@@ -1427,10 +1546,16 @@ function createScreen(options = {}) {
|
|
|
1427
1546
|
}
|
|
1428
1547
|
break;
|
|
1429
1548
|
case "h":
|
|
1430
|
-
for (const code of parts)
|
|
1549
|
+
for (const code of parts) {
|
|
1550
|
+
if (code === 4) insertMode = true;
|
|
1551
|
+
if (code === 20) newLineMode = true;
|
|
1552
|
+
}
|
|
1431
1553
|
break;
|
|
1432
1554
|
case "l":
|
|
1433
|
-
for (const code of parts)
|
|
1555
|
+
for (const code of parts) {
|
|
1556
|
+
if (code === 4) insertMode = false;
|
|
1557
|
+
if (code === 20) newLineMode = false;
|
|
1558
|
+
}
|
|
1434
1559
|
break;
|
|
1435
1560
|
case "s":
|
|
1436
1561
|
if (leftRightMarginMode && intermediates === "") {
|
|
@@ -1551,6 +1676,9 @@ function createScreen(options = {}) {
|
|
|
1551
1676
|
case 4:
|
|
1552
1677
|
value = insertMode ? 1 : 2;
|
|
1553
1678
|
break;
|
|
1679
|
+
case 20:
|
|
1680
|
+
value = newLineMode ? 1 : 2;
|
|
1681
|
+
break;
|
|
1554
1682
|
case 3:
|
|
1555
1683
|
value = decColumnMode ? 1 : 2;
|
|
1556
1684
|
break;
|
|
@@ -1577,11 +1705,11 @@ function createScreen(options = {}) {
|
|
|
1577
1705
|
return;
|
|
1578
1706
|
}
|
|
1579
1707
|
if (finalByte === "J") {
|
|
1580
|
-
handleEraseDisplay(parts[0] ?? 0);
|
|
1708
|
+
handleEraseDisplay(parts[0] ?? 0, true);
|
|
1581
1709
|
return;
|
|
1582
1710
|
}
|
|
1583
1711
|
if (finalByte === "K") {
|
|
1584
|
-
handleEraseLine(parts[0] ?? 0);
|
|
1712
|
+
handleEraseLine(parts[0] ?? 0, true);
|
|
1585
1713
|
return;
|
|
1586
1714
|
}
|
|
1587
1715
|
const set = finalByte === "h";
|
|
@@ -1610,11 +1738,13 @@ function createScreen(options = {}) {
|
|
|
1610
1738
|
useAltScreen = true;
|
|
1611
1739
|
grid = altGrid;
|
|
1612
1740
|
softWrapped = altSoftWrapped;
|
|
1741
|
+
lineAttrs = altLineAttrs;
|
|
1613
1742
|
markAllDirty();
|
|
1614
1743
|
} else if (!set && useAltScreen) {
|
|
1615
1744
|
useAltScreen = false;
|
|
1616
1745
|
grid = mainGrid;
|
|
1617
1746
|
softWrapped = mainSoftWrapped;
|
|
1747
|
+
lineAttrs = mainLineAttrs;
|
|
1618
1748
|
markAllDirty();
|
|
1619
1749
|
}
|
|
1620
1750
|
break;
|
|
@@ -1643,8 +1773,10 @@ function createScreen(options = {}) {
|
|
|
1643
1773
|
useAltScreen = true;
|
|
1644
1774
|
altGrid = makeGrid(cols, rows);
|
|
1645
1775
|
altSoftWrapped = new Array(rows).fill(false);
|
|
1776
|
+
altLineAttrs = new Array(rows).fill(0);
|
|
1646
1777
|
grid = altGrid;
|
|
1647
1778
|
softWrapped = altSoftWrapped;
|
|
1779
|
+
lineAttrs = altLineAttrs;
|
|
1648
1780
|
curX = 0;
|
|
1649
1781
|
curY = 0;
|
|
1650
1782
|
markAllDirty();
|
|
@@ -1652,6 +1784,7 @@ function createScreen(options = {}) {
|
|
|
1652
1784
|
useAltScreen = false;
|
|
1653
1785
|
grid = mainGrid;
|
|
1654
1786
|
softWrapped = mainSoftWrapped;
|
|
1787
|
+
lineAttrs = mainLineAttrs;
|
|
1655
1788
|
curX = savedCurX;
|
|
1656
1789
|
curY = savedCurY;
|
|
1657
1790
|
clampCursor();
|
|
@@ -1698,43 +1831,46 @@ function createScreen(options = {}) {
|
|
|
1698
1831
|
break;
|
|
1699
1832
|
}
|
|
1700
1833
|
}
|
|
1701
|
-
function handleEraseDisplay(mode) {
|
|
1834
|
+
function handleEraseDisplay(mode, selective = false) {
|
|
1702
1835
|
switch (mode) {
|
|
1703
1836
|
case 0:
|
|
1704
|
-
eraseCells(curY, curX, curY, cols - 1);
|
|
1705
|
-
for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1);
|
|
1837
|
+
eraseCells(curY, curX, curY, cols - 1, selective);
|
|
1838
|
+
for (let row = curY + 1; row < rows; row++) eraseCells(row, 0, row, cols - 1, selective);
|
|
1706
1839
|
break;
|
|
1707
1840
|
case 1:
|
|
1708
|
-
for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1);
|
|
1709
|
-
eraseCells(curY, 0, curY, curX);
|
|
1841
|
+
for (let row = 0; row < curY; row++) eraseCells(row, 0, row, cols - 1, selective);
|
|
1842
|
+
eraseCells(curY, 0, curY, curX, selective);
|
|
1710
1843
|
break;
|
|
1711
1844
|
case 2:
|
|
1712
1845
|
case 3:
|
|
1713
|
-
for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1);
|
|
1714
|
-
if (mode === 3)
|
|
1846
|
+
for (let row = 0; row < rows; row++) eraseCells(row, 0, row, cols - 1, selective);
|
|
1847
|
+
if (mode === 3) clearScrollback();
|
|
1715
1848
|
markAllDirty();
|
|
1716
1849
|
break;
|
|
1717
1850
|
}
|
|
1718
1851
|
}
|
|
1719
|
-
function handleEraseLine(mode) {
|
|
1852
|
+
function handleEraseLine(mode, selective = false) {
|
|
1720
1853
|
const eraseRight = leftRightMarginMode ? rightMargin : cols - 1;
|
|
1721
1854
|
const eraseLeft = leftRightMarginMode ? leftMargin : 0;
|
|
1722
1855
|
switch (mode) {
|
|
1723
1856
|
case 0:
|
|
1724
|
-
eraseCells(curY, curX, curY, eraseRight);
|
|
1857
|
+
eraseCells(curY, curX, curY, eraseRight, selective);
|
|
1725
1858
|
break;
|
|
1726
1859
|
case 1:
|
|
1727
|
-
eraseCells(curY, eraseLeft, curY, curX);
|
|
1860
|
+
eraseCells(curY, eraseLeft, curY, curX, selective);
|
|
1728
1861
|
break;
|
|
1729
1862
|
case 2:
|
|
1730
|
-
eraseCells(curY, eraseLeft, curY, eraseRight);
|
|
1863
|
+
eraseCells(curY, eraseLeft, curY, eraseRight, selective);
|
|
1731
1864
|
break;
|
|
1732
1865
|
}
|
|
1733
1866
|
}
|
|
1734
|
-
function eraseCells(row, startCol, _endRow, endCol) {
|
|
1867
|
+
function eraseCells(row, startCol, _endRow, endCol, selective = false) {
|
|
1735
1868
|
const r = grid[row];
|
|
1736
1869
|
if (!r) return;
|
|
1737
|
-
for (let col = startCol; col <= endCol && col < cols; col++)
|
|
1870
|
+
for (let col = startCol; col <= endCol && col < cols; col++) {
|
|
1871
|
+
if (selective && r.getCellRaw(col).protected) continue;
|
|
1872
|
+
r.eraseWithBg(col, attrs.bg);
|
|
1873
|
+
}
|
|
1738
1874
|
markScreenRowDirty(row);
|
|
1739
1875
|
}
|
|
1740
1876
|
function handleInsertLines(count) {
|
|
@@ -1893,7 +2029,7 @@ function createScreen(options = {}) {
|
|
|
1893
2029
|
params.push(subs[0]);
|
|
1894
2030
|
} else params.push(seg === "" ? 0 : parseInt(seg, 10));
|
|
1895
2031
|
if (params.length === 0 || params.length === 1 && params[0] === 0) {
|
|
1896
|
-
attrs =
|
|
2032
|
+
attrs = resetSgrAttrs();
|
|
1897
2033
|
return;
|
|
1898
2034
|
}
|
|
1899
2035
|
let i = 0;
|
|
@@ -1901,7 +2037,7 @@ function createScreen(options = {}) {
|
|
|
1901
2037
|
const code = params[i];
|
|
1902
2038
|
switch (code) {
|
|
1903
2039
|
case 0:
|
|
1904
|
-
attrs =
|
|
2040
|
+
attrs = resetSgrAttrs();
|
|
1905
2041
|
break;
|
|
1906
2042
|
case 1:
|
|
1907
2043
|
attrs.bold = true;
|
|
@@ -2602,6 +2738,7 @@ function createScreen(options = {}) {
|
|
|
2602
2738
|
}
|
|
2603
2739
|
function softReset() {
|
|
2604
2740
|
insertMode = false;
|
|
2741
|
+
newLineMode = false;
|
|
2605
2742
|
originMode = false;
|
|
2606
2743
|
autoWrap = true;
|
|
2607
2744
|
curVisible = true;
|
|
@@ -2659,6 +2796,7 @@ function createScreen(options = {}) {
|
|
|
2659
2796
|
focusTracking = false;
|
|
2660
2797
|
originMode = false;
|
|
2661
2798
|
insertMode = false;
|
|
2799
|
+
newLineMode = false;
|
|
2662
2800
|
reverseVideo = false;
|
|
2663
2801
|
syncOutput = false;
|
|
2664
2802
|
kittyKeyboardFlags = 0;
|
|
@@ -2717,6 +2855,9 @@ function createScreen(options = {}) {
|
|
|
2717
2855
|
mainSoftWrapped = new Array(rows).fill(false);
|
|
2718
2856
|
altSoftWrapped = new Array(rows).fill(false);
|
|
2719
2857
|
softWrapped = mainSoftWrapped;
|
|
2858
|
+
mainLineAttrs = new Array(rows).fill(0);
|
|
2859
|
+
altLineAttrs = new Array(rows).fill(0);
|
|
2860
|
+
lineAttrs = mainLineAttrs;
|
|
2720
2861
|
markAllDirty();
|
|
2721
2862
|
dirtyScrolled = 0;
|
|
2722
2863
|
if (sigDamageActive) sigDirtyScrolled = 0;
|
|
@@ -2868,11 +3009,8 @@ function createScreen(options = {}) {
|
|
|
2868
3009
|
emitExecute(9);
|
|
2869
3010
|
} else if (code === 10 || code === 11 || code === 12) {
|
|
2870
3011
|
softWrapped[curY] = false;
|
|
2871
|
-
|
|
2872
|
-
if (
|
|
2873
|
-
curY = scrollBottom;
|
|
2874
|
-
scrollUp(scrollTop, scrollBottom);
|
|
2875
|
-
}
|
|
3012
|
+
lineFeedDown();
|
|
3013
|
+
if (newLineMode) curX = 0;
|
|
2876
3014
|
emitExecute(code);
|
|
2877
3015
|
} else if (code === 13) {
|
|
2878
3016
|
curX = 0;
|
|
@@ -2910,19 +3048,11 @@ function createScreen(options = {}) {
|
|
|
2910
3048
|
parserState = "ground";
|
|
2911
3049
|
emitEsc("c", "");
|
|
2912
3050
|
} else if (ch === "D") {
|
|
2913
|
-
|
|
2914
|
-
if (curY > scrollBottom) {
|
|
2915
|
-
curY = scrollBottom;
|
|
2916
|
-
scrollUp(scrollTop, scrollBottom);
|
|
2917
|
-
}
|
|
3051
|
+
lineFeedDown();
|
|
2918
3052
|
parserState = "ground";
|
|
2919
3053
|
emitEsc("D", "");
|
|
2920
3054
|
} else if (ch === "M") {
|
|
2921
|
-
|
|
2922
|
-
if (curY < scrollTop) {
|
|
2923
|
-
curY = scrollTop;
|
|
2924
|
-
scrollDown(scrollTop, scrollBottom);
|
|
2925
|
-
}
|
|
3055
|
+
reverseIndexUp();
|
|
2926
3056
|
parserState = "ground";
|
|
2927
3057
|
emitEsc("M", "");
|
|
2928
3058
|
} else if (ch === "7") {
|
|
@@ -3012,7 +3142,10 @@ function createScreen(options = {}) {
|
|
|
3012
3142
|
}
|
|
3013
3143
|
curX = 0;
|
|
3014
3144
|
curY = 0;
|
|
3015
|
-
}
|
|
3145
|
+
} else if (ch === "3") lineAttrs[curY] = 2;
|
|
3146
|
+
else if (ch === "4") lineAttrs[curY] = 3;
|
|
3147
|
+
else if (ch === "5") lineAttrs[curY] = 0;
|
|
3148
|
+
else if (ch === "6") lineAttrs[curY] = 1;
|
|
3016
3149
|
parserState = "ground";
|
|
3017
3150
|
emitEsc(ch, escIntermediate);
|
|
3018
3151
|
break;
|
|
@@ -3129,9 +3262,25 @@ function createScreen(options = {}) {
|
|
|
3129
3262
|
* trailing blanks or one past the last cell) lands after the last emitted
|
|
3130
3263
|
* cell, advanced by the leftover distance, clamped to the row.
|
|
3131
3264
|
*/
|
|
3265
|
+
/**
|
|
3266
|
+
* One line attribute per LOGICAL line, taken from the line's FIRST physical
|
|
3267
|
+
* row — the grouping `getLogicalLines` uses, so the two arrays line up.
|
|
3268
|
+
* A rewrap then re-applies the attribute to every row the line becomes.
|
|
3269
|
+
*/
|
|
3270
|
+
function logicalLineAttrs(srcGrid, srcSoftWrapped, srcLineAttrs, srcRows) {
|
|
3271
|
+
const out = [];
|
|
3272
|
+
let starting = true;
|
|
3273
|
+
for (let r = 0; r < srcRows; r++) {
|
|
3274
|
+
if (!srcGrid[r]) continue;
|
|
3275
|
+
if (starting) out.push(srcLineAttrs[r] ?? 0);
|
|
3276
|
+
starting = !srcSoftWrapped[r];
|
|
3277
|
+
}
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3132
3280
|
function rewrapLines(logicalLines, newCols, track) {
|
|
3133
3281
|
const outRows = [];
|
|
3134
3282
|
const outWrapped = [];
|
|
3283
|
+
const outLine = [];
|
|
3135
3284
|
let tracked = null;
|
|
3136
3285
|
for (let li = 0; li < logicalLines.length; li++) {
|
|
3137
3286
|
const line = logicalLines[li];
|
|
@@ -3144,7 +3293,8 @@ function createScreen(options = {}) {
|
|
|
3144
3293
|
if (lineLen === 0) {
|
|
3145
3294
|
outRows.push(makeRow(newCols));
|
|
3146
3295
|
outWrapped.push(false);
|
|
3147
|
-
|
|
3296
|
+
outLine.push(li);
|
|
3297
|
+
if (track?.line === li) tracked = {
|
|
3148
3298
|
row: outRows.length - 1,
|
|
3149
3299
|
col: Math.min(track.offset, newCols - 1)
|
|
3150
3300
|
};
|
|
@@ -3157,7 +3307,7 @@ function createScreen(options = {}) {
|
|
|
3157
3307
|
while (col < newCols && pos < lineLen) {
|
|
3158
3308
|
const cell = line[pos];
|
|
3159
3309
|
if (cell.wide && col + 2 > newCols) break;
|
|
3160
|
-
if (track
|
|
3310
|
+
if (track?.line === li && pos === track.offset) tracked = {
|
|
3161
3311
|
row: outRows.length,
|
|
3162
3312
|
col
|
|
3163
3313
|
};
|
|
@@ -3168,7 +3318,8 @@ function createScreen(options = {}) {
|
|
|
3168
3318
|
const moreContent = pos < lineLen;
|
|
3169
3319
|
outRows.push(row);
|
|
3170
3320
|
outWrapped.push(moreContent);
|
|
3171
|
-
|
|
3321
|
+
outLine.push(li);
|
|
3322
|
+
if (!moreContent && track?.line === li && tracked === null) {
|
|
3172
3323
|
const extra = track.offset - lineLen;
|
|
3173
3324
|
tracked = {
|
|
3174
3325
|
row: outRows.length - 1,
|
|
@@ -3180,6 +3331,7 @@ function createScreen(options = {}) {
|
|
|
3180
3331
|
return {
|
|
3181
3332
|
rows: outRows,
|
|
3182
3333
|
wrapped: outWrapped,
|
|
3334
|
+
lineIndex: outLine,
|
|
3183
3335
|
tracked
|
|
3184
3336
|
};
|
|
3185
3337
|
}
|
|
@@ -3191,13 +3343,20 @@ function createScreen(options = {}) {
|
|
|
3191
3343
|
/**
|
|
3192
3344
|
* Trim trailing empty rows from reflowed result, so they don't push content off the top
|
|
3193
3345
|
* when we take the last newRows rows.
|
|
3346
|
+
*
|
|
3347
|
+
* Never trims the row the CURSOR is on. A shell that has just emitted its
|
|
3348
|
+
* final newline sits on a blank last row, and dropping it makes the content
|
|
3349
|
+
* measure one row shorter than it is — so a shrink banks one row too few and
|
|
3350
|
+
* every surviving line lands one row low, taking the cursor with it.
|
|
3194
3351
|
*/
|
|
3195
3352
|
function trimTrailingEmptyRows(result) {
|
|
3196
|
-
|
|
3353
|
+
const floor = result.tracked == null ? 1 : Math.max(1, result.tracked.row + 1);
|
|
3354
|
+
while (result.rows.length > floor) {
|
|
3197
3355
|
const lastRow = result.rows[result.rows.length - 1];
|
|
3198
3356
|
if (packedRowIsBlank(lastRow) && !result.wrapped[result.rows.length - 2]) {
|
|
3199
3357
|
result.rows.pop();
|
|
3200
3358
|
result.wrapped.pop();
|
|
3359
|
+
result.lineIndex?.pop();
|
|
3201
3360
|
} else break;
|
|
3202
3361
|
}
|
|
3203
3362
|
}
|
|
@@ -3213,23 +3372,56 @@ function createScreen(options = {}) {
|
|
|
3213
3372
|
line: cursorLineIdx,
|
|
3214
3373
|
offset: (cursorRowPre - cursorLineStart) * cols + curX
|
|
3215
3374
|
};
|
|
3216
|
-
|
|
3375
|
+
let restoredRows = [];
|
|
3376
|
+
let restoredWrapped = [];
|
|
3377
|
+
let restoredAttrs = [];
|
|
3378
|
+
if (!useAltScreen && newRows > rows && scrollback.length > 0) {
|
|
3379
|
+
const probe = rewrapLines(getLogicalLines(mainGrid, mainSoftWrapped, rows), newCols, void 0);
|
|
3380
|
+
trimTrailingEmptyRows(probe);
|
|
3381
|
+
const take = Math.min(Math.max(0, newRows - probe.rows.length), scrollback.length);
|
|
3382
|
+
if (take > 0) {
|
|
3383
|
+
restoredRows = scrollback.splice(scrollback.length - take, take);
|
|
3384
|
+
restoredWrapped = scrollbackSoftWrapped.splice(scrollbackSoftWrapped.length - take, take);
|
|
3385
|
+
restoredAttrs = scrollbackLineAttrs.splice(scrollbackLineAttrs.length - take, take);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
if (restoredRows.length > 0) {
|
|
3389
|
+
let completed = 0;
|
|
3390
|
+
for (const wrapped of restoredWrapped) if (!wrapped) completed++;
|
|
3391
|
+
if (cursorTrack.line === 0 && restoredWrapped[restoredWrapped.length - 1] === true) for (let i = restoredWrapped.length - 1; i >= 0 && restoredWrapped[i]; i--) cursorTrack.offset += restoredRows[i].length;
|
|
3392
|
+
cursorTrack.line += completed;
|
|
3393
|
+
}
|
|
3394
|
+
const mainSrc = restoredRows.length > 0 ? [...restoredRows, ...mainGrid] : mainGrid;
|
|
3395
|
+
const mainSrcWrapped = restoredRows.length > 0 ? [...restoredWrapped, ...mainSoftWrapped] : mainSoftWrapped;
|
|
3396
|
+
const mainSrcAttrs = restoredRows.length > 0 ? [...restoredAttrs, ...mainLineAttrs] : mainLineAttrs;
|
|
3397
|
+
const mainSrcRows = restoredRows.length + rows;
|
|
3398
|
+
const mainLogicalAttrs = logicalLineAttrs(mainSrc, mainSrcWrapped, mainSrcAttrs, mainSrcRows);
|
|
3399
|
+
const mainResult = rewrapLines(getLogicalLines(mainSrc, mainSrcWrapped, mainSrcRows), newCols, useAltScreen ? void 0 : cursorTrack);
|
|
3217
3400
|
trimTrailingEmptyRows(mainResult);
|
|
3401
|
+
const altLogicalAttrs = logicalLineAttrs(altGrid, altSoftWrapped, altLineAttrs, rows);
|
|
3218
3402
|
const altResult = rewrapLines(getLogicalLines(altGrid, altSoftWrapped, rows), newCols, useAltScreen ? cursorTrack : void 0);
|
|
3219
3403
|
trimTrailingEmptyRows(altResult);
|
|
3220
3404
|
const newMain = makeGrid(newCols, newRows);
|
|
3221
3405
|
const newMainWrapped = new Array(newRows).fill(false);
|
|
3406
|
+
const newMainAttrs = new Array(newRows).fill(0);
|
|
3222
3407
|
const mainStartRow = Math.max(0, mainResult.rows.length - newRows);
|
|
3408
|
+
for (let r = 0; r < mainStartRow; r++) {
|
|
3409
|
+
const overflow = mainResult.rows[r];
|
|
3410
|
+
if (overflow !== void 0) bankScrollbackRow(overflow, mainResult.wrapped[r] ?? false, mainLogicalAttrs[mainResult.lineIndex[r] ?? 0] ?? 0);
|
|
3411
|
+
}
|
|
3223
3412
|
for (let r = 0; r < newRows && mainStartRow + r < mainResult.rows.length; r++) {
|
|
3224
3413
|
newMain[r] = mainResult.rows[mainStartRow + r];
|
|
3225
3414
|
newMainWrapped[r] = mainResult.wrapped[mainStartRow + r];
|
|
3415
|
+
newMainAttrs[r] = mainLogicalAttrs[mainResult.lineIndex[mainStartRow + r] ?? 0] ?? 0;
|
|
3226
3416
|
}
|
|
3227
3417
|
const newAlt = makeGrid(newCols, newRows);
|
|
3228
3418
|
const newAltWrapped = new Array(newRows).fill(false);
|
|
3419
|
+
const newAltAttrs = new Array(newRows).fill(0);
|
|
3229
3420
|
const altStartRow = Math.max(0, altResult.rows.length - newRows);
|
|
3230
3421
|
for (let r = 0; r < newRows && altStartRow + r < altResult.rows.length; r++) {
|
|
3231
3422
|
newAlt[r] = altResult.rows[altStartRow + r];
|
|
3232
3423
|
newAltWrapped[r] = altResult.wrapped[altStartRow + r];
|
|
3424
|
+
newAltAttrs[r] = altLogicalAttrs[altResult.lineIndex[altStartRow + r] ?? 0] ?? 0;
|
|
3233
3425
|
}
|
|
3234
3426
|
const activeResult = useAltScreen ? altResult : mainResult;
|
|
3235
3427
|
const activeStartRow = useAltScreen ? altStartRow : mainStartRow;
|
|
@@ -3241,8 +3433,11 @@ function createScreen(options = {}) {
|
|
|
3241
3433
|
altGrid = newAlt;
|
|
3242
3434
|
mainSoftWrapped = newMainWrapped;
|
|
3243
3435
|
altSoftWrapped = newAltWrapped;
|
|
3436
|
+
mainLineAttrs = newMainAttrs;
|
|
3437
|
+
altLineAttrs = newAltAttrs;
|
|
3244
3438
|
grid = useAltScreen ? altGrid : mainGrid;
|
|
3245
3439
|
softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
|
|
3440
|
+
lineAttrs = useAltScreen ? altLineAttrs : mainLineAttrs;
|
|
3246
3441
|
const oldCols = cols;
|
|
3247
3442
|
cols = newCols;
|
|
3248
3443
|
rows = newRows;
|
|
@@ -3277,6 +3472,11 @@ function createScreen(options = {}) {
|
|
|
3277
3472
|
},
|
|
3278
3473
|
scrollback: cloneGridSnapshot(scrollback),
|
|
3279
3474
|
scrollbackSoftWrapped: [...scrollbackSoftWrapped],
|
|
3475
|
+
lineAttrs: {
|
|
3476
|
+
main: toSparseAttrs(mainLineAttrs),
|
|
3477
|
+
alt: toSparseAttrs(altLineAttrs),
|
|
3478
|
+
scrollback: toSparseAttrs(scrollbackLineAttrs)
|
|
3479
|
+
},
|
|
3280
3480
|
cursor: {
|
|
3281
3481
|
x: curX,
|
|
3282
3482
|
y: curY,
|
|
@@ -3306,6 +3506,7 @@ function createScreen(options = {}) {
|
|
|
3306
3506
|
focusTracking,
|
|
3307
3507
|
origin: originMode,
|
|
3308
3508
|
insert: insertMode,
|
|
3509
|
+
newLine: newLineMode,
|
|
3309
3510
|
reverseVideo,
|
|
3310
3511
|
syncOutput,
|
|
3311
3512
|
kittyKeyboardFlags,
|
|
@@ -3372,9 +3573,13 @@ function createScreen(options = {}) {
|
|
|
3372
3573
|
scrollbackSoftWrapped = snapshotValue.scrollbackSoftWrapped ? [...snapshotValue.scrollbackSoftWrapped] : new Array(scrollback.length).fill(false);
|
|
3373
3574
|
mainSoftWrapped = [...snapshotValue.main.softWrapped];
|
|
3374
3575
|
altSoftWrapped = [...snapshotValue.alt.softWrapped];
|
|
3576
|
+
mainLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.main, mainSoftWrapped.length);
|
|
3577
|
+
altLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.alt, altSoftWrapped.length);
|
|
3578
|
+
scrollbackLineAttrs = fromSparseAttrs(snapshotValue.lineAttrs?.scrollback, scrollback.length);
|
|
3375
3579
|
useAltScreen = snapshotValue.activeBuffer === "alt";
|
|
3376
3580
|
grid = useAltScreen ? altGrid : mainGrid;
|
|
3377
3581
|
softWrapped = useAltScreen ? altSoftWrapped : mainSoftWrapped;
|
|
3582
|
+
lineAttrs = useAltScreen ? altLineAttrs : mainLineAttrs;
|
|
3378
3583
|
curX = snapshotValue.cursor.x;
|
|
3379
3584
|
curY = snapshotValue.cursor.y;
|
|
3380
3585
|
curVisible = snapshotValue.cursor.visible;
|
|
@@ -3401,6 +3606,7 @@ function createScreen(options = {}) {
|
|
|
3401
3606
|
focusTracking = snapshotValue.modes.focusTracking;
|
|
3402
3607
|
originMode = snapshotValue.modes.origin;
|
|
3403
3608
|
insertMode = snapshotValue.modes.insert;
|
|
3609
|
+
newLineMode = snapshotValue.modes.newLine ?? false;
|
|
3404
3610
|
reverseVideo = snapshotValue.modes.reverseVideo;
|
|
3405
3611
|
syncOutput = snapshotValue.modes.syncOutput;
|
|
3406
3612
|
kittyKeyboardFlags = snapshotValue.modes.kittyKeyboardFlags;
|
|
@@ -3558,6 +3764,7 @@ function createScreen(options = {}) {
|
|
|
3558
3764
|
case "focusTracking": return focusTracking;
|
|
3559
3765
|
case "originMode": return originMode;
|
|
3560
3766
|
case "insertMode": return insertMode;
|
|
3767
|
+
case "newLineMode": return newLineMode;
|
|
3561
3768
|
case "reverseVideo": return reverseVideo;
|
|
3562
3769
|
case "syncOutput": return syncOutput;
|
|
3563
3770
|
case "sgrMouse": return sgrMouse;
|
|
@@ -3637,6 +3844,7 @@ function createScreen(options = {}) {
|
|
|
3637
3844
|
focusTracking,
|
|
3638
3845
|
originMode,
|
|
3639
3846
|
insertMode,
|
|
3847
|
+
newLineMode,
|
|
3640
3848
|
reverseVideo,
|
|
3641
3849
|
syncOutput,
|
|
3642
3850
|
leftRightMargin: leftRightMarginMode,
|
|
@@ -3647,7 +3855,7 @@ function createScreen(options = {}) {
|
|
|
3647
3855
|
};
|
|
3648
3856
|
}
|
|
3649
3857
|
function modesEq(a, b) {
|
|
3650
|
-
return a.altScreen === b.altScreen && a.cursorVisible === b.cursorVisible && a.bracketedPaste === b.bracketedPaste && a.applicationCursor === b.applicationCursor && a.applicationKeypad === b.applicationKeypad && a.autoWrap === b.autoWrap && a.mouseTracking === b.mouseTracking && a.mouseTrackingMode === b.mouseTrackingMode && a.sgrMouse === b.sgrMouse && a.utf8Mouse === b.utf8Mouse && a.focusTracking === b.focusTracking && a.originMode === b.originMode && a.insertMode === b.insertMode && a.reverseVideo === b.reverseVideo && a.syncOutput === b.syncOutput && a.leftRightMargin === b.leftRightMargin && a.colorSchemeReporting === b.colorSchemeReporting && a.kittyKeyboard === b.kittyKeyboard && a.kittyGraphics === b.kittyGraphics && a.sixel === b.sixel;
|
|
3858
|
+
return a.altScreen === b.altScreen && a.cursorVisible === b.cursorVisible && a.bracketedPaste === b.bracketedPaste && a.applicationCursor === b.applicationCursor && a.applicationKeypad === b.applicationKeypad && a.autoWrap === b.autoWrap && a.mouseTracking === b.mouseTracking && a.mouseTrackingMode === b.mouseTrackingMode && a.sgrMouse === b.sgrMouse && a.utf8Mouse === b.utf8Mouse && a.focusTracking === b.focusTracking && a.originMode === b.originMode && a.insertMode === b.insertMode && a.newLineMode === b.newLineMode && a.reverseVideo === b.reverseVideo && a.syncOutput === b.syncOutput && a.leftRightMargin === b.leftRightMargin && a.colorSchemeReporting === b.colorSchemeReporting && a.kittyKeyboard === b.kittyKeyboard && a.kittyGraphics === b.kittyGraphics && a.sixel === b.sixel;
|
|
3651
3859
|
}
|
|
3652
3860
|
const damageListeners = /* @__PURE__ */ new Set();
|
|
3653
3861
|
let damageLast = {
|
|
@@ -4112,10 +4320,12 @@ function serializeSnapshot(snapshot, options = {}) {
|
|
|
4112
4320
|
if (hyperlinks && snapshot.attrs.url !== null) out.push(`\x1b]8;;${snapshot.attrs.url}\x1b\\`);
|
|
4113
4321
|
out.push(snapshot.cursor.visible ? "\x1B[?25h" : "\x1B[?25l");
|
|
4114
4322
|
const origin = m.origin && !excluded.has("origin");
|
|
4115
|
-
const
|
|
4323
|
+
const originX = snapshot.margins.leftRight ? snapshot.margins.left : 0;
|
|
4324
|
+
const limitX = snapshot.margins.leftRight ? snapshot.margins.right : snapshot.cols - 1;
|
|
4325
|
+
const inRegion = snapshot.cursor.y >= snapshot.margins.scrollTop && snapshot.cursor.y <= snapshot.margins.scrollBottom && snapshot.cursor.x >= originX && snapshot.cursor.x <= limitX;
|
|
4116
4326
|
if (origin && inRegion) {
|
|
4117
4327
|
out.push("\x1B[?6h");
|
|
4118
|
-
out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x + 1)}H`);
|
|
4328
|
+
out.push(`\x1b[${String(snapshot.cursor.y - snapshot.margins.scrollTop + 1)};${String(snapshot.cursor.x - originX + 1)}H`);
|
|
4119
4329
|
} else {
|
|
4120
4330
|
out.push(`\x1b[${String(snapshot.cursor.y + 1)};${String(snapshot.cursor.x + 1)}H`);
|
|
4121
4331
|
if (origin) out.push("\x1B[?6h");
|
|
@@ -4138,6 +4348,7 @@ const F_BLINK = 128;
|
|
|
4138
4348
|
const F_WIDE = 256;
|
|
4139
4349
|
const UL_SHIFT = 9;
|
|
4140
4350
|
const UL_MASK = 7;
|
|
4351
|
+
const F_PROTECTED = 65536;
|
|
4141
4352
|
const UNDERLINE_STYLES = [
|
|
4142
4353
|
"none",
|
|
4143
4354
|
"single",
|
|
@@ -4368,6 +4579,7 @@ function createInterner() {
|
|
|
4368
4579
|
if (cell.inverse) flags |= F_INVERSE;
|
|
4369
4580
|
if (cell.hidden) flags |= F_HIDDEN;
|
|
4370
4581
|
if (cell.blink) flags |= F_BLINK;
|
|
4582
|
+
if (cell.protected) flags |= F_PROTECTED;
|
|
4371
4583
|
if (cell.wide) flags |= F_WIDE;
|
|
4372
4584
|
flags |= ulIdx << UL_SHIFT;
|
|
4373
4585
|
const key = `${flags}|${fgRef}|${bgRef}|${ulRef}|${urlRef}`;
|
|
@@ -4536,6 +4748,7 @@ function makeCell(charIdx, styleIdx, strings, colors, styles) {
|
|
|
4536
4748
|
inverse: (style.flags & F_INVERSE) !== 0,
|
|
4537
4749
|
hidden: (style.flags & F_HIDDEN) !== 0,
|
|
4538
4750
|
blink: (style.flags & F_BLINK) !== 0,
|
|
4751
|
+
protected: (style.flags & F_PROTECTED) !== 0,
|
|
4539
4752
|
wide: (style.flags & F_WIDE) !== 0,
|
|
4540
4753
|
url
|
|
4541
4754
|
};
|
|
@@ -4589,6 +4802,7 @@ function encodeScreenSnapshotBinary(snapshot) {
|
|
|
4589
4802
|
savedState: snapshot.savedState,
|
|
4590
4803
|
attrs: snapshot.attrs,
|
|
4591
4804
|
modes: snapshot.modes,
|
|
4805
|
+
lineAttrs: snapshot.lineAttrs,
|
|
4592
4806
|
margins: snapshot.margins,
|
|
4593
4807
|
colors: snapshot.colors,
|
|
4594
4808
|
tabStops: snapshot.tabStops,
|
|
@@ -4656,6 +4870,7 @@ function decodeScreenSnapshotBinary(bytes) {
|
|
|
4656
4870
|
savedState: header.savedState,
|
|
4657
4871
|
attrs: header.attrs,
|
|
4658
4872
|
modes: header.modes,
|
|
4873
|
+
lineAttrs: header.lineAttrs,
|
|
4659
4874
|
margins: header.margins,
|
|
4660
4875
|
colors: header.colors,
|
|
4661
4876
|
tabStops: header.tabStops,
|