import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun' import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js' import { applyMacOSWindowEffects } from './macos-window-effects.js' import { sendToRenderer, setMessageSender } from './notify.js' const isMacOS = process.platform === 'darwin' const DEV_SERVER_PORT = 5780 const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}` const PROTOCOL_PREFIX = 'farmcontrol://' let mainWindow = null let webviewDomReady = false const pendingNavigations = [] export function getMainWindow() { return mainWindow } export async function getMainViewUrl() { const channel = await Updater.localInfo.channel() if (channel === 'dev' || process.env.NODE_ENV === 'development') { try { await fetch(DEV_SERVER_URL, { method: 'HEAD' }) console.log(`Using Vite dev server at ${DEV_SERVER_URL}`) return DEV_SERVER_URL } catch { console.warn( 'Vite dev server not running. Start it with `bun run dev:renderer`.' ) } } return 'views://mainview/index.html' } function deliverNavigation(redirectPath) { sendToRenderer('navigate', redirectPath) mainWindow?.show?.() mainWindow?.activate?.() } function flushPendingNavigations() { if (!mainWindow || !webviewDomReady) { return } while (pendingNavigations.length > 0) { const redirectPath = pendingNavigations.shift() setTimeout(() => deliverNavigation(redirectPath), 100) } } function sendNavigateToRenderer(redirectPath) { if (!redirectPath || typeof redirectPath !== 'string') { return } if (!mainWindow || !webviewDomReady) { pendingNavigations.push(redirectPath) return } setTimeout(() => deliverNavigation(redirectPath), 100) } export function handleDeepLink(url) { if (!url?.startsWith(`${PROTOCOL_PREFIX}app`)) return const redirectPath = url.replace(`${PROTOCOL_PREFIX}app`, '') || '/' sendNavigateToRenderer(redirectPath) } function findProtocolUrl(args) { return args.find( (arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX) ) } export function handleDeepLinkFromArgv() { if (process.platform === 'darwin') return const url = findProtocolUrl(process.argv) if (url) handleDeepLink(url) } function broadcastWindowState() { sendToRenderer('windowState', getWindowState()) } function setupWindowEvents(window) { // Electrobun emits resize/focus, not Electron's maximize/fullscreen events. window.on?.('resize', broadcastWindowState) window.on?.('focus', broadcastWindowState) } export function setupMainWindowMessaging(window = mainWindow) { if (!window) { return } setMessageSender((channel, data) => { try { const webview = window.webview const channelLiteral = JSON.stringify(channel) const payloadLiteral = JSON.stringify(data ?? null) // Prefer direct JS dispatch — WebSocket RPC pushes can be deferred by // WKWebView until the next interaction during native window transitions. if (webview?.executeJavascript) { webview.executeJavascript( `window.__farmcontrolDispatchRpcMessage?.(${channelLiteral}, ${payloadLiteral})` ) return true } const send = webview?.rpc?.send if (!send) { console.warn( `No RPC sender available for channel: ${channel}. Is the window ready?` ) return false } send[channel](data) return true } catch (error) { console.warn(`Failed to send RPC message on channel: ${channel}`, error) return false } }) } export async function createMainWindow(rpc) { const url = await getMainViewUrl() mainWindow = new BrowserWindow({ title: 'Farm Control', url, rpc, titleBarStyle: 'hiddenInset', ...(isMacOS ? { transparent: true } : {}), frame: { width: 1200, height: 800, x: 100, y: 100 } }) if (isMacOS) { applyMacOSWindowEffects(mainWindow) } setupMainWindowMessaging(mainWindow) applyApplicationMenu() setupApplicationMenuEvents({ onNavigate: sendNavigateToRenderer, onToggleDevTools: () => { mainWindow?.webview?.toggleDevTools?.() } }) setupWindowEvents(mainWindow) Electrobun.events.on('open-url', (event) => { const url = event?.data?.url if (url) { handleDeepLink(url) } }) return new Promise((resolve) => { mainWindow.webview.on('dom-ready', () => { webviewDomReady = true flushPendingNavigations() resolve(mainWindow) }) }) } export async function setupDevAuthServer() { const env = (process.env.NODE_ENV || 'development').trim() if (env !== 'development') return const express = (await import('express')).default const app = express() const port = 3500 app.use((req, res) => { const redirectPath = req.originalUrl res.send(`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`) sendNavigateToRenderer(redirectPath) }) app.listen(port, () => {}) } export function openInternalUrl(url) { sendNavigateToRenderer(url) return true } export function getWindowState() { if (!mainWindow) { return { isFullScreen: false, isMaximized: false } } return { isFullScreen: mainWindow.isFullScreen?.() ?? false, isMaximized: mainWindow.isMaximized?.() ?? false } } export function handleWindowControl(action) { if (!mainWindow) return switch (action) { case 'minimize': mainWindow.minimize?.() break case 'maximize': if (mainWindow.isMaximized?.()) { mainWindow.unmaximize?.() } else { mainWindow.maximize?.() } break case 'close': mainWindow.close?.() break default: break } } export function sendNavigationGesture(direction) { sendToRenderer('navigationGesture', direction) } export function setupNavigationGestures(window) { if (!window) return if (process.platform === 'darwin') { window.on?.('swipe', (_event, direction) => { if (direction === 'left') { sendNavigationGesture('back') } else if (direction === 'right') { sendNavigationGesture('forward') } }) } window.on?.('app-command', (_event, command) => { if (command === 'browser-backward') { sendNavigationGesture('back') } else if (command === 'browser-forward') { sendNavigationGesture('forward') } }) } export function openExternalUrl(url) { console.log('openExternalUrl', url) Utils.openExternal(url) }