feat: wire App + main with persist + hooks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-05-08 18:59:44 -04:00
parent 4dc5604566
commit 0bc76fc95e
3 changed files with 83 additions and 47 deletions

View File

@@ -1,49 +1,33 @@
import { useState } from "react"; import { useCallback, useEffect, useState } from "react";
import reactLogo from "./assets/react.svg"; import { ControlPanel } from "./components/ControlPanel";
import { invoke } from "@tauri-apps/api/core"; import { StatusBar } from "./components/StatusBar";
import "./App.css"; import { useLiveOverlayWiring } from "./hooks/useLiveOverlayWiring";
import { useClickThroughSync } from "./hooks/useClickThroughSync";
import { useOverlayStore } from "./lib/store";
import { isOverlayOpen } from "./lib/overlay";
function App() { function App(): React.JSX.Element {
const [greetMsg, setGreetMsg] = useState(""); useLiveOverlayWiring();
const [name, setName] = useState(""); const setIsOpen = useOverlayStore((s) => s.setIsOpen);
const [transient, setTransient] = useState<string | null>(null);
async function greet() { // Reconcile isOpen with reality on mount (e.g. user closed overlay via taskbar last session).
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ useEffect(() => {
setGreetMsg(await invoke("greet", { name })); 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 ( return (
<main className="container"> <main className="flex h-screen flex-col bg-[var(--color-bg)]">
<h1>Welcome to Tauri + React</h1> <ControlPanel />
<StatusBar transient={transient} />
<div className="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
<form
className="row"
onSubmit={(e) => {
e.preventDefault();
greet();
}}
>
<input
id="greet-input"
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Enter a name..."
/>
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
</main> </main>
); );
} }

32
src/lib/hotkey.ts Normal file
View File

@@ -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<void> {
const maxAttempts = 20;
for (let i = 0; i < maxAttempts; i++) {
let status: HotkeyStatus;
try {
status = await invoke<HotkeyStatus>("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");
}

View File

@@ -1,9 +1,29 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App"; 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<void> {
<React.StrictMode> await hydrateFromDisk();
<App /> startPersistSubscription();
</React.StrictMode>, // 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(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
void bootstrap();