Started electrobun migration.
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

This commit is contained in:
Tom Butcher 2026-03-01 02:56:25 +00:00
parent 8f2cc49f9b
commit 550f0eca06
20 changed files with 8377 additions and 198 deletions

2906
bun.lock Normal file

File diff suppressed because it is too large Load Diff

4805
dist-test/index.js Normal file

File diff suppressed because it is too large Load Diff

31
electrobun.config.ts Normal file
View File

@ -0,0 +1,31 @@
import type { ElectrobunConfig } from 'electrobun'
import { readFileSync } from 'fs'
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'))
export default {
app: {
name: 'Farm Control',
identifier: 'com.tombutcher.farmcontrol',
version: packageJson.version,
urlSchemes: ['farmcontrol']
},
runtime: {
exitOnLastWindowClosed: true
},
build: {
bun: {
entrypoint: 'src/bun/index.ts'
},
views: {
preload: {
entrypoint: 'src/preload/index.ts'
}
},
copy: {
'build/index.html': 'views/main/index.html',
'build/assets': 'views/main/assets'
},
watch: ['build']
}
} satisfies ElectrobunConfig

View File

@ -41,7 +41,6 @@
"dotenv": "^17.2.1",
"gcode-preview": "^2.18.0",
"keycloak-js": "^26.2.0",
"keytar": "^7.9.0",
"lodash": "^4.17.23",
"loglevel": "^1.9.2",
"online-3d-viewer": "^0.16.0",
@ -63,15 +62,15 @@
"tsparticles": "^3.9.1",
"web-vitals": "^5.1.0"
},
"main": "build/electron.js",
"main": "src/bun/index.ts",
"description": "3D Printer ERP and Control Software.",
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .",
"electrobun": "cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev",
"start": "serve -s build",
"build": "vite build",
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 cross-env NODE_ENV=development electron public/electron.js\"",
"build:electron": "vite build && electron-builder",
"dev:electrobun": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTROBUN_START_URL=http://localhost:5173 electrobun dev\"",
"build:electrobun": "vite build && electrobun build",
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main"
},
@ -97,9 +96,8 @@
"@eslint/js": "^9.39.2",
"@vitejs/plugin-react": "^5.0.2",
"concurrently": "^9.2.1",
"electron": "^38.7.1",
"electron-builder": "^26.0.12",
"electron-packager": "^17.1.2",
"electrobun": "^1.13.1",
"typescript": "^5.0.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",

View File

