chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
101
third_party/pi/packages/tui/src/components/loader.ts
vendored
Normal file
101
third_party/pi/packages/tui/src/components/loader.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { TUI } from "../tui.ts";
|
||||
import { Text } from "./text.ts";
|
||||
|
||||
export interface LoaderIndicatorOptions {
|
||||
/** Animation frames. Use an empty array to hide the indicator. */
|
||||
frames?: string[];
|
||||
/** Frame interval in milliseconds for animated indicators. */
|
||||
intervalMs?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
const DEFAULT_INTERVAL_MS = 80;
|
||||
|
||||
/**
|
||||
* Loader component that updates with an optional spinning animation.
|
||||
*/
|
||||
export class Loader extends Text {
|
||||
private frames = [...DEFAULT_FRAMES];
|
||||
private intervalMs = DEFAULT_INTERVAL_MS;
|
||||
private currentFrame = 0;
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
private ui: TUI | null = null;
|
||||
private renderIndicatorVerbatim = false;
|
||||
private spinnerColorFn: (str: string) => string;
|
||||
private messageColorFn: (str: string) => string;
|
||||
private message: string = "Loading...";
|
||||
|
||||
constructor(
|
||||
ui: TUI,
|
||||
spinnerColorFn: (str: string) => string,
|
||||
messageColorFn: (str: string) => string,
|
||||
message: string = "Loading...",
|
||||
indicator?: LoaderIndicatorOptions,
|
||||
) {
|
||||
super("", 1, 0);
|
||||
this.ui = ui;
|
||||
this.spinnerColorFn = spinnerColorFn;
|
||||
this.messageColorFn = messageColorFn;
|
||||
this.message = message;
|
||||
this.setIndicator(indicator);
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
return ["", ...super.render(width)];
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.updateDisplay();
|
||||
this.restartAnimation();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
setMessage(message: string): void {
|
||||
this.message = message;
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
override invalidate(): void {
|
||||
super.invalidate();
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
setIndicator(indicator?: LoaderIndicatorOptions): void {
|
||||
this.renderIndicatorVerbatim = indicator !== undefined;
|
||||
this.frames = indicator?.frames !== undefined ? [...indicator.frames] : [...DEFAULT_FRAMES];
|
||||
this.intervalMs = indicator?.intervalMs && indicator.intervalMs > 0 ? indicator.intervalMs : DEFAULT_INTERVAL_MS;
|
||||
this.currentFrame = 0;
|
||||
this.start();
|
||||
}
|
||||
|
||||
private restartAnimation(): void {
|
||||
this.stop();
|
||||
if (this.frames.length <= 1) {
|
||||
return;
|
||||
}
|
||||
this.intervalId = setInterval(() => {
|
||||
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
||||
this.updateDisplay();
|
||||
}, this.intervalMs);
|
||||
}
|
||||
|
||||
protected getRenderedIndicator(): string {
|
||||
const frame = this.frames[this.currentFrame] ?? "";
|
||||
return this.renderIndicatorVerbatim ? frame : this.spinnerColorFn(frame);
|
||||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
const renderedFrame = this.getRenderedIndicator();
|
||||
const indicator = renderedFrame.length > 0 ? `${renderedFrame} ` : "";
|
||||
this.setText(`${indicator}${this.messageColorFn(this.message)}`);
|
||||
if (this.ui) {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user