e39b48c405
- Added `display.shimmer` setting (`classic`, `kitt`, `disabled`) with default `classic` and UI metadata. - Added shimmer mode resolution with defaulting plus classic/KITT intensity tier profiles and thresholds. - Added `getFgAnsi` handling across shimmer compilation and progress-bar themes, with fallback ANSI output defaults. - Reworked shimmer rendering to coalesce same-tier segments, cache palettes by symbol, and map disabled mode to mid-tier. - Tuned loader timing to a 16ms render interval and 80ms spinner stepping for smoother ~60fps updates.
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|