@sayknow-cli/tui 0.6.8 → 0.6.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.9] - 2026-09-29
6
+
7
+ ### Changed
8
+
9
+ - `TUI.setPostRenderEmitter` passes `PostRenderFrameInfo` (line count, full-redraw count, terminal size) so an overlay can skip re-emitting when nothing that could move or erase it changed.
10
+ - The fast animation cadence is 33 ms (~30 fps) instead of 16 ms; `AnimationCadence` is `33 | 80`.
11
+
12
+ ### Added
13
+
14
+ - `TUI.isChildLineVisible(child, line)`: whether a line of a click-tracked top-level child was on screen in the last frame, so animations can stop before they would force a full redraw.
15
+
5
16
  ## [0.6.8] - 2026-09-29
6
17
 
7
18
  ### Changed
@@ -1,4 +1,8 @@
1
- export type AnimationCadence = 16 | 80;
1
+ /**
2
+ * Shared timer cadences. 33 ms (~30 fps) drives colors that change over time (the
3
+ * working-message shimmer); 80 ms drives frame-stepped animations (spinners, the pet).
4
+ */
5
+ export type AnimationCadence = 33 | 80;
2
6
  type AnimationCallback = (now: number) => void;
3
7
  export interface AnimationRegistration {
4
8
  unregister(): void;
@@ -238,6 +238,18 @@ type TuiRenderCounterSnapshot = {
238
238
  /**
239
239
  * TUI - Main class for managing terminal UI with differential rendering
240
240
  */
241
+ /**
242
+ * What the frame about to be written looks like, for a post-render overlay to decide
243
+ * whether it must redraw: a new line count means the screen may have scrolled, a new
244
+ * full-redraw count means it was cleared or replayed.
245
+ */
246
+ export interface PostRenderFrameInfo {
247
+ totalLines: number;
248
+ fullRedraws: number;
249
+ columns: number;
250
+ rows: number;
251
+ }
252
+ export type PostRenderEmitter = (frame: PostRenderFrameInfo) => string | null;
241
253
  export declare class TUI extends Container {
242
254
  #private;
243
255
  private readonly options;
@@ -312,6 +324,13 @@ export declare class TUI extends Container {
312
324
  */
313
325
  setMouseEnabled(enabled: boolean): void;
314
326
  get mouseEnabled(): boolean;
327
+ /**
328
+ * Whether `line` of a top-level child was on screen in the last frame. Only children
329
+ * with a `handleClick` hook are tracked; others report false. A line scrolled above
330
+ * the viewport is not visible — and changing it would force a full redraw, so
331
+ * animations should stop there.
332
+ */
333
+ isChildLineVisible(child: Component, line: number): boolean;
315
334
  /** The component that currently receives keyboard input. */
316
335
  getFocusedComponent(): Component | null;
317
336
  start(): void;
@@ -369,5 +388,5 @@ export declare class TUI extends Container {
369
388
  * Used for absolute-positioned overlays such as pixel-image pets that live
370
389
  * outside the line-based component model. Return null to emit nothing.
371
390
  */
372
- setPostRenderEmitter(emitter: (() => string | null) | undefined): void;
391
+ setPostRenderEmitter(emitter: PostRenderEmitter | undefined): void;
373
392
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/tui",
4
- "version": "0.6.8",
4
+ "version": "0.6.9",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@sayknow-cli/natives": "0.6.8",
42
- "@sayknow-cli/utils": "0.6.8",
41
+ "@sayknow-cli/natives": "0.6.9",
42
+ "@sayknow-cli/utils": "0.6.9",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -1,4 +1,8 @@
1
- export type AnimationCadence = 16 | 80;
1
+ /**
2
+ * Shared timer cadences. 33 ms (~30 fps) drives colors that change over time (the
3
+ * working-message shimmer); 80 ms drives frame-stepped animations (spinners, the pet).
4
+ */
5
+ export type AnimationCadence = 33 | 80;
2
6
 
3
7
  type TimerHandle = ReturnType<typeof setInterval>;
4
8
  type AnimationCallback = (now: number) => void;
@@ -70,7 +70,10 @@ export class Loader extends Text {
70
70
  }
71
71
  this.#updateDisplay();
72
72
  },
73
- this.#timeDependentColor ? 16 : 80,
73
+ // Time-dependent colors (the working-message shimmer) at ~30 fps: every tick
74
+ // redraws the whole UI, which in a long session costs as much as the rest of
75
+ // the turn. 60 fps doubled that cost for motion the eye cannot tell apart.
76
+ this.#timeDependentColor ? 33 : 80,
74
77
  );
75
78
  }
76
79
 
package/src/tui.ts CHANGED
@@ -673,6 +673,20 @@ type RenderCommitWaiter = {
673
673
  /**
674
674
  * TUI - Main class for managing terminal UI with differential rendering
675
675
  */
676
+ /**
677
+ * What the frame about to be written looks like, for a post-render overlay to decide
678
+ * whether it must redraw: a new line count means the screen may have scrolled, a new
679
+ * full-redraw count means it was cleared or replayed.
680
+ */
681
+ export interface PostRenderFrameInfo {
682
+ totalLines: number;
683
+ fullRedraws: number;
684
+ columns: number;
685
+ rows: number;
686
+ }
687
+
688
+ export type PostRenderEmitter = (frame: PostRenderFrameInfo) => string | null;
689
+
676
690
  export class TUI extends Container {
677
691
  terminal: Terminal;
678
692
  #previousLines: string[] = [];
@@ -1343,6 +1357,22 @@ export class TUI extends Container {
1343
1357
  return this.#mouseEnabled;
1344
1358
  }
1345
1359
 
1360
+ /**
1361
+ * Whether `line` of a top-level child was on screen in the last frame. Only children
1362
+ * with a `handleClick` hook are tracked; others report false. A line scrolled above
1363
+ * the viewport is not visible — and changing it would force a full redraw, so
1364
+ * animations should stop there.
1365
+ */
1366
+ isChildLineVisible(child: Component, line: number): boolean {
1367
+ const rows = this.#childRows.get(child);
1368
+ if (!rows || line < 0 || line >= rows.count) return false;
1369
+ const absolute = rows.start + line;
1370
+ if (this.#manualViewportTop === undefined) {
1371
+ return absolute >= this.#viewportTopRow && absolute < this.#viewportTopRow + this.terminal.rows;
1372
+ }
1373
+ return this.#committedTranscriptRows.includes(absolute);
1374
+ }
1375
+
1346
1376
  /** The component that currently receives keyboard input. */
1347
1377
  getFocusedComponent(): Component | null {
1348
1378
  return this.#focusedComponent;
@@ -3699,11 +3729,11 @@ export class TUI extends Container {
3699
3729
  * Used for absolute-positioned overlays such as pixel-image pets that live
3700
3730
  * outside the line-based component model. Return null to emit nothing.
3701
3731
  */
3702
- setPostRenderEmitter(emitter: (() => string | null) | undefined): void {
3732
+ setPostRenderEmitter(emitter: PostRenderEmitter | undefined): void {
3703
3733
  this.#postRenderEmitter = emitter;
3704
3734
  }
3705
3735
 
3706
- #postRenderEmitter: (() => string | null) | undefined;
3736
+ #postRenderEmitter: PostRenderEmitter | undefined;
3707
3737
 
3708
3738
  #writeRenderBufferAndReanchorImeCursor(
3709
3739
  buffer: string,
@@ -3711,7 +3741,12 @@ export class TUI extends Container {
3711
3741
  totalLines: number,
3712
3742
  onBufferWritten?: () => void,
3713
3743
  ): boolean {
3714
- const overlay = this.#postRenderEmitter?.();
3744
+ const overlay = this.#postRenderEmitter?.({
3745
+ totalLines,
3746
+ fullRedraws: this.#fullRedrawCount,
3747
+ columns: this.terminal.columns,
3748
+ rows: this.terminal.rows,
3749
+ });
3715
3750
  if (overlay) {
3716
3751
  // DECSC/DECRC keep the hardware cursor stable; the dedicated
3717
3752
  // synchronized block prevents visible tearing while the overlay