@vincemakes/kiso-tui 0.15.7 → 0.15.8
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/compositor.js +299 -287
- package/dist/editor.d.ts +4 -4
- package/dist/editor.js +31 -13
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -61,6 +61,9 @@ import { displayVerb, keysSheetRows } from "./strings.js";
|
|
|
61
61
|
export const CURSOR_MARKER = "\x1b_[kiso-cur]\x1b\\";
|
|
62
62
|
const FRAME_MS = 16; // state changes coalesce to ≥16ms frames
|
|
63
63
|
const SPINNER_MS = 200; // the spinner cadence — a ONE-SHOT re-armed on demand
|
|
64
|
+
/** REL-0152-R1: a held row that has never been painted, so the first
|
|
65
|
+
* frame writes every row rather than trusting an empty string. */
|
|
66
|
+
const NOT_PAINTED = "\u0000never";
|
|
64
67
|
const CHROME_ROWS = 4; // box top + input + box bottom + status — the design §03 chrome (V6-3; the box is W6)
|
|
65
68
|
/** W20 — the whole-table-replace comparison: the live task block only
|
|
66
69
|
* redraws when the items actually changed (the task extension's
|
|
@@ -83,6 +86,61 @@ export class Body {
|
|
|
83
86
|
#lastLiveTop = 0; // the recorded live region top — the resize clear starts here
|
|
84
87
|
#lastLiveRows = 0; // the recorded live row count (incl. the chrome)
|
|
85
88
|
#lastSkip = 0; // the last frame's window top in model rows — the A8b scroll's leaving-count base
|
|
89
|
+
/**
|
|
90
|
+
* REL-0152-R1 — the SCREEN, held.
|
|
91
|
+
*
|
|
92
|
+
* H strings: what the terminal is showing right now, as this
|
|
93
|
+
* compositor believes it. Every frame computes the screen it WANTS
|
|
94
|
+
* and writes only the rows where the two differ.
|
|
95
|
+
*
|
|
96
|
+
* This is the whole round. The old renderer's correctness came from
|
|
97
|
+
* remembering what it had written — which rows it had painted, which
|
|
98
|
+
* it had scrolled, what the window's top had been last time — and
|
|
99
|
+
* every defect in the D series that survived a patch came from that
|
|
100
|
+
* memory disagreeing with the terminal. A renderer that HOLDS the
|
|
101
|
+
* screen cannot disagree with itself: it computes a difference and
|
|
102
|
+
* emits it, and a row that is wrong for any reason is repaired on the
|
|
103
|
+
* next frame because the difference includes it.
|
|
104
|
+
*
|
|
105
|
+
* Six fixes were built and reverted before this — one for the tear,
|
|
106
|
+
* five for the chip loss — and each traded a loss for a duplicate
|
|
107
|
+
* somewhere else. They were all attempts to make the memory agree.
|
|
108
|
+
*
|
|
109
|
+
* The one thing a diff cannot repair is the SCROLLBACK, which is
|
|
110
|
+
* irreversible: see #scrolledOff.
|
|
111
|
+
*/
|
|
112
|
+
#screen = [];
|
|
113
|
+
/**
|
|
114
|
+
* REL-0152-R1 — the row the cursor is on, as far as this compositor
|
|
115
|
+
* knows.
|
|
116
|
+
*
|
|
117
|
+
* The park at the end of a frame is a RELATIVE move, and the old
|
|
118
|
+
* renderer could hard-code its base: the bottom-up march always ended
|
|
119
|
+
* on the status row at H, so `from` was H by construction. A diff
|
|
120
|
+
* ends wherever the last CHANGED row happens to be — which on a
|
|
121
|
+
* streaming frame is the live band, not the bottom of the screen — so
|
|
122
|
+
* the base has to be tracked rather than assumed.
|
|
123
|
+
*
|
|
124
|
+
* Getting this wrong does not damage the frame; it parks the cursor
|
|
125
|
+
* in the wrong place, which is what the PTY gates that assert "every
|
|
126
|
+
* frame ends with the cursor on an input row" are for. They caught it.
|
|
127
|
+
*/
|
|
128
|
+
#cursorRow = 0;
|
|
129
|
+
/**
|
|
130
|
+
* REL-0152-R1 — how many model rows have gone into the terminal's
|
|
131
|
+
* scrollback. Monotonic, because scrolling is.
|
|
132
|
+
*
|
|
133
|
+
* A row may only be scrolled off when it can never be shown again.
|
|
134
|
+
* The window is bottom-anchored, so its top moves BOTH ways as the
|
|
135
|
+
* live band grows and shrinks; scrolling on that movement pushed rows
|
|
136
|
+
* away that the next shrink brought back, which is the A7 replay's
|
|
137
|
+
* frame-106 duplicate. The floor below is where the window's top
|
|
138
|
+
* would sit with the live band empty and the chrome at its minimum —
|
|
139
|
+
* the only part of the movement that is one-way.
|
|
140
|
+
*/
|
|
141
|
+
#scrolledOff = 0;
|
|
142
|
+
/** REL-0152-R1: this frame is the repaint after a SIGWINCH. */
|
|
143
|
+
#resizeFrame = false;
|
|
86
144
|
#lastH = 0;
|
|
87
145
|
// KC1 §6: the composer's recorded extent — the row count the last
|
|
88
146
|
// frame drew (exit's clear walks it) and the row its CHA parked the
|
|
@@ -166,9 +224,35 @@ export class Body {
|
|
|
166
224
|
* a DECRQM round-trip would race the editor for stdin at boot. */
|
|
167
225
|
#conservative;
|
|
168
226
|
#frameMs;
|
|
227
|
+
/** REL-0152-R1: the frame's own writer — the ONE path that does not
|
|
228
|
+
* invalidate the held screen, because it is what produced it. */
|
|
229
|
+
#writeFrame;
|
|
230
|
+
/** REL-0152-R1: true only while the frame's own bytes are going out. */
|
|
231
|
+
#inFrame = false;
|
|
232
|
+
#unguard = null;
|
|
169
233
|
constructor(opts) {
|
|
170
234
|
this.#opts = opts;
|
|
171
|
-
|
|
235
|
+
// REL-0152-R1: every write that is NOT this frame's own goes
|
|
236
|
+
// through here and INVALIDATES the held screen.
|
|
237
|
+
//
|
|
238
|
+
// A banner, a bodyLog line, the terminal gap, the pipe path's
|
|
239
|
+
// prose — all of them paint the terminal directly, and the old
|
|
240
|
+
// renderer survived that because every frame repainted every row.
|
|
241
|
+
// A diff does not: after such a write the held screen describes a
|
|
242
|
+
// terminal that no longer exists, and the next frame skips exactly
|
|
243
|
+
// the rows it should have repaired. That is a stale composer row
|
|
244
|
+
// left standing while the real one moves — which is what the PTY
|
|
245
|
+
// cursor gates saw.
|
|
246
|
+
//
|
|
247
|
+
// Forgetting is cheap and cannot be forgotten to do: one frame
|
|
248
|
+
// repaints, and correctness stops depending on a list of writers
|
|
249
|
+
// staying complete as the file grows.
|
|
250
|
+
const raw = opts.write ?? ((str) => process.stdout.write(str));
|
|
251
|
+
this.#writeFrame = raw;
|
|
252
|
+
this.#write = (str) => {
|
|
253
|
+
this.#screen = [];
|
|
254
|
+
raw(str);
|
|
255
|
+
};
|
|
172
256
|
this.#conservative = (opts.termProgram ?? process.env.TERM_PROGRAM) === "Apple_Terminal";
|
|
173
257
|
this.#frameMs = this.#conservative ? 40 : FRAME_MS;
|
|
174
258
|
this.#active = opts.active();
|
|
@@ -783,6 +867,7 @@ export class Body {
|
|
|
783
867
|
if (process.stdout.isTTY !== true || palette().bold === "" || rows < 4)
|
|
784
868
|
return;
|
|
785
869
|
this.#docked = true;
|
|
870
|
+
this.#guardStdout();
|
|
786
871
|
this.#attachResize();
|
|
787
872
|
this.#fullRedraw = true;
|
|
788
873
|
this.#dirty = true;
|
|
@@ -791,6 +876,7 @@ export class Body {
|
|
|
791
876
|
/** Teardown — CSI r (the "no broken terminal" contract byte), the
|
|
792
877
|
* chrome rows cleared, the cursor home at the input line. */
|
|
793
878
|
exit() {
|
|
879
|
+
this.#unguard?.();
|
|
794
880
|
// TUI2-R3v2 ②: the mouse disable rides the SAME teardown as CSI r,
|
|
795
881
|
// and rides it BEFORE the docked guard — an un-docked compositor is
|
|
796
882
|
// exactly the state a superseded or half-torn-down one is in, and
|
|
@@ -832,6 +918,47 @@ export class Body {
|
|
|
832
918
|
out.push(`\x1b[${Math.max(1, H - 1)};1H`);
|
|
833
919
|
this.#write(out.join(""));
|
|
834
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* REL-0152-R1 — the compositor is not the only writer, and a diff has
|
|
923
|
+
* to know.
|
|
924
|
+
*
|
|
925
|
+
* The old renderer repainted every row of every frame, so anything
|
|
926
|
+
* else that printed to the terminal was corrected within one frame
|
|
927
|
+
* and nobody had to think about it. A diff writes only what changed,
|
|
928
|
+
* so an outside write leaves the held screen describing a terminal
|
|
929
|
+
* that no longer exists — and the next frame skips exactly the rows
|
|
930
|
+
* it should have repaired.
|
|
931
|
+
*
|
|
932
|
+
* That is not hypothetical: a bare `console.log` in the CLI's boot
|
|
933
|
+
* path prints the faux-mode notice with a trailing newline, which
|
|
934
|
+
* SCROLLS the terminal two rows while the dock is up. The chrome then
|
|
935
|
+
* sits two rows above where the model puts it, the diff sees no
|
|
936
|
+
* change, and the cursor parks on the wrong row. Three PTY gates
|
|
937
|
+
* caught it.
|
|
938
|
+
*
|
|
939
|
+
* The fix is not a list of writers to remember. stdout is wrapped
|
|
940
|
+
* while the dock is up: anything written that is not this frame's own
|
|
941
|
+
* bytes forgets the screen, and the next frame repaints. New callers
|
|
942
|
+
* cannot get it wrong because they are not asked to get it right.
|
|
943
|
+
*/
|
|
944
|
+
#guardStdout() {
|
|
945
|
+
if (this.#unguard !== null || this.#opts.write !== undefined)
|
|
946
|
+
return;
|
|
947
|
+
const real = process.stdout.write.bind(process.stdout);
|
|
948
|
+
const patched = ((chunk, ...rest) => {
|
|
949
|
+
if (!this.#inFrame)
|
|
950
|
+
this.#screen = [];
|
|
951
|
+
return real(chunk, ...rest);
|
|
952
|
+
});
|
|
953
|
+
process.stdout.write = patched;
|
|
954
|
+
this.#unguard = () => {
|
|
955
|
+
// only restore what we installed — another wrapper may have
|
|
956
|
+
// been layered on top since (the byte trace does exactly that)
|
|
957
|
+
if (process.stdout.write === patched)
|
|
958
|
+
process.stdout.write = real;
|
|
959
|
+
this.#unguard = null;
|
|
960
|
+
};
|
|
961
|
+
}
|
|
835
962
|
/** TUI2-R3v2 ②: the panel's option rows as this frame placed them —
|
|
836
963
|
* absolute, 1-based. The click hit-test's single source. */
|
|
837
964
|
panelOptionRows() {
|
|
@@ -852,6 +979,7 @@ export class Body {
|
|
|
852
979
|
const from = Math.max(1, (this.#lastH > 0 ? this.#lastH : H) - liveRows + 1);
|
|
853
980
|
this.#write(`\x1b[${Math.min(from, Math.max(1, H))};1H\x1b[0J`);
|
|
854
981
|
this.#fullRedraw = true;
|
|
982
|
+
this.#resizeFrame = true;
|
|
855
983
|
this.#dirty = true;
|
|
856
984
|
this.render(); // the immediate redraw at the NEW geometry
|
|
857
985
|
}
|
|
@@ -1230,16 +1358,25 @@ export class Body {
|
|
|
1230
1358
|
// erased rows are all re-painted); a shrink takes the full-redraw
|
|
1231
1359
|
// path: the window re-paints at the model's positions, every row
|
|
1232
1360
|
// covered (the V6-1 every-row rule).
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1361
|
+
// REL-0152-R1: ONE renderer. The steady/full split existed because
|
|
1362
|
+
// the steady path moved rows by scrolling and could not handle a
|
|
1363
|
+
// window that moved the wrong way, so the frames it could not draw
|
|
1364
|
+
// were handed to a full repaint. A diff has no such frames: it
|
|
1365
|
+
// emits the rows that differ, and on a frame where everything
|
|
1366
|
+
// differs that IS a full repaint. The dispatch, the two geometries
|
|
1367
|
+
// and the invariant about which moves each path may use all go
|
|
1368
|
+
// with it.
|
|
1369
|
+
this.#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1370
|
+
this.#fullRedraw = false;
|
|
1240
1371
|
out.push(this.#conservative ? "\x1b[?25h" : "\x1b[?2026l");
|
|
1241
1372
|
out.push("\x1b[?7h"); // REL-0152-D14: autowrap back on for everything outside the frame
|
|
1242
|
-
this.#
|
|
1373
|
+
this.#inFrame = true;
|
|
1374
|
+
try {
|
|
1375
|
+
this.#writeFrame(out.join("")); // the frame IS the new screen — it does not invalidate it
|
|
1376
|
+
}
|
|
1377
|
+
finally {
|
|
1378
|
+
this.#inFrame = false;
|
|
1379
|
+
}
|
|
1243
1380
|
this.#lastLiveTop = liveTop;
|
|
1244
1381
|
this.#lastLiveRows = liveRowsTotal;
|
|
1245
1382
|
this.#lastInputRows = editor.rows.length;
|
|
@@ -1735,72 +1872,34 @@ export class Body {
|
|
|
1735
1872
|
// shrink EVERY frame) loses the scrolled-away turns from the
|
|
1736
1873
|
// terminal's scrollback entirely (finding #A8b — the queued-flood
|
|
1737
1874
|
// content loss).
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
out.push(`\x1b[${H};1H`);
|
|
1760
|
-
for (let k = 0; k < chunk; k += 1)
|
|
1761
|
-
out.push("\n");
|
|
1762
|
-
p += chunk;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
else {
|
|
1766
|
-
// A8b (the fresh leaving share): a leaving row whose old-screen
|
|
1767
|
-
// copy is stale — the committed-this-frame lines (their old rows
|
|
1768
|
-
// held the previous live/chrome) — is pre-painted at its OLD row
|
|
1769
|
-
// so the LF scroll carries it into the scrollback; the frozen
|
|
1770
|
-
// leaving rows are already on screen and scroll as-is. The first
|
|
1771
|
-
// overflow frame of a batch: the window's top row is the freshly
|
|
1772
|
-
// committed line, NEVER on the old screen — without the
|
|
1773
|
-
// pre-paint the scroll pushes a blank and the line's only paint
|
|
1774
|
-
// (the clamped march at row 1) is overwritten by its neighbor.
|
|
1775
|
-
if (skip > frozen.length) {
|
|
1776
|
-
const fromIdx = Math.max(0, this.#lastSkip - frozen.length);
|
|
1777
|
-
const top = Math.min(skip - frozen.length, all.length - frozen.length);
|
|
1778
|
-
for (let i = fromIdx; i < top; i += 1) {
|
|
1779
|
-
out.push(`\x1b[${Math.max(1, frozen.length + i - this.#lastSkip + 1)};1H\x1b[0K${this.#checked(all[frozen.length + i], W)}`);
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
if (leaving < skip)
|
|
1783
|
-
out.push(`\x1b[${leaving + 1};1H\x1b[0J`);
|
|
1784
|
-
out.push(`\x1b[${H};1H`);
|
|
1785
|
-
// TUI2-R2pre ② — scroll the rows that LEFT THE WINDOW SINCE THE
|
|
1786
|
-
// LAST FRAME (`leaving`), never `skip`, which is the window's
|
|
1787
|
-
// ABSOLUTE top. Scrolling the absolute top re-pushed the whole
|
|
1788
|
-
// history's worth of rows on EVERY full redraw — and a live-region
|
|
1789
|
-
// shrink takes this path, so that was most frames of a real
|
|
1790
|
-
// session. The ED above had just blanked everything below row
|
|
1791
|
-
// `leaving`, so what those surplus LFs carried into the terminal's
|
|
1792
|
-
// scrollback was blank rows: the large blank bands mid-history of
|
|
1793
|
-
// the owner's field report. The SCREEN never showed it because the
|
|
1794
|
-
// repaint below covers every row 1..H (the V6-1 rule), and the
|
|
1795
|
-
// house emulator drops scrolled rows on the floor — so no gate
|
|
1796
|
-
// could see it either. Measured on the 5-turn 80x24 repro: the
|
|
1797
|
-
// scrollback went from 162 blank rows of 168 to 14 of 52.
|
|
1798
|
-
for (let i = 0; i < leaving; i += 1)
|
|
1799
|
-
out.push("\n");
|
|
1800
|
-
}
|
|
1875
|
+
// REL-0152-R1: the scroll is the frame's only irreversible act and
|
|
1876
|
+
// lives in one place now. The FLOOR is where the window's top would
|
|
1877
|
+
// sit with the live band empty and the chrome at its minimum — the
|
|
1878
|
+
// one-way part of a movement that otherwise goes both ways.
|
|
1879
|
+
if (this.#resizeFrame) {
|
|
1880
|
+
// REL-0152-R1: a resize scrolls NOTHING of ours. Shrinking the
|
|
1881
|
+
// window is the terminal's own scroll — it reflows the old
|
|
1882
|
+
// content and pushes the overflow into its scrollback before we
|
|
1883
|
+
// are called — so emitting our own LFs on top put the same rows
|
|
1884
|
+
// in twice (TT-1B: twelve rows of a forty-line burst). The
|
|
1885
|
+
// counter adopts the terminal's work, and the held screen is
|
|
1886
|
+
// discarded because a reflow invalidates every row of it: the
|
|
1887
|
+
// next diff repaints the whole screen, which is exactly what a
|
|
1888
|
+
// resize needs.
|
|
1889
|
+
this.#scrolledOff = Math.max(this.#scrolledOff, Math.max(0, Math.min(skip, all.length)));
|
|
1890
|
+
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
1891
|
+
this.#resizeFrame = false;
|
|
1892
|
+
}
|
|
1893
|
+
else if (!overlay) {
|
|
1894
|
+
const floor = Math.max(0, this.#committedLines + CHROME_ROWS - H);
|
|
1895
|
+
this.#emitScroll(out, W, H, all, floor);
|
|
1801
1896
|
}
|
|
1802
1897
|
if (!overlay)
|
|
1803
1898
|
this.#lastSkip = skip;
|
|
1899
|
+
// REL-0152-R1: the rows this frame WANTS on the screen — the same
|
|
1900
|
+
// placement the full redraw has always computed, lifted out of the
|
|
1901
|
+
// emission so it can be compared against what is there.
|
|
1902
|
+
const desired = new Array(H).fill("");
|
|
1804
1903
|
let r = 1;
|
|
1805
1904
|
// the window's content rows: everything above the chrome. With the
|
|
1806
1905
|
// overlay up `all` can exceed it, and the rows that give way are the
|
|
@@ -1809,227 +1908,127 @@ export class Body {
|
|
|
1809
1908
|
const contentRows = Math.max(0, H - CHROME_ROWS - inputExtra - queueRows.length - menuRows.length);
|
|
1810
1909
|
const march = all.slice(skip);
|
|
1811
1910
|
for (const line of march.length > contentRows ? march.slice(march.length - contentRows) : march) {
|
|
1812
|
-
|
|
1911
|
+
desired[r - 1] = this.#checked(line, W);
|
|
1813
1912
|
r += 1;
|
|
1814
1913
|
}
|
|
1815
|
-
// 3. the GAP rows (between the live content and the chrome) —
|
|
1816
|
-
for (let rr = r; rr <= H - 4 - inputExtra; rr += 1)
|
|
1817
|
-
|
|
1818
|
-
}
|
|
1914
|
+
// 3. the GAP rows (between the live content and the chrome) — blank.
|
|
1915
|
+
for (let rr = r; rr <= H - 4 - inputExtra; rr += 1)
|
|
1916
|
+
desired[rr - 1] = "";
|
|
1819
1917
|
// W22: the queue chips sit directly above the box top (the
|
|
1820
1918
|
// "pre-render ABOVE the input row"), the menu above the queue.
|
|
1821
1919
|
const queueTop = H - 3 - inputExtra - queueRows.length;
|
|
1822
1920
|
const menuTop = queueTop - menuRows.length;
|
|
1823
|
-
for (let i = 0; i < queueRows.length; i += 1)
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
out.push(`\x1b[${menuTop + i};1H\x1b[0K${this.#checked(menuRows[i], W)}`);
|
|
1828
|
-
}
|
|
1921
|
+
for (let i = 0; i < queueRows.length; i += 1)
|
|
1922
|
+
desired[queueTop + i - 1] = this.#checked(queueRows[i], W);
|
|
1923
|
+
for (let i = 0; i < menuRows.length; i += 1)
|
|
1924
|
+
desired[menuTop + i - 1] = this.#checked(menuRows[i], W);
|
|
1829
1925
|
// V6-3 + W6 + KC1 §6: the design §03 chrome — box top (H−2−N),
|
|
1830
1926
|
// the composer's N input rows (H−1−N .. H−2), box bottom (H−1),
|
|
1831
1927
|
// status (H). N = 1 is the retired four-row chrome exactly.
|
|
1832
|
-
|
|
1833
|
-
for (let i = 0; i < editor.rows.length; i += 1)
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
out.push(`\x1b[${H - 1};1H\x1b[0K${boxBottom(W)}`);
|
|
1928
|
+
desired[H - 3 - inputExtra - 1] = boxTop(W);
|
|
1929
|
+
for (let i = 0; i < editor.rows.length; i += 1)
|
|
1930
|
+
desired[H - 2 - inputExtra + i - 1] = this.#checked(editor.rows[i], W);
|
|
1931
|
+
desired[H - 1 - 1] = boxBottom(W);
|
|
1837
1932
|
const statusRow = this.#statusSource();
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
//
|
|
1841
|
-
//
|
|
1842
|
-
//
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
// Delegate the oversized frame to #drawFull's chunked emission (the
|
|
1871
|
-
// full path repaints every row, so the hand-off is self-contained).
|
|
1872
|
-
if (leaving > H) {
|
|
1873
|
-
this.#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1874
|
-
return;
|
|
1933
|
+
desired[H - 1] = this.#checked(statusLine(statusRow.status, this.#tail, W, statusRow.hint), W);
|
|
1934
|
+
this.#emitDiff(out, W, H, desired);
|
|
1935
|
+
// REL-0152-R1: park from where the cursor ACTUALLY is — see
|
|
1936
|
+
// #cursorRow. It used to be parked from H, which the bottom-up
|
|
1937
|
+
// march guaranteed and a diff does not.
|
|
1938
|
+
this.#parkCursor(out, this.#cursorRow, H - 2 - inputExtra + editor.markerRow, editor.markerCol);
|
|
1939
|
+
this.#cursorRow = H - 2 - inputExtra + editor.markerRow;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* REL-0152-R1 — emit the difference between the screen that is up and
|
|
1943
|
+
* the screen this frame wants, and adopt the new one.
|
|
1944
|
+
*
|
|
1945
|
+
* One CUP + erase + content per CHANGED row and nothing for the rest.
|
|
1946
|
+
* A streaming delta moves the live band and leaves the status row,
|
|
1947
|
+
* the box, the composer and the queue exactly as they were — 13 rows
|
|
1948
|
+
* were being erased per keystroke for a change that touches one, and
|
|
1949
|
+
* those twelve are the tear's whole surface (REL-0152-D1).
|
|
1950
|
+
*
|
|
1951
|
+
* Correctness does not depend on the diff being clever. It depends on
|
|
1952
|
+
* #screen being what the terminal shows, which is why every path that
|
|
1953
|
+
* moves rows updates it: a wrong row is repaired by the next frame,
|
|
1954
|
+
* because the difference includes it.
|
|
1955
|
+
*/
|
|
1956
|
+
#emitDiff(out, W, H, desired) {
|
|
1957
|
+
if (this.#screen.length !== H)
|
|
1958
|
+
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
1959
|
+
for (let i = 0; i < H; i += 1) {
|
|
1960
|
+
const want = desired[i] ?? "";
|
|
1961
|
+
if (this.#screen[i] === want)
|
|
1962
|
+
continue;
|
|
1963
|
+
out.push(want === "" ? `\x1b[${i + 1};1H\x1b[0K` : `\x1b[${i + 1};1H\x1b[0K${want}`);
|
|
1964
|
+
this.#cursorRow = i + 1; // a CUP write leaves the cursor on that row
|
|
1875
1965
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
for (let r = overlapFrom; r <= oldBottom; r += 1) {
|
|
1918
|
-
out.push(`\x1b[${r};1H\x1b[0K`);
|
|
1966
|
+
this.#screen = [...desired];
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* REL-0152-R1 — the scroll, and the only irreversible thing a frame
|
|
1970
|
+
* does.
|
|
1971
|
+
*
|
|
1972
|
+
* `leaving` rows go into the terminal's scrollback, which cannot be
|
|
1973
|
+
* rewritten, so two things have to be right BEFORE the LFs: the count,
|
|
1974
|
+
* and what is standing in rows 1..leaving.
|
|
1975
|
+
*
|
|
1976
|
+
* The count is bounded by the FLOOR — where the window's top would sit
|
|
1977
|
+
* with the live band empty and the chrome at its minimum. That is the
|
|
1978
|
+
* one-way part of the window's movement; the rest of it follows a band
|
|
1979
|
+
* whose height goes up and down, and scrolling on that pushed rows
|
|
1980
|
+
* away that the next shrink brought back (the A7 replay's frame 106).
|
|
1981
|
+
*
|
|
1982
|
+
* What is standing there is STAGED from the model rather than assumed.
|
|
1983
|
+
* The old renderer assumed the leaving rows were already correct on
|
|
1984
|
+
* screen and was right until a growing live band painted over one of
|
|
1985
|
+
* them; then the LFs carried the live band's leftover into the
|
|
1986
|
+
* scrollback and the committed row it had displaced was in no
|
|
1987
|
+
* scrollback and on no screen. That is the owner's swallowed message
|
|
1988
|
+
* (REL-0152-D7), and staging is what makes it structurally impossible.
|
|
1989
|
+
*/
|
|
1990
|
+
#emitScroll(out, W, H, all, floor) {
|
|
1991
|
+
const target = Math.max(0, Math.min(floor, all.length));
|
|
1992
|
+
const leaving = Math.max(0, target - this.#scrolledOff);
|
|
1993
|
+
if (leaving === 0)
|
|
1994
|
+
return 0;
|
|
1995
|
+
if (this.#screen.length !== H)
|
|
1996
|
+
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
1997
|
+
// a transit taller than the screen cannot be staged in one pass —
|
|
1998
|
+
// rows placed past H are clamped by the terminal itself and the
|
|
1999
|
+
// pile keeps only its last member (finding TUI2-MD-1). Chunked, at
|
|
2000
|
+
// most H rows at a time, every row on screen when its slot comes up.
|
|
2001
|
+
let p = this.#scrolledOff;
|
|
2002
|
+
while (p < target) {
|
|
2003
|
+
const chunk = Math.min(target - p, H);
|
|
2004
|
+
for (let k = 0; k < chunk; k += 1) {
|
|
2005
|
+
const row = all[p + k];
|
|
2006
|
+
out.push(row === undefined || row === "" ? `\x1b[${k + 1};1H\x1b[0K` : `\x1b[${k + 1};1H\x1b[0K${this.#checked(row, W)}`);
|
|
1919
2007
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
// the
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
//
|
|
1931
|
-
//
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1934
|
-
//
|
|
1935
|
-
//
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
// composer's N input rows (H−2 up to H−1−N), box top (H−2−N)
|
|
1945
|
-
const statusRow = this.#statusSource();
|
|
1946
|
-
out.push(`\x1b[1G\x1b[0K${this.#checked(statusLine(statusRow.status, this.#tail, W, statusRow.hint), W)}`); // H — the status
|
|
1947
|
-
out.push(`\x1b[1A\x1b[1G\x1b[0K${boxBottom(W)}`); // H−1 — the box bottom
|
|
1948
|
-
for (let i = editor.rows.length - 1; i >= 0; i -= 1) {
|
|
1949
|
-
out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(editor.rows[i], W)}`); // the input rows, bottom-up
|
|
1950
|
-
}
|
|
1951
|
-
out.push(`\x1b[1A\x1b[1G\x1b[0K${boxTop(W)}`); // H−2−N — the box top
|
|
1952
|
-
// W22: the queue chips sit directly above the box top (the
|
|
1953
|
-
// "pre-render ABOVE the input row"), the menu above the queue —
|
|
1954
|
-
// the bottom-up order mirrors the row order.
|
|
1955
|
-
for (let i = queueRows.length - 1; i >= 0; i -= 1) {
|
|
1956
|
-
out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(queueRows[i], W)}`);
|
|
1957
|
-
}
|
|
1958
|
-
for (let i = menuRows.length - 1; i >= 0; i -= 1) {
|
|
1959
|
-
out.push(`\x1b[1A\x1b[1G\x1b[0K${this.#checked(menuRows[i], W)}`);
|
|
1960
|
-
}
|
|
1961
|
-
// the LIVE lines at their MODEL rows — CUP, never the relative
|
|
1962
|
-
// march: the march drew them adjacent to the chrome (rows
|
|
1963
|
-
// H−3−n..H−3) while the model placed them at [liveTop..liveTop+n−1]
|
|
1964
|
-
// — in the unclamped geometry the gap ELs below erased the march's
|
|
1965
|
-
// copy and the streamed text was INVISIBLE until the commit frame.
|
|
1966
|
-
// The CUP rows land in the content area (≤ H−4−menu ≤ 21), inside
|
|
1967
|
-
// the frozen CUP budget of invariant ②.
|
|
1968
|
-
for (let i = 0; i < liveLines.length; i += 1) {
|
|
1969
|
-
out.push(`\x1b[${liveTop + i};1H\x1b[0K${this.#checked(liveLines[i], W)}`);
|
|
1970
|
-
}
|
|
1971
|
-
// the FROZEN area — CUP (absolute rows; this is the FREEZE path —
|
|
1972
|
-
// the old code's frozen writes were CUP too. The frozen rows are
|
|
1973
|
-
// computed from the current geometry, so external writes (the CLI's
|
|
1974
|
-
// console.error CRLF) cannot misplace them the way a relative march
|
|
1975
|
-
// could).
|
|
1976
|
-
// 1. the GAP rows (between the live content and the chrome) — EL'd
|
|
1977
|
-
// so the old content there cannot ghost; the range stops ABOVE
|
|
1978
|
-
// the queue + menu bands (their rows at [H−3−queue−menu..H−4]
|
|
1979
|
-
// are marched and must survive — the unclamped geometry erased
|
|
1980
|
-
// them).
|
|
1981
|
-
for (let r = liveTop + liveLines.length; r <= H - 4 - inputExtra - queueRows.length - menuRows.length; r += 1) {
|
|
1982
|
-
out.push(`\x1b[${r};1H\x1b[0K`);
|
|
1983
|
-
}
|
|
1984
|
-
// 2. the STALE rows above the committed section — the scrolled old
|
|
1985
|
-
// live copies (a live-drawn cell's pre-commit position): EL.
|
|
1986
|
-
// TUI2-R2pre ②: the old band's POST-SCROLL origin shifted up by
|
|
1987
|
-
// the rows that actually left — `leaving`. It read the commit
|
|
1988
|
-
// count only because the scroll above used to BE the commit
|
|
1989
|
-
// count; with the two decoupled, the old expression erases rows
|
|
1990
|
-
// of frozen content that never moved.
|
|
1991
|
-
const staleFrom = Math.max(1, this.#lastLiveTop - committed.length);
|
|
1992
|
-
for (let r = staleFrom; r < liveTop - committed.length; r += 1) {
|
|
1993
|
-
out.push(`\x1b[${r};1H\x1b[0K`);
|
|
1994
|
-
}
|
|
1995
|
-
// 3. the committed lines at [liveTop−N .. liveTop−1] — the rows
|
|
1996
|
-
// CLAMP at 1: a super-tall force-commit's early lines have no
|
|
1997
|
-
// on-screen row (they would need a negative CUP — terminal
|
|
1998
|
-
// undefined behavior); their content stays in the scrollback.
|
|
1999
|
-
// A8b: the first (skip − frozenCount) lines are ABOVE the new
|
|
2000
|
-
// window — they were pre-painted and scrolled; re-painting them
|
|
2001
|
-
// would re-clamp them into row 1 and the band's second line
|
|
2002
|
-
// overwrites the first in the same frame (the row-1 clamp pile —
|
|
2003
|
-
// the queued flood lost the user chips 1..8 that way). The
|
|
2004
|
-
// window's share starts at the line whose model row is the
|
|
2005
|
-
// window's top (skip).
|
|
2006
|
-
for (let i = Math.max(0, skip - frozenCount); i < committed.length; i += 1) {
|
|
2007
|
-
out.push(`\x1b[${Math.max(1, liveTop - committed.length + i)};1H\x1b[0K${this.#checked(committed[i], W)}`);
|
|
2008
|
-
}
|
|
2009
|
-
// the cursor: down to the anchor (H−2, the input row) + left to the marker —
|
|
2010
|
-
// the down-distance from the LAST written row, in byte order: the
|
|
2011
|
-
// committed band's bottom, then the stale ELs' bottom, then the gap
|
|
2012
|
-
// ELs' bottom, then the live lines' bottom, then the menu's top
|
|
2013
|
-
// (its last marched row), else the chrome's box top.
|
|
2014
|
-
const lastRow = committed.length > 0
|
|
2015
|
-
? Math.max(1, liveTop - 1)
|
|
2016
|
-
: staleFrom < liveTop
|
|
2017
|
-
? liveTop - 1
|
|
2018
|
-
: liveTop + liveLines.length <= H - 4 - inputExtra - queueRows.length - menuRows.length
|
|
2019
|
-
? H - 4 - inputExtra - queueRows.length - menuRows.length
|
|
2020
|
-
: liveLines.length > 0
|
|
2021
|
-
? liveTop + liveLines.length - 1
|
|
2022
|
-
: menuRows.length > 0
|
|
2023
|
-
? H - 3 - inputExtra - menuRows.length - queueRows.length
|
|
2024
|
-
: H - 3 - inputExtra;
|
|
2025
|
-
// the anchor: the MARKER'S row inside the composer (N = 1,
|
|
2026
|
-
// markerRow 0 ⇒ the retired H−2)
|
|
2027
|
-
this.#parkCursor(out, lastRow, H - 2 - inputExtra + editor.markerRow, editor.markerCol);
|
|
2028
|
-
// A8b: the steady path moves the window too (the scroll + the
|
|
2029
|
-
// repaint) — record its top so the next full-redraw's leaving count
|
|
2030
|
-
// is the rows the window dropped since the last frame, whatever the
|
|
2031
|
-
// path of the frames between (same formula as `skip` above).
|
|
2032
|
-
this.#lastSkip = skip;
|
|
2008
|
+
if (chunk < H)
|
|
2009
|
+
out.push(`\x1b[${chunk + 1};1H\x1b[0J`);
|
|
2010
|
+
out.push(`\x1b[${H};1H`);
|
|
2011
|
+
for (let k = 0; k < chunk; k += 1)
|
|
2012
|
+
out.push("\n");
|
|
2013
|
+
this.#cursorRow = H; // the LFs at the last row leave it there
|
|
2014
|
+
p += chunk;
|
|
2015
|
+
}
|
|
2016
|
+
this.#scrolledOff = target;
|
|
2017
|
+
// The screen after a scroll is not worth modelling row by row: the
|
|
2018
|
+
// staging painted rows 1..chunk, the ED blanked everything below
|
|
2019
|
+
// them, and the LFs then shifted all of it up — per chunk. An
|
|
2020
|
+
// earlier version modelled only the shift, forgot the ED, and the
|
|
2021
|
+
// diff below then skipped rows the terminal had already blanked:
|
|
2022
|
+
// a 19-row blank band on a 24-row screen, which is most of it.
|
|
2023
|
+
//
|
|
2024
|
+
// So a scrolled frame forgets the screen and repaints. That costs
|
|
2025
|
+
// nothing worth having: a scroll happens only when the COMMITTED
|
|
2026
|
+
// height crosses the floor, and a commit frame changes most rows
|
|
2027
|
+
// anyway. The frames the diff exists for — a streaming delta, a
|
|
2028
|
+
// keystroke — commit nothing, scroll nothing, and repaint only
|
|
2029
|
+
// what moved.
|
|
2030
|
+
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
2031
|
+
return leaving;
|
|
2033
2032
|
}
|
|
2034
2033
|
/**
|
|
2035
2034
|
* TUI2-R2 ⑤ (the R1.5 parked ⑩) — CURSOR AUTHORITY: the ONE frame-tail
|
|
@@ -2057,12 +2056,25 @@ export class Body {
|
|
|
2057
2056
|
* independent of wherever the last write ended (the A3 finding: the
|
|
2058
2057
|
* retired CUB's base was the gap EL's column 1, left of the lead).
|
|
2059
2058
|
*/
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2059
|
+
/**
|
|
2060
|
+
* REL-0152-R1 — park ABSOLUTELY.
|
|
2061
|
+
*
|
|
2062
|
+
* This was a relative move, and it could be: the old bottom-up march
|
|
2063
|
+
* always ended on the status row, so the base was H by construction.
|
|
2064
|
+
* A diff ends on whatever row changed last, and on a streaming frame
|
|
2065
|
+
* that is the live band. Tracking the base is possible but it makes
|
|
2066
|
+
* the cursor's correctness depend on every writer in this file
|
|
2067
|
+
* remembering to update a counter — and the PTY gates that assert
|
|
2068
|
+
* "every frame ends with the cursor on an input row" caught exactly
|
|
2069
|
+
* that going wrong.
|
|
2070
|
+
*
|
|
2071
|
+
* A CUP to the row needs no base at all. The CHA for the column
|
|
2072
|
+
* stays: it was already absolute (the A3 finding retired the CUB
|
|
2073
|
+
* whose base was the last write's end column), and the frame-derived
|
|
2074
|
+
* column contract is asserted on it.
|
|
2075
|
+
*/
|
|
2076
|
+
#parkCursor(out, _fromRow, toRow, col) {
|
|
2077
|
+
out.push(`\x1b[${toRow};1H`);
|
|
2066
2078
|
out.push(`\x1b[${col}G`);
|
|
2067
2079
|
}
|
|
2068
2080
|
/** Invariant ①: every emitted line fits the width — a violation is a
|
package/dist/editor.d.ts
CHANGED
|
@@ -68,10 +68,10 @@ export declare class Editor {
|
|
|
68
68
|
* over while the run is told to stop. Mirrors onEscape (a list, so
|
|
69
69
|
* listeners can coexist); the line arrives already gone from the
|
|
70
70
|
* composer, exactly as a submit's does. */
|
|
71
|
-
/** REL-0152-D11:
|
|
72
|
-
*
|
|
73
|
-
* moment. */
|
|
74
|
-
|
|
71
|
+
/** REL-0152-D11/D15: where to get the clipboard's contents when the
|
|
72
|
+
* human asks for them — ctrl+V, or an empty paste. The CLI owns the
|
|
73
|
+
* platform, the editor owns the moment. */
|
|
74
|
+
onClipboardPaste(cb: () => string | null): void;
|
|
75
75
|
onRedirect(cb: (line: string) => void): void;
|
|
76
76
|
/** W22: bind the pending-turn queue — the CLI's live slots. The ↑
|
|
77
77
|
* pop walks them (each pop leaves the queue, cancelling the turn);
|
package/dist/editor.js
CHANGED
|
@@ -154,13 +154,19 @@ export class Editor {
|
|
|
154
154
|
*/
|
|
155
155
|
#pasteRun = null;
|
|
156
156
|
/**
|
|
157
|
-
* REL-0152-D11 —
|
|
157
|
+
* REL-0152-D11/D15 — the clipboard hook.
|
|
158
158
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
159
|
+
* A terminal cannot put binary into a byte stream, so pasting an image
|
|
160
|
+
* sends no image. D11 keyed on an EMPTY bracketed paste, reasoning
|
|
161
|
+
* that the paste would still arrive with nothing in it. Half right:
|
|
162
|
+
* with no TEXT on the clipboard many terminals send no paste at all,
|
|
163
|
+
* so there was no empty paste to react to and the owner's cmd+V and
|
|
164
|
+
* ctrl+V both did nothing.
|
|
165
|
+
*
|
|
166
|
+
* ctrl+V is the gesture that always arrives — 0x16, a byte the editor
|
|
167
|
+
* has always received and thrown away as "other control". The empty
|
|
168
|
+
* paste stays wired too, because terminals differ and one that does
|
|
169
|
+
* send it should behave the same. Two doors, one room.
|
|
164
170
|
*
|
|
165
171
|
* The editor does not know what a clipboard is and must not: this
|
|
166
172
|
* package renders and reads keys, and reaching into the operating
|
|
@@ -169,7 +175,7 @@ export class Editor {
|
|
|
169
175
|
* moment. It returns the text to insert (a path, for the CLI's
|
|
170
176
|
* attachment scan to pick up) or null when there was nothing.
|
|
171
177
|
*/
|
|
172
|
-
#
|
|
178
|
+
#onClipboardPaste = null;
|
|
173
179
|
/** TUI2-R3v2 ①: one-shot — a panel that just closed swallows the
|
|
174
180
|
* habitual trailing enter rather than submitting the restored draft. */
|
|
175
181
|
#swallowEnter = false;
|
|
@@ -299,11 +305,11 @@ export class Editor {
|
|
|
299
305
|
* over while the run is told to stop. Mirrors onEscape (a list, so
|
|
300
306
|
* listeners can coexist); the line arrives already gone from the
|
|
301
307
|
* composer, exactly as a submit's does. */
|
|
302
|
-
/** REL-0152-D11:
|
|
303
|
-
*
|
|
304
|
-
* moment. */
|
|
305
|
-
|
|
306
|
-
this.#
|
|
308
|
+
/** REL-0152-D11/D15: where to get the clipboard's contents when the
|
|
309
|
+
* human asks for them — ctrl+V, or an empty paste. The CLI owns the
|
|
310
|
+
* platform, the editor owns the moment. */
|
|
311
|
+
onClipboardPaste(cb) {
|
|
312
|
+
this.#onClipboardPaste = cb;
|
|
307
313
|
}
|
|
308
314
|
onRedirect(cb) {
|
|
309
315
|
this.#redirectCbs.push(cb);
|
|
@@ -1195,6 +1201,18 @@ export class Editor {
|
|
|
1195
1201
|
this.#atAccept();
|
|
1196
1202
|
i += 1;
|
|
1197
1203
|
}
|
|
1204
|
+
else if (c === "\x16") {
|
|
1205
|
+
// REL-0152-D15: ctrl+V asks for the clipboard. Inert with no
|
|
1206
|
+
// hook wired, and 0x16 must NEVER reach the buffer — an
|
|
1207
|
+
// unhandled control byte in a prompt is a corrupt prompt.
|
|
1208
|
+
const text = this.#onClipboardPaste?.() ?? null;
|
|
1209
|
+
if (text !== null && text !== "") {
|
|
1210
|
+
for (const ch of text)
|
|
1211
|
+
this.#insert(ch.codePointAt(0));
|
|
1212
|
+
this.#onRender();
|
|
1213
|
+
}
|
|
1214
|
+
i += 1;
|
|
1215
|
+
}
|
|
1198
1216
|
else if (c === "\x12") {
|
|
1199
1217
|
// W15: the expand key (ctrl+r) — rides the chain like a
|
|
1200
1218
|
// command, the editor just forwards it.
|
|
@@ -1933,7 +1951,7 @@ export class Editor {
|
|
|
1933
1951
|
// REL-0152-D11: an empty paste is the image case — see
|
|
1934
1952
|
// #onEmptyPaste. Anything it returns is ordinary text from here
|
|
1935
1953
|
// on and takes the same route as if it had been typed.
|
|
1936
|
-
const substitute = this.#
|
|
1954
|
+
const substitute = this.#onClipboardPaste?.() ?? null;
|
|
1937
1955
|
if (substitute === null || substitute === "")
|
|
1938
1956
|
return;
|
|
1939
1957
|
run = [...substitute].map((ch) => ch.codePointAt(0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.8",
|
|
4
4
|
"description": "kiso tui \u2014 the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vincemakes/kiso-tui-cells": "0.15.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.15.8"
|
|
39
39
|
}
|
|
40
40
|
}
|