@vincemakes/kiso-tui 0.20.2 → 0.20.3
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 +129 -7
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -292,6 +292,20 @@ export class Body {
|
|
|
292
292
|
#needsReset = false;
|
|
293
293
|
#resizeTimer = null;
|
|
294
294
|
#lastH = 0;
|
|
295
|
+
/** DC-34 — did THIS FRAME refold the committed cells?
|
|
296
|
+
*
|
|
297
|
+
* It must be reset where the question is asked, not only where it
|
|
298
|
+
* is answered. Armed once and consumed later, it latched: the
|
|
299
|
+
* session's FIRST frame ran a vacuous refold over zero committed
|
|
300
|
+
* cells and set it, and nothing cleared it until the first resize —
|
|
301
|
+
* so every session's first widen still ran the adopt it was
|
|
302
|
+
* supposed to skip, and swallowed the live band's worth of
|
|
303
|
+
* committed rows. Three paragraphs, in the measurement that found
|
|
304
|
+
* it. */
|
|
305
|
+
#refolded = false;
|
|
306
|
+
/** DC-34 — the previous frame's width; the reach-back guard is for a
|
|
307
|
+
* WIDTH change, which re-indexes the model, not a height change. */
|
|
308
|
+
#lastW = 0;
|
|
295
309
|
// KC1 §6: the composer's recorded extent — the row count the last
|
|
296
310
|
// frame drew (exit's clear walks it) and the row its CHA parked the
|
|
297
311
|
// cursor on (the steady frame's relative anchor). N = 1 reproduces
|
|
@@ -2443,15 +2457,86 @@ export class Body {
|
|
|
2443
2457
|
// the NEW width, so the cached folds are stale. Re-fold the
|
|
2444
2458
|
// committed cells so the every-row draw below re-paints them at the
|
|
2445
2459
|
// current geometry — the frame's model and the screen agree.
|
|
2460
|
+
// DC-34 — A WIDEN DOES NOT REFOLD WHAT IS ALREADY COMMITTED.
|
|
2461
|
+
//
|
|
2462
|
+
// Every count here is physical ROWS at the fold width in force
|
|
2463
|
+
// when it was computed. Refolding the committed cells at a new W
|
|
2464
|
+
// changes what every index MEANS while `#scrolledOff` is carried
|
|
2465
|
+
// across untranslated — and no translation exists, because the
|
|
2466
|
+
// row the scroll stopped at does not occur in the new fold. On a
|
|
2467
|
+
// widen the stale count then points at text the terminal already
|
|
2468
|
+
// holds, and the frame paints it a second time.
|
|
2469
|
+
//
|
|
2470
|
+
// A committed row is ink (ADR-0046): the rows still on screen are
|
|
2471
|
+
// the same thing as the rows in the scrollback minus a scroll
|
|
2472
|
+
// that has not happened, and no terminal reflows either. Leaving
|
|
2473
|
+
// them folded as they were printed keeps every index valid.
|
|
2474
|
+
//
|
|
2475
|
+
// NARROWING still refolds — an old wide row does not FIT, and
|
|
2476
|
+
// `#checked` would throw invariant ①. The comparison is against
|
|
2477
|
+
// the CACHE's fold width, not the last render's: after 60 → 100
|
|
2478
|
+
// (no refold, the cache is still 60) a narrowing to 80 must NOT
|
|
2479
|
+
// refold, because 80 columns hold a 60-column row.
|
|
2480
|
+
this.#refolded = false;
|
|
2446
2481
|
if (this.#fullRedraw) {
|
|
2447
|
-
|
|
2482
|
+
// DC-34 — THE REFOLD IS SCOPED BY THE FRONTIER.
|
|
2483
|
+
//
|
|
2484
|
+
// `#scrolledOff` is the record of what reached the terminal:
|
|
2485
|
+
// rows [0, #scrolledOff) are in its scrollback, immutable, and
|
|
2486
|
+
// no path of ours may contradict them. A cell with any row
|
|
2487
|
+
// down there keeps the fold it was COMMITTED at, forever — in
|
|
2488
|
+
// either direction. A cell entirely above the frontier has
|
|
2489
|
+
// never left the screen, so re-folding it is free.
|
|
2490
|
+
//
|
|
2491
|
+
// Two scalar predicates were tried before this and both
|
|
2492
|
+
// failed, in different ways: the last-refold width crashed on
|
|
2493
|
+
// 60 → 100 → 80 (a cell committed at 100 emitted into an
|
|
2494
|
+
// 80-column screen), and the cache's widest fold fires a FULL
|
|
2495
|
+
// refold at the first narrowing, which re-wraps rows the
|
|
2496
|
+
// scrollback already holds — the original defect, alive in
|
|
2497
|
+
// the other direction. The frontier is not an approximation
|
|
2498
|
+
// of them; it is the question they were both approximating.
|
|
2499
|
+
// A cell is refolded when EITHER is true:
|
|
2500
|
+
// - it is entirely above the frontier (never left the
|
|
2501
|
+
// screen, so re-wrapping it contradicts nothing), or
|
|
2502
|
+
// - it does not FIT: some cached row is wider than W.
|
|
2503
|
+
//
|
|
2504
|
+
// The second is not a compromise of the first, it is the
|
|
2505
|
+
// answer to a question the first cannot reach. A cell can
|
|
2506
|
+
// STRADDLE the frontier — its head in the scrollback, its
|
|
2507
|
+
// tail still on screen — and the tail must be painted at the
|
|
2508
|
+
// current width. Holding its commit fold there emitted a
|
|
2509
|
+
// 100-column row into an 80-column screen and invariant ①
|
|
2510
|
+
// threw (60 → 100 → 80, measured). Fitting wins: a crash is
|
|
2511
|
+
// worse than a seam, and the seam a narrowing leaves is
|
|
2512
|
+
// rider 2's, stated rather than hidden.
|
|
2513
|
+
let row = 0;
|
|
2514
|
+
const refold = new Array(this.#committed).fill(false);
|
|
2515
|
+
for (let i = 0; i < this.#committed; i += 1) {
|
|
2516
|
+
const lines = this.#lineCache[i];
|
|
2517
|
+
if (lines === null || lines === undefined) {
|
|
2518
|
+
refold[i] = true;
|
|
2519
|
+
continue;
|
|
2520
|
+
}
|
|
2521
|
+
const above = row >= this.#scrolledOff;
|
|
2522
|
+
const fits = lines.every((l) => visibleWidth(l) <= W);
|
|
2523
|
+
refold[i] = above || !fits;
|
|
2524
|
+
const prev = i > 0 ? this.#lineCache[i - 1] : null;
|
|
2525
|
+
row += this.#space(i, prev ?? null, lines).length;
|
|
2526
|
+
}
|
|
2527
|
+
for (let i = 0; i < this.#committed; i += 1) {
|
|
2528
|
+
if (refold[i])
|
|
2529
|
+
this.#lineCache[i] = cellComponent(this.#cells[i]).render(W, ctx);
|
|
2530
|
+
}
|
|
2531
|
+
// #committedLines is re-derived over the WHOLE cache, because
|
|
2532
|
+
// the frozen prefix still occupies its own rows.
|
|
2448
2533
|
this.#committedLines = 0;
|
|
2449
2534
|
for (let i = 0; i < this.#committed; i += 1) {
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
this.#
|
|
2453
|
-
this.#committedLines += this.#space(i, i > 0 ? this.#lineCache[i - 1] : null, lines).length;
|
|
2535
|
+
const lines = this.#lineCache[i] ?? cellComponent(this.#cells[i]).render(W, ctx);
|
|
2536
|
+
this.#lineCache[i] = lines;
|
|
2537
|
+
this.#committedLines += this.#space(i, i > 0 ? (this.#lineCache[i - 1] ?? []) : null, lines).length;
|
|
2454
2538
|
}
|
|
2539
|
+
this.#refolded = refold.some(Boolean);
|
|
2455
2540
|
}
|
|
2456
2541
|
// 1. the natural commits — the leading DONE cells freeze: their
|
|
2457
2542
|
// lines leave the live region, the scrolls + the committed
|
|
@@ -3577,7 +3662,25 @@ export class Body {
|
|
|
3577
3662
|
// discarded because a reflow invalidates every row of it: the
|
|
3578
3663
|
// next diff repaints the whole screen, which is exactly what a
|
|
3579
3664
|
// resize needs.
|
|
3580
|
-
|
|
3665
|
+
// DC-34 — NO HIGH-WATER MARK ON A RESIZE.
|
|
3666
|
+
//
|
|
3667
|
+
// This was `max(#scrolledOff, …)`, which held a stale count
|
|
3668
|
+
// whenever a widen made the fresh one smaller; `leaving` then
|
|
3669
|
+
// stayed <= 0 and the text that marched past in the meantime
|
|
3670
|
+
// never entered the scrollback at all — the hole, the other
|
|
3671
|
+
// half of the same off-by-a-refold.
|
|
3672
|
+
//
|
|
3673
|
+
// The other implementation in this space reached the same
|
|
3674
|
+
// conclusion independently and says so in its own source: a
|
|
3675
|
+
// historical high-water mark "caused self-reinforcing
|
|
3676
|
+
// inflation that pushed content into scrollback on terminal
|
|
3677
|
+
// widen". Dropping it alone brings the DUPLICATE back — it is
|
|
3678
|
+
// the pair with the no-refold rule above, not a substitute
|
|
3679
|
+
// for it.
|
|
3680
|
+
// PROBE 3: a widen leaves it ALONE; a narrow keeps REL-0152-R1.
|
|
3681
|
+
if (this.#refolded)
|
|
3682
|
+
this.#scrolledOff = Math.max(this.#scrolledOff, Math.max(0, Math.min(skip, all.length)));
|
|
3683
|
+
this.#refolded = false;
|
|
3581
3684
|
this.#screen = new Array(H).fill(NOT_PAINTED);
|
|
3582
3685
|
this.#resizeFrame = false;
|
|
3583
3686
|
}
|
|
@@ -3597,7 +3700,26 @@ export class Body {
|
|
|
3597
3700
|
// OLDEST on screen — they are still in the model and come back on
|
|
3598
3701
|
// the close.
|
|
3599
3702
|
const contentRows = Math.max(0, H - CHROME_ROWS - inputExtra - queueRows.length - menuRows.length);
|
|
3600
|
-
|
|
3703
|
+
// DC-34 — THE MARCH NEVER REACHES BELOW THE FRONTIER.
|
|
3704
|
+
//
|
|
3705
|
+
// Rows [0, #scrolledOff) are in the terminal's scrollback and are
|
|
3706
|
+
// immutable; painting one puts the same prose on screen twice,
|
|
3707
|
+
// which is the owner's report. `skip` can drop below it whenever
|
|
3708
|
+
// the model shrinks under a fixed screen — a widen refolding the
|
|
3709
|
+
// cells above the frontier, or the live band collapsing — and
|
|
3710
|
+
// nothing stopped it (rider 3's ungated reach-back).
|
|
3711
|
+
//
|
|
3712
|
+
// Clamping costs a gap under short content for one frame, which
|
|
3713
|
+
// the next commit fills. Reaching back costs a duplicate that
|
|
3714
|
+
// stands in the transcript forever.
|
|
3715
|
+
// ...but only when the WIDTH moved. A height change re-indexes
|
|
3716
|
+
// nothing — the folds are untouched, every row means what it
|
|
3717
|
+
// meant — so reaching back there is the pre-existing behaviour a
|
|
3718
|
+
// gate already covers (the A8 windowing case: grow the screen and
|
|
3719
|
+
// the banner returns). The duplication measured in this round is
|
|
3720
|
+
// width-driven, and so is the guard.
|
|
3721
|
+
const march = all.slice(this.#lastW !== 0 && this.#lastW !== W ? Math.max(skip, this.#scrolledOff) : skip);
|
|
3722
|
+
this.#lastW = W;
|
|
3601
3723
|
for (const line of march.length > contentRows ? march.slice(march.length - contentRows) : march) {
|
|
3602
3724
|
desired[r - 1] = this.#checked(line, W);
|
|
3603
3725
|
r += 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
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.20.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.20.3"
|
|
39
39
|
}
|
|
40
40
|
}
|