@vincemakes/kiso-tui 0.31.1 → 0.32.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/dist/compositor.d.ts +17 -0
- package/dist/compositor.js +38 -5
- package/dist/editor.d.ts +34 -0
- package/dist/editor.js +349 -76
- package/dist/panel-input.d.ts +4 -3
- package/package.json +2 -2
package/dist/compositor.d.ts
CHANGED
|
@@ -284,8 +284,25 @@ export declare class Body {
|
|
|
284
284
|
* reaching into a card whose content is still arriving.
|
|
285
285
|
*/
|
|
286
286
|
toggleExpanded(): void;
|
|
287
|
+
/** §2.4 — the terminal holds someone else's drawing (an external
|
|
288
|
+
* editor had it) and the model is the only authority on what should
|
|
289
|
+
* be there. The same act as a resize; the same path. */
|
|
290
|
+
reprint(): void;
|
|
287
291
|
/** DC-50 — the switch itself, for the CLI's affordance text. */
|
|
288
292
|
expandedAll(): boolean;
|
|
293
|
+
/** §2.3 — ctrl+t: the committed thinking blocks fold, and fold back.
|
|
294
|
+
*
|
|
295
|
+
* DC-50's mechanism exactly: one boolean, then the session is printed
|
|
296
|
+
* again, so the blocks ALREADY on screen obey the switch rather than
|
|
297
|
+
* only the next ones. Nothing durable moves — the events are
|
|
298
|
+
* untouched and `/think` still reaches the last block whole.
|
|
299
|
+
*
|
|
300
|
+
* COMMITTED blocks only. The live `thinking…` placeholder belongs to
|
|
301
|
+
* the live region, not to a cell, and an OPEN thinking cell is text
|
|
302
|
+
* still arriving; a global "quiet down" has no business reaching into
|
|
303
|
+
* either, which is the same exemption `toggleExpanded` states for a
|
|
304
|
+
* card that is still growing. */
|
|
305
|
+
toggleThinking(): void;
|
|
289
306
|
/** W18: the status row's right-aligned hint is part of the status
|
|
290
307
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
291
308
|
* must survive repaints). */
|
package/dist/compositor.js
CHANGED
|
@@ -202,6 +202,9 @@ export class Body {
|
|
|
202
202
|
#lastTool = null;
|
|
203
203
|
#pendingCalls = new Map();
|
|
204
204
|
#pipeBuf = ""; // the passthrough's thinking buffer
|
|
205
|
+
// §2.3 — the ctrl+t switch. A block that completes AFTER the press
|
|
206
|
+
// inherits it, so the session stays one way up rather than mixing.
|
|
207
|
+
#thinkingFolded = false;
|
|
205
208
|
/** TUI2-MD ⑤ — the markdown scanner of the message currently
|
|
206
209
|
* streaming, and the cell index its first block landed at. Null
|
|
207
210
|
* between messages: the scanner's life is one assistant message. */
|
|
@@ -390,6 +393,9 @@ export class Body {
|
|
|
390
393
|
const last = this.#cells[this.#cells.length - 1];
|
|
391
394
|
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
392
395
|
last.done = true;
|
|
396
|
+
// §2.3: a block that SETTLES after the switch was thrown is
|
|
397
|
+
// folded like the rest — the session stays one way up.
|
|
398
|
+
last.folded = this.#thinkingFolded;
|
|
393
399
|
this.#lastThinking = last.text;
|
|
394
400
|
if (!this.#isActive())
|
|
395
401
|
this.#write(foldThinking(last.text));
|
|
@@ -1303,11 +1309,12 @@ export class Body {
|
|
|
1303
1309
|
/**
|
|
1304
1310
|
* R14 — ERASE THE TERMINAL AND PRINT THE SESSION AGAIN.
|
|
1305
1311
|
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1312
|
+
* Four callers now: a settled resize, DC-50's ctrl+o, §2.3's ctrl+t,
|
|
1313
|
+
* and §2.4's return from an external editor. They are the same act —
|
|
1314
|
+
* the rendering the terminal holds is wrong (wrong geometry, wrong
|
|
1315
|
+
* expansion state, wrong fold, or another program drew over it) and
|
|
1316
|
+
* the model is the only authority on what it should be — so they
|
|
1317
|
+
* share the path rather than growing four.
|
|
1311
1318
|
*/
|
|
1312
1319
|
#reprint() {
|
|
1313
1320
|
const H = this.#opts.height();
|
|
@@ -1367,10 +1374,36 @@ export class Body {
|
|
|
1367
1374
|
}
|
|
1368
1375
|
this.#reprint();
|
|
1369
1376
|
}
|
|
1377
|
+
/** §2.4 — the terminal holds someone else's drawing (an external
|
|
1378
|
+
* editor had it) and the model is the only authority on what should
|
|
1379
|
+
* be there. The same act as a resize; the same path. */
|
|
1380
|
+
reprint() {
|
|
1381
|
+
this.#reprint();
|
|
1382
|
+
}
|
|
1370
1383
|
/** DC-50 — the switch itself, for the CLI's affordance text. */
|
|
1371
1384
|
expandedAll() {
|
|
1372
1385
|
return this.#expandedAll;
|
|
1373
1386
|
}
|
|
1387
|
+
/** §2.3 — ctrl+t: the committed thinking blocks fold, and fold back.
|
|
1388
|
+
*
|
|
1389
|
+
* DC-50's mechanism exactly: one boolean, then the session is printed
|
|
1390
|
+
* again, so the blocks ALREADY on screen obey the switch rather than
|
|
1391
|
+
* only the next ones. Nothing durable moves — the events are
|
|
1392
|
+
* untouched and `/think` still reaches the last block whole.
|
|
1393
|
+
*
|
|
1394
|
+
* COMMITTED blocks only. The live `thinking…` placeholder belongs to
|
|
1395
|
+
* the live region, not to a cell, and an OPEN thinking cell is text
|
|
1396
|
+
* still arriving; a global "quiet down" has no business reaching into
|
|
1397
|
+
* either, which is the same exemption `toggleExpanded` states for a
|
|
1398
|
+
* card that is still growing. */
|
|
1399
|
+
toggleThinking() {
|
|
1400
|
+
this.#thinkingFolded = !this.#thinkingFolded;
|
|
1401
|
+
for (const cell of this.#cells) {
|
|
1402
|
+
if (cell.kind === "thinking" && cell.done)
|
|
1403
|
+
cell.folded = this.#thinkingFolded;
|
|
1404
|
+
}
|
|
1405
|
+
this.#reprint();
|
|
1406
|
+
}
|
|
1374
1407
|
/** W18: the status row's right-aligned hint is part of the status
|
|
1375
1408
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
1376
1409
|
* must survive repaints). */
|
package/dist/editor.d.ts
CHANGED
|
@@ -86,6 +86,26 @@ export declare class Editor {
|
|
|
86
86
|
/** E1 §3 — the copy key (ctrl+x). */
|
|
87
87
|
onCopy(cb: () => void): void;
|
|
88
88
|
onExpand(cb: () => void): void;
|
|
89
|
+
/** §2.3: the thinking key (ctrl+t) — the chain-level action, wired
|
|
90
|
+
* exactly like ctrl+o because it is the same kind of thing: a switch
|
|
91
|
+
* the compositor throws, never an interpretation the editor makes. */
|
|
92
|
+
onThink(cb: () => void): void;
|
|
93
|
+
/** §2.4: the external-editor key (ctrl+g). */
|
|
94
|
+
onEditor(cb: () => void): void;
|
|
95
|
+
/** §2.4 — hand the terminal to an external program, and take it back.
|
|
96
|
+
*
|
|
97
|
+
* `run` receives the composer's text and returns what should replace
|
|
98
|
+
* it, or null to leave it alone (no editor configured, the editor
|
|
99
|
+
* failed, nothing changed). The SPAWN is the caller's: this package is
|
|
100
|
+
* pure terminal — input is data, output is bytes, zero runtime deps —
|
|
101
|
+
* so what lives here is the handover and nothing else.
|
|
102
|
+
*
|
|
103
|
+
* NOT `exit()` / `enter()`. Those are the session's boundary: exit()
|
|
104
|
+
* also resolves the closed promise, which tells the layer above that
|
|
105
|
+
* the session is over. Suspending is a different act with the same
|
|
106
|
+
* terminal moves, and conflating them would end the session every time
|
|
107
|
+
* someone opened their editor. */
|
|
108
|
+
externalEdit(run: (text: string) => string | null): void;
|
|
89
109
|
/** KC2 §2: the redirect chain — the gesture hands the buffer's text
|
|
90
110
|
* over while the run is told to stop. Mirrors onEscape (a list, so
|
|
91
111
|
* listeners can coexist); the line arrives already gone from the
|
|
@@ -168,6 +188,20 @@ export declare class Editor {
|
|
|
168
188
|
panelState(): PanelState | null;
|
|
169
189
|
enter(): void;
|
|
170
190
|
exit(): void;
|
|
191
|
+
/** OR-11 (a) — the lead the composer's row is DRAWN with, which is not
|
|
192
|
+
* always the brick. The CLI binds the compositor's input lead as `""`
|
|
193
|
+
* (a prompt character is a third thing saying "input lives here", and
|
|
194
|
+
* it cost the row a column), while the editor measured against PROMPT
|
|
195
|
+
* — so the editor believed the row two columns narrower than the
|
|
196
|
+
* compositor drew it, and W23's "the two width authorities can never
|
|
197
|
+
* disagree" was off by two.
|
|
198
|
+
*
|
|
199
|
+
* A PROVIDER, not a string, because both renderers are live in the
|
|
200
|
+
* same process: the dock draws the row when it is active and
|
|
201
|
+
* `selfRender` draws it when it is not, and they lead it differently.
|
|
202
|
+
* Whoever binds the row answers for whichever is drawing. The default
|
|
203
|
+
* is the brick, which is what `selfRender` has always drawn. */
|
|
204
|
+
setInputLead(lead: () => string): void;
|
|
171
205
|
/** The row's own render when the dock is inactive (a TTY without a
|
|
172
206
|
* real size): \r + clear + blue brick prompt + visible + cursor
|
|
173
207
|
* column. */
|
package/dist/editor.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* source funnels through the ONE normalizer in feed() (§3).
|
|
23
23
|
*/
|
|
24
24
|
var _a;
|
|
25
|
-
import { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
25
|
+
import { breakable, charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
26
26
|
// the width primitives moved to width.ts (W1, the single width
|
|
27
27
|
// authority) — re-exported so the editor's public surface is unchanged.
|
|
28
28
|
export { charWidth, displayWidth, widthOf };
|
|
@@ -146,8 +146,16 @@ const N_MAX = 6;
|
|
|
146
146
|
* payload for someone else, and holding it is how the editor goes
|
|
147
147
|
* deaf. */
|
|
148
148
|
const OSC_MAX = 1024;
|
|
149
|
-
/**
|
|
150
|
-
*
|
|
149
|
+
/** TMUX-F1 ①: a CSI parameter string longer than this with no final byte
|
|
150
|
+
* is not a sequence any terminal sends; it is dropped rather than held, so
|
|
151
|
+
* a runaway can never park the editor. */
|
|
152
|
+
const CSI_MAX = 64;
|
|
153
|
+
/** TMUX-F1 ②: identical arrow sequences in ONE read at or past this count
|
|
154
|
+
* are a wheel notch, not a hand — treated as one press. */
|
|
155
|
+
const ARROW_BURST = 3;
|
|
156
|
+
/** The dim "…" — the ONE truncation mark. OR-11 retired its other use
|
|
157
|
+
* (the horizontal scroll's prefix) with the scroll itself; what is left
|
|
158
|
+
* is the viewport's hidden-rows markers, above and below. */
|
|
151
159
|
/** R3: built per call from the palette — `dim` is an absolute grey once
|
|
152
160
|
* the ground is known, so a frozen SGR 2 here would be the one span
|
|
153
161
|
* that ignores it. */
|
|
@@ -198,11 +206,10 @@ const shown = (chars) => {
|
|
|
198
206
|
export class Editor {
|
|
199
207
|
#chars = [];
|
|
200
208
|
#cursor = 0;
|
|
201
|
-
#scroll = 0; // chars scrolled off the left of the CURSOR'S LINE (width-based reflow; KC1: line-local, so a single-line buffer is unchanged)
|
|
202
209
|
// KC1 §2 — the ONE new ephemeral field: the desired column for the
|
|
203
210
|
// ↑/↓ walk (a long line's column 20 → a short line clamps to 5 → the
|
|
204
211
|
// next long line RETURNS to 20). Set on the first vertical move,
|
|
205
|
-
// kept across consecutive ones, reset by any
|
|
212
|
+
// kept across consecutive ones, reset by any left/right move, insert
|
|
206
213
|
// or delete. Never stashed — it is a walk's state, not the buffer's.
|
|
207
214
|
#verticalGoalCol = null;
|
|
208
215
|
#questionCb = null;
|
|
@@ -322,6 +329,8 @@ export class Editor {
|
|
|
322
329
|
// expanded block). Mirrors the escape list: multiple listeners can
|
|
323
330
|
// coexist; the editor never interprets the key itself.
|
|
324
331
|
#expandCbs = [];
|
|
332
|
+
#thinkCbs = [];
|
|
333
|
+
#editorCbs = [];
|
|
325
334
|
/** E1 §3 — ctrl+x. Same shape as the expand key: the editor owns the
|
|
326
335
|
* KEY, the CLI owns what it means. */
|
|
327
336
|
#copyCbs = [];
|
|
@@ -455,6 +464,57 @@ export class Editor {
|
|
|
455
464
|
onExpand(cb) {
|
|
456
465
|
this.#expandCbs.push(cb);
|
|
457
466
|
}
|
|
467
|
+
/** §2.3: the thinking key (ctrl+t) — the chain-level action, wired
|
|
468
|
+
* exactly like ctrl+o because it is the same kind of thing: a switch
|
|
469
|
+
* the compositor throws, never an interpretation the editor makes. */
|
|
470
|
+
onThink(cb) {
|
|
471
|
+
this.#thinkCbs.push(cb);
|
|
472
|
+
}
|
|
473
|
+
/** §2.4: the external-editor key (ctrl+g). */
|
|
474
|
+
onEditor(cb) {
|
|
475
|
+
this.#editorCbs.push(cb);
|
|
476
|
+
}
|
|
477
|
+
/** §2.4 — hand the terminal to an external program, and take it back.
|
|
478
|
+
*
|
|
479
|
+
* `run` receives the composer's text and returns what should replace
|
|
480
|
+
* it, or null to leave it alone (no editor configured, the editor
|
|
481
|
+
* failed, nothing changed). The SPAWN is the caller's: this package is
|
|
482
|
+
* pure terminal — input is data, output is bytes, zero runtime deps —
|
|
483
|
+
* so what lives here is the handover and nothing else.
|
|
484
|
+
*
|
|
485
|
+
* NOT `exit()` / `enter()`. Those are the session's boundary: exit()
|
|
486
|
+
* also resolves the closed promise, which tells the layer above that
|
|
487
|
+
* the session is over. Suspending is a different act with the same
|
|
488
|
+
* terminal moves, and conflating them would end the session every time
|
|
489
|
+
* someone opened their editor. */
|
|
490
|
+
externalEdit(run) {
|
|
491
|
+
const before = this.line();
|
|
492
|
+
let next = null;
|
|
493
|
+
process.stdin.off("data", this.#onData);
|
|
494
|
+
process.stdout.write(MOUSE_OFF);
|
|
495
|
+
process.stdout.write("\x1b[?2004l"); // bracketed paste OFF — the child's, not ours
|
|
496
|
+
process.stdout.write("\x1b[?25h"); // and it needs a cursor to draw
|
|
497
|
+
process.stdin.setRawMode(false);
|
|
498
|
+
try {
|
|
499
|
+
next = run(before);
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
// the handover comes back whatever the child did, including
|
|
503
|
+
// throwing: a terminal left in the child's mode is unusable and
|
|
504
|
+
// the human has no way to ask for it back.
|
|
505
|
+
process.stdin.setRawMode(true);
|
|
506
|
+
process.stdout.write(MOUSE_OFF);
|
|
507
|
+
this.#mouseOn = false;
|
|
508
|
+
process.stdout.write("\x1b[?2004h");
|
|
509
|
+
process.stdin.on("data", this.#onData);
|
|
510
|
+
}
|
|
511
|
+
if (next !== null && next !== before) {
|
|
512
|
+
this.#chars = [...next].map((ch) => ch.codePointAt(0));
|
|
513
|
+
this.#cursor = this.#chars.length;
|
|
514
|
+
this.#reflow();
|
|
515
|
+
}
|
|
516
|
+
this.#onRender();
|
|
517
|
+
}
|
|
458
518
|
/** KC2 §2: the redirect chain — the gesture hands the buffer's text
|
|
459
519
|
* over while the run is told to stop. Mirrors onEscape (a list, so
|
|
460
520
|
* listeners can coexist); the line arrives already gone from the
|
|
@@ -509,7 +569,6 @@ export class Editor {
|
|
|
509
569
|
this.#redoStack.length = 0;
|
|
510
570
|
this.#chars = [];
|
|
511
571
|
this.#cursor = 0;
|
|
512
|
-
this.#scroll = 0;
|
|
513
572
|
this.#verticalGoalCol = null;
|
|
514
573
|
this.#onRender();
|
|
515
574
|
}
|
|
@@ -539,12 +598,129 @@ export class Editor {
|
|
|
539
598
|
}
|
|
540
599
|
return bounds.length - 1;
|
|
541
600
|
}
|
|
542
|
-
/** The cursor's OWN line — the unit of the
|
|
543
|
-
* the
|
|
601
|
+
/** The cursor's OWN LOGICAL line — the unit of the line-local A/E/U/K
|
|
602
|
+
* (A3) and of the `@` token scan. The visual rows a long line folds
|
|
603
|
+
* into are `#visualRows`; these two answer different questions and
|
|
604
|
+
* OR-11 kept them apart deliberately. */
|
|
544
605
|
#cursorBounds() {
|
|
545
606
|
const bounds = this.#lineBounds();
|
|
546
607
|
return bounds[this.#cursorLine(bounds)];
|
|
547
608
|
}
|
|
609
|
+
/** OR-11 — the row's width budget: the SAME number the compositor
|
|
610
|
+
* gives the row (W23's one formula), against the lead that is
|
|
611
|
+
* actually drawn. The fold and the cursor math both ask here. */
|
|
612
|
+
#budget() {
|
|
613
|
+
const W = (process.stdout.columns ?? 0) || 80;
|
|
614
|
+
const ps = this.#panelInput.state();
|
|
615
|
+
const lead = ps !== null ? panelLead(ps.view, ps.phase, ps.cursor, ps.ask) : this.#inputLead();
|
|
616
|
+
return Math.max(1, W - leadWidth(lead) - 1);
|
|
617
|
+
}
|
|
618
|
+
/** The `[Pasted text #N +M lines]` tokens inside [start, end).
|
|
619
|
+
* A capsule is ONE thing on screen and one thing to the cursor, so a
|
|
620
|
+
* fold never lands inside it: it is cut when drawn if it is wider
|
|
621
|
+
* than the row, and it is never split into two. */
|
|
622
|
+
#capsules(start, end) {
|
|
623
|
+
const text = String.fromCodePoint(...this.#chars.slice(start, end));
|
|
624
|
+
const units = [];
|
|
625
|
+
// the regex indexes UTF-16 units; the buffer is code points
|
|
626
|
+
let cp = start;
|
|
627
|
+
for (const ch of text) {
|
|
628
|
+
units.push(cp);
|
|
629
|
+
cp += 1;
|
|
630
|
+
if (ch.length === 2)
|
|
631
|
+
units.push(cp - 1); // a surrogate pair is one code point
|
|
632
|
+
}
|
|
633
|
+
units.push(end);
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const m of text.matchAll(_a.#CAPSULE)) {
|
|
636
|
+
const i = m.index ?? 0;
|
|
637
|
+
out.push({ start: units[i] ?? start, end: units[i + m[0].length] ?? end });
|
|
638
|
+
}
|
|
639
|
+
return out;
|
|
640
|
+
}
|
|
641
|
+
/** OR-11 — one logical line's VISUAL rows, as [start, end) into
|
|
642
|
+
* #chars. The rows TILE the line exactly: no character is dropped and
|
|
643
|
+
* none is shown twice, which is what lets the cursor map back.
|
|
644
|
+
*
|
|
645
|
+
* The break, in order:
|
|
646
|
+
* 1. everything fits — one row;
|
|
647
|
+
* 2. the cut falls on a CJK boundary (either side breakable) — take
|
|
648
|
+
* it, because looking further back for a space would leave the
|
|
649
|
+
* row half empty for text that breaks anywhere;
|
|
650
|
+
* 3. otherwise the last whitespace RUN that fits: the whole run ends
|
|
651
|
+
* the row it is on, so a continuation row never begins with a
|
|
652
|
+
* space — unless the run itself straddles the budget, where ①
|
|
653
|
+
* wins and the leftover spaces open the next row (OR11-F1);
|
|
654
|
+
* 4. otherwise a hard break at the last code point that fits — never
|
|
655
|
+
* inside a wide character, because `#indexAtWidth` is that walk. */
|
|
656
|
+
#foldLine(start, end, budget) {
|
|
657
|
+
const rows = [];
|
|
658
|
+
const atoms = this.#capsules(start, end);
|
|
659
|
+
let from = start;
|
|
660
|
+
while (from < end) {
|
|
661
|
+
const fits = this.#fitsWithin(from, end, budget);
|
|
662
|
+
if (fits >= end)
|
|
663
|
+
break;
|
|
664
|
+
let cut = fits;
|
|
665
|
+
const atom = atoms.find((a) => a.start < cut && cut < a.end);
|
|
666
|
+
if (atom !== undefined) {
|
|
667
|
+
// the capsule keeps its own row: start one before it when
|
|
668
|
+
// there is text ahead of it, otherwise let it run whole.
|
|
669
|
+
cut = atom.start > from ? atom.start : atom.end;
|
|
670
|
+
}
|
|
671
|
+
else if (!breakable(this.#chars[fits] ?? 0) && !breakable(this.#chars[fits - 1] ?? 0)) {
|
|
672
|
+
let w = -1;
|
|
673
|
+
for (let i = cut - 1; i > from; i -= 1) {
|
|
674
|
+
if (this.#chars[i] === SPACE) {
|
|
675
|
+
w = i;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (w >= 0) {
|
|
680
|
+
while (this.#chars[w + 1] === SPACE && w + 1 < end)
|
|
681
|
+
w += 1;
|
|
682
|
+
// OR11-F1: capped at `fits`. Ending the row with the WHOLE
|
|
683
|
+
// run is what keeps a continuation row from opening with a
|
|
684
|
+
// space — but that is a preference, and invariant ① is
|
|
685
|
+
// not: every row kiso produces measures ≤ W, because
|
|
686
|
+
// autowrap is off and the terminal will not save it. A run
|
|
687
|
+
// STRADDLING the boundary was carrying the row past the
|
|
688
|
+
// budget (34 letters + 12 spaces + 20 letters at width 40
|
|
689
|
+
// measured 46 against 39). When the run itself does not
|
|
690
|
+
// fit, the row stops at the budget and the leftover spaces
|
|
691
|
+
// open the next row.
|
|
692
|
+
cut = Math.min(w + 1, fits);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (cut <= from || cut >= end)
|
|
696
|
+
break; // no progress, or the rest fits
|
|
697
|
+
rows.push({ start: from, end: cut });
|
|
698
|
+
from = cut;
|
|
699
|
+
}
|
|
700
|
+
rows.push({ start: from, end });
|
|
701
|
+
return rows;
|
|
702
|
+
}
|
|
703
|
+
/** OR-11 — every VISUAL row of the buffer, in order. ONE fold, built
|
|
704
|
+
* once per read; `dockState`, the cursor mapping and the ↑/↓ walk all
|
|
705
|
+
* READ this table rather than folding again. */
|
|
706
|
+
#visualRows() {
|
|
707
|
+
const budget = this.#budget();
|
|
708
|
+
const out = [];
|
|
709
|
+
for (const b of this.#lineBounds())
|
|
710
|
+
out.push(...this.#foldLine(b.start, b.end, budget));
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
/** The visual row the cursor is on: the LAST row it can belong to. At
|
|
714
|
+
* a fold boundary that is the row the next character would go on; at
|
|
715
|
+
* a newline it is the row the newline closes, never the next line. */
|
|
716
|
+
#cursorVisualRow(rows) {
|
|
717
|
+
let hit = 0;
|
|
718
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
719
|
+
if (rows[i].start <= this.#cursor && this.#cursor <= rows[i].end)
|
|
720
|
+
hit = i;
|
|
721
|
+
}
|
|
722
|
+
return hit;
|
|
723
|
+
}
|
|
548
724
|
/** KC1 §5 — N_visible = min(lineCount, N_MAX, max(1, H − 3 − the
|
|
549
725
|
* menu/queue bands)). The height clamp guarantees legal geometry
|
|
550
726
|
* down to the compositor's enter gate; the compositor re-applies the
|
|
@@ -568,27 +744,27 @@ export class Editor {
|
|
|
568
744
|
* stash / restore / clear / submit path has new state to carry. A
|
|
569
745
|
* dim "…" marks whichever edge hides rows. */
|
|
570
746
|
dockState() {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
747
|
+
// OR-11: VISUAL rows. One logical line may be several of them, and
|
|
748
|
+
// the window, the markers and the cursor all count them the same
|
|
749
|
+
// way — N_MAX has always been about how tall the box may get, and
|
|
750
|
+
// a folded line is as tall as a pasted one.
|
|
751
|
+
const rows = this.#visualRows();
|
|
752
|
+
const cursorLine = this.#cursorVisualRow(rows);
|
|
753
|
+
const n = this.#visibleRows(rows.length);
|
|
754
|
+
const first = Math.max(0, Math.min(cursorLine - n + 1, rows.length - n));
|
|
575
755
|
const lines = [];
|
|
576
756
|
for (let i = first; i < first + n; i += 1) {
|
|
577
|
-
const b =
|
|
578
|
-
// the cursor's own row carries the horizontal scroll (and its
|
|
579
|
-
// "…"); the other rows render whole and cap at the frame's wall
|
|
580
|
-
const from = i === cursorLine ? b.start + this.#scroll : b.start;
|
|
581
|
-
const scrolled = i === cursorLine && this.#scroll > 0 ? ellipsis() : "";
|
|
757
|
+
const b = rows[i];
|
|
582
758
|
const above = i === first && first > 0 ? ellipsis() : "";
|
|
583
|
-
const below = i === first + n - 1 && first + n <
|
|
584
|
-
lines.push(`${above}${
|
|
759
|
+
const below = i === first + n - 1 && first + n < rows.length ? ellipsis() : "";
|
|
760
|
+
lines.push(`${above}${shown(this.#chars.slice(b.start, b.end))}${below}`);
|
|
585
761
|
}
|
|
586
762
|
const cursorRow = cursorLine - first;
|
|
587
763
|
// the window trails the cursor, so the hidden-above marker can only
|
|
588
764
|
// share the cursor's row in the degenerate one-row window (a tiny
|
|
589
|
-
// terminal) — where it shifts the column
|
|
590
|
-
const marks =
|
|
591
|
-
const cursorCol = marks + widthOf(this.#chars.slice(
|
|
765
|
+
// terminal) — where it shifts the column by its one cell
|
|
766
|
+
const marks = cursorRow === 0 && first > 0 ? 1 : 0;
|
|
767
|
+
const cursorCol = marks + widthOf(this.#chars.slice(rows[cursorLine].start, this.#cursor));
|
|
592
768
|
return { line: lines[cursorRow], cursor: cursorCol, lines, cursorRow, cursorCol };
|
|
593
769
|
}
|
|
594
770
|
/** v3 §04: the menu's visible state for the dock — null when closed. */
|
|
@@ -852,6 +1028,24 @@ export class Editor {
|
|
|
852
1028
|
#syncMouse() {
|
|
853
1029
|
this.#setMouse(this.#panelInput.up() || this.#pickInput.up() || this.#atUp());
|
|
854
1030
|
}
|
|
1031
|
+
/** OR-11 (a) — the lead the composer's row is DRAWN with, which is not
|
|
1032
|
+
* always the brick. The CLI binds the compositor's input lead as `""`
|
|
1033
|
+
* (a prompt character is a third thing saying "input lives here", and
|
|
1034
|
+
* it cost the row a column), while the editor measured against PROMPT
|
|
1035
|
+
* — so the editor believed the row two columns narrower than the
|
|
1036
|
+
* compositor drew it, and W23's "the two width authorities can never
|
|
1037
|
+
* disagree" was off by two.
|
|
1038
|
+
*
|
|
1039
|
+
* A PROVIDER, not a string, because both renderers are live in the
|
|
1040
|
+
* same process: the dock draws the row when it is active and
|
|
1041
|
+
* `selfRender` draws it when it is not, and they lead it differently.
|
|
1042
|
+
* Whoever binds the row answers for whichever is drawing. The default
|
|
1043
|
+
* is the brick, which is what `selfRender` has always drawn. */
|
|
1044
|
+
setInputLead(lead) {
|
|
1045
|
+
this.#inputLead = lead;
|
|
1046
|
+
this.#reflow();
|
|
1047
|
+
}
|
|
1048
|
+
#inputLead = () => PROMPT;
|
|
855
1049
|
/** The row's own render when the dock is inactive (a TTY without a
|
|
856
1050
|
* real size): \r + clear + blue brick prompt + visible + cursor
|
|
857
1051
|
* column. */
|
|
@@ -862,7 +1056,7 @@ export class Editor {
|
|
|
862
1056
|
// W21: the panel's lead owns the row while up (the brick returns
|
|
863
1057
|
// when the panel closes).
|
|
864
1058
|
const panel = this.#panelInput.state();
|
|
865
|
-
const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.cursor, panel.ask) : `${p.bold}${
|
|
1059
|
+
const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.cursor, panel.ask) : `${p.bold}${this.#inputLead()}${p.reset}`;
|
|
866
1060
|
// W23: the ONE width authority — leadWidth(lead), the ANSI-stripped
|
|
867
1061
|
// visible width (the styled panel lead / the styled brick measure
|
|
868
1062
|
// the same as their plain text — a lead can never measure
|
|
@@ -952,13 +1146,47 @@ export class Editor {
|
|
|
952
1146
|
// never complete. The editor went deaf. It never happened
|
|
953
1147
|
// because nothing ever enabled reporting — which is exactly
|
|
954
1148
|
// the kind of latent break turning a feature on discovers.
|
|
955
|
-
|
|
1149
|
+
//
|
|
1150
|
+
// TMUX-F1 ①: the whole CSI grammar — parameters (the private
|
|
1151
|
+
// markers `?` `<` `>` `=` included), intermediates (0x20–0x2F),
|
|
1152
|
+
// a final (0x40–0x7E). The class above could not END a sequence
|
|
1153
|
+
// carrying an intermediate — tmux answers DECRQM with
|
|
1154
|
+
// `ESC[?69;0$y` — so it was parked as "incomplete" and every
|
|
1155
|
+
// later keystroke joined a sequence that never completed: the
|
|
1156
|
+
// editor went deaf for the session. A sequence with
|
|
1157
|
+
// intermediates is a reply kiso did not ask for; it is skipped
|
|
1158
|
+
// whole. A parameter string past CSI_MAX with no final is
|
|
1159
|
+
// dropped, not held.
|
|
1160
|
+
const m = rest.match(/^\[([0-9;?<>=]*)([ -/]*)([@-~])/);
|
|
956
1161
|
if (m === null) {
|
|
1162
|
+
const junk = /^\[[0-9;?<>=]*[ -/]*/.exec(rest)[0];
|
|
1163
|
+
if (junk.length > CSI_MAX) {
|
|
1164
|
+
i += 1 + junk.length; // the introducer and its runaway parameters go; parsing continues
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
957
1167
|
this.#pending = text.slice(i); // incomplete CSI — wait for more
|
|
958
1168
|
break;
|
|
959
1169
|
}
|
|
960
|
-
|
|
961
|
-
|
|
1170
|
+
const seqLen = m[0].length + 1;
|
|
1171
|
+
if (m[2] !== "") {
|
|
1172
|
+
i += seqLen; // intermediates: a report kiso did not ask for — skipped whole
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
// TMUX-F1 ②: a BURST of identical arrows in ONE read is a wheel,
|
|
1176
|
+
// not a hand. Apple Terminal turns wheel and trackpad scrolling
|
|
1177
|
+
// into arrow keys for an alternate-screen app (tmux's client is
|
|
1178
|
+
// one) — three or more per notch in a single write, which a
|
|
1179
|
+
// person never produces in one read — and each one walked the
|
|
1180
|
+
// history. The burst is one press (owner ruling 2026-09-10, c);
|
|
1181
|
+
// separate reads stay separate presses.
|
|
1182
|
+
const seq = `\x1b${m[0]}`;
|
|
1183
|
+
let run = 1;
|
|
1184
|
+
if (m[1] === "" && "ABCD".includes(m[3])) {
|
|
1185
|
+
while (text.startsWith(seq, i + run * seq.length))
|
|
1186
|
+
run += 1;
|
|
1187
|
+
}
|
|
1188
|
+
this.#csi(m[1], m[3], run);
|
|
1189
|
+
i += run * seqLen;
|
|
962
1190
|
}
|
|
963
1191
|
else if (rest.startsWith("]")) {
|
|
964
1192
|
// DC-7: an OSC is a message FROM the terminal — a background
|
|
@@ -1041,7 +1269,6 @@ export class Editor {
|
|
|
1041
1269
|
this.#checkpoint(); // UD-1
|
|
1042
1270
|
this.#chars = [];
|
|
1043
1271
|
this.#cursor = 0;
|
|
1044
|
-
this.#scroll = 0;
|
|
1045
1272
|
this.#verticalGoalCol = null;
|
|
1046
1273
|
this.#refreshMenu();
|
|
1047
1274
|
i += 1;
|
|
@@ -1209,6 +1436,33 @@ export class Editor {
|
|
|
1209
1436
|
cb();
|
|
1210
1437
|
i += 1;
|
|
1211
1438
|
}
|
|
1439
|
+
else if (c === "\x14") {
|
|
1440
|
+
// §2.3 — ctrl+t folds the committed thinking blocks, and
|
|
1441
|
+
// folds them back. `\x14` was unbound across the tree
|
|
1442
|
+
// (checked before the round), and it is the key the
|
|
1443
|
+
// reference implementation uses for this same gesture, so a
|
|
1444
|
+
// reader arriving from it is not retrained.
|
|
1445
|
+
//
|
|
1446
|
+
// Forwarded, not interpreted: the switch is the
|
|
1447
|
+
// compositor's, exactly as ctrl+o's is.
|
|
1448
|
+
for (const cb of [...this.#thinkCbs])
|
|
1449
|
+
cb();
|
|
1450
|
+
i += 1;
|
|
1451
|
+
}
|
|
1452
|
+
else if (c === "\x07") {
|
|
1453
|
+
// §2.4 — ctrl+g opens $VISUAL / $EDITOR on the composer.
|
|
1454
|
+
//
|
|
1455
|
+
// 0x07 is BEL, which is also the terminator a terminal puts
|
|
1456
|
+
// on an OSC reply (DC-7). This branch is only reached by a
|
|
1457
|
+
// BARE 0x07: the OSC arm above consumes its own terminator,
|
|
1458
|
+
// so the terminal's answer never arrives here. One byte, two
|
|
1459
|
+
// meanings, told apart by what precedes it — and there is a
|
|
1460
|
+
// gate for exactly that, because "should not reach here" is
|
|
1461
|
+
// not something to take on trust.
|
|
1462
|
+
for (const cb of [...this.#editorCbs])
|
|
1463
|
+
cb();
|
|
1464
|
+
i += 1;
|
|
1465
|
+
}
|
|
1212
1466
|
else if (c === "\x18" && this.#composerIdle()) {
|
|
1213
1467
|
// E1 §3 — ctrl+x copies the last answer. `\x18` was unbound
|
|
1214
1468
|
// across the whole tree (checked before the round started),
|
|
@@ -1323,7 +1577,22 @@ export class Editor {
|
|
|
1323
1577
|
bindPanelRows(fn) {
|
|
1324
1578
|
this.#panelRows = fn;
|
|
1325
1579
|
}
|
|
1326
|
-
|
|
1580
|
+
/** TMUX-F1 ②: `run` identical arrow sequences arrived in ONE read. Every
|
|
1581
|
+
* surface but one gets every press — a wheel over the transcript viewer,
|
|
1582
|
+
* a panel, a multi-row draft scrolls, as a wheel should. The HISTORY
|
|
1583
|
+
* walk is the one that replaces the draft's content, and there a burst
|
|
1584
|
+
* (ARROW_BURST or more in one read — a wheel notch, never a hand) is one
|
|
1585
|
+
* step. The first press decides which branch it was, so the gate is not
|
|
1586
|
+
* written twice. */
|
|
1587
|
+
#csi(params, final, run = 1) {
|
|
1588
|
+
const took = this.#csiOnce(params, final);
|
|
1589
|
+
if (took === "history" && run >= ARROW_BURST)
|
|
1590
|
+
return;
|
|
1591
|
+
for (let k = 1; k < run; k += 1)
|
|
1592
|
+
this.#csiOnce(params, final);
|
|
1593
|
+
}
|
|
1594
|
+
#csiOnce(params, final) {
|
|
1595
|
+
let took;
|
|
1327
1596
|
// TUI2-R3v2 ②: an SGR 1006 report — `\x1b[<b;col;rowM` (press) or
|
|
1328
1597
|
// `...m` (release). It is routed FIRST because a `<` parameter is
|
|
1329
1598
|
// never anything else, and because a mouse byte must never fall
|
|
@@ -1434,11 +1703,18 @@ export class Editor {
|
|
|
1434
1703
|
const view = this.#atView();
|
|
1435
1704
|
this.#atSel = final === "A" ? Math.max(0, view.selected - 1) : Math.min(view.matches.length - 1, view.selected + 1);
|
|
1436
1705
|
}
|
|
1437
|
-
else if (this.#
|
|
1438
|
-
// KC1 §4
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1706
|
+
else if (this.#visualRows().length > 1) {
|
|
1707
|
+
// KC1 §4, restated by OR-11 over VISUAL rows: a buffer that
|
|
1708
|
+
// occupies more than one row has its ↑↓ walk them. It used
|
|
1709
|
+
// to read `#chars.includes(NEWLINE)`, which was the same
|
|
1710
|
+
// question while one logical line was always one row; a
|
|
1711
|
+
// folded line is several now, and the walk is about what the
|
|
1712
|
+
// eye sees.
|
|
1713
|
+
//
|
|
1714
|
+
// The history and the queue-pop below stay gated on an EMPTY
|
|
1715
|
+
// buffer, and an empty buffer is exactly one row — so the
|
|
1716
|
+
// precedence can only ever add, never take, which is what
|
|
1717
|
+
// T-E4 pins.
|
|
1442
1718
|
this.#verticalMove(final === "A" ? -1 : 1);
|
|
1443
1719
|
}
|
|
1444
1720
|
else if (final === "A" && this.#queuePop !== null && (this.#queuePopMode || this.line() === "") && this.#queueState().length > 0) {
|
|
@@ -1452,6 +1728,7 @@ export class Editor {
|
|
|
1452
1728
|
}
|
|
1453
1729
|
else if (this.#historyIdx !== null || this.line() === "") {
|
|
1454
1730
|
this.#historyMove(final === "A" ? -1 : 1);
|
|
1731
|
+
took = "history"; // the repaint below is still owed — an early return here left the recall unpainted (r3a red)
|
|
1455
1732
|
}
|
|
1456
1733
|
this.#onRender();
|
|
1457
1734
|
}
|
|
@@ -1468,22 +1745,31 @@ export class Editor {
|
|
|
1468
1745
|
this.#move(1);
|
|
1469
1746
|
}
|
|
1470
1747
|
else if (final === "H") {
|
|
1748
|
+
// OR-11 (b): and REPAINT. Ctrl+A and Ctrl+E render; their arrow
|
|
1749
|
+
// spellings reflowed and stopped there, so the caret stayed
|
|
1750
|
+
// where it had been until some later key happened to draw. A
|
|
1751
|
+
// gesture that moves the cursor is a gesture that shows it.
|
|
1471
1752
|
this.#cursor = this.#cursorBounds().start; // A3: Home follows Ctrl+A — line-local
|
|
1472
1753
|
this.#reflow();
|
|
1754
|
+
this.#onRender();
|
|
1473
1755
|
}
|
|
1474
1756
|
else if (final === "F") {
|
|
1475
1757
|
this.#cursor = this.#cursorBounds().end; // A3: End follows Ctrl+E — line-local
|
|
1476
1758
|
this.#reflow();
|
|
1759
|
+
this.#onRender();
|
|
1477
1760
|
}
|
|
1761
|
+
return took;
|
|
1478
1762
|
}
|
|
1479
1763
|
/** KC1 §4 — the ↑/↓ walk. The cursor keeps its DESIRED column across
|
|
1480
1764
|
* a short line: the goal is captured at the FIRST vertical move and
|
|
1481
|
-
* survives consecutive ones (#reflow clears it, so any
|
|
1765
|
+
* survives consecutive ones (#reflow clears it, so any left/right
|
|
1482
1766
|
* move / insert / delete ends the walk); a step past either end
|
|
1483
1767
|
* stays put. */
|
|
1484
1768
|
#verticalMove(delta) {
|
|
1485
|
-
|
|
1486
|
-
|
|
1769
|
+
// OR-11: VISUAL rows — one long logical line is several of them, and
|
|
1770
|
+
// ↑/↓ walk what the eye sees. The goal column is unchanged.
|
|
1771
|
+
const bounds = this.#visualRows();
|
|
1772
|
+
const cur = this.#cursorVisualRow(bounds);
|
|
1487
1773
|
const next = cur + delta;
|
|
1488
1774
|
if (next < 0 || next >= bounds.length)
|
|
1489
1775
|
return;
|
|
@@ -1508,16 +1794,14 @@ export class Editor {
|
|
|
1508
1794
|
clear: () => {
|
|
1509
1795
|
this.#chars = [];
|
|
1510
1796
|
this.#cursor = 0;
|
|
1511
|
-
this.#scroll = 0;
|
|
1512
1797
|
this.#verticalGoalCol = null;
|
|
1513
1798
|
},
|
|
1514
1799
|
insert: (cp) => this.#insert(cp),
|
|
1515
1800
|
newline: () => this.#insert(NEWLINE),
|
|
1516
|
-
stash: () => ({ chars: this.#chars, cursor: this.#cursor
|
|
1801
|
+
stash: () => ({ chars: this.#chars, cursor: this.#cursor }),
|
|
1517
1802
|
restore: (st) => {
|
|
1518
1803
|
this.#chars = [...st.chars];
|
|
1519
1804
|
this.#cursor = st.cursor;
|
|
1520
|
-
this.#scroll = st.scroll;
|
|
1521
1805
|
},
|
|
1522
1806
|
reflow: () => this.#reflow(),
|
|
1523
1807
|
render: () => this.#onRender(),
|
|
@@ -1615,7 +1899,6 @@ export class Editor {
|
|
|
1615
1899
|
#restoreSnap(s) {
|
|
1616
1900
|
this.#chars = [...s.chars];
|
|
1617
1901
|
this.#cursor = s.cursor;
|
|
1618
|
-
this.#scroll = 0;
|
|
1619
1902
|
this.#verticalGoalCol = null;
|
|
1620
1903
|
this.#reflow();
|
|
1621
1904
|
this.#refreshMenu();
|
|
@@ -1778,8 +2061,8 @@ export class Editor {
|
|
|
1778
2061
|
this.#cursor = this.#wordEdge(this.#cursor, dir);
|
|
1779
2062
|
this.#reflow();
|
|
1780
2063
|
}
|
|
1781
|
-
/** KC1/KC2 — the buffer LEAVES: the flat chars, the cursor, the
|
|
1782
|
-
*
|
|
2064
|
+
/** KC1/KC2 — the buffer LEAVES: the flat chars, the cursor, the ↑/↓
|
|
2065
|
+
* goal, the menu and the pop-walk all
|
|
1783
2066
|
* reset together (W22: a departing line ends the pop-walk, so the
|
|
1784
2067
|
* next esc at rest interrupts again). Shared by the submit and the
|
|
1785
2068
|
* redirect — the two doors a line can leave by. */
|
|
@@ -1892,7 +2175,6 @@ export class Editor {
|
|
|
1892
2175
|
this.#redoStack.length = 0;
|
|
1893
2176
|
this.#chars = [];
|
|
1894
2177
|
this.#cursor = 0;
|
|
1895
|
-
this.#scroll = 0;
|
|
1896
2178
|
this.#verticalGoalCol = null;
|
|
1897
2179
|
this.#menuOpen = false;
|
|
1898
2180
|
this.#menuSel = 0;
|
|
@@ -2061,50 +2343,41 @@ export class Editor {
|
|
|
2061
2343
|
this.#checkpoint(); // UD-1: a mid-walk edit is recoverable
|
|
2062
2344
|
this.#chars = [...line].map((ch) => ch.codePointAt(0));
|
|
2063
2345
|
this.#cursor = this.#chars.length;
|
|
2064
|
-
this.#scroll = 0;
|
|
2065
2346
|
this.#verticalGoalCol = null;
|
|
2066
2347
|
this.#onRender();
|
|
2067
2348
|
}
|
|
2068
|
-
// ---- width
|
|
2349
|
+
// ---- the width walks ----
|
|
2069
2350
|
#reflow() {
|
|
2070
2351
|
// KC1: any key that reaches the reflow ended a ↑/↓ walk (the walk
|
|
2071
2352
|
// itself re-arms the goal right after its own reflow call).
|
|
2072
2353
|
this.#verticalGoalCol = null;
|
|
2073
|
-
|
|
2074
|
-
//
|
|
2075
|
-
// line
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
// owns the keys, the brick otherwise): maxW = W − walls − lead.
|
|
2080
|
-
const ps = this.#panelInput.state();
|
|
2081
|
-
const lead = ps !== null ? panelLead(ps.view, ps.phase, ps.cursor, ps.ask) : PROMPT;
|
|
2082
|
-
const leadW = leadWidth(lead);
|
|
2083
|
-
// DC-17: ONE column, not four. W6's box took 2+2 and this kept
|
|
2084
|
-
// reserving them after law 1.1 retired it — so the horizontal
|
|
2085
|
-
// scroll fired three columns early, the cursor could never reach
|
|
2086
|
-
// the row's last three cells, and the two width authorities the
|
|
2087
|
-
// W23 contract says must never disagree disagreed by 3. The
|
|
2088
|
-
// compositor's walk caps at W−1 (the drawn cursor's own cell);
|
|
2089
|
-
// this is the same one column, on the same row.
|
|
2090
|
-
const maxW = Math.max(1, W - leadW - 1);
|
|
2091
|
-
// KC1: the scroll is the CURSOR LINE's own offset — a single-line
|
|
2092
|
-
// buffer's line starts at 0, so the math is today's exactly. The
|
|
2093
|
-
// clamp catches a walk onto a line SHORTER than the old offset.
|
|
2094
|
-
const { start, end } = this.#cursorBounds();
|
|
2095
|
-
this.#scroll = Math.min(this.#scroll, end - start);
|
|
2096
|
-
const curCol = widthOf(this.#chars.slice(start, this.#cursor));
|
|
2097
|
-
const scrolledW = widthOf(this.#chars.slice(start, start + this.#scroll));
|
|
2098
|
-
if (curCol < scrolledW) {
|
|
2099
|
-
this.#scroll = this.#indexAtWidth(start, end, curCol) - start;
|
|
2100
|
-
}
|
|
2101
|
-
else if (curCol >= scrolledW + maxW) {
|
|
2102
|
-
this.#scroll = this.#indexAtWidth(start, end, Math.max(0, curCol - maxW + 1)) - start;
|
|
2103
|
-
}
|
|
2354
|
+
// OR-11 — DECLARED SUPERSESSION of ADR-0039 Amendment 2's horizontal
|
|
2355
|
+
// scrolling. There is nothing to reflow horizontally any more: a
|
|
2356
|
+
// long line FOLDS (see #foldLine), so every character is on screen
|
|
2357
|
+
// and the row offset that used to be kept here is gone with it.
|
|
2358
|
+
// What survives is the goal-column reset above, which every key
|
|
2359
|
+
// that is not a ↑/↓ walk still owes.
|
|
2104
2360
|
}
|
|
2105
2361
|
/** The first index in [start, end] whose display width from `start`
|
|
2106
2362
|
* reaches `target` — the width-based column walk (a wide char never
|
|
2107
2363
|
* splits: the index lands BEFORE it). */
|
|
2364
|
+
/** OR-11 — the end of the WIDEST PREFIX of [start, end) that fits in
|
|
2365
|
+
* `budget` columns. Distinct from `#indexAtWidth`, which answers the
|
|
2366
|
+
* scroll's question ("the first index AT or past this width") and is
|
|
2367
|
+
* one character too generous for a fold: 20 wide characters are 40
|
|
2368
|
+
* columns and do not fit in 39. The walk stops BEFORE a character
|
|
2369
|
+
* that would overflow, so a wide character is never split — the same
|
|
2370
|
+
* `w + cw > limit` the compositor's own row cut uses. */
|
|
2371
|
+
#fitsWithin(start, end, budget) {
|
|
2372
|
+
let w = 0;
|
|
2373
|
+
for (let i = start; i < end; i += 1) {
|
|
2374
|
+
const cw = charWidth(this.#chars[i]);
|
|
2375
|
+
if (w + cw > budget)
|
|
2376
|
+
return i;
|
|
2377
|
+
w += cw;
|
|
2378
|
+
}
|
|
2379
|
+
return end;
|
|
2380
|
+
}
|
|
2108
2381
|
#indexAtWidth(start, end, target) {
|
|
2109
2382
|
let w = 0;
|
|
2110
2383
|
for (let i = start; i < end; i += 1) {
|
package/dist/panel-input.d.ts
CHANGED
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* goes through the host.
|
|
18
18
|
*/
|
|
19
19
|
import { type PanelState, type PanelVerdict, type PanelView, type SaferAnswer } from "./approval-panel.js";
|
|
20
|
-
/** The composer's buffer, as a band puts it aside and gets it back.
|
|
20
|
+
/** The composer's buffer, as a band puts it aside and gets it back.
|
|
21
|
+
* OR-11 dropped `scroll`: a long line folds now, so there is no
|
|
22
|
+
* horizontal offset left to put aside. */
|
|
21
23
|
export interface BufferStash {
|
|
22
24
|
readonly chars: number[];
|
|
23
25
|
readonly cursor: number;
|
|
24
|
-
readonly scroll: number;
|
|
25
26
|
}
|
|
26
27
|
/** The composer, as a band controller is allowed to see it. Every
|
|
27
28
|
* entry is something a panel or the session picker does to the
|
|
@@ -31,7 +32,7 @@ export interface BandHost {
|
|
|
31
32
|
line(): string;
|
|
32
33
|
/** The buffer with its paste capsules expanded — the text that would leave the editor. */
|
|
33
34
|
expandPastes(line: string): string;
|
|
34
|
-
/** Empty the buffer: chars, cursor
|
|
35
|
+
/** Empty the buffer: chars, cursor and the ↑↓ goal column. */
|
|
35
36
|
clear(): void;
|
|
36
37
|
/** Type one code point at the cursor. */
|
|
37
38
|
insert(cp: number): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "kiso tui — 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.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.32.0"
|
|
39
39
|
}
|
|
40
40
|
}
|