How OpenWhispr Implements URL-Based Routing for Dual-Window Electron Components
OpenWhispr distinguishes its overlay and control-panel windows by injecting a window query parameter into the load URL, allowing a single React bundle to conditionally render entirely different component trees.
OpenWhispr is an open-source Electron application that manages two distinct UI windows—a minimal dictation overlay and a full-featured control panel—from one shared React codebase. By implementing URL-based routing through query-string flags, the application eliminates redundant builds while ensuring both windows share global state and core utilities.
Electron Main Process Configuration in src/main.js
The Electron main process creates both windows in src/main.js, pointing each at the same bundled index.html while appending a unique query string to differentiate their purpose. This approach relies on standard BrowserWindow.loadURL calls with injected parameters.
When creating the overlay window (a frameless, always-on-top dictation interface), the main process loads:
// src/main.js – overlay window creation
const overlay = new BrowserWindow({
width: 360,
height: 80,
frame: false,
alwaysOnTop: true,
transparent: true,
webPreferences: {
preload: path.join(__dirname, "preload.js")
},
});
overlay.loadURL(`app://./index.html#/?window=overlay`);
For the control-panel window (the full settings and history interface), the process loads the same file with a different flag:
// src/main.js – control-panel window creation
const panel = new BrowserWindow({
width: 1024,
height: 768,
show: false,
webPreferences: {
preload: path.join(__dirname, "preload.js")
},
});
panel.loadURL(`app://./index.html#/?window=panel`);
panel.once("ready-to-show", () => panel.show());
By varying only the window query parameter, the main process avoids conditional logic for asset loading and ensures both instances initialize with identical preload scripts and security contexts.
React Router Implementation in src/App.jsx
The renderer process handles URL-based routing inside src/App.jsx by reading the query string at runtime. The component wraps the application in a BrowserRouter from react-router-dom, then parses the window parameter to determine which root component to mount.
Parsing the Window Query Parameter
The router extracts the flag using the useLocation hook and URLSearchParams:
// src/App.jsx
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import OverlayWindow from "./components/OverlayWindow";
import ControlPanel from "./components/ControlPanel";
function App() {
const { search } = useLocation();
const params = new URLSearchParams(search);
const win = params.get("window"); // "overlay" or "panel"
return (
<Routes>
{win === "overlay" && (
<Route path="*" element={<OverlayWindow />} />
)}
{win === "panel" && (
<Route path="*" element={<ControlPanel />} />
)}
</Routes>
);
}
Conditional Route Rendering
The routing logic does not rely on URL path matching; instead, it evaluates the window variable to render the appropriate component tree. This architecture means:
- Single entry point: Both windows execute the same
App.jsxinitialization code. - Shared providers: Redux, Zustand, or React Context providers wrap both routes, enabling instantaneous state synchronization between windows.
- Wildcard paths: Each route uses
path="*"to capture all navigation within that window's scope.
Window-Specific Component Architecture
The URL-based routing strategy delegates all UI differentiation to the component layer, keeping the main process lean.
The Overlay Window (src/components/OverlayWindow.jsx)
When the window parameter equals "overlay", src/App.jsx renders OverlayWindow.jsx. This component presents a minimal chrome-less interface containing the dictation pill, microphone status indicator, and a compact toolbar. The component assumes a small viewport (360×80 pixels) and handles transparent background rendering for seamless desktop integration.
The Control Panel (src/components/ControlPanel.tsx)
When the parameter equals "panel", the router mounts ControlPanel.tsx from src/components/ControlPanel.tsx. This component provides the full-screen settings interface, including model selection, transcription history, and configuration screens. It manages larger layouts (1024×768 pixels) and complex navigation stacks within its own window instance.
Why URL-Based Routing?
OpenWhispr chose URL-based routing over separate HTML entry points or conditional module loading for three critical reasons:
- Single JavaScript bundle: Both windows load identical assets, reducing build size and ensuring shared utilities (hot-key managers, API clients) are parsed only once.
- Automatic state sharing: Because both windows run the same renderer process context, global stores (Redux or Zustand) remain synchronized without IPC overhead—settings changed in the panel instantly reflect in the overlay.
- Simplified window creation: The Electron side only modifies the load URL; no complex branching logic or multiple preload scripts are required to support distinct window types.
Summary
- OpenWhispr creates dual windows in
src/main.jsby callingloadURL()with?window=overlayor?window=panelquery strings. - The React layer in
src/App.jsxparses the query parameter viaURLSearchParamsand conditionally renders eitherOverlayWindoworControlPanel. - Both windows share a single codebase and global state, eliminating build redundancy while maintaining distinct UI architectures.
- This URL-based routing approach scales cleanly because the main process only needs to append new query flags to support additional window types.
Frequently Asked Questions
How does OpenWhispr differentiate between its two windows?
The application appends a window query parameter to the load URL in the Electron main process. The overlay loads app://./index.html#/?window=overlay, while the control panel loads app://./index.html#/?window=panel. The React router in src/App.jsx reads this parameter to determine which component tree to render.
Why use URL query parameters instead of separate HTML files?
Using query parameters allows both windows to share a single JavaScript bundle and asset set, significantly reducing application size. It also ensures that global state stores (Redux or Zustand) are automatically shared between windows since they run the same renderer context, eliminating the need for complex IPC synchronization.
Can both windows access the same global state?
Yes. Because URL-based routing routes both windows through the same React entry point and bundle, they share the same JavaScript execution context. Global state providers wrap both the OverlayWindow and ControlPanel components, allowing real-time state updates across windows without additional messaging overhead.
Is this routing approach scalable for additional window types?
Absolutely. To add a third window type (for example, a history viewer), the main process would simply call loadURL() with a new query parameter like ?window=history, and src/App.jsx would add a corresponding conditional route to render the new component. No changes to the build configuration or webpack entry points are necessary.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →