1import {
2 measureElement,
3 useProgress,
4 useTitle,
5 type DOMElement,
6} from "@alchemy.run/sigil";
7import { useLayoutEffect, useRef, useState } from "@alchemy.run/sigil/react";
8import { useTerminalSize } from "../ui/index.ts";
9import type { PlanTree, PlanTreeState } from "./PlanTree.ts";
10
11export const usePlanPresentation = (options: {
12 readonly tree: PlanTree;
13 readonly state: PlanTreeState;
14 readonly busy: boolean;
15 readonly collapsible: boolean;
16 readonly hasFooter: boolean;
17}) => {
18 const { tree, state, busy, collapsible, hasFooter } = options;
19 const { mode } = tree;
20 const { label, expanded, viewport, outcome } = state;
21 const { completed, failures, total } = tree.progress();
22 const collapsed = collapsible && !expanded;
23 const settled = !busy && completed >= total;
24
25 useProgress(
26 mode === "apply"
27 ? {
28 state:
29 outcome === "failure" || failures > 0
30 ? "error"
31 : busy
32 ? "normal"
33 : "inactive",
34 value: total === 0 ? undefined : (completed / total) * 100,
35 }
36 : { state: "inactive" },
37 );
38 const titleProgress = settled || total === 0 ? "" : ` ${completed}/${total}`;
39 const titleDetail = tree.titleDetail ? ` · ${tree.titleDetail}` : "";
40 useTitle(
41 mode === "apply" ? `${label}${titleProgress}${titleDetail}` : undefined,
42 );
43
44 const beforeRef = useRef<DOMElement>(null);
45 const summaryRef = useRef<DOMElement>(null);
46 const controlsRef = useRef<DOMElement>(null);
47 const afterRef = useRef<DOMElement>(null);
48 const [chromeRows, setChromeRows] = useState<number>();
49 const { rows: terminalRows } = useTerminalSize();
50 const showControls =
51 mode === "apply" || (!collapsed && (collapsible || hasFooter));
52
53 useLayoutEffect(() => {
54 if (collapsed || viewport !== "virtual") return;
55 const height = (ref: { readonly current: DOMElement | null }) =>
56 ref.current === null ? 0 : measureElement(ref.current).height;
57 const measured =
58 height(beforeRef) +
59 height(summaryRef) +
60 height(controlsRef) +
61 height(afterRef) +
62 1 +
63 (showControls ? 1 : 0);
64 setChromeRows((current) => (current === measured ? current : measured));
65 });
66
67 const lineBudget =
68 viewport === "virtual"
69 ? Math.max(
70 1,
71 terminalRows - (mode === "apply" ? 3 : 0) - (chromeRows ?? 8),
72 )
73 : Number.POSITIVE_INFINITY;
74
75 return {
76 progress: { completed, failures, total },
77 collapsed,
78 showControls,
79 lineBudget,
80 refs: { beforeRef, summaryRef, controlsRef, afterRef },
81 } as const;
82};