From 0bc76fc95e0feaea2bf874a09623c1b4893140df Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Fri, 8 May 2026 18:59:44 -0400 Subject: [PATCH] feat: wire App + main with persist + hooks Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.tsx | 68 ++++++++++++++++++----------------------------- src/lib/hotkey.ts | 32 ++++++++++++++++++++++ src/main.tsx | 30 +++++++++++++++++---- 3 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 src/lib/hotkey.ts diff --git a/src/App.tsx b/src/App.tsx index 8286a76..a1232ee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,49 +1,33 @@ -import { useState } from "react"; -import reactLogo from "./assets/react.svg"; -import { invoke } from "@tauri-apps/api/core"; -import "./App.css"; +import { useCallback, useEffect, useState } from "react"; +import { ControlPanel } from "./components/ControlPanel"; +import { StatusBar } from "./components/StatusBar"; +import { useLiveOverlayWiring } from "./hooks/useLiveOverlayWiring"; +import { useClickThroughSync } from "./hooks/useClickThroughSync"; +import { useOverlayStore } from "./lib/store"; +import { isOverlayOpen } from "./lib/overlay"; -function App() { - const [greetMsg, setGreetMsg] = useState(""); - const [name, setName] = useState(""); +function App(): React.JSX.Element { + useLiveOverlayWiring(); + const setIsOpen = useOverlayStore((s) => s.setIsOpen); + const [transient, setTransient] = useState(null); - async function greet() { - // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ - setGreetMsg(await invoke("greet", { name })); - } + // Reconcile isOpen with reality on mount (e.g. user closed overlay via taskbar last session). + useEffect(() => { + void isOverlayOpen().then(setIsOpen); + }, [setIsOpen]); + + const handleNoOverlay = useCallback(() => { + setTransient("No overlay to toggle"); + const t = setTimeout(() => setTransient(null), 2500); + return () => clearTimeout(t); + }, []); + + useClickThroughSync(handleNoOverlay); return ( -
-

Welcome to Tauri + React

- -
- - Vite logo - - - Tauri logo - - - React logo - -
-

Click on the Tauri, Vite, and React logos to learn more.

- -
{ - e.preventDefault(); - greet(); - }} - > - setName(e.currentTarget.value)} - placeholder="Enter a name..." - /> - -
-

{greetMsg}

+
+ +
); } diff --git a/src/lib/hotkey.ts b/src/lib/hotkey.ts new file mode 100644 index 0000000..98efb82 --- /dev/null +++ b/src/lib/hotkey.ts @@ -0,0 +1,32 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useOverlayStore } from "./store"; + +type HotkeyStatus = + | { kind: "pending" } + | { kind: "ok" } + | { kind: "failed"; error: string }; + +/** Polls the backend until hotkey registration is decided, then surfaces any error + * via the store. Bounded retry: setup-time decision is essentially synchronous + * on the Rust side, so a missed first call is at worst a few-ms wait. */ +export async function fetchHotkeyStatus(): Promise { + const maxAttempts = 20; + for (let i = 0; i < maxAttempts; i++) { + let status: HotkeyStatus; + try { + status = await invoke("get_hotkey_status"); + } catch (err) { + console.warn("get_hotkey_status invocation failed", err); + return; + } + if (status.kind === "pending") { + await new Promise((r) => setTimeout(r, 50)); + continue; + } + useOverlayStore.getState().setHotkeyError( + status.kind === "failed" ? status.error : null, + ); + return; + } + console.warn("hotkey status remained pending after retries"); +} diff --git a/src/main.tsx b/src/main.tsx index 2be325e..22c74a2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,29 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import "./index.css"; +import { hydrateFromDisk, startPersistSubscription, flushPendingPersistSync } from "./lib/persist"; +import { fetchHotkeyStatus } from "./lib/hotkey"; -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - , -); +async function bootstrap(): Promise { + await hydrateFromDisk(); + startPersistSubscription(); + // Pull the hotkey registration result. Pull-style avoids the + // listener-not-yet-registered race that an event-based approach would have. + void fetchHotkeyStatus(); + + // beforeunload handlers must be synchronous; we use a sync flush helper + // that issues the write. The async save will race with window destruction + // — whichever wins is fine, the write either lands or we retry next session. + window.addEventListener("beforeunload", () => { + flushPendingPersistSync(); + }); + + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + ); +} + +void bootstrap();