@vincemakes/kiso-tui 0.16.0 → 0.16.2
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 +52 -7
- package/dist/editor.js +99 -0
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -1473,7 +1473,11 @@ export class Body {
|
|
|
1473
1473
|
// frame, and the PTY gates that wait on the boot stream feel
|
|
1474
1474
|
// it; inside the frame it is four sequences and no new event.
|
|
1475
1475
|
this.#needsReset = false;
|
|
1476
|
-
|
|
1476
|
+
// REL-0161: the reset ESTABLISHES the hidden cursor (?25l) —
|
|
1477
|
+
// the steady state for the session's whole life. It repairs a
|
|
1478
|
+
// killed predecessor straight into the same state. The visible
|
|
1479
|
+
// cursor comes back exactly once, in editor.exit().
|
|
1480
|
+
out.push("\x1b[r\x1b[?69l\x1b[?7h\x1b[?25l");
|
|
1477
1481
|
// REL-0152-D20 — the mechanism is understood and the obvious fix
|
|
1478
1482
|
// is NOT taken here. See the finding.
|
|
1479
1483
|
//
|
|
@@ -1493,7 +1497,12 @@ export class Body {
|
|
|
1493
1497
|
// for stdin).
|
|
1494
1498
|
}
|
|
1495
1499
|
out.push("\x1b[?7l");
|
|
1496
|
-
|
|
1500
|
+
// REL-0161: ?25l is the STEADY state now, not a frame bracket —
|
|
1501
|
+
// re-asserted at every frame open as self-healing (a subprocess
|
|
1502
|
+
// or stray output that showed the cursor is corrected here), and
|
|
1503
|
+
// never paired with a ?25h. Sync (2026) still brackets the frame
|
|
1504
|
+
// where the terminal understands it.
|
|
1505
|
+
out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h\x1b[?25l");
|
|
1497
1506
|
// A8: the bottom-anchored window (the model's last H rows) shifts
|
|
1498
1507
|
// DOWN when the live region SHRINKS — the done-fold, the fold-hold
|
|
1499
1508
|
// release at the terminal event. The steady path's scroll syncs
|
|
@@ -1517,7 +1526,12 @@ export class Body {
|
|
|
1517
1526
|
// with it.
|
|
1518
1527
|
this.#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1519
1528
|
this.#fullRedraw = false;
|
|
1520
|
-
|
|
1529
|
+
// REL-0161: the frame close no longer shows the cursor — hidden IS
|
|
1530
|
+
// the steady state (Terminal.app infers "a prompt line" from
|
|
1531
|
+
// bracketed-paste + a visible cursor and decorates it with a Mark;
|
|
1532
|
+
// the composer draws its own cursor cell instead — #inputRowBytes).
|
|
1533
|
+
if (!this.#conservative)
|
|
1534
|
+
out.push("\x1b[?2026l");
|
|
1521
1535
|
out.push("\x1b[?7h"); // REL-0152-D14: autowrap back on for everything outside the frame
|
|
1522
1536
|
this.#inFrame = true;
|
|
1523
1537
|
try {
|
|
@@ -1902,16 +1916,47 @@ export class Body {
|
|
|
1902
1916
|
markerLine += CURSOR_MARKER;
|
|
1903
1917
|
markerCell = w;
|
|
1904
1918
|
}
|
|
1905
|
-
const stripped0 = markerLine.replace(CURSOR_MARKER, "");
|
|
1906
1919
|
if (W < 4) {
|
|
1907
1920
|
// the degenerate screen: the box cannot hold its walls — the
|
|
1908
1921
|
// bare row (the pre-W6 bytes; the fold probe's pass-through
|
|
1909
1922
|
// line still crashes invariant ① downstream, as before)
|
|
1910
|
-
return { stripped:
|
|
1923
|
+
return { stripped: markerLine.replace(CURSOR_MARKER, ""), markerCell };
|
|
1924
|
+
}
|
|
1925
|
+
// REL-0161 — the DRAWN cursor. The hardware cursor is hidden for
|
|
1926
|
+
// the session's life (Terminal.app infers "a prompt line" from
|
|
1927
|
+
// bracketed-paste + a visible cursor and Marks it), so the cell
|
|
1928
|
+
// at the marker renders inverse instead. The hardware cursor
|
|
1929
|
+
// still PARKS at the marker — the IME anchor is its position.
|
|
1930
|
+
const mi = markerLine.indexOf(CURSOR_MARKER);
|
|
1931
|
+
let stripped0;
|
|
1932
|
+
let cursorPad = 0; // 1 when the drawn cursor consumed a pad cell
|
|
1933
|
+
if (mi < 0) {
|
|
1934
|
+
stripped0 = markerLine; // a row that does not own the cursor
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
const before = markerLine.slice(0, mi);
|
|
1938
|
+
const after = markerLine.slice(mi + CURSOR_MARKER.length);
|
|
1939
|
+
if (after.length === 0) {
|
|
1940
|
+
// the cursor rests past the content — an inverse space,
|
|
1941
|
+
// taken OUT of the pad (the walk capped content at W−4,
|
|
1942
|
+
// so the pad is ≥ 1 and the row still totals W)
|
|
1943
|
+
stripped0 = `${before}\x1b[7m \x1b[27m`;
|
|
1944
|
+
cursorPad = 1;
|
|
1945
|
+
}
|
|
1946
|
+
else if (after[0] === "\x1b" || (after.codePointAt(0) >= 0xd800 && after.codePointAt(0) <= 0xdfff)) {
|
|
1947
|
+
// never wrap a sequence, never split a surrogate pair —
|
|
1948
|
+
// the cell keeps its bytes, the parked cursor still marks
|
|
1949
|
+
// the position for the terminal's own affordances
|
|
1950
|
+
stripped0 = before + after;
|
|
1951
|
+
}
|
|
1952
|
+
else {
|
|
1953
|
+
const glyph = String.fromCodePoint(after.codePointAt(0));
|
|
1954
|
+
stripped0 = `${before}\x1b[7m${glyph}\x1b[27m${after.slice(glyph.length)}`;
|
|
1955
|
+
}
|
|
1911
1956
|
}
|
|
1912
1957
|
// the pad completes the row to W — the content stopped at W−4,
|
|
1913
|
-
// so the pad is ≥ 1
|
|
1914
|
-
const padW = W - 3 - w;
|
|
1958
|
+
// so the pad is ≥ 1 (≥ 0 after the drawn cursor consumed one)
|
|
1959
|
+
const padW = W - 3 - w - cursorPad;
|
|
1915
1960
|
return { stripped: `\x1b[2m│ \x1b[0m${stripped0}\x1b[2m${" ".repeat(padW)}│\x1b[0m`, markerCell };
|
|
1916
1961
|
}
|
|
1917
1962
|
/** KC1 §6 — the focus component's input ROWS (N = 1 today's single
|
package/dist/editor.js
CHANGED
|
@@ -270,6 +270,19 @@ export class Editor {
|
|
|
270
270
|
#history = [];
|
|
271
271
|
#historyIdx = null;
|
|
272
272
|
#preBrowse = [];
|
|
273
|
+
// UD-1: minimal draft undo. Two stacks of FROZEN snapshots beside
|
|
274
|
+
// the one mutable buffer (the KC1 flat-buffer discipline holds —
|
|
275
|
+
// the stacks are history, never a second projection). A checkpoint
|
|
276
|
+
// is pushed only by a gesture about to discard ≥1 code point (the
|
|
277
|
+
// kills, the menu-esc clear, each queue-pop replacement, the
|
|
278
|
+
// @-apply splice); typing and single backspace push nothing — v1
|
|
279
|
+
// is loss-recovery, not char-granular edit history. ctrl+z
|
|
280
|
+
// restores text+cursor exactly; ctrl+y mirrors; undo never
|
|
281
|
+
// discards (what it replaces always lands on the redo stack).
|
|
282
|
+
// Both stacks clear on submit/clearLine — a sent turn is in the
|
|
283
|
+
// durable log and the ↑ history, not a loss.
|
|
284
|
+
#undoStack = [];
|
|
285
|
+
#redoStack = [];
|
|
273
286
|
// W22: the pending-turn queue's bound state — the CLI's live slots
|
|
274
287
|
// (chat.ts). ↑ pops the LAST queued message into the buffer and
|
|
275
288
|
// enters the pop-mode (the walk: repeated ↑ pop older ones, each
|
|
@@ -355,6 +368,8 @@ export class Editor {
|
|
|
355
368
|
return this.#sheetOpen;
|
|
356
369
|
}
|
|
357
370
|
clearLine() {
|
|
371
|
+
this.#undoStack.length = 0; // UD-1
|
|
372
|
+
this.#redoStack.length = 0;
|
|
358
373
|
this.#chars = [];
|
|
359
374
|
this.#cursor = 0;
|
|
360
375
|
this.#scroll = 0;
|
|
@@ -577,6 +592,8 @@ export class Editor {
|
|
|
577
592
|
if (view === null)
|
|
578
593
|
return;
|
|
579
594
|
const insert = [...`@${view.matches[view.selected].path} `].map((ch) => ch.codePointAt(0));
|
|
595
|
+
if (this.#cursor - view.start >= 1)
|
|
596
|
+
this.#checkpoint(); // UD-1
|
|
580
597
|
this.#chars.splice(view.start, this.#cursor - view.start, ...insert);
|
|
581
598
|
this.#cursor = view.start + insert.length;
|
|
582
599
|
this.#atClose();
|
|
@@ -748,6 +765,12 @@ export class Editor {
|
|
|
748
765
|
process.stdout.write(MOUSE_OFF);
|
|
749
766
|
this.#mouseOn = false;
|
|
750
767
|
process.stdout.write("\x1b[?2004l"); // bracketed paste OFF
|
|
768
|
+
// REL-0161: the hardware cursor was hidden for the session's whole
|
|
769
|
+
// life (the compositor's entry reset); this is the one place kiso
|
|
770
|
+
// hands the terminal back. kill -9 skips it — the same exposure
|
|
771
|
+
// the reference implementation accepts; the entry repair covers
|
|
772
|
+
// the next kiso, and `reset` covers the shell.
|
|
773
|
+
process.stdout.write("\x1b[?25h");
|
|
751
774
|
process.stdin.setRawMode(false);
|
|
752
775
|
this.#closedResolve();
|
|
753
776
|
}
|
|
@@ -1096,6 +1119,8 @@ export class Editor {
|
|
|
1096
1119
|
// v3 §04: Esc closes the menu and clears the buffer.
|
|
1097
1120
|
// CA-4: the closing esc consumes its burst (the `i += 1`
|
|
1098
1121
|
// convention) — a double-esc can never abort the turn.
|
|
1122
|
+
if (this.#chars.length > 0)
|
|
1123
|
+
this.#checkpoint(); // UD-1
|
|
1099
1124
|
this.#chars = [];
|
|
1100
1125
|
this.#cursor = 0;
|
|
1101
1126
|
this.#scroll = 0;
|
|
@@ -1195,6 +1220,14 @@ export class Editor {
|
|
|
1195
1220
|
this.#killWord();
|
|
1196
1221
|
i += 1;
|
|
1197
1222
|
}
|
|
1223
|
+
else if (c === "\x1a" || c === "\x1f") {
|
|
1224
|
+
this.#undoOp(); // UD-1: ctrl+z (and the readline ctrl+_)
|
|
1225
|
+
i += 1;
|
|
1226
|
+
}
|
|
1227
|
+
else if (c === "\x19") {
|
|
1228
|
+
this.#redoOp(); // UD-1: ctrl+y
|
|
1229
|
+
i += 1;
|
|
1230
|
+
}
|
|
1198
1231
|
else if (c === "\x01") {
|
|
1199
1232
|
this.#cursor = this.#cursorBounds().start; // A3: line-local (a single line starts at 0 — unchanged)
|
|
1200
1233
|
this.#reflow();
|
|
@@ -1885,8 +1918,66 @@ export class Editor {
|
|
|
1885
1918
|
if (!this.#pasting)
|
|
1886
1919
|
this.#onRender();
|
|
1887
1920
|
}
|
|
1921
|
+
// ---- UD-1: the undo machinery ----
|
|
1922
|
+
/** The caps: entries and total code points (a D13-class 260KB paste
|
|
1923
|
+
* fits with room). Evict oldest; both stacks share the shape. */
|
|
1924
|
+
static #UNDO_CAP = 64;
|
|
1925
|
+
static #UNDO_CP_CAP = 2 * 1024 * 1024;
|
|
1926
|
+
#snap() {
|
|
1927
|
+
return { chars: [...this.#chars], cursor: this.#cursor };
|
|
1928
|
+
}
|
|
1929
|
+
#sameSnap(a, b) {
|
|
1930
|
+
return a.cursor === b.cursor && a.chars.length === b.chars.length && a.chars.every((c, i) => c === b.chars[i]);
|
|
1931
|
+
}
|
|
1932
|
+
#capStack(stack) {
|
|
1933
|
+
while (stack.length > _a.#UNDO_CAP)
|
|
1934
|
+
stack.shift();
|
|
1935
|
+
let total = stack.reduce((n, s) => n + s.chars.length, 0);
|
|
1936
|
+
while (stack.length > 1 && total > _a.#UNDO_CP_CAP)
|
|
1937
|
+
total -= stack.shift().chars.length;
|
|
1938
|
+
}
|
|
1939
|
+
/** Push the CURRENT state before a destructive gesture. Always
|
|
1940
|
+
* clears the redo stack (a new destruction forks history); the
|
|
1941
|
+
* push itself is deduped against the top. */
|
|
1942
|
+
#checkpoint() {
|
|
1943
|
+
this.#redoStack.length = 0;
|
|
1944
|
+
const cur = this.#snap();
|
|
1945
|
+
const top = this.#undoStack.at(-1);
|
|
1946
|
+
if (top !== undefined && this.#sameSnap(top, cur))
|
|
1947
|
+
return;
|
|
1948
|
+
this.#undoStack.push(cur);
|
|
1949
|
+
this.#capStack(this.#undoStack);
|
|
1950
|
+
}
|
|
1951
|
+
#restoreSnap(s) {
|
|
1952
|
+
this.#chars = [...s.chars];
|
|
1953
|
+
this.#cursor = s.cursor;
|
|
1954
|
+
this.#scroll = 0;
|
|
1955
|
+
this.#verticalGoalCol = null;
|
|
1956
|
+
this.#reflow();
|
|
1957
|
+
this.#refreshMenu();
|
|
1958
|
+
if (!this.#pasting)
|
|
1959
|
+
this.#onRender();
|
|
1960
|
+
}
|
|
1961
|
+
#undoOp() {
|
|
1962
|
+
const prev = this.#undoStack.pop();
|
|
1963
|
+
if (prev === undefined)
|
|
1964
|
+
return;
|
|
1965
|
+
this.#redoStack.push(this.#snap());
|
|
1966
|
+
this.#capStack(this.#redoStack);
|
|
1967
|
+
this.#restoreSnap(prev);
|
|
1968
|
+
}
|
|
1969
|
+
#redoOp() {
|
|
1970
|
+
const next = this.#redoStack.pop();
|
|
1971
|
+
if (next === undefined)
|
|
1972
|
+
return;
|
|
1973
|
+
this.#undoStack.push(this.#snap());
|
|
1974
|
+
this.#capStack(this.#undoStack);
|
|
1975
|
+
this.#restoreSnap(next);
|
|
1976
|
+
}
|
|
1888
1977
|
#killToStart() {
|
|
1889
1978
|
const { start } = this.#cursorBounds(); // A3: line-local (0 on a single line — unchanged)
|
|
1979
|
+
if (this.#cursor > start)
|
|
1980
|
+
this.#checkpoint(); // UD-1
|
|
1890
1981
|
this.#chars.splice(start, this.#cursor - start);
|
|
1891
1982
|
this.#cursor = start;
|
|
1892
1983
|
this.#reflow();
|
|
@@ -1895,6 +1986,8 @@ export class Editor {
|
|
|
1895
1986
|
}
|
|
1896
1987
|
#killToEnd() {
|
|
1897
1988
|
const { end } = this.#cursorBounds(); // A3: line-local (the buffer's end on a single line — unchanged)
|
|
1989
|
+
if (end > this.#cursor)
|
|
1990
|
+
this.#checkpoint(); // UD-1
|
|
1898
1991
|
this.#chars.splice(this.#cursor, end - this.#cursor);
|
|
1899
1992
|
this.#reflow();
|
|
1900
1993
|
if (!this.#pasting)
|
|
@@ -1909,6 +2002,8 @@ export class Editor {
|
|
|
1909
2002
|
i -= 1; // trailing spaces
|
|
1910
2003
|
while (i > 0 && this.#chars[i - 1] !== 0x20)
|
|
1911
2004
|
i -= 1; // the word
|
|
2005
|
+
if (i < this.#cursor)
|
|
2006
|
+
this.#checkpoint(); // UD-1
|
|
1912
2007
|
this.#chars.splice(i, this.#cursor - i);
|
|
1913
2008
|
this.#cursor = i;
|
|
1914
2009
|
this.#reflow();
|
|
@@ -2023,6 +2118,8 @@ export class Editor {
|
|
|
2023
2118
|
}
|
|
2024
2119
|
#takeLine() {
|
|
2025
2120
|
const line = String.fromCodePoint(...this.#chars);
|
|
2121
|
+
this.#undoStack.length = 0; // UD-1: a sent turn is not a loss
|
|
2122
|
+
this.#redoStack.length = 0;
|
|
2026
2123
|
this.#chars = [];
|
|
2027
2124
|
this.#cursor = 0;
|
|
2028
2125
|
this.#scroll = 0;
|
|
@@ -2190,6 +2287,8 @@ export class Editor {
|
|
|
2190
2287
|
const line = this.#queuePop();
|
|
2191
2288
|
if (line === null)
|
|
2192
2289
|
return;
|
|
2290
|
+
if (this.#chars.length > 0)
|
|
2291
|
+
this.#checkpoint(); // UD-1: a mid-walk edit is recoverable
|
|
2193
2292
|
this.#chars = [...line].map((ch) => ch.codePointAt(0));
|
|
2194
2293
|
this.#cursor = this.#chars.length;
|
|
2195
2294
|
this.#scroll = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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.16.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.16.2"
|
|
39
39
|
}
|
|
40
40
|
}
|