export type NamedTargetRuntime = { getName(targetId: string): Promise; setName(targetId: string, name: string): Promise; }; export type SimpleTitleRuntime = { getTitle(): Promise | string; setTitle(title: string): Promise | boolean | void; }; export type TitleSpinnerRuntime = { getTitle(targetId: string): Promise; setTitle(targetId: string, title: string): Promise; }; export type SpinnerConfig = { enabled: boolean; style: string; speed: string; }; export const SPINNER_STYLES: Record = { default: ["·", "✢", "✳", "✶", "✻", "✽"], braille: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], dots: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], classic: ["-", "\\", "|", "/"], arrows: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"], pipe: ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"], star: ["✶", "✸", "✹", "✺", "✹", "✸"], moon: ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"], pulse: ["·", "•", "●", "•"], }; export const SPINNER_SPEEDS: Record = { slow: 300, normal: 150, fast: 80, }; export function stripSpinnerPrefix(name: string, frame: string | null): string { if (!frame) return name; const prefix = `${frame} `; return name.startsWith(prefix) ? name.slice(prefix.length) : name; } export function createTitleSpinner(runtime: TitleSpinnerRuntime, config: SpinnerConfig) { let timer: ReturnType | null = null; let frameIndex = 0; let running = false; let lastFrame: string | null = null; let activeTargetId: string | undefined; let tickInFlight: Promise | null = null; let stopInFlight: Promise | null = null; const scheduleTick = (): void => { const ms = SPINNER_SPEEDS[config.speed] ?? SPINNER_SPEEDS.normal; timer = setTimeout(() => { void startTick(); }, ms); }; const tick = async (): Promise => { const targetId = activeTargetId; if (!running || !targetId) return; const frames = SPINNER_STYLES[config.style] ?? SPINNER_STYLES.default; const frame = frames[frameIndex % frames.length] ?? "·"; frameIndex += 1; const current = await runtime.getTitle(targetId); if (!running || activeTargetId !== targetId) return; const base = stripSpinnerPrefix(current, lastFrame); await runtime.setTitle(targetId, `${frame} ${base}`); lastFrame = frame; if (running && activeTargetId === targetId) { scheduleTick(); } }; const startTick = (): Promise => { const work = tick().finally(() => { if (tickInFlight === work) { tickInFlight = null; } }); tickInFlight = work; return work; }; const stop = async (): Promise => { running = false; if (timer !== null) { clearTimeout(timer); timer = null; } const targetId = activeTargetId; const inFlight = tickInFlight; if (inFlight) await inFlight; activeTargetId = undefined; frameIndex = 0; const frameToStrip = lastFrame; lastFrame = null; if (!targetId || !frameToStrip) return; const current = await runtime.getTitle(targetId); await runtime.setTitle(targetId, stripSpinnerPrefix(current, frameToStrip)); }; return { async start(targetId: string): Promise { if (!config.enabled || running) return; if (stopInFlight) await stopInFlight; if (running) return; activeTargetId = targetId; running = true; frameIndex = 0; await startTick(); }, async stop(): Promise { if (stopInFlight) return stopInFlight; const work = stop().finally(() => { if (stopInFlight === work) { stopInFlight = null; } }); stopInFlight = work; return work; }, }; } export function createNamedTargetSpinner(runtime: NamedTargetRuntime, config: SpinnerConfig) { return createTitleSpinner( { getTitle(targetId) { return runtime.getName(targetId); }, setTitle(targetId, title) { return runtime.setName(targetId, title); }, }, config, ); } export function createSimpleSpinner(runtime: SimpleTitleRuntime, config: SpinnerConfig) { const spinner = createTitleSpinner( { async getTitle() { return runtime.getTitle(); }, async setTitle(_targetId, title) { const result = await runtime.setTitle(title); return result !== false; }, }, config, ); return { start(): Promise { return spinner.start("simple-title"); }, stop(): Promise { return spinner.stop(); }, }; }