import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../types'; import { IPC_CHANNELS } from '../../shared/ipc/contracts'; import { createFieldGroupingCallbackRuntime, sendToVisibleOverlayRuntime } from './overlay-bridge'; interface WindowLike { isDestroyed: () => boolean; webContents: { send: (channel: string, payload?: unknown) => void; }; } const KIKU_FIELD_GROUPING_MODAL_OPEN_TIMEOUT_MS = 1500; const KIKU_FIELD_GROUPING_MODAL_RETRY_WARNING = 'Kiku field grouping modal did not acknowledge modal open on first attempt; retrying dedicated modal window.'; export interface FieldGroupingOverlayRuntimeOptions { getMainWindow: () => WindowLike | null; getVisibleOverlayVisible: () => boolean; setVisibleOverlayVisible: (visible: boolean) => void; getResolver: () => ((choice: KikuFieldGroupingChoice) => void) | null; setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => void; getRestoreVisibleOverlayOnModalClose: () => Set; waitForModalOpen?: (modal: T, timeoutMs: number) => Promise; handleOverlayModalClosed?: (modal: T) => void; logWarn?: (message: string) => void; /** * Prepare the overlay runtime and (re)create the visible overlay window before opening the * modal — the same prerequisites every other modal runs via `openOverlayHostedModal`. Without * them the field grouping modal can open with no sibling overlay window present, and on * Hyprland it then fails to sit above / take focus over fullscreen mpv the way the other * modals do. */ ensureOverlayStartupPrereqs?: () => void; ensureOverlayWindowsReadyForVisibilityActions?: () => void; sendToVisibleOverlay?: ( channel: string, payload?: unknown, runtimeOptions?: { restoreOnModalClose?: T; preferModalWindow?: boolean }, ) => boolean; } export function createFieldGroupingOverlayRuntime( options: FieldGroupingOverlayRuntimeOptions, ): { sendToVisibleOverlay: ( channel: string, payload?: unknown, runtimeOptions?: { restoreOnModalClose?: T; preferModalWindow?: boolean }, ) => boolean; createFieldGroupingCallback: () => ( data: KikuFieldGroupingRequestData, ) => Promise; } { const sendToVisibleOverlay = ( channel: string, payload?: unknown, runtimeOptions?: { restoreOnModalClose?: T; preferModalWindow?: boolean }, ): boolean => { if (options.sendToVisibleOverlay) { const wasVisible = options.getVisibleOverlayVisible(); const sent = options.sendToVisibleOverlay(channel, payload, runtimeOptions); if (sent && !wasVisible && !options.getVisibleOverlayVisible()) { options.setVisibleOverlayVisible(true); } return sent; } return sendToVisibleOverlayRuntime({ mainWindow: options.getMainWindow() as never, visibleOverlayVisible: options.getVisibleOverlayVisible(), setVisibleOverlayVisible: options.setVisibleOverlayVisible, channel, payload, restoreOnModalClose: runtimeOptions?.restoreOnModalClose, restoreVisibleOverlayOnModalClose: options.getRestoreVisibleOverlayOnModalClose(), }); }; const sendKikuFieldGroupingRequest = async ( data: KikuFieldGroupingRequestData, ): Promise => { const kikuModal = 'kiku' as T; const sendOpen = (): boolean => { // Match every other modal's open path (openOverlayHostedModal): ensure the overlay runtime // and visible overlay window exist before handing off to the dedicated modal window. options.ensureOverlayStartupPrereqs?.(); options.ensureOverlayWindowsReadyForVisibilityActions?.(); return sendToVisibleOverlay('kiku:field-grouping-request', data, { restoreOnModalClose: kikuModal, preferModalWindow: true, }); }; if (!options.waitForModalOpen) { return sendOpen(); } if (!sendOpen()) { return false; } if (await options.waitForModalOpen(kikuModal, KIKU_FIELD_GROUPING_MODAL_OPEN_TIMEOUT_MS)) { return true; } options.logWarn?.(KIKU_FIELD_GROUPING_MODAL_RETRY_WARNING); if (!sendOpen()) { options.handleOverlayModalClosed?.(kikuModal); return false; } const opened = await options.waitForModalOpen( kikuModal, KIKU_FIELD_GROUPING_MODAL_OPEN_TIMEOUT_MS, ); if (!opened) { options.handleOverlayModalClosed?.(kikuModal); } return opened; }; const dismissModalUi = (): void => { const kikuModal = 'kiku' as T; // Best-effort: tell the renderer hosting the modal to close its dialog. When the modal // lives in the dedicated modal window this is redundant with the teardown below, but it // also covers the case where the request was routed into the visible overlay. sendToVisibleOverlay(IPC_CHANNELS.event.kikuFieldGroupingCancel, undefined, { preferModalWindow: true, }); // Reliable teardown of main-side modal state (restore set, main-overlay passthrough, // dedicated modal window). This is what recovers the frozen overlay when a grouping // request times out or fails to reach a visible modal. options.handleOverlayModalClosed?.(kikuModal); }; const createFieldGroupingCallback = (): (( data: KikuFieldGroupingRequestData, ) => Promise) => { return createFieldGroupingCallbackRuntime({ getVisibleOverlayVisible: options.getVisibleOverlayVisible, setVisibleOverlayVisible: options.setVisibleOverlayVisible, getResolver: options.getResolver, setResolver: options.setResolver, sendToVisibleOverlay, sendKikuFieldGroupingRequest, dismissModalUi, }); }; return { sendToVisibleOverlay, createFieldGroupingCallback, }; }