/** * run-progress.tsx — Standalone launcher for progress screens and confirmations. * * Provides helper functions to run a progress spinner screen or a confirm * dialog as standalone Ink instances that return Promises. */ import React, { useState, useEffect, useCallback } from "react"; import { getStableStdin } from "./bun-stdin"; import { render, useInput } from "ink"; import { ProgressView, type ProgressResult } from "./progress"; import { ConfirmDialog } from "./confirm"; import { useSpinner } from "./use-spinner"; // --------------------------------------------------------------------------- // Progress Screen // --------------------------------------------------------------------------- export interface RunProgressOptions { /** Title of the operation. */ title: string; /** Optional context string. */ context?: string; } /** * A progress controller returned by `runProgressInk`. * * Call `update()` to change the status text, `finish()` to display a result * and wait for the user to dismiss. */ export interface ProgressController { /** Update the status text while the operation is running. */ update: (status: string) => void; /** Show a result and wait for the user to press any key. Unmounts when dismissed. */ finish: (result: ProgressResult) => Promise; /** Force-unmount (e.g., on error). */ unmount: () => void; } /** Internal component that bridges imperative controller → React props. */ function ProgressApp({ title, context, statusRef, resultRef, onDismiss, }: { title: string; context?: string; statusRef: React.MutableRefObject; resultRef: React.MutableRefObject; onDismiss: () => void; }) { const frame = useSpinner(!resultRef.current); const [status, setStatus] = useState(statusRef.current); const [result, setResult] = useState(resultRef.current); // Poll for updates from the imperative controller useEffect(() => { const interval = setInterval(() => { if (statusRef.current !== status) setStatus(statusRef.current); if (resultRef.current !== result) setResult(resultRef.current); }, 100); return () => clearInterval(interval); }, [status, result, statusRef, resultRef]); useInput(() => { if (result) onDismiss(); }); return ( ); } /** * Launch a progress screen. Returns a controller for updating status * and finishing with a result. */ export function runProgressInk(opts: RunProgressOptions): ProgressController { const statusRef = { current: "" }; const resultRef = { current: undefined as ProgressResult | undefined }; let dismissResolve: (() => void) | null = null; process.stdin.resume(); // keep event loop alive between renders const instance = render( { instance.unmount(); dismissResolve?.(); }} />, { exitOnCtrlC: false, stdin: getStableStdin() } ); return { update(status: string) { statusRef.current = status; }, finish(result: ProgressResult): Promise { resultRef.current = result; return new Promise((resolve) => { dismissResolve = resolve; }); }, unmount() { instance.unmount(); dismissResolve?.(); }, }; } // --------------------------------------------------------------------------- // Confirm Dialog // --------------------------------------------------------------------------- export interface RunConfirmOptions { /** Title for the confirm modal. */ title: string; /** The confirmation message/question. */ message: string; } /** * Show a confirm dialog as a standalone Ink instance. * Returns true if the user confirmed (Y), false if cancelled (N/Escape). */ export function runConfirmInk(opts: RunConfirmOptions): Promise { return new Promise((resolve) => { let instance: ReturnType; process.stdin.resume(); // keep event loop alive between renders instance = render( { instance.unmount(); resolve(confirmed); }} />, { exitOnCtrlC: false, stdin: getStableStdin() } ); }); }