Refactor single instance and deep link handling
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Removed deprecated single instance handlers and streamlined deep link processing.
- Updated `ensureSingleInstanceLock` to initiate signal watching for better instance management.
- Enhanced deep link handling by introducing `setupWindowsDeepLinkHandling` for improved navigation and focus management.
- Refactored URL parsing in `findProtocolUrl` for better reliability in deep link resolution.
This commit is contained in:
Tom Butcher 2026-08-02 19:45:54 +01:00
parent 37113b1b41
commit 4caa7a4bd1
3 changed files with 57 additions and 24 deletions

View File

@ -1,7 +1,6 @@
import { import {
closeSingleInstanceServer, closeSingleInstanceServer,
ensureSingleInstanceLock, ensureSingleInstanceLock
setSingleInstanceHandlers
} from '../desktop/single-instance.js' } from '../desktop/single-instance.js'
const gotSingleInstanceLock = await ensureSingleInstanceLock() const gotSingleInstanceLock = await ensureSingleInstanceLock()
@ -17,21 +16,16 @@ const {
} = await import('../desktop/spotlight.js') } = await import('../desktop/spotlight.js')
const { const {
createMainWindow, createMainWindow,
handleDeepLink,
handleDeepLinkFromArgv, handleDeepLinkFromArgv,
setupDevAuthServer, setupDevAuthServer,
setupNavigationGestures, setupNavigationGestures,
showMainWindow setupWindowsDeepLinkHandling
} = await import('../desktop/window.js') } = await import('../desktop/window.js')
setSingleInstanceHandlers({
onDeepLink: handleDeepLink,
onFocus: showMainWindow
})
const rpc = createAppRpc() const rpc = createAppRpc()
const mainWindow = await createMainWindow(rpc) const mainWindow = await createMainWindow(rpc)
setupWindowsDeepLinkHandling()
setupNavigationGestures(mainWindow) setupNavigationGestures(mainWindow)
registerGlobalShortcuts(rpc) registerGlobalShortcuts(rpc)
setupDevAuthServer() setupDevAuthServer()

View File

@ -114,10 +114,11 @@ export function findProtocolUrl(args) {
const trimmed = arg.trim().replace(/^['"]+|['"]+$/g, '') const trimmed = arg.trim().replace(/^['"]+|['"]+$/g, '')
const match = trimmed.match(/farmcontrol:\/\/\S+/i) const match = trimmed.match(/farmcontrol:\/\/\S+/i)
if (match) { if (match) {
const rawUrl = match[0].replace(/['"]+$/g, '')
try { try {
return decodeURI(match[0]) return decodeURI(rawUrl)
} catch { } catch {
return match[0] return rawUrl
} }
} }
} }
@ -125,10 +126,11 @@ export function findProtocolUrl(args) {
const combined = sources.join(' ') const combined = sources.join(' ')
const combinedMatch = combined.match(/farmcontrol:\/\/\S+/i) const combinedMatch = combined.match(/farmcontrol:\/\/\S+/i)
if (combinedMatch) { if (combinedMatch) {
const rawUrl = combinedMatch[0].replace(/['"]+$/g, '')
try { try {
return decodeURI(combinedMatch[0]) return decodeURI(rawUrl)
} catch { } catch {
return combinedMatch[0] return rawUrl
} }
} }
@ -212,7 +214,6 @@ function dispatchMessage(message) {
if (resolved.type === 'deeplink' && resolved.url) { if (resolved.type === 'deeplink' && resolved.url) {
handlers.onDeepLink?.(resolved.url) handlers.onDeepLink?.(resolved.url)
handlers.onFocus?.()
return return
} }
@ -243,7 +244,7 @@ function startInstanceSignalWatcher() {
// Polling below covers filesystems without reliable watch support. // Polling below covers filesystems without reliable watch support.
} }
pollInterval = setInterval(processInstanceSignal, 250) pollInterval = setInterval(processInstanceSignal, 100)
} }
export async function ensureSingleInstanceLock() { export async function ensureSingleInstanceLock() {
@ -252,6 +253,7 @@ export async function ensureSingleInstanceLock() {
} }
if (tryAcquirePrimaryLock()) { if (tryAcquirePrimaryLock()) {
startInstanceSignalWatcher()
return true return true
} }
@ -261,7 +263,6 @@ export async function ensureSingleInstanceLock() {
export function setSingleInstanceHandlers({ onDeepLink, onFocus }) { export function setSingleInstanceHandlers({ onDeepLink, onFocus }) {
handlers = { onDeepLink, onFocus } handlers = { onDeepLink, onFocus }
startInstanceSignalWatcher()
while (pendingMessages.length > 0) { while (pendingMessages.length > 0) {
dispatchMessage(pendingMessages.shift()) dispatchMessage(pendingMessages.shift())

View File

@ -10,7 +10,7 @@ const isMacOS = process.platform === 'darwin'
const DEV_SERVER_PORT = 5780 const DEV_SERVER_PORT = 5780
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}` const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
import { findProtocolUrl } from './single-instance.js' import { findProtocolUrl, setSingleInstanceHandlers } from './single-instance.js'
import { getWindowsLaunchSources } from './windows-launch-args.js' import { getWindowsLaunchSources } from './windows-launch-args.js'
let mainWindow = null let mainWindow = null
@ -86,14 +86,46 @@ function sendNavigateToRenderer(redirectPath) {
setTimeout(() => deliverNavigation(redirectPath), 100) setTimeout(() => deliverNavigation(redirectPath), 100)
} }
export function handleDeepLink(url) { export function openInternalUrl(url) {
if (!url || typeof url !== 'string') return sendNavigateToRenderer(url)
return true
}
function parseDeepLinkPath(url) {
if (!url || typeof url !== 'string') {
return null
}
if (url.startsWith('/')) {
return url
}
const match = url.match(/^farmcontrol:\/\/app(.*)$/i) const match = url.match(/^farmcontrol:\/\/app(.*)$/i)
if (!match) return if (!match) {
return null
}
const redirectPath = match[1] || '/' const redirectPath = match[1] || '/'
sendNavigateToRenderer(redirectPath.startsWith('/') ? redirectPath : `/${redirectPath}`) const normalizedPath = redirectPath.startsWith('/')
? redirectPath
: `/${redirectPath}`
try {
return decodeURI(normalizedPath)
} catch {
return normalizedPath
}
}
export function handleDeepLink(url) {
const path = parseDeepLinkPath(url)
if (!path) {
showMainWindow()
return false
}
openInternalUrl(path)
return true
} }
export function handleDeepLinkFromArgv() { export function handleDeepLinkFromArgv() {
@ -262,9 +294,15 @@ export async function setupDevAuthServer() {
app.listen(port, () => {}) app.listen(port, () => {})
} }
export function openInternalUrl(url) { export function setupWindowsDeepLinkHandling() {
sendNavigateToRenderer(url) if (process.platform === 'darwin') {
return true return
}
setSingleInstanceHandlers({
onDeepLink: handleDeepLink,
onFocus: showMainWindow
})
} }
export function getWindowState() { export function getWindowState() {