/** * Pomodoro Timer Extension * * Puts a Pomodoro timer in the pi status bar. * * Phases: * 🍅 Work — 25 minutes * ☕ Break — 5 minutes (short break) * 🛋️ Long Break — 15 minutes (after every 4 pomodoros) * * Commands: * /pomodoro start — start (or resume) the timer * /pomodoro pause — pause the timer * /pomodoro stop — stop and reset the current phase * /pomodoro skip — skip to the next phase * /pomodoro reset — stop and reset everything, including the count * /pomodoro status — print current timer state * * Shortcut: * Ctrl+Shift+T — toggle start / pause */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Key } from "@earendil-works/pi-tui"; // ─── Durations ─────────────────────────────────────────────────────────────── const WORK_SECS = 25 * 60; const SHORT_BREAK_SECS = 5 * 60; const LONG_BREAK_SECS = 15 * 60; const POMODOROS_PER_LONG_BREAK = 4; // ─── Types ─────────────────────────────────────────────────────────────────── type Phase = "work" | "shortBreak" | "longBreak"; interface UI { theme: { fg(color: string, text: string): string; bold?(text: string): string; }; setStatus(id: string, text: string | undefined): void; notify(message: string, level: "info" | "error" | "warning" | "success"): void; } // ─── Helpers ───────────────────────────────────────────────────────────────── function formatTime(seconds: number): string { const m = Math.floor(seconds / 60) .toString() .padStart(2, "0"); const s = (seconds % 60).toString().padStart(2, "0"); return `${m}:${s}`; } function phaseEmoji(phase: Phase): string { switch (phase) { case "work": return "🍅"; case "shortBreak": return "☕"; case "longBreak": return "🛋️"; } } function phaseLabel(phase: Phase): string { switch (phase) { case "work": return "Work"; case "shortBreak": return "Break"; case "longBreak": return "Long Break"; } } function phaseDuration(phase: Phase): number { switch (phase) { case "work": return WORK_SECS; case "shortBreak": return SHORT_BREAK_SECS; case "longBreak": return LONG_BREAK_SECS; } } // ─── Extension ─────────────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { // Mutable state – captured from session_start / command handlers let ui: UI | null = null; let interval: ReturnType | null = null; let running = false; let phase: Phase = "work"; let secondsLeft = WORK_SECS; let pomodoroCount = 0; // completed work sessions this session // ─── Status bar rendering ────────────────────────────────────────────────── function renderStatus(): void { if (!ui) return; const theme = ui.theme; // Idle: show nothing if (!running && secondsLeft === phaseDuration(phase) && pomodoroCount === 0) { ui.setStatus("pomodoro", undefined); return; } const emoji = phaseEmoji(phase); const time = formatTime(secondsLeft); const count = pomodoroCount > 0 ? ` ×${pomodoroCount}` : ""; const pause = !running ? " ⏸" : ""; // Work phases get accent color; breaks get success (greenish) const color = phase === "work" ? "accent" : "success"; ui.setStatus("pomodoro", theme.fg(color, `${emoji} ${time}${count}${pause}`)); } // ─── Timer logic ────────────────────────────────────────────────────────── function startInterval(): void { if (interval) return; interval = setInterval(() => { if (!running) return; secondsLeft--; if (secondsLeft <= 0) { advancePhase(); } else { renderStatus(); } }, 1000); } function clearTimerInterval(): void { if (interval) { clearInterval(interval); interval = null; } } function advancePhase(): void { if (phase === "work") { pomodoroCount++; const isLongBreak = pomodoroCount % POMODOROS_PER_LONG_BREAK === 0; phase = isLongBreak ? "longBreak" : "shortBreak"; secondsLeft = phaseDuration(phase); ui?.notify( `🍅 Pomodoro #${pomodoroCount} done! ${isLongBreak ? "Take a long break 🛋️" : "Take a short break ☕"}`, "info", ); } else { phase = "work"; secondsLeft = WORK_SECS; ui?.notify("⏰ Break over — time to focus! 🍅", "info"); } renderStatus(); } function start(): void { running = true; startInterval(); renderStatus(); } function pause(): void { running = false; renderStatus(); } function stop(): void { running = false; clearTimerInterval(); secondsLeft = phaseDuration(phase); renderStatus(); } function reset(): void { running = false; clearTimerInterval(); phase = "work"; secondsLeft = WORK_SECS; pomodoroCount = 0; renderStatus(); } function skip(): void { advancePhase(); } // ─── Events ─────────────────────────────────────────────────────────────── pi.on("session_start", async (_event, ctx) => { ui = ctx.ui as unknown as UI; renderStatus(); }); pi.on("session_shutdown", async () => { clearTimerInterval(); running = false; ui = null; }); // ─── /pomodoro command ──────────────────────────────────────────────────── pi.registerCommand("pomodoro", { description: "Pomodoro timer — sub-commands: start, pause, stop, skip, reset, status", handler: async (args, ctx) => { // Refresh ui reference so the timer can fire notifications even if the // session_start capture happened on a previous session. ui = ctx.ui as unknown as UI; const sub = args?.trim().toLowerCase() || "start"; switch (sub) { case "start": case "resume": if (running) { ctx.ui.notify("Timer is already running.", "info"); } else { start(); ctx.ui.notify( `▶ ${phaseEmoji(phase)} ${phaseLabel(phase)} started — ${formatTime(secondsLeft)} remaining`, "info", ); } break; case "pause": if (!running) { ctx.ui.notify("Timer is not running.", "info"); } else { pause(); ctx.ui.notify(`⏸ Paused at ${formatTime(secondsLeft)}`, "info"); } break; case "stop": stop(); ctx.ui.notify("⏹ Timer stopped.", "info"); break; case "skip": skip(); ctx.ui.notify( `⏭ Skipped to ${phaseEmoji(phase)} ${phaseLabel(phase)} — ${formatTime(secondsLeft)} remaining`, "info", ); break; case "reset": reset(); ctx.ui.notify("🔄 Timer reset.", "info"); break; case "status": { const state = running ? `▶ ${phaseEmoji(phase)} ${phaseLabel(phase)} — ${formatTime(secondsLeft)} left` : secondsLeft < phaseDuration(phase) ? `⏸ ${phaseEmoji(phase)} ${phaseLabel(phase)} — ${formatTime(secondsLeft)} left (paused)` : `⏹ Stopped (${phaseLabel(phase)})`; const summary = pomodoroCount > 0 ? `Completed this session: ${pomodoroCount} 🍅` : "No pomodoros completed yet."; ctx.ui.notify(`${state}\n${summary}`, "info"); break; } default: ctx.ui.notify( `Unknown sub-command "${sub}". Try: start, pause, stop, skip, reset, status`, "error", ); } }, }); // ─── Ctrl+Shift+T shortcut — toggle start / pause ───────────────────────── pi.registerShortcut(Key.ctrlShift("t"), { description: "Toggle Pomodoro timer (start / pause)", handler: async (ctx) => { ui = ctx.ui as unknown as UI; if (running) { pause(); ctx.ui.notify(`⏸ Pomodoro paused at ${formatTime(secondsLeft)}`, "info"); } else { start(); ctx.ui.notify( `▶ ${phaseEmoji(phase)} ${phaseLabel(phase)} running — ${formatTime(secondsLeft)} remaining`, "info", ); } }, }); }