UNPKG

@dugongjs/cli

Version:

129 lines (128 loc) 5.69 kB
// hooks/use-log-overlay.tsx import { Box, measureElement, Text, useInput } from "ink"; import React from "react"; import { useStdoutDimensions } from "./use-stdout-dimensions.js"; const levelColor = { debug: "white", info: "blueBright", warn: "yellow", error: "redBright" }; function fmtArg(a) { if (a instanceof Error) return a.stack || a.message; if (typeof a === "string") return a; try { return JSON.stringify(a, null, 2); } catch { return String(a); } } function ts(d = new Date()) { const h = String(d.getHours()).padStart(2, "0"); const m = String(d.getMinutes()).padStart(2, "0"); const s = String(d.getSeconds()).padStart(2, "0"); return `${h}:${m}:${s}`; } export function useLogOverlay() { const [open, setOpen] = React.useState(false); const [logs, setLogs] = React.useState([]); const [cols, rows] = useStdoutDimensions(); const containerRef = React.useRef(null); const [visibleLines, setVisibleLines] = React.useState(10); const [contentWidth, setContentWidth] = React.useState(60); const [scrollOffset, setScrollOffset] = React.useState(0); const [stickBottom, setStickBottom] = React.useState(true); useInput((input, key) => { if (!open) { if (key.ctrl && input.toLowerCase() === "l") setOpen(true); return; } if (key.escape || input.toLowerCase() === "q") { setOpen(false); return; } if (key.upArrow || input === "k") { setScrollOffset((v) => Math.max(0, v - 1)); setStickBottom(false); } else if (key.downArrow || input === "j") { setScrollOffset((v) => v + 1); } else if (input === "u") { setScrollOffset((v) => Math.max(0, v - Math.max(1, Math.floor(visibleLines / 2)))); setStickBottom(false); } else if (input === "d") { setScrollOffset((v) => v + Math.max(1, Math.floor(visibleLines / 2))); } else if (input === "g" && !key.shift) { setScrollOffset(0); setStickBottom(false); } else if (input === "G" || (input === "g" && key.shift)) { setStickBottom(true); } }, { isActive: true }); const log = React.useCallback((level, ...args) => { const msg = args.map(fmtArg).join(" "); setLogs((prev) => { const next = prev.length >= 800 ? prev.slice(-799) : prev.slice(); next.push({ level, message: msg, ts: Date.now() }); return next; }); }, []); const debug = React.useCallback((...args) => log("debug", ...args), [log]); const info = React.useCallback((...args) => log("info", ...args), [log]); const warn = React.useCallback((...args) => log("warn", ...args), [log]); const error = React.useCallback((...args) => log("error", ...args), [log]); React.useEffect(() => { if (!containerRef.current) return; const { width, height } = measureElement(containerRef.current); setContentWidth(Math.max(40, Math.min(width - 2, 120))); setVisibleLines(Math.max(5, height - 3)); }, [cols, rows, open]); const rendered = React.useMemo(() => { const lines = []; for (let i = 0; i < logs.length; i++) { const entry = logs[i]; const prefix = `[${ts(new Date(entry.ts))}] [${entry.level.toUpperCase()}] `; const width = Math.max(10, contentWidth); const avail = Math.max(1, width - prefix.length); const chunks = String(entry.message).split("\n"); for (let j = 0; j < chunks.length; j++) { const raw = chunks[j]; if (raw.length <= avail) { lines.push({ level: entry.level, text: prefix + raw }); } else { for (let k = 0; k < raw.length; k += avail) { const part = raw.slice(k, k + avail); const pad = k === 0 ? prefix : " ".repeat(prefix.length); lines.push({ level: entry.level, text: pad + part }); } } } } return lines; }, [logs, contentWidth]); React.useEffect(() => { if (!stickBottom) return; const overflow = Math.max(0, rendered.length - visibleLines); setScrollOffset(overflow); }, [rendered.length, visibleLines, stickBottom]); const start = Math.max(0, Math.min(rendered.length, scrollOffset)); const end = Math.min(rendered.length, start + visibleLines); const page = rendered.slice(start, end); const width = Math.max(10, contentWidth); const Overlay = open ? (React.createElement(Box, { position: "absolute", width: "100%", height: "100%", alignItems: "center", justifyContent: "center" }, React.createElement(Box, { ref: containerRef, borderStyle: "round", borderColor: "yellow", backgroundColor: "black", width: Math.min(Math.max(50, Math.floor(cols * 0.8)), 120), height: Math.min(Math.max(12, Math.floor(rows * 0.8)), 40), flexDirection: "column" }, React.createElement(Text, { inverse: true }, ` LOGS ↑/↓ j/k u/d g/G q/esc `.padEnd(width, " ")), page.length === 0 ? (React.createElement(Text, null, " ".repeat(width))) : (page.map((l, i) => (React.createElement(Text, { key: i, color: levelColor[l.level] }, l.text.length > width ? l.text.slice(0, width) : l.text.padEnd(width, " ")))))))) : null; return { log, debug, info, warn, error, Overlay, isOpen: open }; }