@ -25,7 +25,7 @@ import {
import AppError from './components/App/AppError'
import { ApiServerProvider } from './components/Dashboard/context/ApiServerContext.jsx'
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
import { ElectrobunProvider } from './components/Dashboard/context/ElectrobunContext.jsx'
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx'
@ -56,7 +56,7 @@ const AppContent = () => {
<ConfigProvider theme={themeConfig}>
<App>
<Router>
<ElectronProvider>
<ElectrobunProvider>
<AuthProvider>
<PrintServerProvider>
<ApiServerProvider>
@ -122,7 +122,7 @@ const AppContent = () => {
</ApiServerProvider>
</PrintServerProvider>
</AuthProvider>
</ElectronProvider>
</ElectrobunProvider>
</Router>
</App>
</ConfigProvider>

157
src/bun/index.ts Normal file
View File

@ -0,0 +1,157 @@
import Electrobun, {
BrowserWindow,
BrowserView,
Utils,
GlobalShortcut,
ApplicationMenu
} from 'electrobun/bun'
import { join } from 'path'
import { type FarmControlRPCType } from '../shared/rpcTypes'
import { createMainWindow, getMainWindow } from './mainWindow'
import {
openSpotlightContentWindow,
registerGlobalShortcuts,
setupSpotlightRPC
} from './spotlightWindow'
// Auth session storage (file-based, keytar alternative for Bun)
const AUTH_FILE = join(Utils.paths.userData, 'auth-session.json')
async function readAuthSession(): Promise<object | null> {
try {
const f = Bun.file(AUTH_FILE)
if (!(await f.exists())) return null
const raw = await f.text()
return JSON.parse(raw) as object
} catch {
return null
}
}
async function writeAuthSession(session: object): Promise<boolean> {
try {
await Bun.write(AUTH_FILE, JSON.stringify(session))
return true
} catch (e) {
console.warn('[auth] Failed to write session:', e)
return false
}
}
async function clearAuthSession(): Promise<boolean> {
try {
const f = Bun.file(AUTH_FILE)
if (await f.exists()) {
await Bun.write(AUTH_FILE, '{}')
}
return true
} catch (e) {
console.warn('[auth] Failed to clear session:', e)
return false
}
}
const farmControlRPC = BrowserView.defineRPC<FarmControlRPCType>({
maxRequestTime: 5000,
handlers: {
requests: {
osInfo: () => ({ platform: process.platform }),
windowState: () => {
const win = getMainWindow()
if (!win || win.isDestroyed?.()) {
return { isFullScreen: false, isMaximized: false }
}
return {
isFullScreen: win.isFullScreen?.(),
isMaximized: win.isMaximized?.()
}
},
openExternalUrl: ({ url }) => {
Utils.openExternal(url)
},
openInternalUrl: ({ url }) => {
const win = getMainWindow()
if (!win || win.isDestroyed?.()) {
createMainWindow(farmControlRPC)
const newWin = getMainWindow()
if (newWin?.webview) {
newWin.webview.on?.('dom-ready', () => {
;(newWin.webview as any).rpc?.navigate?.({ url })
newWin.show?.()
newWin.focus?.()
})
}
} else if (win.webview) {
;(win.webview as any).rpc?.navigate?.({ url })
win.show?.()
win.focus?.()
}
return true
},
windowControl: ({ action }) => {
const win = getMainWindow()
if (!win) return
switch (action) {
case 'minimize':
win.minimize?.()
break
case 'maximize':
if (win.isMaximized?.()) {
win.unmaximize?.()
} else {
win.maximize?.()
}
break
case 'close':
win.close?.()
break
}
},
authSessionGet: () => readAuthSession(),
authSessionSet: ({ session }) => writeAuthSession(session),
authSessionClear: () => clearAuthSession(),
spotlightWindowResize: ({ height }) => setupSpotlightRPC(height)
},
messages: {}
}
})
createMainWindow(farmControlRPC)
registerGlobalShortcuts()
// Application menu
const env = (process.env.NODE_ENV || 'development').trim()
if (env === 'development') {
ApplicationMenu.setApplicationMenu([
{
label: 'Developer',
submenu: [
{
label: 'Toggle Developer Tools',
accelerator:
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
action: 'toggle-devtools'
}
]
}
])
} else {
ApplicationMenu.setApplicationMenu([])
}
// open-url for farmcontrol:// scheme
Electrobun.events.on('open-url', (e: { data: { url: string } }) => {
const url = e.data.url
if (url.startsWith('farmcontrol://app')) {
const redirectPath = url.replace('farmcontrol://app', '') || '/'
const win = getMainWindow()
if (win?.webview) {
;(win.webview as any).rpc?.navigate?.({ url: redirectPath })
}
}
})
// Unregister shortcuts on quit
Electrobun.events.on('before-quit', () => {
GlobalShortcut.unregisterAll()
})

90
src/bun/mainWindow.ts Normal file
View File

@ -0,0 +1,90 @@
import Electrobun, { BrowserWindow, Utils } from 'electrobun/bun'
import { join } from 'path'
import type { FarmControlRPCType } from '../shared/rpcTypes'
let win: InstanceType<typeof BrowserWindow> | null = null
function getAppUrl(): string {
if (process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL) {
return (
process.env.ELECTROBUN_START_URL ||
process.env.ELECTRON_START_URL ||
'http://localhost:5173'
)
}
return 'views://main/index.html'
}
function setupWindowEvents(
browserWindow: InstanceType<typeof BrowserWindow>,
rpc: any
): void {
if (!browserWindow) return
browserWindow.on?.('resize', () => {
const webview = browserWindow.webview
if (webview?.rpc) {
try {
;(webview.rpc as any).windowState?.({
isMaximized: browserWindow.isMaximized?.(),
isFullScreen: browserWindow.isFullScreen?.()
})
} catch (_) {}
}
})
// Electrobun may use different event names - check docs
const sendWindowState = (state: { isMaximized?: boolean; isFullScreen?: boolean }) => {
const webview = browserWindow.webview
if (webview?.rpc) {
try {
;(webview.rpc as any).windowState?.(state)
} catch (_) {}
}
}
// Listen for maximize/unmaximize/fullscreen - Electrobun BrowserWindow events
browserWindow.on?.('maximize', () => sendWindowState({ isMaximized: true }))
browserWindow.on?.('unmaximize', () => sendWindowState({ isMaximized: false }))
browserWindow.on?.('enter-full-screen', () => sendWindowState({ isFullScreen: true }))
browserWindow.on?.('leave-full-screen', () => sendWindowState({ isFullScreen: false }))
}
export function createMainWindow(rpc: any): void {
const url = getAppUrl()
win = new BrowserWindow({
title: 'Farm Control',
url,
frame: {
width: 1200,
height: 800
},
titleBarStyle: 'hiddenInset',
preload: 'views://preload/index.js',
rpc,
styleMask: {
Titled: true,
Closable: true,
Miniaturizable: true,
Resizable: true,
FullSizeContentView: true
}
})
setupWindowEvents(win, rpc)
// Handle window-all-closed
Electrobun.events.on('close', () => {
const wins = (BrowserWindow as any).getAll?.() ?? []
if (wins.length <= 1) {
if (process.platform !== 'darwin') {
Utils.quit?.()
}
}
})
}
export function getMainWindow(): InstanceType<typeof BrowserWindow> | null {
return win
}

View File

@ -0,0 +1,80 @@
import { BrowserWindow, GlobalShortcut } from 'electrobun/bun'
import { join } from 'path'
let spotlightWin: InstanceType<typeof BrowserWindow> | null = null
function getSpotlightRouteUrl(): string {
const routePath = '/dashboard/electron/spotlightcontent'
if (process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL) {
const base = String(
process.env.ELECTROBUN_START_URL || process.env.ELECTRON_START_URL
).replace(/\/$/, '')
return `${base}${routePath}`
}
return `views://main/index.html#${routePath}`
}
export function openSpotlightContentWindow(): void {
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
spotlightWin.show?.()
spotlightWin.focus?.()
return
}
const target = getSpotlightRouteUrl()
spotlightWin = new BrowserWindow({
title: 'Spotlight',
url: target,
frame: {
width: 700,
height: 40
},
titleBarStyle: 'hidden',
transparent: true,
resizable: false
})
spotlightWin.on?.('close', (e: any) => {
if (e?.preventDefault) e.preventDefault()
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
spotlightWin.hide?.()
}
})
spotlightWin.on?.('blur', () => {
if (spotlightWin && !spotlightWin.isDestroyed?.()) {
spotlightWin.hide?.()
}
})
}
export function registerGlobalShortcuts(): void {
try {
const success = GlobalShortcut.register('Alt+Shift+Q', () => {
openSpotlightContentWindow()
})
if (!success) {
console.warn('[GlobalShortcut] Failed to register Alt+Shift+Q')
}
} catch (e) {
console.warn('[GlobalShortcut] Error:', (e as Error)?.message)
}
}
export function setupSpotlightRPC(height: number): boolean {
if (!spotlightWin || spotlightWin.isDestroyed?.()) return false
try {
const frame = spotlightWin.getFrame?.()
if (frame) {
spotlightWin.setSize?.(frame.width, height)
spotlightWin.center?.()
}
return true
} catch (e) {
console.warn('[spotlight] Failed to resize:', (e as Error)?.message)
return false
}
}

View File

@ -39,7 +39,7 @@ import BellIcon from '../../Icons/BellIcon'
import SearchIcon from '../../Icons/SearchIcon'
import SettingsIcon from '../../Icons/SettingsIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import { ElectronContext } from '../context/ElectronContext'
import { ElectrobunContext } from '../context/ElectrobunContext'
import DashboardWindowButtons from './DashboardWindowButtons'
const { Text } = Typography
@ -60,7 +60,7 @@ const DashboardNavigation = () => {
icon: <ProductionIcon />
})
const isMobile = useMediaQuery({ maxWidth: 768 })
const { platform, isElectron, isFullScreen } = useContext(ElectronContext)
const { platform, isElectron, isFullScreen } = useContext(ElectrobunContext)
const mainMenuItems = useMemo(
() => [

View File

@ -6,7 +6,7 @@ import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon'
import { useMediaQuery } from 'react-responsive'
import { useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
import { ElectronContext } from '../context/ElectronContext'
import { ElectrobunContext } from '../context/ElectrobunContext'
const { Sider } = Layout
const DashboardSidebar = ({
@ -24,7 +24,7 @@ const DashboardSidebar = ({
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
const { isElectron } = useContext(ElectronContext)
const { isElectron } = useContext(ElectrobunContext)
useEffect(() => {
if (typeof collapsedProp === 'boolean') {

View File

@ -1,6 +1,6 @@
import { useContext } from 'react'
import { Flex, Button } from 'antd'
import { ElectronContext } from '../context/ElectronContext'
import { ElectrobunContext } from '../context/ElectrobunContext'
import XMarkIcon from '../../Icons/XMarkIcon'
import MinusIcon from '../../Icons/MinusIcon'
import ContractIcon from '../../Icons/ContractIcon'
@ -8,7 +8,7 @@ import ExpandIcon from '../../Icons/ExpandIcon'
const DashboardWindowButtons = () => {
const { isMaximized, handleWindowControl, platform, isFullScreen } =
useContext(ElectronContext)
useContext(ElectrobunContext)
const closeButton = (
<Button

View File

@ -40,7 +40,7 @@ import CheckIcon from '../../Icons/CheckIcon'
import { useNavigate } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import { ElectrobunContext } from '../context/ElectrobunContext'
import ActionsIcon from '../../Icons/ActionsIcon'
const logger = loglevel.getLogger('DasboardTable')
@ -103,7 +103,7 @@ const ObjectTable = forwardRef(
ref
) => {
const { token } = useContext(AuthContext)
const { isElectron } = useContext(ElectronContext)
const { isElectron } = useContext(ElectrobunContext)
const onStateChangeRef = useRef(onStateChange)
useEffect(() => {

View File

@ -22,7 +22,7 @@ import ExclamationOctogonIcon from '../../Icons/ExclamationOctagonIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import config from '../../../config'
import loglevel from 'loglevel'
import { ElectronContext } from './ElectronContext'
import { ElectrobunContext } from './ElectrobunContext'
import { useLocation, useNavigate } from 'react-router-dom'
import {
getAuthCookies,
@ -63,7 +63,7 @@ const AuthProvider = ({ children }) => {
getAuthSession,
setAuthSession,
clearAuthSession
} = useContext(ElectronContext)
} = useContext(ElectrobunContext)
const location = useLocation()
const navigate = useNavigate()

View File

@ -0,0 +1,166 @@
import { createContext, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
// Electrobun RPC - only available when running in Electrobun
const getRpc = () =>
typeof window !== 'undefined' && window.__ELECTROBUN_RPC__ ? window.__ELECTROBUN_RPC__ : null
// Utility to check if running in Electrobun
// eslint-disable-next-line react-refresh/only-export-components
export function isElectrobun() {
if (typeof window === 'undefined') return false
if (getRpc()) return true
if (
typeof navigator === 'object' &&
typeof navigator.userAgent === 'string' &&
navigator.userAgent.toLowerCase().includes('electrobun')
) {
return true
}
return false
}
const ElectrobunContext = createContext()
const ElectrobunProvider = ({ children }) => {
const [platform, setPlatform] = useState('unknown')
const [isMaximized, setIsMaximized] = useState(false)
const [isFullScreen, setIsFullScreen] = useState(false)
const [electrobunAvailable] = useState(isElectrobun())
const navigate = useNavigate()
const rpc = getRpc()
// Function to open external URL via Electrobun
const openExternalUrl = (url) => {
if (electrobunAvailable && rpc?.request?.openExternalUrl) {
rpc.request.openExternalUrl({ url })
return true
}
return false
}
// Function to open internal URL via Electrobun
const openInternalUrl = (url) => {
if (electrobunAvailable && rpc?.request?.openInternalUrl) {
return rpc.request.openInternalUrl({ url })
}
return false
}
useEffect(() => {
if (!rpc) return
// Get initial platform
rpc.request?.osInfo?.()?.then?.((info) => {
if (info?.platform) setPlatform(info.platform)
})
// Get initial window state
rpc.request?.windowState?.()?.then?.((state) => {
if (state && typeof state.isMaximized === 'boolean') {
setIsMaximized(state.isMaximized)
}
if (state && typeof state.isFullScreen === 'boolean') {
setIsFullScreen(state.isFullScreen)
}
})
// Register for window state updates from bun
const unregister =
window.__ELECTROBUN_REGISTER__?.('windowState', (state) => {
if (state && typeof state.isMaximized === 'boolean') {
setIsMaximized(state.isMaximized)
}
if (state && typeof state.isFullScreen === 'boolean') {
setIsFullScreen(state.isFullScreen)
}
})
// Register for navigate from bun
const unregisterNav =
window.__ELECTROBUN_REGISTER__?.('navigate', (url) => {
navigate(url)
})
return () => {
unregister?.()
unregisterNav?.()
}
}, [navigate, rpc])
// Window control handler
const handleWindowControl = (action) => {
if (electrobunAvailable && rpc?.request?.windowControl) {
rpc.request.windowControl({ action })
}
}
const getAuthSession = async () => {
if (!electrobunAvailable || !rpc?.request?.authSessionGet) return null
return await rpc.request.authSessionGet()
}
const setAuthSession = async (session) => {
if (!electrobunAvailable || !rpc?.request?.authSessionSet) return false
return await rpc.request.authSessionSet({ session })
}
const clearAuthSession = async () => {
if (!electrobunAvailable || !rpc?.request?.authSessionClear) return false
return await rpc.request.authSessionClear()
}
// Backwards-compatible helpers
const getToken = async () => {
const session = await getAuthSession()
return session?.token ?? null
}
const setToken = async (token) => {
const session = (await getAuthSession()) ?? {}
return await setAuthSession({ ...session, token })
}
const resizeSpotlightWindow = async (height) => {
if (!electrobunAvailable || !rpc?.request?.spotlightWindowResize) return false
try {
return await rpc.request.spotlightWindowResize({ height })
} catch (error) {
console.warn(
'[ElectrobunContext] Failed to resize spotlight window:',
error
)
return false
}
}
return (
<ElectrobunContext.Provider
value={{
platform,
isMaximized,
isFullScreen,
isElectron: electrobunAvailable,
handleWindowControl,
openExternalUrl,
openInternalUrl,
getAuthSession,
setAuthSession,
clearAuthSession,
getToken,
setToken,
resizeSpotlightWindow
}}
>
{children}
</ElectrobunContext.Provider>
)
}
ElectrobunProvider.propTypes = {
children: PropTypes.node.isRequired
}
export { ElectrobunContext, ElectrobunProvider }

View File

@ -1,174 +0,0 @@
import { createContext, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
// Only available in Electron renderer
const electron = window.require ? window.require('electron') : null
const ipcRenderer = electron ? electron.ipcRenderer : null
// Utility to check if running in Electron
// eslint-disable-next-line react-refresh/only-export-components
export function isElectron() {
// Renderer process
if (
typeof window !== 'undefined' &&
window.process &&
window.process.type === 'renderer'
) {
return true
}
// User agent
if (
typeof navigator === 'object' &&
typeof navigator.userAgent === 'string' &&
navigator.userAgent.indexOf('Electron') >= 0
) {
return true
}
return false
}
const ElectronContext = createContext()
const ElectronProvider = ({ children }) => {
const [platform, setPlatform] = useState('unknown')
const [isMaximized, setIsMaximized] = useState(false)
const [isFullScreen, setIsFullScreen] = useState(false)
const [electronAvailable] = useState(isElectron())
const navigate = useNavigate()
// Function to open external URL via Electron
const openExternalUrl = (url) => {
if (electronAvailable && ipcRenderer) {
ipcRenderer.invoke('open-external-url', url)
return true
}
return false
}
// Function to open internal URL via Electron
const openInternalUrl = (url) => {
if (electronAvailable && ipcRenderer) {
ipcRenderer.invoke('open-internal-url', url)
return true
}
return false
}
useEffect(() => {
if (!ipcRenderer) return
// Get initial platform
ipcRenderer.invoke('os-info').then((info) => {
if (info && info.platform) setPlatform(info.platform)
})
// Get initial window state
ipcRenderer.invoke('window-state').then((state) => {
if (state && typeof state.isMaximized === 'boolean') {
setIsMaximized(state.isMaximized)
}
if (state && typeof state.isFullScreen === 'boolean') {
setIsFullScreen(state.isFullScreen)
}
})
// Listen for window state changes
const windowStateHandler = (event, state) => {
if (state && typeof state.isMaximized === 'boolean') {
setIsMaximized(state.isMaximized)
}
if (state && typeof state.isFullScreen === 'boolean') {
setIsFullScreen(state.isFullScreen)
}
}
ipcRenderer.on('window-state', windowStateHandler)
// Listen for navigate
const navigateHandler = (event, url) => {
navigate(url)
}
ipcRenderer.on('navigate', navigateHandler)
return () => {
ipcRenderer.removeListener('navigate', navigateHandler)
ipcRenderer.removeListener('window-state', windowStateHandler)
}
}, [navigate])
// Window control handler
const handleWindowControl = (action) => {
if (electronAvailable && ipcRenderer) {
ipcRenderer.send('window-control', action)
}
}
const getAuthSession = async () => {
if (!electronAvailable || !ipcRenderer) return null
return await ipcRenderer.invoke('auth-session-get')
}
const setAuthSession = async (session) => {
if (!electronAvailable || !ipcRenderer) return false
return await ipcRenderer.invoke('auth-session-set', session)
}
const clearAuthSession = async () => {
if (!electronAvailable || !ipcRenderer) return false
return await ipcRenderer.invoke('auth-session-clear')
}
// Backwards-compatible helpers
const getToken = async () => {
const session = await getAuthSession()
return session?.token || null
}
const setToken = async (token) => {
const session = (await getAuthSession()) || {}
return await setAuthSession({ ...session, token })
}
const resizeSpotlightWindow = async (height) => {
if (!electronAvailable || !ipcRenderer) return false
try {
return await ipcRenderer.invoke('spotlight-window-resize', height)
} catch (error) {
console.warn(
'[ElectronContext] Failed to resize spotlight window:',
error
)
return false
}
}
return (
<ElectronContext.Provider
value={{
platform,
isMaximized,
isFullScreen,
isElectron: electronAvailable,
handleWindowControl,
openExternalUrl,
openInternalUrl,
getAuthSession,
setAuthSession,
clearAuthSession,
getToken,
setToken,
resizeSpotlightWindow
}}
>
{children}
</ElectronContext.Provider>
)
}
ElectronProvider.propTypes = {
children: PropTypes.node.isRequired
}
export { ElectronContext, ElectronProvider }

View File

@ -35,7 +35,7 @@ import {
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { ApiServerContext } from './ApiServerContext'
import { AuthContext } from './AuthContext'
import { ElectronContext } from './ElectronContext'
import { ElectrobunContext } from './ElectrobunContext'
const SpotlightContext = createContext()
@ -55,7 +55,7 @@ const SpotlightContent = ({
const [inputPrefix, setInputPrefix] = useState(null)
const { fetchSpotlightData } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const { openInternalUrl } = useContext(ElectronContext)
const { openInternalUrl } = useContext(ElectrobunContext)
// Refs for throttling/debouncing
const lastFetchTime = useRef(0)
@ -606,7 +606,7 @@ SpotlightProvider.propTypes = {
const ElectronSpotlightContentPage = () => {
const cardRef = useRef(null)
const resizeTimeoutRef = useRef(null)
const { resizeSpotlightWindow, isElectron } = useContext(ElectronContext)
const { resizeSpotlightWindow, isElectron } = useContext(ElectrobunContext)
// Function to measure and resize window
const updateWindowHeight = useCallback(() => {

26
src/electrobun.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
/**
* Electrobun global declarations for the browser context.
*/
declare global {
interface Window {
__ELECTROBUN_RPC__?: {
request?: {
osInfo?: () => Promise<{ platform: string }>
windowState?: () => Promise<{ isMaximized?: boolean; isFullScreen?: boolean }>
openExternalUrl?: (params: { url: string }) => void
openInternalUrl?: (params: { url: string }) => Promise<boolean>
windowControl?: (params: { action: string }) => void
authSessionGet?: () => Promise<object | null>
authSessionSet?: (params: { session: object }) => Promise<boolean>
authSessionClear?: () => Promise<boolean>
spotlightWindowResize?: (params: { height: number }) => Promise<boolean>
}
}
__ELECTROBUN_REGISTER__?: (
type: 'navigate' | 'windowState',
cb: (...args: unknown[]) => void
) => () => void
}
}
export {}

52
src/preload/index.ts Normal file
View File

@ -0,0 +1,52 @@
/**
* Electrobun preload script - runs before the page, sets up RPC bridge.
*/
import { Electroview } from 'electrobun/view'
import type { FarmControlRPCType } from '../shared/rpcTypes'
const callbacks: {
navigate: Array<(url: string) => void>
windowState: Array<(state: { isMaximized?: boolean; isFullScreen?: boolean }) => void>
} = {
navigate: [],
windowState: []
}
const rpc = Electroview.defineRPC<FarmControlRPCType>({
handlers: {
requests: {},
messages: {
navigate: ({ url }) => {
callbacks.navigate.forEach((cb) => cb(url))
},
windowState: (state) => {
callbacks.windowState.forEach((cb) => cb(state))
}
}
}
})
const electroview = new Electroview({ rpc })
declare global {
interface Window {
__ELECTROBUN_RPC__: typeof electroview.rpc
__ELECTROBUN_REGISTER__: (
type: 'navigate' | 'windowState',
cb: (...args: any[]) => void
) => () => void
}
}
window.__ELECTROBUN_RPC__ = electroview.rpc
window.__ELECTROBUN_REGISTER__ = (type, cb) => {
if (type === 'navigate' || type === 'windowState') {
callbacks[type].push(cb)
return () => {
const i = callbacks[type].indexOf(cb)
if (i >= 0) callbacks[type].splice(i, 1)
}
}
return () => {}
}

29
src/shared/rpcTypes.ts Normal file
View File

@ -0,0 +1,29 @@
/**
* RPC types for Electrobun main <-> browser communication.
* Bun handlers respond to requests from the browser.
* Browser handlers respond to messages from bun.
*/
export type FarmControlRPCType = {
bun: {
requests: {
osInfo: { params: void; response: { platform: string } }
windowState: { params: void; response: { isMaximized?: boolean; isFullScreen?: boolean } }
openExternalUrl: { params: { url: string }; response: void }
openInternalUrl: { params: { url: string }; response: boolean }
windowControl: { params: { action: 'minimize' | 'maximize' | 'close' }; response: void }
authSessionGet: { params: void; response: object | null }
authSessionSet: { params: { session: object }; response: boolean }
authSessionClear: { params: void; response: boolean }
spotlightWindowResize: { params: { height: number }; response: boolean }
}
messages: Record<string, never>
}
webview: {
requests: Record<string, never>
messages: {
navigate: { url: string }
windowState: { isMaximized?: boolean; isFullScreen?: boolean }
}
}
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["bun-types"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "electrobun.config.ts"]
}