Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions src/main/windows.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { app, BrowserWindow, Display, screen } from 'electron';
import path from 'path';
import type { StreamWindowBounds } from '../shared/types';
import { DISPLAY_INFO_ARG_PREFIX, type DisplayInfo, type StreamWindowBounds } from '../shared/types';

const isDev = !app.isPackaged && process.env.VITE_DEV_SERVER === '1';

Expand Down Expand Up @@ -51,9 +51,18 @@ export function createPanelWindow(): BrowserWindow {
return win;
}

function toDisplayInfo(display: Display): DisplayInfo {
return {
id: display.id,
bounds: display.bounds,
scaleFactor: display.scaleFactor,
};
}

/** A transparent, click-through overlay covering one display. */
export function createOverlayWindow(display: Display): BrowserWindow {
const { x, y, width, height } = display.bounds;
const displayInfo = toDisplayInfo(display);

const win = new BrowserWindow({
x,
Expand All @@ -77,6 +86,15 @@ export function createOverlayWindow(display: Display): BrowserWindow {
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
// Hand the renderer its coordinate space up front. The IPC push
// below can land before React has attached its listener, so the
// overlay needs a value it can read synchronously on mount.
//
// Safe as plain JSON only because every DisplayInfo field is
// numeric, so the serialized value can never contain a space.
// Windows splits additionalArguments on spaces — if this type ever
// grows a string field (a display label, say), base64 it first.
additionalArguments: [DISPLAY_INFO_ARG_PREFIX + JSON.stringify(displayInfo)],
},
});

Expand All @@ -91,13 +109,13 @@ export function createOverlayWindow(display: Display): BrowserWindow {

loadPage(win, 'overlay');

// Pass display info to overlay so it knows its coordinate space
win.webContents.once('did-finish-load', () => {
win.webContents.send('display-info', {
id: display.id,
bounds: display.bounds,
scaleFactor: display.scaleFactor,
});
// Belt-and-braces: the preload already reads this same snapshot out of
// argv on every load, including reloads, so this send is redundant
// rather than an update path. It stays as a cheap safety net in case
// the argv read ever fails. Bounds changes do NOT arrive here — the
// window is destroyed and recreated by rebuildOverlays instead.
win.webContents.on('did-finish-load', () => {
win.webContents.send('display-info', displayInfo);
});

return win;
Expand Down
44 changes: 28 additions & 16 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron';
import { IPC } from '../shared/types';
import { IPC, DISPLAY_INFO_ARG_PREFIX } from '../shared/types';
import type {
ApiKeyName,
ClaudeModel,
Expand All @@ -19,9 +19,28 @@ import type {
LocalConnection,
OllamaModelInfo,
OllamaPullProgress,
DisplayInfo,
} from '../shared/types';
import type { OllamaTestResult } from '../main/services/ollama-api';

/**
* Display info handed to overlay windows via `additionalArguments`.
* Read once at preload time so the renderer never has to wait for IPC
* to learn its coordinate space.
*/
const initialDisplayInfo: DisplayInfo | null = (() => {
const arg = process.argv.find((a) => a.startsWith(DISPLAY_INFO_ARG_PREFIX));
if (!arg) return null;
try {
return JSON.parse(arg.slice(DISPLAY_INFO_ARG_PREFIX.length)) as DisplayInfo;
} catch (err) {
// Falling back to null silently would reproduce the exact bug this
// argument exists to fix, so make the failure visible.
console.warn('[Flicky] Could not parse display info from launch args:', err);
return null;
}
})();

const api = {
// ── Settings ───────────────────────────────────────────────────────
getSettings: (): Promise<FlickySettings> => ipcRenderer.invoke(IPC.GET_SETTINGS),
Expand Down Expand Up @@ -188,21 +207,14 @@ const api = {

// ── Audio Capture (overlay ↔ main) ──────────────────────────────────
// ── Overlay / display info ────────────────────────────────────────
onDisplayInfo: (
cb: (info: {
id: number;
bounds: { x: number; y: number; width: number; height: number };
scaleFactor: number;
}) => void,
) => {
const handler = (
_e: Electron.IpcRendererEvent,
info: {
id: number;
bounds: { x: number; y: number; width: number; height: number };
scaleFactor: number;
},
) => cb(info);
/**
* The overlay's display, available synchronously on first paint.
* Null in windows that aren't overlays.
*/
getDisplayInfo: (): DisplayInfo | null => initialDisplayInfo,

onDisplayInfo: (cb: (info: DisplayInfo) => void) => {
const handler = (_e: Electron.IpcRendererEvent, info: DisplayInfo) => cb(info);
ipcRenderer.on('display-info', handler);
return () => ipcRenderer.removeListener('display-info', handler);
},
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/components/OverlayApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import type { VoiceState, DetectedElement } from '../../shared/types';
import type { VoiceState, DetectedElement, DisplayInfo } from '../../shared/types';
import { Waveform } from './Waveform';

// Offset the companion cursor ~1/5 inch (≈19px at 96dpi) down-right
Expand Down Expand Up @@ -32,7 +32,9 @@ export function OverlayApp() {
const [cursorMode, setCursorMode] = useState<CursorMode>('following');
const [companionPos, setCompanionPos] = useState({ x: 0, y: 0 });
const [isCursorOnThisDisplay, setIsCursorOnThisDisplay] = useState(false);
const displayRef = useRef<{ id: number; bounds: { x: number; y: number; width: number; height: number } } | null>(null);
// Seeded synchronously from the window's launch arguments so the first
// cursor-position message already has a coordinate space to map into.
const displayRef = useRef<DisplayInfo | null>(window.flicky.getDisplayInfo());
const holdTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const returnAnimRef = useRef<number | null>(null);
const cursorPosRef = useRef({ x: 0, y: 0 });
Expand Down Expand Up @@ -195,7 +197,7 @@ export function OverlayApp() {

useEffect(() => {
const unsubDisplayInfo = window.flicky.onDisplayInfo((info) => {
displayRef.current = { id: info.id, bounds: info.bounds };
displayRef.current = info;
});

const unsubs = [
Expand Down
15 changes: 15 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ export interface TranscriptionResult {
isFinal: boolean;
}

// ── Overlay / Displays ─────────────────────────────────────────────────

export interface DisplayInfo {
id: number;
bounds: { x: number; y: number; width: number; height: number };
scaleFactor: number;
}

/**
* Prefix used to hand an overlay window its display info through
* `webPreferences.additionalArguments`, so the renderer can read it
* synchronously at startup instead of racing an IPC message.
*/
export const DISPLAY_INFO_ARG_PREFIX = '--flicky-display-info=';

// ── Screen Capture ─────────────────────────────────────────────────────

export interface ScreenCapture {
Expand Down
Loading