Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/afk/src/widget.ts

Raw
import type { Theme } from "@earendil-works/pi-coding-agent";
import {
	type Component,
	type TUI,
	truncateToWidth,
} from "@earendil-works/pi-tui";

const AWAKE = ["  /\\_/\\", " ( o.o )  working", "  > ^ <"];
const BLINK = ["  /\\_/\\", " ( -.- )  working", "  > ^ <"];
const NAPPING = ["  /\\_/\\", " ( -.- )  zzz", "  > ^ <"];

export type AfkWidget = Component & { invalidate(): void; dispose(): void };

export function createAfkWidget(
	tui: TUI,
	theme: Theme,
	isNapping: () => boolean,
): AfkWidget {
	let blinking = false;
	let disposed = false;
	let timer: ReturnType<typeof setTimeout> | undefined;

	const syncAnimation = (): void => {
		if (disposed || isNapping()) {
			clearTimeout(timer);
			timer = undefined;
			return;
		}
		if (timer !== undefined) return;
		// Only the blink transitions change pixels; naps need no timer.
		timer = setTimeout(
			() => {
				timer = undefined;
				if (!isNapping()) blinking = !blinking;
				tui.requestRender();
				syncAnimation();
			},
			blinking ? 700 : 2_100,
		);
		timer.unref?.();
	};
	syncAnimation();

	return {
		render(width: number): string[] {
			const art = isNapping() ? NAPPING : blinking ? BLINK : AWAKE;
			return art.map((line) =>
				truncateToWidth(theme.fg("accent", line), width),
			);
		},
		invalidate(): void {
			syncAnimation();
			tui.requestRender();
		},
		dispose(): void {
			if (disposed) return;
			disposed = true;
			syncAnimation();
		},
	};
}