import type { TUI } from "../tui";
import { sliceByColumn, visibleWidth } from "../utils";
import { Text } from "./text";
* Loader component that drives display refresh at ~60fps so callers whose
* message colorizer is time-dependent (e.g. shimmer/KITT) animate smoothly.
*
* Two cadences are interleaved on a single timer:
* - **Render tick** (every `RENDER_INTERVAL_MS`) → asks the TUI to redraw.
* The TUI already throttles at 16ms (`MIN_RENDER_INTERVAL_MS`), so this
* is the natural upper bound; static messageColorFns produce identical
* output and the differ drops the no-op redraw at ~zero cost.
* - **Spinner advance** (every `SPINNER_ADVANCE_MS`) → bumps the spinner
* frame index. Decoupled from the render cadence so the spinner keeps
* its classic ~12.5fps step pace regardless of shimmer state.
*/
const RENDER_INTERVAL_MS = 16;
const SPINNER_ADVANCE_MS = 80;
export class Loader extends Text {
#frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
#currentFrame = 0;
#intervalId?: NodeJS.Timeout;
#ui: TUI | null = null;
#lastSpinnerTick = 0;
constructor(
ui: TUI,
private spinnerColorFn: (str: string) => string,
private messageColorFn: (str: string) => string,
private message: string = "Loading...",
spinnerFrames?: string[],
) {
super("", 1, 0);
this.#ui = ui;
if (spinnerFrames && spinnerFrames.length > 0) {
this.#frames = spinnerFrames;
}
this.start();
}
render(width: number): string[] {
const lines = ["", ...super.render(width)];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (visibleWidth(line) > width) {
lines[i] = sliceByColumn(line, 0, width, true);
}
}
return lines;
}
start() {
this.#lastSpinnerTick = performance.now();
this.#updateDisplay();
this.#intervalId = setInterval(() => {
const now = performance.now();
if (now - this.#lastSpinnerTick >= SPINNER_ADVANCE_MS) {
this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
this.#lastSpinnerTick = now;
}
this.#updateDisplay();
}, RENDER_INTERVAL_MS);
}
stop() {
if (this.#intervalId) {
clearInterval(this.#intervalId);
this.#intervalId = undefined;
}
}
setMessage(message: string) {
this.message = message;
this.#updateDisplay();
}
#updateDisplay() {
const frame = this.#frames[this.#currentFrame];
this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);
if (this.#ui) {
this.#ui.requestRender();
}
}
}