@vincemakes/kiso-tui 0.15.8 → 0.15.10
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.d.ts +26 -0
- package/dist/compositor.js +150 -18
- package/dist/editor.d.ts +4 -0
- package/dist/editor.js +36 -6
- package/package.json +2 -2
package/dist/compositor.d.ts
CHANGED
|
@@ -212,6 +212,32 @@ export declare class Body {
|
|
|
212
212
|
* live region's first row) — NEVER at the formula's committed-count
|
|
213
213
|
* top: external writes (the CLI's console.error CRLF) can shift the
|
|
214
214
|
* committed content down, and a formula-top ED0 would clear it. */
|
|
215
|
+
/**
|
|
216
|
+
* REL-0152-D18 — a window DRAG is one resize, not forty.
|
|
217
|
+
*
|
|
218
|
+
* SIGWINCH fires continuously while a drag is in progress — every few
|
|
219
|
+
* pixels is another signal — and this used to answer each one at once
|
|
220
|
+
* with an erase and a full repaint. A real terminal REFLOWS on a width
|
|
221
|
+
* change and pushes the rows it displaces into its scrollback, so
|
|
222
|
+
* every one of those repaints deposited another copy of the screen
|
|
223
|
+
* into the history. The owner's two-second drag left six identical
|
|
224
|
+
* copies of the same tool block in the transcript.
|
|
225
|
+
*
|
|
226
|
+
* Nothing clever can undo that afterwards: the scrollback is not ours
|
|
227
|
+
* to rewrite. The only fix is to not paint into the middle of a drag.
|
|
228
|
+
*
|
|
229
|
+
* The geometry is adopted IMMEDIATELY — every cap and bound is read
|
|
230
|
+
* from `#opts` at frame time, so the model is already at the new size
|
|
231
|
+
* and nothing is computed against a stale width. What waits is the
|
|
232
|
+
* PAINT. When the signals stop, one erase and one repaint.
|
|
233
|
+
*
|
|
234
|
+
* The competitor does not have this problem, for a structural reason
|
|
235
|
+
* worth naming rather than envying: it runs on the alternate screen,
|
|
236
|
+
* which has no scrollback to accumulate into. kiso is on the primary
|
|
237
|
+
* screen deliberately — the transcript IS the terminal's own
|
|
238
|
+
* scrollback, which is the product's whole claim — so it pays for
|
|
239
|
+
* that choice here, and has to pay carefully.
|
|
240
|
+
*/
|
|
215
241
|
onResize(): void;
|
|
216
242
|
/** W18: the status row's right-aligned hint is part of the status
|
|
217
243
|
* state — the compacting row passes "esc to cancel" (the affordance
|
package/dist/compositor.js
CHANGED
|
@@ -64,6 +64,10 @@ const SPINNER_MS = 200; // the spinner cadence — a ONE-SHOT re-armed on demand
|
|
|
64
64
|
/** REL-0152-R1: a held row that has never been painted, so the first
|
|
65
65
|
* frame writes every row rather than trusting an empty string. */
|
|
66
66
|
const NOT_PAINTED = "\u0000never";
|
|
67
|
+
/** REL-0152-D18 — how long a drag has to be quiet before it is over.
|
|
68
|
+
* Long enough that a continuous drag never crosses it, short enough
|
|
69
|
+
* that a single resize still feels immediate. */
|
|
70
|
+
const RESIZE_SETTLE_MS = 80;
|
|
67
71
|
const CHROME_ROWS = 4; // box top + input + box bottom + status — the design §03 chrome (V6-3; the box is W6)
|
|
68
72
|
/** W20 — the whole-table-replace comparison: the live task block only
|
|
69
73
|
* redraws when the items actually changed (the task extension's
|
|
@@ -141,6 +145,11 @@ export class Body {
|
|
|
141
145
|
#scrolledOff = 0;
|
|
142
146
|
/** REL-0152-R1: this frame is the repaint after a SIGWINCH. */
|
|
143
147
|
#resizeFrame = false;
|
|
148
|
+
/** REL-0152-D18: a drag's signals are still arriving. */
|
|
149
|
+
#resizePending = false;
|
|
150
|
+
/** REL-0152-D19: the inherited terminal has not been released yet. */
|
|
151
|
+
#needsReset = false;
|
|
152
|
+
#resizeTimer = null;
|
|
144
153
|
#lastH = 0;
|
|
145
154
|
// KC1 §6: the composer's recorded extent — the row count the last
|
|
146
155
|
// frame drew (exit's clear walks it) and the row its CHA parked the
|
|
@@ -867,7 +876,43 @@ export class Body {
|
|
|
867
876
|
if (process.stdout.isTTY !== true || palette().bold === "" || rows < 4)
|
|
868
877
|
return;
|
|
869
878
|
this.#docked = true;
|
|
870
|
-
this.#
|
|
879
|
+
this.#guardOutput();
|
|
880
|
+
// REL-0152-D19: RELEASE the terminal we were handed, before the
|
|
881
|
+
// first frame.
|
|
882
|
+
//
|
|
883
|
+
// The dock resets on the way OUT and reset nothing on the way IN,
|
|
884
|
+
// so every session began in whatever state the previous occupant
|
|
885
|
+
// left. For THIS product that is not an edge case: kiso's claim is
|
|
886
|
+
// that it survives kill -9, which makes "the last instance died
|
|
887
|
+
// without running its teardown" a supported and advertised way to
|
|
888
|
+
// arrive here.
|
|
889
|
+
//
|
|
890
|
+
// Both of these confine writes to a SUB-REGION of the screen,
|
|
891
|
+
// which is the only way content can survive at a column the dock
|
|
892
|
+
// paints — every chrome row is exactly W wide and written from
|
|
893
|
+
// column 1, so an erase plus a W-wide write covers the row end to
|
|
894
|
+
// end unless the write cannot reach the ends.
|
|
895
|
+
//
|
|
896
|
+
// ESC[r the scroll region back to the whole screen
|
|
897
|
+
// ESC[?69l left/right margin mode OFF (DECLRMM) — ESC[r does
|
|
898
|
+
// NOT release margins, which is why resetting only the
|
|
899
|
+
// region on exit was never enough
|
|
900
|
+
//
|
|
901
|
+
// And two that a KILLED kiso leaves set, which REL-0152-D14
|
|
902
|
+
// introduced and this is the first thing to defend against it: a
|
|
903
|
+
// frame turns autowrap off and the cursor invisible and restores
|
|
904
|
+
// both at its end, so a process that dies BETWEEN those two points
|
|
905
|
+
// hands the shell — and the next kiso — a terminal with wrapping
|
|
906
|
+
// off and no cursor. `kill -9` cannot be caught, so the entry is
|
|
907
|
+
// the only place this can be repaired, and a product whose claim
|
|
908
|
+
// is that it survives kill -9 has to repair it there.
|
|
909
|
+
//
|
|
910
|
+
// ESC[?7h autowrap back to its default
|
|
911
|
+
// ESC[?25h the cursor visible again
|
|
912
|
+
//
|
|
913
|
+
// Idempotent, sixteen bytes, once per session, and correct whether
|
|
914
|
+
// or not anything was actually left set.
|
|
915
|
+
this.#needsReset = true;
|
|
871
916
|
this.#attachResize();
|
|
872
917
|
this.#fullRedraw = true;
|
|
873
918
|
this.#dirty = true;
|
|
@@ -877,6 +922,13 @@ export class Body {
|
|
|
877
922
|
* chrome rows cleared, the cursor home at the input line. */
|
|
878
923
|
exit() {
|
|
879
924
|
this.#unguard?.();
|
|
925
|
+
// REL-0152-D18: a drag in flight must not repaint into a torn-down
|
|
926
|
+
// dock — the timer outlives the compositor otherwise.
|
|
927
|
+
if (this.#resizeTimer !== null) {
|
|
928
|
+
clearTimeout(this.#resizeTimer);
|
|
929
|
+
this.#resizeTimer = null;
|
|
930
|
+
}
|
|
931
|
+
this.#resizePending = false;
|
|
880
932
|
// TUI2-R3v2 ②: the mouse disable rides the SAME teardown as CSI r,
|
|
881
933
|
// and rides it BEFORE the docked guard — an un-docked compositor is
|
|
882
934
|
// exactly the state a superseded or half-torn-down one is in, and
|
|
@@ -936,26 +988,56 @@ export class Body {
|
|
|
936
988
|
* change, and the cursor parks on the wrong row. Three PTY gates
|
|
937
989
|
* caught it.
|
|
938
990
|
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
991
|
+
* REL-0152-D17: BOTH descriptors, and the second one is the one that
|
|
992
|
+
* mattered. The first version of this guard wrapped stdout alone,
|
|
993
|
+
* while kiso's degradation notices go to stderr through
|
|
994
|
+
* `console.error` — and several of them BEGIN with `[` and contain
|
|
995
|
+
* `]`:
|
|
996
|
+
*
|
|
997
|
+
* [extensions] … [project .kiso] …
|
|
998
|
+
* [KISO_FAUX_SCRIPT] … [run failed] …
|
|
999
|
+
*
|
|
1000
|
+
* Those land on the tty wherever the cursor is, and a renderer that
|
|
1001
|
+
* does not know they were printed skips exactly the rows that would
|
|
1002
|
+
* repair them. The residue survives on the rows whose desired content
|
|
1003
|
+
* NEVER changes — the composer box's own edges — which is a stray `[`
|
|
1004
|
+
* at the left and `]` at the right for the rest of the session,
|
|
1005
|
+
* clearing only on a resize and returning on the next launch.
|
|
1006
|
+
*
|
|
1007
|
+
* The reasoning that missed it was mine, and it is worth keeping: the
|
|
1008
|
+
* renderer emits no OSC and no bare `]`, so a `]` on screen CANNOT
|
|
1009
|
+
* have come from its stream. I had that fact and concluded "therefore
|
|
1010
|
+
* the terminal invents it" when the only sound conclusion is
|
|
1011
|
+
* "therefore something else wrote it".
|
|
1012
|
+
*
|
|
1013
|
+
* The fix is not a list of writers to remember, and not silencing the
|
|
1014
|
+
* loggers. A compositor cannot hold a belief about a terminal it does
|
|
1015
|
+
* not own: any write it did not make forgets the screen, whichever
|
|
1016
|
+
* descriptor carried it. New callers cannot get it wrong because they
|
|
1017
|
+
* are not asked to get it right.
|
|
943
1018
|
*/
|
|
944
|
-
#
|
|
1019
|
+
#guardOutput() {
|
|
945
1020
|
if (this.#unguard !== null || this.#opts.write !== undefined)
|
|
946
1021
|
return;
|
|
947
|
-
const
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1022
|
+
const restores = [];
|
|
1023
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
1024
|
+
const real = stream.write.bind(stream);
|
|
1025
|
+
const patched = ((chunk, ...rest) => {
|
|
1026
|
+
if (!this.#inFrame)
|
|
1027
|
+
this.#screen = [];
|
|
1028
|
+
return real(chunk, ...rest);
|
|
1029
|
+
});
|
|
1030
|
+
stream.write = patched;
|
|
1031
|
+
restores.push(() => {
|
|
1032
|
+
// only restore what we installed — another wrapper may have
|
|
1033
|
+
// been layered on top since (the byte trace does exactly that)
|
|
1034
|
+
if (stream.write === patched)
|
|
1035
|
+
stream.write = real;
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
954
1038
|
this.#unguard = () => {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (process.stdout.write === patched)
|
|
958
|
-
process.stdout.write = real;
|
|
1039
|
+
for (const r of restores)
|
|
1040
|
+
r();
|
|
959
1041
|
this.#unguard = null;
|
|
960
1042
|
};
|
|
961
1043
|
}
|
|
@@ -971,9 +1053,50 @@ export class Body {
|
|
|
971
1053
|
* live region's first row) — NEVER at the formula's committed-count
|
|
972
1054
|
* top: external writes (the CLI's console.error CRLF) can shift the
|
|
973
1055
|
* committed content down, and a formula-top ED0 would clear it. */
|
|
1056
|
+
/**
|
|
1057
|
+
* REL-0152-D18 — a window DRAG is one resize, not forty.
|
|
1058
|
+
*
|
|
1059
|
+
* SIGWINCH fires continuously while a drag is in progress — every few
|
|
1060
|
+
* pixels is another signal — and this used to answer each one at once
|
|
1061
|
+
* with an erase and a full repaint. A real terminal REFLOWS on a width
|
|
1062
|
+
* change and pushes the rows it displaces into its scrollback, so
|
|
1063
|
+
* every one of those repaints deposited another copy of the screen
|
|
1064
|
+
* into the history. The owner's two-second drag left six identical
|
|
1065
|
+
* copies of the same tool block in the transcript.
|
|
1066
|
+
*
|
|
1067
|
+
* Nothing clever can undo that afterwards: the scrollback is not ours
|
|
1068
|
+
* to rewrite. The only fix is to not paint into the middle of a drag.
|
|
1069
|
+
*
|
|
1070
|
+
* The geometry is adopted IMMEDIATELY — every cap and bound is read
|
|
1071
|
+
* from `#opts` at frame time, so the model is already at the new size
|
|
1072
|
+
* and nothing is computed against a stale width. What waits is the
|
|
1073
|
+
* PAINT. When the signals stop, one erase and one repaint.
|
|
1074
|
+
*
|
|
1075
|
+
* The competitor does not have this problem, for a structural reason
|
|
1076
|
+
* worth naming rather than envying: it runs on the alternate screen,
|
|
1077
|
+
* which has no scrollback to accumulate into. kiso is on the primary
|
|
1078
|
+
* screen deliberately — the transcript IS the terminal's own
|
|
1079
|
+
* scrollback, which is the product's whole claim — so it pays for
|
|
1080
|
+
* that choice here, and has to pay carefully.
|
|
1081
|
+
*/
|
|
974
1082
|
onResize() {
|
|
975
1083
|
if (!this.#isActive())
|
|
976
1084
|
return;
|
|
1085
|
+
this.#resizePending = true;
|
|
1086
|
+
if (this.#resizeTimer !== null)
|
|
1087
|
+
clearTimeout(this.#resizeTimer);
|
|
1088
|
+
this.#resizeTimer = setTimeout(() => {
|
|
1089
|
+
this.#resizeTimer = null;
|
|
1090
|
+
this.#settleResize();
|
|
1091
|
+
}, RESIZE_SETTLE_MS);
|
|
1092
|
+
if (this.#resizeTimer.unref !== undefined)
|
|
1093
|
+
this.#resizeTimer.unref();
|
|
1094
|
+
}
|
|
1095
|
+
/** The one repaint a drag earns, once its signals have stopped. */
|
|
1096
|
+
#settleResize() {
|
|
1097
|
+
if (!this.#resizePending || !this.#isActive())
|
|
1098
|
+
return;
|
|
1099
|
+
this.#resizePending = false;
|
|
977
1100
|
const H = this.#opts.height();
|
|
978
1101
|
const liveRows = this.#lastLiveRows > 0 ? this.#lastLiveRows : 3;
|
|
979
1102
|
const from = Math.max(1, (this.#lastH > 0 ? this.#lastH : H) - liveRows + 1);
|
|
@@ -981,7 +1104,7 @@ export class Body {
|
|
|
981
1104
|
this.#fullRedraw = true;
|
|
982
1105
|
this.#resizeFrame = true;
|
|
983
1106
|
this.#dirty = true;
|
|
984
|
-
this.render();
|
|
1107
|
+
this.render();
|
|
985
1108
|
}
|
|
986
1109
|
/** W18: the status row's right-aligned hint is part of the status
|
|
987
1110
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
@@ -1343,6 +1466,15 @@ export class Body {
|
|
|
1343
1466
|
// Restored at the end of every frame: prose written OUTSIDE a
|
|
1344
1467
|
// frame (bodyLog, an error) is ordinary output and must still
|
|
1345
1468
|
// wrap, and the shell inherits the terminal when kiso exits.
|
|
1469
|
+
if (this.#needsReset) {
|
|
1470
|
+
// REL-0152-D19: released as the FIRST bytes of the FIRST frame
|
|
1471
|
+
// rather than as a write of its own. A separate write would be
|
|
1472
|
+
// one more thing between the dock coming up and its first
|
|
1473
|
+
// frame, and the PTY gates that wait on the boot stream feel
|
|
1474
|
+
// it; inside the frame it is four sequences and no new event.
|
|
1475
|
+
this.#needsReset = false;
|
|
1476
|
+
out.push("\x1b[r\x1b[?69l\x1b[?7h\x1b[?25h");
|
|
1477
|
+
}
|
|
1346
1478
|
out.push("\x1b[?7l");
|
|
1347
1479
|
out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h"); // D1: sync ON, or cursor-hide where 2026 is dead bytes
|
|
1348
1480
|
// A8: the bottom-anchored window (the model's last H rows) shifts
|
package/dist/editor.d.ts
CHANGED
|
@@ -72,6 +72,10 @@ export declare class Editor {
|
|
|
72
72
|
* human asks for them — ctrl+V, or an empty paste. The CLI owns the
|
|
73
73
|
* platform, the editor owns the moment. */
|
|
74
74
|
onClipboardPaste(cb: () => string | null): void;
|
|
75
|
+
/** REL-0152-D16: the files this line's `[Image #N]` capsules stand
|
|
76
|
+
* for, by their number. The CLI resolves them when it builds the
|
|
77
|
+
* turn; a capsule the human deleted is simply never looked up. */
|
|
78
|
+
attachments(): Map<number, string>;
|
|
75
79
|
onRedirect(cb: (line: string) => void): void;
|
|
76
80
|
/** W22: bind the pending-turn queue — the CLI's live slots. The ↑
|
|
77
81
|
* pop walks them (each pop leaves the queue, cancelling the turn);
|
package/dist/editor.js
CHANGED
|
@@ -131,6 +131,24 @@ export class Editor {
|
|
|
131
131
|
* every entry is text they chose to paste and may still submit.
|
|
132
132
|
*/
|
|
133
133
|
#pastes = new Map();
|
|
134
|
+
/**
|
|
135
|
+
* REL-0152-D16 — which file each `[Image #N]` capsule stands for.
|
|
136
|
+
*
|
|
137
|
+
* D15 made ctrl+V fetch the clipboard and it worked; then it inserted
|
|
138
|
+
* the PATH, the path began with `/`, and the composer handed it to
|
|
139
|
+
* the slash-command dispatcher. A feature that reaches the last step
|
|
140
|
+
* and gives the result to the wrong parser has not shipped.
|
|
141
|
+
*
|
|
142
|
+
* So the buffer carries a token and this carries the file. The token
|
|
143
|
+
* is what the LINE is — the dispatcher sees `[Image #1]`, which is
|
|
144
|
+
* not a command and never could be — and the CLI reads this map when
|
|
145
|
+
* it builds the turn. Unlike the text capsule, the image is NOT
|
|
146
|
+
* expanded into the line on the way out: a path is not something the
|
|
147
|
+
* model should be sent, and a transcript full of temp-file names is
|
|
148
|
+
* not something the human should have to read.
|
|
149
|
+
*/
|
|
150
|
+
#attachments = new Map();
|
|
151
|
+
#attachSeq = 0;
|
|
134
152
|
#pasteSeq = 0;
|
|
135
153
|
/** The buffer index where the in-flight paste began; null outside one. */
|
|
136
154
|
#pasteAt = null;
|
|
@@ -311,6 +329,12 @@ export class Editor {
|
|
|
311
329
|
onClipboardPaste(cb) {
|
|
312
330
|
this.#onClipboardPaste = cb;
|
|
313
331
|
}
|
|
332
|
+
/** REL-0152-D16: the files this line's `[Image #N]` capsules stand
|
|
333
|
+
* for, by their number. The CLI resolves them when it builds the
|
|
334
|
+
* turn; a capsule the human deleted is simply never looked up. */
|
|
335
|
+
attachments() {
|
|
336
|
+
return new Map(this.#attachments);
|
|
337
|
+
}
|
|
314
338
|
onRedirect(cb) {
|
|
315
339
|
this.#redirectCbs.push(cb);
|
|
316
340
|
}
|
|
@@ -1205,9 +1229,13 @@ export class Editor {
|
|
|
1205
1229
|
// REL-0152-D15: ctrl+V asks for the clipboard. Inert with no
|
|
1206
1230
|
// hook wired, and 0x16 must NEVER reach the buffer — an
|
|
1207
1231
|
// unhandled control byte in a prompt is a corrupt prompt.
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1232
|
+
const file = this.#onClipboardPaste?.() ?? null;
|
|
1233
|
+
if (file !== null && file !== "") {
|
|
1234
|
+
// REL-0152-D16: the capsule goes in the buffer, the file
|
|
1235
|
+
// goes beside it. See #attachments.
|
|
1236
|
+
this.#attachSeq += 1;
|
|
1237
|
+
this.#attachments.set(this.#attachSeq, file);
|
|
1238
|
+
for (const ch of `[Image #${this.#attachSeq}]`)
|
|
1211
1239
|
this.#insert(ch.codePointAt(0));
|
|
1212
1240
|
this.#onRender();
|
|
1213
1241
|
}
|
|
@@ -1951,10 +1979,12 @@ export class Editor {
|
|
|
1951
1979
|
// REL-0152-D11: an empty paste is the image case — see
|
|
1952
1980
|
// #onEmptyPaste. Anything it returns is ordinary text from here
|
|
1953
1981
|
// on and takes the same route as if it had been typed.
|
|
1954
|
-
const
|
|
1955
|
-
if (
|
|
1982
|
+
const file = this.#onClipboardPaste?.() ?? null;
|
|
1983
|
+
if (file === null || file === "")
|
|
1956
1984
|
return;
|
|
1957
|
-
|
|
1985
|
+
this.#attachSeq += 1;
|
|
1986
|
+
this.#attachments.set(this.#attachSeq, file);
|
|
1987
|
+
run = [...`[Image #${this.#attachSeq}]`].map((ch) => ch.codePointAt(0));
|
|
1958
1988
|
}
|
|
1959
1989
|
if (this.#historyIdx !== null)
|
|
1960
1990
|
this.#historyIdx = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.10",
|
|
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.10"
|
|
39
39
|
}
|
|
40
40
|
}
|