import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js" import { useTheme } from "@tui/context/theme" import { Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { Clipboard } from "@tui/util/clipboard" import { useToast } from "./toast" export function Dialog( props: ParentProps<{ size?: "medium" | "large" onClose: () => void }>, ) { const dimensions = useTerminalDimensions() const { theme } = useTheme() const renderer = useRenderer() return ( { if (renderer.getSelection()) return props.onClose?.() }} width={dimensions().width} height={dimensions().height} alignItems="center" position="absolute" paddingTop={dimensions().height / 4} left={0} top={0} backgroundColor={RGBA.fromInts(0, 0, 0, 150)} > { if (renderer.getSelection()) return e.stopPropagation() }} width={props.size === "large" ? 80 : 60} maxWidth={dimensions().width - 2} backgroundColor={theme.backgroundPanel} paddingTop={1} > {props.children} ) } function init() { const [store, setStore] = createStore({ stack: [] as { element: JSX.Element onClose?: () => void }[], size: "medium" as "medium" | "large", }) useKeyboard((evt) => { if (evt.name === "escape" && store.stack.length > 0) { const current = store.stack.at(-1)! current.onClose?.() setStore("stack", store.stack.slice(0, -1)) evt.preventDefault() evt.stopPropagation() refocus() } }) const renderer = useRenderer() let focus: Renderable | null function refocus() { setTimeout(() => { if (!focus) return if (focus.isDestroyed) return function find(item: Renderable) { for (const child of item.getChildren()) { if (child === focus) return true if (find(child)) return true } return false } const found = find(renderer.root) if (!found) return focus.focus() }, 1) } return { clear() { for (const item of store.stack) { if (item.onClose) item.onClose() } batch(() => { setStore("size", "medium") setStore("stack", []) }) refocus() }, replace(input: any, onClose?: () => void) { if (store.stack.length === 0) { focus = renderer.currentFocusedRenderable focus?.blur() } for (const item of store.stack) { if (item.onClose) item.onClose() } setStore("size", "medium") setStore("stack", [ { element: input, onClose, }, ]) }, get stack() { return store.stack }, get size() { return store.size }, setSize(size: "medium" | "large") { setStore("size", size) }, } } export type DialogContext = ReturnType const ctx = createContext() export function DialogProvider(props: ParentProps) { const value = init() const renderer = useRenderer() const toast = useToast() return ( {props.children} { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { const base64 = Buffer.from(text).toString("base64") const osc52 = `\x1b]52;c;${base64}\x07` const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 /* @ts-expect-error */ renderer.writeOut(finalOsc52) await Clipboard.copy(text) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) renderer.clearSelection() } }} > value.clear()} size={value.size}> {value.stack.at(-1)!.element} ) } export function useDialog() { const value = useContext(ctx) if (!value) { throw new Error("useDialog must be used within a DialogProvider") } return value }