Small library for generating claude-code like unicde block mascots, and providing animations when they do stuff
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add clood splash screen header extension

Replaces the default pi splash screen with a cloud-themed ASCII art
header displaying 'clood' (configurable name).

Features:
- Configurable name displayed inside the cloud
- Three animation styles: float (gentle horizontal movement),
pulse (color breathing), rain (falling droplets)
- Animation can be disabled with 'none'
- Configurable subtitle text
- /default-header command to restore built-in header
- /clood-animation command to preview animation styles

goose.art 1ad98505 e936dd86

verified
+164
+164
.pi/extensions/clood-header.ts
··· 1 + /** 2 + * Clood Header Extension 3 + * 4 + * Replaces the default pi splash screen with a cloud-themed header. 5 + * Configurable name and animation style. 6 + * 7 + * Configuration (set values below): 8 + * - NAME: The name displayed in the header (default: "clood") 9 + * - ANIMATION: Animation style - "float", "pulse", "rain", or "none" 10 + * - SUBTITLE: Optional subtitle text below the cloud 11 + */ 12 + 13 + import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent"; 14 + import type { TUI } from "@mariozechner/pi-tui"; 15 + 16 + // ═══════════════════════════════════════ 17 + // CONFIGURATION 18 + // ═══════════════════════════════════════ 19 + const NAME = "clood"; 20 + const ANIMATION: "float" | "pulse" | "rain" | "none" = "float"; 21 + const SUBTITLE = ""; // e.g. "your friendly cloud coding agent" 22 + const ANIMATION_SPEED = 150; // ms per frame 23 + 24 + // ═══════════════════════════════════════ 25 + // CLOUD ASCII ART 26 + // ═══════════════════════════════════════ 27 + 28 + function getCloudLines(theme: Theme, frame: number, animation: string, width: number): string[] { 29 + const accent = (s: string) => theme.fg("accent", s); 30 + const muted = (s: string) => theme.fg("muted", s); 31 + const dim = (s: string) => theme.fg("dim", s); 32 + 33 + // Cloud shape 34 + const cloudRaw = [ 35 + " .-~~~-. ", 36 + " .- ~ ~-( )_ _ ", 37 + " / \\", 38 + " | |", 39 + " \\ /", 40 + " ~- . _____________ .~ ", 41 + ]; 42 + 43 + // Center the name inside the cloud body (line index 3, the widest) 44 + const nameUpper = NAME.toUpperCase(); 45 + const namePad = Math.max(0, Math.floor((25 - nameUpper.length) / 2)); 46 + const nameLine = " ".repeat(namePad) + nameUpper + " ".repeat(Math.max(0, 25 - namePad - nameUpper.length)); 47 + 48 + const cloudWithName = [ 49 + cloudRaw[0], 50 + cloudRaw[1], 51 + cloudRaw[2], 52 + ` | ${nameLine} |`, 53 + cloudRaw[4], 54 + cloudRaw[5], 55 + ]; 56 + 57 + // Apply animation transforms 58 + let lines: string[]; 59 + switch (animation) { 60 + case "float": { 61 + // Gentle horizontal floating 62 + const offsets = [0, 1, 2, 3, 3, 2, 1, 0, -1, -2, -3, -3, -2, -1]; 63 + const offset = offsets[frame % offsets.length]; 64 + const pad = Math.max(0, 2 + offset); 65 + lines = cloudWithName.map((l) => " ".repeat(pad) + l); 66 + break; 67 + } 68 + case "pulse": { 69 + // Pulse the accent color brightness using different color tokens 70 + const pulseStages = ["dim", "muted", "accent", "accent", "muted", "dim"] as const; 71 + const stage = pulseStages[frame % pulseStages.length]; 72 + const colorFn = (s: string) => theme.fg(stage, s); 73 + lines = ["", ...cloudWithName.map((l) => " " + colorFn(l)), ""]; 74 + return lines; 75 + } 76 + case "rain": { 77 + // Little rain drops falling under the cloud 78 + const rainFrames = [ 79 + [" ' ' ' ' "], 80 + [" ' ' ' ' "], 81 + [" ' ' ' ' "], 82 + [" ' ' ' ' "], 83 + ]; 84 + const rainLine = rainFrames[frame % rainFrames.length][0]; 85 + lines = ["", ...cloudWithName.map((l) => " " + l), " " + dim(rainLine), ""]; 86 + return lines.map((l) => " " + accent(l.includes(nameUpper) ? l : l)); 87 + } 88 + case "none": 89 + default: 90 + lines = cloudWithName.map((l) => " " + l); 91 + break; 92 + } 93 + 94 + // Colorize 95 + const result = ["", ...lines.map((l) => accent(l)), ""]; 96 + return result; 97 + } 98 + 99 + // ═══════════════════════════════════════ 100 + // EXTENSION 101 + // ═══════════════════════════════════════ 102 + 103 + export default function (pi: ExtensionAPI) { 104 + pi.on("session_start", async (_event, ctx) => { 105 + if (!ctx.hasUI) return; 106 + 107 + ctx.ui.setHeader((tui: TUI, theme: Theme) => { 108 + let frame = 0; 109 + let timer: ReturnType<typeof setInterval> | null = null; 110 + 111 + if (ANIMATION !== "none") { 112 + timer = setInterval(() => { 113 + frame++; 114 + tui.requestRender(); 115 + }, ANIMATION_SPEED); 116 + } 117 + 118 + return { 119 + render(width: number): string[] { 120 + const lines = getCloudLines(theme, frame, ANIMATION, width); 121 + 122 + if (SUBTITLE) { 123 + const subtitleLine = ` ${theme.fg("muted", SUBTITLE)}`; 124 + lines.push(subtitleLine); 125 + } 126 + 127 + return lines; 128 + }, 129 + invalidate() {}, 130 + dispose() { 131 + if (timer) { 132 + clearInterval(timer); 133 + timer = null; 134 + } 135 + }, 136 + }; 137 + }); 138 + }); 139 + 140 + // Command to restore built-in header 141 + pi.registerCommand("default-header", { 142 + description: "Restore the default pi header", 143 + handler: async (_args, ctx) => { 144 + ctx.ui.setHeader(undefined); 145 + ctx.ui.notify("Default header restored", "info"); 146 + }, 147 + }); 148 + 149 + // Command to switch animation 150 + pi.registerCommand("clood-animation", { 151 + description: "Change the clood header animation (float, pulse, rain, none)", 152 + handler: async (args, ctx) => { 153 + const valid = ["float", "pulse", "rain", "none"]; 154 + if (!args || !valid.includes(args.trim())) { 155 + const choice = await ctx.ui.select("Choose animation:", valid); 156 + if (choice !== undefined) { 157 + ctx.ui.notify(`Animation set to: ${valid[choice]}. Use /reload to apply.`, "info"); 158 + } 159 + return; 160 + } 161 + ctx.ui.notify(`Animation: ${args.trim()}. Edit .pi/extensions/clood-header.ts to persist.`, "info"); 162 + }, 163 + }); 164 + }