import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; const RESET = "\x1b[0m"; const BOLD = "\x1b[1m"; type RGB = readonly [number, number, number]; function rgb([r, g, b]: RGB, text: string, bold = false): string { return `${bold ? BOLD : ""}\x1b[38;2;${r};${g};${b}m${text}${RESET}`; } function gradient(text: string, from: RGB, to: RGB, bold = false): string { const chars = [...text]; const span = Math.max(1, chars.length - 1); return chars.map((char, index) => { if (char === " ") return char; const t = index / span; const color: RGB = [ Math.round(from[0] + (to[0] - from[0]) * t), Math.round(from[1] + (to[1] - from[1]) * t), Math.round(from[2] + (to[2] - from[2]) * t), ]; return rgb(color, char, bold); }).join(""); } const ANIME_ART = [ "⠀⠀⠂⠈⣿⣷⣿⣿⣿⡅⡹⢿⠆⠙⠋⠉⠻⠿⣿⣿⣿⣿⣿⣿⣮⠻⣦⡙⢷⡑⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣌⠡⠌⠂⣙⠻⣛⠻⠷⠐⠈⠛⢱⣮⠁⠐⠀", "⠀⠂⠈⣿⡇⢿⢹⣿⣶⠐⠁⠀⣀⣠⣤⠄⠀⠀⠈⠙⠻⣿⣿⣿⣦⣵⣌⠻⣷⢝⠦⠚⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⣻⣿⣊⡃⠀⣙⠿⣿⣿⣿⣎⢮⡀⢮⣽⠁⠐", "⠂⠈⣿⣿⣧⡸⡎⡛⡩⠖⠀⣴⣿⣿⣿⠀⠀⠀⠀⠸⠇⠀⠙⢿⣿⣿⣿⣷⣌⢷⣑⢷⣄⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣫⠶⠛⠉⠀⠁⠀⠈⠈⠀⠠⠜⠻⣿⣆⢿⣼⣿⣿⠁", " ⠈⣿⣿⣿⣧⢧⣧⢻⣦⢀⣹⣿⣿⣿⣇⠀⠄⠀⠀⠀⡀⠀⠈⢻⣿⣿⣿⣿⣷⣝⢦⡹⠷⡙⢿⣿⣿⣿⣿⣿⣿⣿⣿⠈⠁⠀⠀⠀⠁⠀⠀⠀⠱⣶⣄⡀⠀⠈⠛⠜⣿⣿⣿⣿", "⠀⠊⢫⣿⣏⣿⡌⣼⣄⢫⡌⣿⣿⣿⣿⣿⣦⡈⠲⣄⣤⣤⡡⢀⣠⣿⣿⣿⣿⣿⣿⣷⣼⣍⢬⣦⡙⣿⣿⣿⣿⣿⣯⢁⡄⠀⡀⡀⠀⠄⢈⣠⢪⠀⣿⣿⣿⣦⠀⢉⢂⠹⡿⣿⣿", "⠀⠀⠄⢹⢃⢻⣟⠙⣿⣦⠱⢻⣿⣿⣿⣿⣿⣿⣷⣬⣍⣭⣥⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡙⢿⣼⡿⣿⣿⣿⣿⣿⣷⣄⠘⣱⢦⣤⡴⡿⢈⣼⣿⣿⣿⣇⣴⣶⣮⣅⢻⣿⡏", "⠀⠀⠈⠹⣇⢡⢿⡆⠻⣿⣷⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣍⡻⣿⣟⣻⣿⣿⣿⣿⣷⣦⣥⣬⣤⣴⣾⣿⣿⣿⣿⣷⣿⣿⣿⣿⣷⡜⠃", "⠀⠀⠀⢀⣘⠈⢂⠃⣧⡹⣿⣷⡄⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⣅⡙⢿⣟⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⡕⠂", "⠀⠀⠀⠀⠀⠀⠛⢷⣜⢷⡌⠻⣿⣿⣦⣝⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣹⣷⣦⣹⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠉⠃⠀", ] as const; function getAvailableRows(tui: unknown): number { try { const terminal = (tui as { terminal?: { rows?: unknown } }).terminal; const rows = terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? Math.max(0, Math.floor(rows)) : 0; } catch { return 0; } } // Eyelid blink animation: `rows` is how many artwork rows are visible around the // vertical center (odd numbers keep the slit symmetric). Hidden rows are kept as // blank lines so the header height never changes mid-animation. type BlinkFrame = { readonly rows: number; readonly delay: number }; const BLINK_FRAMES: readonly BlinkFrame[] = [ { rows: 1, delay: 90 }, { rows: 3, delay: 70 }, { rows: 5, delay: 70 }, { rows: 7, delay: 70 }, { rows: 9, delay: 260 }, { rows: 7, delay: 60 }, { rows: 3, delay: 55 }, { rows: 1, delay: 70 }, { rows: 3, delay: 55 }, { rows: 7, delay: 60 }, { rows: 9, delay: 0 }, ]; function isRowVisible(index: number, total: number, visibleRows: number): boolean { if (visibleRows >= total) return true; const center = Math.floor((total - 1) / 2); const half = Math.floor((visibleRows - 1) / 2); return Math.abs(index - center) <= half; } function renderHeader(width: number, availableRows = 0, visibleRows = ANIME_ART.length): string[] { if (width <= 0) return []; const sakura: RGB = [242, 167, 198]; const peach: RGB = [252, 201, 185]; const lavender: RGB = [199, 184, 245]; const sky: RGB = [159, 211, 242]; const telemetry = "◈ SAKURA CYBERDECK ◈"; const artWidth = Math.max(...ANIME_ART.map((line) => [...line].length)); const visibleArtWidth = Math.min(width, artWidth); const artPad = " ".repeat(Math.max(0, Math.floor((width - visibleArtWidth) / 2) - 2)); // Keep the divider visually subordinate: inset it symmetrically from the artwork. const railInset = visibleArtWidth >= 8 ? Math.max(2, Math.round(visibleArtWidth * 0.15)) : 0; const railWidth = Math.max(1, visibleArtWidth - railInset * 2); const rail = "━".repeat(railWidth); const railPad = " ".repeat(Math.max(0, Math.min(width - railWidth, Math.floor((width - railWidth) / 2) + 1))); const visibleTelemetry = [...telemetry].slice(0, width).join(""); const telemetryWidth = [...visibleTelemetry].length; const telemetryPad = " ".repeat(Math.max(0, Math.min(width - telemetryWidth, Math.floor((width - telemetryWidth) / 2) + 1))); const art = ANIME_ART.map((line, index) => { if (!isRowVisible(index, ANIME_ART.length, visibleRows)) return ""; const clipped = [...line].slice(0, visibleArtWidth).join(""); return `${artPad}${gradient(clipped, sakura, sky)}`; }); const visualHeight = ANIME_ART.length + 3; // artwork + gap + divider + label const extraTopPadding = Math.max(0, Math.floor((availableRows - visualHeight) / 2) - 1); return [ ...Array(extraTopPadding).fill(""), "", ...art, "", `${railPad}${gradient(rail, sakura, sky)}`, `${telemetryPad}${gradient(visibleTelemetry, lavender, peach, true)}`, "", ]; } export default function sakuraCyberdeckHeader(pi: ExtensionAPI): void { let blinkTimer: ReturnType | undefined; const stopBlink = (): void => { if (blinkTimer) clearTimeout(blinkTimer); blinkTimer = undefined; }; pi.on("session_start", (_event, ctx) => { if (!ctx.hasUI) return; ctx.ui.setHeader((tui) => { let frame = 0; const advance = (): void => { const current = BLINK_FRAMES[frame]; if (!current || current.delay <= 0 || frame >= BLINK_FRAMES.length - 1) { // Last frame is the fully open artwork: leave it static. stopBlink(); return; } blinkTimer = setTimeout(() => { frame += 1; tui.requestRender(); advance(); }, current.delay); blinkTimer.unref?.(); }; stopBlink(); advance(); return { render: (width) => renderHeader(width, getAvailableRows(tui), BLINK_FRAMES[frame]?.rows ?? ANIME_ART.length), invalidate() {}, }; }); }); pi.on("session_shutdown", (_event, ctx) => { stopBlink(); if (ctx.hasUI) ctx.ui.setHeader(undefined); }); }