@kolisachint/hoocode-tui 0.5.68 → 0.5.71
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/components/spacer.d.ts +31 -0
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js +44 -0
- package/dist/components/spacer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tui.d.ts +74 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +106 -2
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -284,6 +284,35 @@ export class TUI extends Container {
|
|
|
284
284
|
previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
|
285
285
|
fullRedrawCount = 0;
|
|
286
286
|
stopped = false;
|
|
287
|
+
/**
|
|
288
|
+
* The filler that keeps the app the size of the screen.
|
|
289
|
+
*
|
|
290
|
+
* ## Why the app is full-screen at all
|
|
291
|
+
*
|
|
292
|
+
* This renderer appends: a frame is the whole component tree flattened into
|
|
293
|
+
* a line buffer, written from wherever the cursor happens to be. On a fresh
|
|
294
|
+
* session that buffer is a dozen rows, so the banner sat halfway up a
|
|
295
|
+
* terminal with the prompt under it and forty rows of the user's shell
|
|
296
|
+
* history above — and the prompt walked down the screen as the conversation
|
|
297
|
+
* grew, only reaching the bottom row once the session was long enough to
|
|
298
|
+
* scroll. Two different layouts for the same app, and the one you meet first
|
|
299
|
+
* is the one that does not look like an app.
|
|
300
|
+
*
|
|
301
|
+
* ## What this does
|
|
302
|
+
*
|
|
303
|
+
* Before the frame is diffed, the root measures it and gives the leftover
|
|
304
|
+
* rows to one designated child. The buffer is therefore never shorter than
|
|
305
|
+
* the terminal, so the terminal's last row is always the buffer's last row:
|
|
306
|
+
* the header stays at the top, the prompt and the footer stay on the bottom,
|
|
307
|
+
* and everything between them is conversation. Nothing else changes — this
|
|
308
|
+
* is still the normal screen, so scrollback, selection and search all still
|
|
309
|
+
* work, and the session is still on screen after you quit.
|
|
310
|
+
*
|
|
311
|
+
* Set to `undefined` (no flex child) and the old append-only behaviour is
|
|
312
|
+
* exactly what you get back, which is what the tests that predate this and
|
|
313
|
+
* any embedder outside the app rely on.
|
|
314
|
+
*/
|
|
315
|
+
flexSpacer;
|
|
287
316
|
/**
|
|
288
317
|
* The pinned viewport.
|
|
289
318
|
*
|
|
@@ -362,6 +391,64 @@ export class TUI extends Container {
|
|
|
362
391
|
setScrollStatusFormatter(formatter) {
|
|
363
392
|
this.scrollStatusFormatter = formatter;
|
|
364
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Nominate the child that absorbs the leftover rows (see `flexSpacer`).
|
|
396
|
+
*
|
|
397
|
+
* It must already be a child of the root, and it should sit between the part
|
|
398
|
+
* of the tree that flows from the top and the chrome that hangs off the
|
|
399
|
+
* bottom — everything after it is what gets pinned to the foot of the screen.
|
|
400
|
+
*/
|
|
401
|
+
setFlexSpacer(spacer) {
|
|
402
|
+
this.flexSpacer = spacer;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Give the flex child whatever the frame did not use, at `height` rows.
|
|
406
|
+
*
|
|
407
|
+
* Returns true when the height moved, meaning the caller has to flatten
|
|
408
|
+
* again — the measurement can only be made from a finished frame, so the
|
|
409
|
+
* frame that answers it is always the second one. Both passes are cheap
|
|
410
|
+
* after the first: every other child returns its memoized array untouched.
|
|
411
|
+
*
|
|
412
|
+
* Two cases, and the second one is the one with a bug behind it.
|
|
413
|
+
*
|
|
414
|
+
* **The frame fits on the screen.** Fill it out to the screen's height and
|
|
415
|
+
* the frame starts on the first row, which is the whole point of the fill.
|
|
416
|
+
*
|
|
417
|
+
* **The frame is taller than the screen.** Then the fill is not what puts
|
|
418
|
+
* the prompt on the floor — the terminal's own scroll is, and the buffer's
|
|
419
|
+
* last row *is* the screen's last row. So a buffer that gets *shorter*
|
|
420
|
+
* takes the prompt up the screen with it: the renderer clears the rows that
|
|
421
|
+
* came off the end and there is nothing it can do to scroll the transcript
|
|
422
|
+
* back down into them, because those rows are in the terminal's scrollback
|
|
423
|
+
* and only the terminal can move them. That is a picker closing, a
|
|
424
|
+
* notification fading, a task ledger emptying — the prompt stranded
|
|
425
|
+
* mid-screen with a band of blank rows under it, and it stays stranded
|
|
426
|
+
* until enough output arrives to push it back down.
|
|
427
|
+
*
|
|
428
|
+
* So the fill takes what the shrinking content gave up, which keeps the
|
|
429
|
+
* buffer the length it already was and the last row where it already is.
|
|
430
|
+
* The blank band ends up *above* the chrome instead of below it, where it
|
|
431
|
+
* reads as room rather than as a layout that came apart, and the next
|
|
432
|
+
* output to arrive lands in it rather than scrolling the screen. Capped at
|
|
433
|
+
* a screenful: a fill longer than the screen is rows nobody can see, and it
|
|
434
|
+
* is given up altogether on the frames that repaint the whole screen anyway.
|
|
435
|
+
*/
|
|
436
|
+
fitFlexSpacer(lines, height, repaint = false) {
|
|
437
|
+
const spacer = this.flexSpacer;
|
|
438
|
+
if (!spacer)
|
|
439
|
+
return false;
|
|
440
|
+
const content = lines.length - spacer.currentHeight;
|
|
441
|
+
if (content < height)
|
|
442
|
+
return spacer.setHeight(height - content);
|
|
443
|
+
// A frame that is about to be repainted from the top of a cleared screen
|
|
444
|
+
// has no floor to hold: the resize already threw the old screen away, and
|
|
445
|
+
// holding its length would only bank a band of blank rows into the middle
|
|
446
|
+
// of the new one.
|
|
447
|
+
if (repaint)
|
|
448
|
+
return spacer.setHeight(0);
|
|
449
|
+
const held = Math.min(this.previousLines.length - content, height);
|
|
450
|
+
return spacer.setHeight(Math.max(0, held));
|
|
451
|
+
}
|
|
365
452
|
/**
|
|
366
453
|
* The screen rows a pinned window shows, the last one being the indicator.
|
|
367
454
|
*
|
|
@@ -373,9 +460,13 @@ export class TUI extends Container {
|
|
|
373
460
|
return Math.max(1, this.terminal.rows - 1);
|
|
374
461
|
}
|
|
375
462
|
/** Rows available to scroll through — the live buffer while live, the
|
|
376
|
-
* measured one while pinned.
|
|
463
|
+
* measured one while pinned. The filler is not transcript: counting it would
|
|
464
|
+
* let a session with nothing above the fold pin itself one row off the
|
|
465
|
+
* bottom and paint a screen of blanks. */
|
|
377
466
|
transcriptLength() {
|
|
378
|
-
|
|
467
|
+
if (this.scrollOffset !== null)
|
|
468
|
+
return this.scrollTotalLines;
|
|
469
|
+
return this.previousLines.length - (this.flexSpacer?.currentHeight ?? 0);
|
|
379
470
|
}
|
|
380
471
|
/**
|
|
381
472
|
* Move the view by `delta` rows; negative is towards the start.
|
|
@@ -1502,6 +1593,10 @@ export class TUI extends Container {
|
|
|
1502
1593
|
renderScrollView() {
|
|
1503
1594
|
const width = this.terminal.columns;
|
|
1504
1595
|
const height = this.terminal.rows;
|
|
1596
|
+
// No fill while pinned: the window is already the height of the screen,
|
|
1597
|
+
// and blank rows in the buffer would be rows of the transcript the reader
|
|
1598
|
+
// has to scroll past. The next live frame puts it back.
|
|
1599
|
+
this.flexSpacer?.setHeight(0);
|
|
1505
1600
|
let lines = this.render(width);
|
|
1506
1601
|
// A patch computed while pinned describes rows nothing painted to the
|
|
1507
1602
|
// normal screen, so the live path must never be handed it.
|
|
@@ -1591,6 +1686,15 @@ export class TUI extends Container {
|
|
|
1591
1686
|
// Render all components to get new lines. The root render() reports what
|
|
1592
1687
|
// it changed via lastPatch; consume it here (it is per-frame state).
|
|
1593
1688
|
let newLines = this.render(width);
|
|
1689
|
+
// The frame has to exist before its leftover rows can be counted, so the
|
|
1690
|
+
// fill is settled by re-flattening rather than predicted. The second pass
|
|
1691
|
+
// invalidates the first's patch — it describes the buffer from before the
|
|
1692
|
+
// splice — so fall back to the full scan, which is always correct and only
|
|
1693
|
+
// runs on frames where the content height actually changed.
|
|
1694
|
+
if (this.fitFlexSpacer(newLines, height, widthChanged || heightChanged)) {
|
|
1695
|
+
newLines = this.render(width);
|
|
1696
|
+
this.lastPatch = "full";
|
|
1697
|
+
}
|
|
1594
1698
|
const patch = this.lastPatch;
|
|
1595
1699
|
this.lastPatch = "full";
|
|
1596
1700
|
// Composite overlays into the rendered lines (before differential compare)
|