@oh-my-pi/pi-tui 15.2.2 → 15.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.2.3] - 2026-05-22
6
+ ### Added
7
+
8
+ - Added `SettingsList#setItems` to replace the entire settings list with a new items array while automatically clamping selection to a valid index
9
+
10
+ ### Changed
11
+
12
+ - Updated `Loader` to drive renders at ~60fps (16ms tick) while keeping spinner-frame advancement at 80ms so shimmer/animated message colorizers update smoothly without altering spinner cadence
13
+
5
14
  ## [15.1.9] - 2026-05-21
6
15
 
7
16
  ### Fixed
@@ -1,8 +1,5 @@
1
1
  import type { TUI } from "../tui";
2
2
  import { Text } from "./text";
3
- /**
4
- * Loader component that updates every 80ms with spinning animation
5
- */
6
3
  export declare class Loader extends Text {
7
4
  #private;
8
5
  private spinnerColorFn;
@@ -25,6 +25,14 @@ export declare class SettingsList implements Component {
25
25
  constructor(items: SettingItem[], maxVisible: number, theme: SettingsListTheme, onChange: (id: string, newValue: string) => void, onCancel: () => void);
26
26
  /** Update an item's currentValue */
27
27
  updateValue(id: string, newValue: string): void;
28
+ /**
29
+ * Replace the entire items array. Selection is preserved when the prior
30
+ * index is still valid, otherwise clamped to the last item (or 0 if the
31
+ * list is now empty). An open submenu is left untouched — its lifetime
32
+ * is bounded by its own done callback, and `#closeSubmenu` re-clamps the
33
+ * restored index against the new list on the way out.
34
+ */
35
+ setItems(items: SettingItem[]): void;
28
36
  invalidate(): void;
29
37
  render(width: number): string[];
30
38
  handleInput(data: string): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "15.2.2",
4
+ "version": "15.2.3",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "15.2.2",
41
- "@oh-my-pi/pi-utils": "15.2.2",
40
+ "@oh-my-pi/pi-natives": "15.2.3",
41
+ "@oh-my-pi/pi-utils": "15.2.3",
42
42
  "lru-cache": "11.3.6",
43
43
  "marked": "^18.0.3"
44
44
  },
@@ -3,13 +3,27 @@ import { sliceByColumn, visibleWidth } from "../utils";
3
3
  import { Text } from "./text";
4
4
 
5
5
  /**
6
- * Loader component that updates every 80ms with spinning animation
6
+ * Loader component that drives display refresh at ~60fps so callers whose
7
+ * message colorizer is time-dependent (e.g. shimmer/KITT) animate smoothly.
8
+ *
9
+ * Two cadences are interleaved on a single timer:
10
+ * - **Render tick** (every `RENDER_INTERVAL_MS`) → asks the TUI to redraw.
11
+ * The TUI already throttles at 16ms (`MIN_RENDER_INTERVAL_MS`), so this
12
+ * is the natural upper bound; static messageColorFns produce identical
13
+ * output and the differ drops the no-op redraw at ~zero cost.
14
+ * - **Spinner advance** (every `SPINNER_ADVANCE_MS`) → bumps the spinner
15
+ * frame index. Decoupled from the render cadence so the spinner keeps
16
+ * its classic ~12.5fps step pace regardless of shimmer state.
7
17
  */
18
+ const RENDER_INTERVAL_MS = 16;
19
+ const SPINNER_ADVANCE_MS = 80;
20
+
8
21
  export class Loader extends Text {
9
22
  #frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10
23
  #currentFrame = 0;
11
24
  #intervalId?: NodeJS.Timeout;
12
25
  #ui: TUI | null = null;
26
+ #lastSpinnerTick = 0;
13
27
 
14
28
  constructor(
15
29
  ui: TUI,
@@ -38,11 +52,16 @@ export class Loader extends Text {
38
52
  }
39
53
 
40
54
  start() {
55
+ this.#lastSpinnerTick = performance.now();
41
56
  this.#updateDisplay();
42
57
  this.#intervalId = setInterval(() => {
43
- this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
58
+ const now = performance.now();
59
+ if (now - this.#lastSpinnerTick >= SPINNER_ADVANCE_MS) {
60
+ this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
61
+ this.#lastSpinnerTick = now;
62
+ }
44
63
  this.#updateDisplay();
45
- }, 80);
64
+ }, RENDER_INTERVAL_MS);
46
65
  }
47
66
 
48
67
  stop() {
@@ -59,6 +59,22 @@ export class SettingsList implements Component {
59
59
  }
60
60
  }
61
61
 
62
+ /**
63
+ * Replace the entire items array. Selection is preserved when the prior
64
+ * index is still valid, otherwise clamped to the last item (or 0 if the
65
+ * list is now empty). An open submenu is left untouched — its lifetime
66
+ * is bounded by its own done callback, and `#closeSubmenu` re-clamps the
67
+ * restored index against the new list on the way out.
68
+ */
69
+ setItems(items: SettingItem[]): void {
70
+ this.#items = items;
71
+ if (this.#items.length === 0) {
72
+ this.#selectedIndex = 0;
73
+ } else if (this.#selectedIndex >= this.#items.length) {
74
+ this.#selectedIndex = this.#items.length - 1;
75
+ }
76
+ }
77
+
62
78
  invalidate(): void {
63
79
  this.#submenuComponent?.invalidate?.();
64
80
  }