From d6250c72962c8715fd97e9b6841e11b741fddd78 Mon Sep 17 00:00:00 2001
From: Matthew Holder <sixxgate@hotmail.com>
Date: Fri, 1 Nov 2024 18:22:27 -0500
Subject: [PATCH 1/3] Ported the drivers over to tRPC and removed the handle
 services

---
 PLAN.md                                      |   9 +-
 src/main/drivers/extron/sis.ts               |  22 +-
 src/main/drivers/sony/rs485.ts               |  37 +--
 src/main/drivers/tesla-smart/kvm.ts          |  18 +-
 src/main/drivers/tesla-smart/matrix.ts       |  20 +-
 src/main/drivers/tesla-smart/sdi.ts          |  18 +-
 src/main/main.ts                             |  13 -
 src/main/plugins/drivers.ts                  |  16 --
 src/main/routes/drivers.ts                   |  31 +++
 src/main/routes/router.ts                    |   2 +
 src/main/services/driver.ts                  | 148 ----------
 src/main/services/drivers.ts                 | 219 +++++++++++++++
 src/main/services/handle.ts                  | 268 -------------------
 src/main/services/level.js                   |   3 +-
 src/main/services/locale.ts                  |   2 +
 src/main/services/{ => support}/sonyRs485.ts |   0
 src/preload/api.d.ts                         |  84 +-----
 src/preload/index.ts                         |   7 +-
 src/preload/plugins/services.ts              |   9 +-
 src/preload/plugins/services/driver.ts       |  22 --
 src/renderer/modals/SwitchDialog.vue         |   2 +-
 src/renderer/modals/TieDialog.vue            |   2 +-
 src/renderer/pages/SourcePage.vue            |   3 +-
 src/renderer/pages/SwitchList.vue            |   3 +-
 src/renderer/services/backup/import.ts       |   2 +-
 src/renderer/services/dashboard.ts           |  18 +-
 src/renderer/services/driver.ts              | 127 ++++-----
 src/tests/drivers/extron/sis.test.ts         |  68 ++---
 src/tests/drivers/sony/rs485.test.ts         |  66 ++---
 src/tests/drivers/tesla-smart/kvm.test.ts    |  66 ++---
 src/tests/drivers/tesla-smart/matrix.test.ts |  66 ++---
 src/tests/drivers/tesla-smart/sdi.test.ts    |  66 ++---
 src/tests/level.test.ts                      |   7 +-
 src/tests/support/mock.ts                    |  42 ---
 34 files changed, 481 insertions(+), 1005 deletions(-)
 delete mode 100644 src/main/plugins/drivers.ts
 create mode 100644 src/main/routes/drivers.ts
 delete mode 100644 src/main/services/driver.ts
 create mode 100644 src/main/services/drivers.ts
 delete mode 100644 src/main/services/handle.ts
 create mode 100644 src/main/services/locale.ts
 rename src/main/services/{ => support}/sonyRs485.ts (100%)
 delete mode 100644 src/preload/plugins/services/driver.ts

diff --git a/PLAN.md b/PLAN.md
index 8cf22e7..41658be 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -6,10 +6,11 @@
       - Drivers will require an overhaul to no longer need handles.
     - More drivers.
     - Move more modules to core.
+    - Wrap some Electron APIs as services for easier mocking without electron itself.
 - Drivers
   - Shinybow
   - Monoprice Blackbird
-  - J-Tech Digital
-  - ASHATA
-  - TESmart
-  - No Hassle AV
+  - J-Tech Digital -- Need to find actual command list.
+  - ASHATA -- Now unable to find.
+  - TESmart -- Brand of Tesla Elec (like TelsaSmart)
+  - No Hassle AV -- Need to contact.
diff --git a/src/main/drivers/extron/sis.ts b/src/main/drivers/extron/sis.ts
index 2c7fae5..5d2fa34 100644
--- a/src/main/drivers/extron/sis.ts
+++ b/src/main/drivers/extron/sis.ts
@@ -1,9 +1,9 @@
 import Logger from 'electron-log'
-import { defineDriver, kDeviceCanDecoupleAudioOutput, kDeviceSupportsMultipleOutputs } from '../../services/driver'
+import { defineDriver, kDeviceCanDecoupleAudioOutput, kDeviceSupportsMultipleOutputs } from '../../services/drivers'
 import { createCommandStream } from '../../services/stream'
 
 const extronSisDriver = defineDriver({
-  enable: true,
+  enabled: true,
   guid: '4C8F2838-C91D-431E-84DD-3666D14A6E2C',
   localized: {
     en: {
@@ -13,8 +13,8 @@ const extronSisDriver = defineDriver({
     }
   },
   capabilities: kDeviceSupportsMultipleOutputs | kDeviceCanDecoupleAudioOutput,
-  setup: async function setup(uri) {
-    async function sendCommand(command: string) {
+  setup: () => {
+    async function sendCommand(uri: string, command: string) {
       const connection = await createCommandStream(uri, {
         baudRate: 9600,
         dataBits: 8,
@@ -36,11 +36,11 @@ const extronSisDriver = defineDriver({
       await connection.close()
     }
 
-    async function activate(inputChannel: number, videoOutputChannel: number, audioOutputChannel: number) {
-      Logger.log(`extronSisDriver.activate(${inputChannel}, ${videoOutputChannel}, ${audioOutputChannel})`)
-      const videoCommand = `${inputChannel}*${videoOutputChannel}%`
-      const audioCommand = `${inputChannel}*${audioOutputChannel}$`
-      await sendCommand(`${videoCommand}\r\n${audioCommand}\r\n`)
+    async function activate(uri: string, input: number, videoOutput: number, audioOutput: number) {
+      Logger.log(`extronSisDriver.activate(${input}, ${videoOutput}, ${audioOutput})`)
+      const videoCommand = `${input}*${videoOutput}%`
+      const audioCommand = `${input}*${audioOutput}$`
+      await sendCommand(uri, `${videoCommand}\r\n${audioCommand}\r\n`)
     }
 
     async function powerOn() {
@@ -55,11 +55,11 @@ const extronSisDriver = defineDriver({
       await Promise.resolve()
     }
 
-    return await Promise.resolve({
+    return {
       activate,
       powerOn,
       powerOff
-    })
+    }
   }
 })
 
diff --git a/src/main/drivers/sony/rs485.ts b/src/main/drivers/sony/rs485.ts
index 4f81975..ff8970c 100644
--- a/src/main/drivers/sony/rs485.ts
+++ b/src/main/drivers/sony/rs485.ts
@@ -1,11 +1,18 @@
 import Logger from 'electron-log'
-import { defineDriver, kDeviceHasNoExtraCapabilities } from '../../services/driver'
-import { createAddress, createCommand, kAddressAll, kPowerOff, kPowerOn, kSetChannel } from '../../services/sonyRs485'
+import { defineDriver, kDeviceHasNoExtraCapabilities } from '../../services/drivers'
 import { createCommandStream } from '../../services/stream'
-import type { Command, CommandArg } from '../../services/sonyRs485'
+import {
+  createAddress,
+  createCommand,
+  kAddressAll,
+  kPowerOff,
+  kPowerOn,
+  kSetChannel
+} from '../../services/support/sonyRs485'
+import type { Command, CommandArg } from '../../services/support/sonyRs485'
 
 const sonyRs485Driver = defineDriver({
-  enable: true,
+  enabled: true,
   guid: '8626D6D3-C211-4D21-B5CC-F5E3B50D9FF0',
   localized: {
     en: {
@@ -15,8 +22,8 @@ const sonyRs485Driver = defineDriver({
     }
   },
   capabilities: kDeviceHasNoExtraCapabilities,
-  setup: async function setup(uri) {
-    async function sendCommand(command: Command, arg0?: CommandArg, arg1?: CommandArg) {
+  setup: () => {
+    async function sendCommand(uri: string, command: Command, arg0?: CommandArg, arg1?: CommandArg) {
       const source = createAddress(kAddressAll, 0)
       const destination = createAddress(kAddressAll, 0)
       const packet = createCommand(destination, source, command, arg0, arg1)
@@ -42,26 +49,26 @@ const sonyRs485Driver = defineDriver({
       await connection.close()
     }
 
-    async function activate(inputChannel: number) {
-      Logger.log(`sonyRs485Driver.activate(${inputChannel})`)
-      await sendCommand(kSetChannel, 1, inputChannel)
+    async function activate(uri: string, input: number) {
+      Logger.log(`sonyRs485Driver.activate(${input})`)
+      await sendCommand(uri, kSetChannel, 1, input)
     }
 
-    async function powerOn() {
+    async function powerOn(uri: string) {
       Logger.log('sonyRs485Driver.powerOn')
-      await sendCommand(kPowerOn)
+      await sendCommand(uri, kPowerOn)
     }
 
-    async function powerOff() {
+    async function powerOff(uri: string) {
       Logger.log('sonyRs485Driver.powerOff')
-      await sendCommand(kPowerOff)
+      await sendCommand(uri, kPowerOff)
     }
 
-    return await Promise.resolve({
+    return {
       activate,
       powerOn,
       powerOff
-    })
+    }
   }
 })
 
diff --git a/src/main/drivers/tesla-smart/kvm.ts b/src/main/drivers/tesla-smart/kvm.ts
index 56b1413..ccf874d 100644
--- a/src/main/drivers/tesla-smart/kvm.ts
+++ b/src/main/drivers/tesla-smart/kvm.ts
@@ -1,9 +1,9 @@
 import Logger from 'electron-log'
-import { defineDriver, kDeviceHasNoExtraCapabilities } from '../../services/driver'
+import { defineDriver, kDeviceHasNoExtraCapabilities } from '../../services/drivers'
 import { createCommandStream } from '../../services/stream'
 
 const teslaSmartKvmDriver = defineDriver({
-  enable: true,
+  enabled: true,
   guid: '91D5BC95-A8E2-4F58-BCAC-A77BA1054D61',
   localized: {
     en: {
@@ -13,8 +13,8 @@ const teslaSmartKvmDriver = defineDriver({
     }
   },
   capabilities: kDeviceHasNoExtraCapabilities,
-  setup: async function setup(uri) {
-    async function sendCommand(command: Buffer) {
+  setup: () => {
+    async function sendCommand(uri: string, command: Buffer) {
       const connection = await createCommandStream(uri, {
         baudRate: 9600,
         dataBits: 8,
@@ -36,9 +36,9 @@ const teslaSmartKvmDriver = defineDriver({
       await connection.close()
     }
 
-    async function activate(inputChannel: number) {
-      Logger.log(`teslaSmartKvmDriver.activate(${inputChannel})`)
-      await sendCommand(Buffer.of(0xaa, 0xbb, 0x03, 0x01, inputChannel, 0xee))
+    async function activate(uri: string, input: number) {
+      Logger.log(`teslaSmartKvmDriver.activate(${input})`)
+      await sendCommand(uri, Buffer.of(0xaa, 0xbb, 0x03, 0x01, input, 0xee))
     }
 
     async function powerOn() {
@@ -53,11 +53,11 @@ const teslaSmartKvmDriver = defineDriver({
       await Promise.resolve()
     }
 
-    return await Promise.resolve({
+    return {
       activate,
       powerOn,
       powerOff
-    })
+    }
   }
 })
 
diff --git a/src/main/drivers/tesla-smart/matrix.ts b/src/main/drivers/tesla-smart/matrix.ts
index a9369db..47eda42 100644
--- a/src/main/drivers/tesla-smart/matrix.ts
+++ b/src/main/drivers/tesla-smart/matrix.ts
@@ -1,9 +1,9 @@
 import Logger from 'electron-log'
-import { defineDriver, kDeviceSupportsMultipleOutputs } from '../../services/driver'
+import { defineDriver, kDeviceSupportsMultipleOutputs } from '../../services/drivers'
 import { createCommandStream } from '../../services/stream'
 
 const teslaSmartMatrixDriver = defineDriver({
-  enable: true,
+  enabled: true,
   guid: '671824ED-0BC4-43A6-85CC-4877890A7722',
   localized: {
     en: {
@@ -13,8 +13,8 @@ const teslaSmartMatrixDriver = defineDriver({
     }
   },
   capabilities: kDeviceSupportsMultipleOutputs,
-  setup: async function setup(uri) {
-    const sendCommand = async (command: Buffer) => {
+  setup: () => {
+    const sendCommand = async (uri: string, command: Buffer) => {
       const connection = await createCommandStream(uri, {
         baudRate: 9600,
         dataBits: 8,
@@ -38,10 +38,10 @@ const teslaSmartMatrixDriver = defineDriver({
 
     const toChannel = (n: number) => String(n).padStart(2, '0')
 
-    async function activate(inputChannel: number, outputChannel: number) {
-      Logger.log(`teslaSmartMatrixDriver.activate(${inputChannel}, ${outputChannel})`)
-      const command = `MT00SW${toChannel(inputChannel)}${toChannel(outputChannel)}NT`
-      await sendCommand(Buffer.from(command, 'ascii'))
+    async function activate(uri: string, input: number, output: number) {
+      Logger.log(`teslaSmartMatrixDriver.activate(${input}, ${output})`)
+      const command = `MT00SW${toChannel(input)}${toChannel(output)}NT`
+      await sendCommand(uri, Buffer.from(command, 'ascii'))
 
       await Promise.resolve()
     }
@@ -58,11 +58,11 @@ const teslaSmartMatrixDriver = defineDriver({
       await Promise.resolve()
     }
 
-    return await Promise.resolve({
+    return {
       activate,
       powerOn,
       powerOff
-    })
+    }
   }
 })
 
diff --git a/src/main/drivers/tesla-smart/sdi.ts b/src/main/drivers/tesla-smart/sdi.ts
index 38bc5fd..4b66e7b 100644
--- a/src/main/drivers/tesla-smart/sdi.ts
+++ b/src/main/drivers/tesla-smart/sdi.ts
@@ -1,9 +1,9 @@
 import Logger from 'electron-log'
-import { defineDriver, kDeviceHasNoExtraCapabilities } from '../../services/driver'
+import { defineDriver, kDeviceHasNoExtraCapabilities } from '../../services/drivers'
 import { createCommandStream } from '../../services/stream'
 
 const teslaSmartSdiDriver = defineDriver({
-  enable: true,
+  enabled: true,
   guid: 'DDB13CBC-ABFC-405E-9EA6-4A999F9A16BD',
   localized: {
     en: {
@@ -13,8 +13,8 @@ const teslaSmartSdiDriver = defineDriver({
     }
   },
   capabilities: kDeviceHasNoExtraCapabilities,
-  setup: async function setup(uri) {
-    async function sendCommand(command: Buffer) {
+  setup: () => {
+    async function sendCommand(uri: string, command: Buffer) {
       const connection = await createCommandStream(uri, {
         baudRate: 9600,
         dataBits: 8,
@@ -36,9 +36,9 @@ const teslaSmartSdiDriver = defineDriver({
       await connection.close()
     }
 
-    async function activate(inputChannel: number) {
-      Logger.log(`teslaSmartSdiDriver.activate(${inputChannel})`)
-      await sendCommand(Buffer.of(0xaa, 0xcc, 0x01, inputChannel))
+    async function activate(uri: string, input: number) {
+      Logger.log(`teslaSmartSdiDriver.activate(${input})`)
+      await sendCommand(uri, Buffer.of(0xaa, 0xcc, 0x01, input))
     }
 
     async function powerOn() {
@@ -53,11 +53,11 @@ const teslaSmartSdiDriver = defineDriver({
       await Promise.resolve()
     }
 
-    return await Promise.resolve({
+    return {
       activate,
       powerOn,
       powerOff
-    })
+    }
   }
 })
 
diff --git a/src/main/main.ts b/src/main/main.ts
index b244c17..ac1637e 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -5,11 +5,8 @@ import { app, shell, BrowserWindow, nativeTheme } from 'electron'
 import Logger from 'electron-log'
 import appIcon from '../../resources/icon.png?asset&asarUnpack'
 import useAppConfig from './info/config'
-import registerDrivers from './plugins/drivers'
 import useCrypto from './plugins/webcrypto'
 import useApiServer from './server'
-import useDrivers from './services/driver'
-import useHandles from './services/handle'
 import useSystem from './services/system'
 import useUpdater from './services/updater'
 import { logError } from './utilities'
@@ -98,14 +95,6 @@ app.on('window-all-closed', () => {
   }
 })
 
-// Make sure all handles have been closed.
-const { shutDown } = useHandles()
-app.on('will-quit', () => {
-  process.nextTick(async () => {
-    await shutDown()
-  })
-})
-
 // Default open or close DevTools by F12 in development
 // and ignore CommandOrControl + R in production.
 // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
@@ -144,7 +133,5 @@ useApiServer()
 useCrypto()
 useUpdater()
 useSystem()
-useDrivers()
-registerDrivers()
 
 await createWindow()
diff --git a/src/main/plugins/drivers.ts b/src/main/plugins/drivers.ts
deleted file mode 100644
index 782db22..0000000
--- a/src/main/plugins/drivers.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import extronSisDriver from '../drivers/extron/sis'
-import sonyRs485Driver from '../drivers/sony/rs485'
-import teslaSmartKvmDriver from '../drivers/tesla-smart/kvm'
-import teslaSmartMatrixDriver from '../drivers/tesla-smart/matrix'
-import teslaSmartSdiDriver from '../drivers/tesla-smart/sdi'
-import useDrivers from '../services/driver'
-
-export default function registerDrivers() {
-  const { register } = useDrivers()
-
-  register(extronSisDriver)
-  register(sonyRs485Driver)
-  register(teslaSmartMatrixDriver)
-  register(teslaSmartKvmDriver)
-  register(teslaSmartSdiDriver)
-}
diff --git a/src/main/routes/drivers.ts b/src/main/routes/drivers.ts
new file mode 100644
index 0000000..ae6fb8f
--- /dev/null
+++ b/src/main/routes/drivers.ts
@@ -0,0 +1,31 @@
+import { memo } from 'radash'
+import { z } from 'zod'
+import useDrivers from '../services/drivers'
+import { procedure, router } from '../services/trpc'
+
+const Channel = z.number().int().nonnegative().finite()
+
+const ActivateInputs = z.tuple([z.string().uuid(), z.string().url(), Channel, Channel, Channel])
+const PowerInputs = z.tuple([z.string().uuid(), z.string().url()])
+
+const useDriversRouter = memo(function useDriversRoute() {
+  const drivers = useDrivers()
+
+  return router({
+    all: procedure.query(drivers.all),
+    get: procedure.input(z.string().uuid()).query(async ({ input }) => {
+      await drivers.get(input)
+    }),
+    activate: procedure.input(ActivateInputs).mutation(async ({ input }) => {
+      await drivers.activate(...input)
+    }),
+    powerOn: procedure.input(PowerInputs).mutation(async ({ input }) => {
+      await drivers.powerOn(...input)
+    }),
+    powerOff: procedure.input(PowerInputs).mutation(async ({ input }) => {
+      await drivers.powerOff(...input)
+    })
+  })
+})
+
+export default useDriversRouter
diff --git a/src/main/routes/router.ts b/src/main/routes/router.ts
index ed86d32..1329e95 100644
--- a/src/main/routes/router.ts
+++ b/src/main/routes/router.ts
@@ -6,6 +6,7 @@ import useSourcesRouter from './data/sources'
 import useUserStoreRouter from './data/storage'
 import useSwitchesRouter from './data/switches'
 import useTiesRouter from './data/ties'
+import useDriversRouter from './drivers'
 import useSerialPortRouter from './ports'
 import useStartupRouter from './startup'
 
@@ -17,6 +18,7 @@ export const useAppRouter = memo(() =>
     // Functional service routes
     ports: useSerialPortRouter(),
     startup: useStartupRouter(),
+    drivers: useDriversRouter(),
     // Data service routes
     storage: useUserStoreRouter(),
     ties: useTiesRouter(),
diff --git a/src/main/services/driver.ts b/src/main/services/driver.ts
deleted file mode 100644
index dce87c0..0000000
--- a/src/main/services/driver.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import { ipcMain } from 'electron'
-import { memo } from 'radash'
-import { ipcHandle, ipcProxy, logError } from '../utilities'
-import useHandles from './handle'
-import type { HandleKey } from './handle'
-import type { DriverData, Handle } from '../../preload/api'
-
-//
-// Device capabilities
-//
-
-/** The device has no extended capabilities. */
-export const kDeviceHasNoExtraCapabilities = 0
-/** The device has multiple output channels. */
-export const kDeviceSupportsMultipleOutputs = 1
-/** The device support sending the audio output to a different channel. */
-export const kDeviceCanDecoupleAudioOutput = 2
-
-//
-// Driver definition
-//
-
-/** Interacts with a device. */
-export interface DriverBindings {
-  /**
-   * Activates input and output ties.
-   *
-   * @param inputChannel - The input channel to tie.
-   * @param videoOutputChannel - The output video channel to tie.
-   * @param audioOutputChannel - The output audio channel to tie.
-   */
-  readonly activate: (inputChannel: number, videoOutputChannel: number, audioOutputChannel: number) => Promise<void>
-
-  /** Powers on the switch or monitor. */
-  readonly powerOn?: () => Promise<void>
-
-  /** Powers off the switch or monitor. */
-  readonly powerOff?: () => Promise<void>
-
-  /** Closes the driver to free any used resources. */
-  readonly close?: () => Promise<void>
-}
-
-/** Driver to interact with switching devices. */
-export interface Driver extends DriverBindings {
-  /** The URI to the device being driven. */
-  readonly uri: string
-}
-
-/** Provides the basic information to create a driver factory registration. */
-export interface DriverFactory {
-  /** Provides information about the driver. */
-  readonly data: DriverData
-  /** Loads a driver. */
-  readonly load: (uri: string) => Promise<Driver>
-}
-
-/**
- * Initializes a driver.
- *
- * @param uri A uri to the device.
- */
-export type DriverSetup = (uri: string) => Promise<DriverBindings>
-
-export interface DriverOptions extends DriverData {
-  setup: DriverSetup
-}
-
-/** Defines a driver. */
-export function defineDriver(options: DriverOptions): DriverFactory | undefined {
-  const { setup, ...data } = options
-  if (!data.enable) {
-    return undefined
-  }
-
-  return Object.freeze({
-    data,
-    load: async (uri: string): Promise<Driver> =>
-      Object.freeze({
-        ...(await setup(uri)),
-        uri
-      })
-  })
-}
-
-//
-// Driver API back-end
-//
-
-interface DriverBackEnd {
-  register: (factory: DriverFactory | undefined) => void
-}
-
-const useDrivers = memo(function useDrivers() {
-  const { createHandle, openHandle } = useHandles()
-  const kDriverHandle = Symbol.for('@driver') as HandleKey<Driver>
-
-  /** The driver registry. */
-  const registry = new Map<string, DriverFactory>()
-
-  /** Registers a driver. */
-  function register(factory: DriverFactory | undefined) {
-    if (factory != null) {
-      registry.set(factory.data.guid, factory)
-    }
-  }
-
-  /** Lists available drivers. */
-  async function list() {
-    return await Promise.resolve(Array.from(registry.values()).map((d) => d.data))
-  }
-
-  /** Loads a driver registered in the registry. */
-  const open = ipcHandle(async function open(event, guid: string, path: string) {
-    const factory = registry.get(guid)
-    if (factory == null) {
-      throw logError(new Error(`No such driver registered as "${guid}"`))
-    }
-
-    return createHandle(event, kDriverHandle, await factory.load(path), async (driver) => {
-      await driver.close?.()
-    })
-  })
-
-  const powerOn = ipcHandle(async (event, h: Handle) => {
-    await openHandle(event, kDriverHandle, h).powerOn?.()
-  })
-
-  const powerOff = ipcHandle(async (event, h: Handle) => {
-    await openHandle(event, kDriverHandle, h).powerOff?.()
-  })
-
-  const activate = ipcHandle(
-    async (event, h: Handle, inputChannel: number, videoOutputChannel: number, audioOutputChannel: number) => {
-      await openHandle(event, kDriverHandle, h).activate(inputChannel, videoOutputChannel, audioOutputChannel)
-    }
-  )
-
-  ipcMain.handle('driver:list', ipcProxy(list))
-  ipcMain.handle('driver:open', open)
-  ipcMain.handle('driver:powerOn', powerOn)
-  ipcMain.handle('driver:powerOff', powerOff)
-  ipcMain.handle('driver:activate', activate)
-
-  return { register } satisfies DriverBackEnd
-})
-
-export default useDrivers
diff --git a/src/main/services/drivers.ts b/src/main/services/drivers.ts
new file mode 100644
index 0000000..c97aa17
--- /dev/null
+++ b/src/main/services/drivers.ts
@@ -0,0 +1,219 @@
+//
+// Device capabilities
+//
+
+import { memo } from 'radash'
+import type { ApiLocales } from './locale'
+import type { MaybePromise } from '@/basics'
+
+/** The device has no extended capabilities. */
+export type kDeviceHasNoExtraCapabilities = typeof kDeviceHasNoExtraCapabilities
+export const kDeviceHasNoExtraCapabilities = 0
+/** The device has multiple output channels. */
+export type kDeviceSupportsMultipleOutputs = typeof kDeviceSupportsMultipleOutputs
+export const kDeviceSupportsMultipleOutputs = 1
+/** The device support sending the audio output to a different channel. */
+export type kDeviceCanDecoupleAudioOutput = typeof kDeviceCanDecoupleAudioOutput
+export const kDeviceCanDecoupleAudioOutput = 2
+
+//
+// Driver definition
+//
+
+/** Defines the localized metadata about a driver. */
+export interface LocalizedDriverDescriptor {
+  /** Defines the title for the driver in a specific locale. */
+  readonly title: string
+  /** Defines the company for the driver in a specific locale. */
+  readonly company: string
+  /** Defines the provider for the driver in a specific locale. */
+  readonly provider: string
+}
+
+/** Defines basic metadata about a device and driver. */
+export interface DriverData {
+  /**
+   * Indicates whether the driver is enabled, this is to allow partially coded drivers to be
+   * commited, but not usable to the UI or other code.
+   */
+  readonly enabled: boolean
+  /** A unique identifier for the driver. */
+  readonly guid: string
+  /** Defines the localized driver information in all supported locales. */
+  readonly localized: Readonly<Record<ApiLocales, LocalizedDriverDescriptor>>
+  /** Defines the capabilities of the device driven by the driver. */
+  readonly capabilities: number
+}
+
+/** Interacts with a device. */
+export interface DriverBindings {
+  /**
+   * Activates input and output ties.
+   *
+   * @param uri - URI identifying the location of the device.
+   * @param inputChannel - The input channel to tie.
+   * @param videoOutputChannel - The output video channel to tie.
+   * @param audioOutputChannel - The output audio channel to tie.
+   */
+  readonly activate: (
+    uri: string,
+    inputChannel: number,
+    videoOutputChannel: number,
+    audioOutputChannel: number
+  ) => Promise<void>
+
+  /**
+   * Powers on the switch or monitor.
+   *
+   * @param uri - URI identifying the location of the device.
+   */
+  readonly powerOn?: (uri: string) => Promise<void>
+
+  /**
+   * Powers off the switch or monitor.
+   *
+   * @param uri - URI identifying the location of the device.
+   */
+  readonly powerOff?: (uri: string) => Promise<void>
+}
+
+export interface DefineDriverOptions extends DriverData {
+  setup: () => DriverBindings
+}
+
+async function noOpPower() {
+  /* no-op is not defined in setup */ await Promise.resolve()
+}
+
+const registry = new Map<string, Driver>()
+
+export interface Driver {
+  /**
+   * Indicates whether the driver is enabled, this is to allow partially coded drivers to be
+   * commited, but not usable to the UI or other code.
+   */
+  readonly enabled: boolean
+  /** A unique identifier for the driver. */
+  readonly guid: string
+  /** Defines the capabilities of the device driven by the driver. */
+  readonly capabilities: number
+  /** Raw metadata from the registration options. */
+  readonly metadata: DriverData
+  /** Gets the localized driver information. */
+  getInfo: (locale: ApiLocales) => LocalizedDriverDescriptor
+  /**
+   * Activates input and output ties.
+   *
+   * @param uri - URI identifying the location of the device.
+   * @param inputChannel - The input channel to tie.
+   * @param videoOutputChannel - The output video channel to tie.
+   * @param audioOutputChannel - The output audio channel to tie.
+   */
+  readonly activate: (
+    uri: string,
+    inputChannel: number,
+    videoOutputChannel: number,
+    audioOutputChannel: number
+  ) => Promise<void>
+
+  /**
+   * Powers on the switch or monitor.
+   *
+   * @param uri - URI identifying the location of the device.
+   */
+  readonly powerOn: (uri: string) => Promise<void>
+
+  /**
+   * Powers off the switch or monitor.
+   *
+   * @param uri - URI identifying the location of the device.
+   */
+  readonly powerOff: (uri: string) => Promise<void>
+}
+
+export function defineDriver(options: DefineDriverOptions) {
+  let existing = registry.get(options.guid)
+  if (existing != null) return existing
+
+  const { setup, ...info } = options
+  const implemented = setup()
+
+  const getInfo = memo(function getInfo(locale: ApiLocales) {
+    const localizedInfo = info.localized[locale]
+
+    return {
+      enabled: info.enabled,
+      guid: info.guid,
+      ...localizedInfo,
+      capabilities: info.capabilities
+    }
+  })
+
+  existing = Object.freeze({
+    powerOn: noOpPower,
+    powerOff: noOpPower,
+    ...implemented,
+    enabled: info.enabled,
+    guid: info.guid,
+    capabilities: info.capabilities,
+    metadata: info,
+    getInfo
+  })
+
+  registry.set(options.guid, existing)
+  return existing
+}
+
+const useDrivers = memo(function useDriver() {
+  const drivers = import.meta.glob('../drivers/**/*')
+  const booted = Promise.all(
+    Object.values(drivers).map(async (factory) => {
+      await factory()
+    })
+  )
+
+  function defineOperation<Args extends unknown[], Result>(op: (...args: Args) => MaybePromise<Result>) {
+    return async (...args: Args) => {
+      await booted
+      return await op(...args)
+    }
+  }
+
+  const all = defineOperation(() => Array.from(registry.values()))
+
+  const get = defineOperation((guid: string) => registry.get(guid) ?? null)
+
+  function defineDriverOperation<Args extends unknown[], Result>(
+    op: (driver: Driver, ...args: Args) => MaybePromise<Result>
+  ) {
+    return async (guid: string, ...args: Args) => {
+      const driver = await get(guid)
+      if (driver == null) throw new ReferenceError(`No such driver: "${guid}"`)
+      return await op(driver, ...args)
+    }
+  }
+
+  const activate = defineDriverOperation(
+    async (driver, uri: string, input: number, videoOutput: number, audioOutput: number) => {
+      await driver.activate(uri, input, videoOutput, audioOutput)
+    }
+  )
+
+  const powerOn = defineDriverOperation(async (driver, uri: string) => {
+    await driver.powerOn(uri)
+  })
+
+  const powerOff = defineDriverOperation(async (driver, uri: string) => {
+    await driver.powerOff(uri)
+  })
+
+  return {
+    all,
+    get,
+    activate,
+    powerOn,
+    powerOff
+  }
+})
+
+export default useDrivers
diff --git a/src/main/services/handle.ts b/src/main/services/handle.ts
deleted file mode 100644
index bf73d10..0000000
--- a/src/main/services/handle.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-import { ipcMain } from 'electron'
-import { memo } from 'radash'
-import { ipcHandle, logError } from '../utilities'
-import type { Handle } from '../../preload/api'
-import type { SymbolKey } from '@/keys'
-import type { IpcMainInvokeEvent, WebContents } from 'electron'
-import { warnPromiseFailures } from '@/error-handling'
-
-export type HandleKey<T> = SymbolKey<'handle', T>
-
-/** Close routine for a handle. */
-export type Close<T> = (resource: T) => Promise<unknown>
-
-/** Handle transparent structure. */
-export interface Descriptor<T> {
-  key: HandleKey<T>
-  resource: T
-  close: Close<T>
-}
-
-async function dummyClose() {
-  await Promise.resolve()
-}
-
-/**
- * Allows the use of an opaque handles for referencing
- * resources by the renderer process.
- */
-const useHandles = memo(function useHandles() {
-  /** The maximum number of handles sans 256. */
-  const kMaxHandles = 131072
-
-  /** The NIL/NULL handle. */
-  const kNullHandle = 0 as Handle
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Prevent need to cast
-  type AnyDescriptor = Descriptor<any>
-
-  // The first 256 will be reserved, and used as a
-  // sanity check for validity.
-  const handleMap = new Array<number | AnyDescriptor>(kMaxHandles)
-  let nextFree = 0x100
-
-  // Initial the map, ensuring all new element
-  // point to their neighbor as the next free.
-  // The final entry will point to one past
-  // the end of the map, which will mean
-  // it has been exhausted.
-  for (let i = 0; i !== kMaxHandles; ++i) {
-    handleMap[i] = i + 1
-  }
-
-  /**
-   * Determines whether a handle is valid; and optionally, of a given type.
-   * @param handle The handle to check.
-   * @param key The key to confirm the type of the handle.
-   */
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  function isValidHandle<T = any>(handle: Handle, key?: HandleKey<T>) {
-    // Get the handle descriptor
-    // and check its validity.
-    const descriptor = handleMap[handle]
-    if (descriptor == null || typeof descriptor !== 'object') {
-      return false
-    }
-
-    return key != null ? key === descriptor.key : true
-  }
-
-  /**
-   * Gets the descriptor of a handle.
-   * @param handle The handle from which to get a descriptor.
-   */
-  function getDescriptor(handle: Handle): AnyDescriptor
-  /**
-   * Gets the descriptor of a handle and confirms its type.
-   * @param handle The handle from which to get a descriptor.
-   * @param key The key to confirm the handle type.
-   */
-  function getDescriptor<T>(handle: Handle, key: HandleKey<T>): Descriptor<T>
-  /** Implementation */
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  function getDescriptor<T = any>(handle: Handle, key?: HandleKey<T>) {
-    // Get the handle descriptor
-    // and check its validity.
-    const descriptor = handleMap[handle]
-    if (descriptor == null || typeof descriptor !== 'object') {
-      throw logError(new ReferenceError('Invalid handle'))
-    }
-
-    if (key == null) {
-      return descriptor
-    }
-
-    if (descriptor.key !== key) {
-      throw logError(new TypeError('Wrong handle'))
-    }
-
-    return descriptor
-  }
-
-  const handleTrackers = new WeakMap<WebContents, Set<Handle>>()
-
-  function getHandleTracker(sender: WebContents) {
-    let handleTracker = handleTrackers.get(sender)
-    if (handleTracker != null) return handleTracker
-    handleTracker = new Set<Handle>()
-    handleTrackers.set(sender, handleTracker)
-    return handleTracker
-  }
-
-  /**
-   * Creates a new handle, with the given type key, resource, and clean up routine.
-   * @param event - The event from which the handle is being opened.
-   * @param key - A key used to identify and validate the type of the handle.
-   * @param resource - A resource that will be attached to the handle.
-   * @param close - A callback to clean up the resource attached to the handle when it closes.
-   */
-  function createHandle<T>(event: IpcMainInvokeEvent, key: HandleKey<T>, resource: T, close?: Close<T>) {
-    const { sender } = event
-
-    // Get the handle value and ensure
-    // there is no handle exhaustion.
-    const handle = nextFree
-    if (handle === kMaxHandles) {
-      throw logError(new Error('Out of handles'))
-    }
-
-    // Ensure the handle is not already in use,
-    // indicating a possible map corruption.
-    const opaque = handleMap[handle]
-    if (typeof opaque !== 'number') {
-      throw logError(new Error(`'Handle map corrupt: tNF!=N' ${handle}`))
-    }
-
-    // Record the next free and put the
-    // descriptor in the map so the
-    // handle can be closed.
-    nextFree = opaque
-    handleMap[handle] = { key, resource, close: close ?? dummyClose }
-
-    // Add the handle to the tracker tree.
-    getHandleTracker(sender).add(handle as Handle)
-
-    return handle as Handle
-  }
-
-  /**
-   * Opens a handle to access its attached resource.
-   * @param key - The handle key that tags the handle type.
-   * @param handle - The handle to open.
-   */
-  function openHandle<T>(event: IpcMainInvokeEvent, key: HandleKey<T>, handle: Handle) {
-    const { sender } = event
-
-    // Check that the handle belongs to this sender and frame.
-    const handles = getHandleTracker(sender)
-    if (!handles.has(handle)) {
-      throw logError(new ReferenceError('Invalid handle'))
-    }
-
-    // Get the handle descriptor
-    // and check the resource.
-    const descriptor = getDescriptor(handle, key)
-    if (descriptor.resource == null) {
-      throw logError(new ReferenceError('Invalid handle'))
-    }
-
-    return descriptor.resource // as T
-  }
-
-  /**
-   * Closes a handle and cleans up its resource.
-   * @param event - The event from which the handle is being closed.
-   * @param handle - The handle to close.
-   */
-  async function freeHandle(event: IpcMainInvokeEvent, handle: Handle) {
-    const { sender } = event
-
-    // Check that the handle belongs to this sender and frame.
-    const handles = getHandleTracker(sender)
-    if (!handles.has(handle)) {
-      throw logError(new ReferenceError('Invalid handle'))
-    }
-
-    // Remove the handle from the tracker tree,
-    // do this ealier so if any other task
-    // tries to close this handle, it
-    // cannot see it.
-    handles.delete(handle)
-
-    // Get the handle descriptor
-    // and check the resource.
-    const descriptor = getDescriptor(handle)
-    if (descriptor.resource == null) {
-      throw logError(new ReferenceError('Invalid handle'))
-    }
-
-    // Now, point the next free to the
-    // handle just closed, and update
-    // the next free to point to
-    // said handle.
-    handleMap[handle] = nextFree
-    nextFree = handle
-
-    // Ensure that any asynchronous call out are at the end,
-    // to ensure all changes to the handle map or tracker
-    // don't interleave. Attempt to close the resource.
-    // If it fails to close, it must throw an
-    // error or be silent.
-    await descriptor.close(descriptor.resource)
-  }
-
-  /**
-   * Closes all handles in a frame.
-   */
-  async function freeAllHandle(event: IpcMainInvokeEvent) {
-    const { sender } = event
-
-    const handles = getHandleTracker(sender)
-    while (handles.size > 0) {
-      // Right now, must use a new iterator to always pull the first item.
-      // This is to attempt to make this as synchronous as possible
-      // until the actual close, allowing the item to be removed
-      // before any coroutine pauses for this task cycle.
-      const next = handles.values().next()
-      if (next.done === true) return
-
-      // eslint-disable-next-line no-await-in-loop -- Must be serialized to prevent issues.
-      await freeHandle(event, next.value)
-    }
-  }
-
-  /**
-   * Closes all handles on shutdown.
-   *
-   * There is no need to handle the free-list or ownership.
-   */
-  async function shutDown() {
-    warnPromiseFailures(
-      'closing handle failure',
-      await Promise.allSettled(
-        handleMap.map(async function closeHandle(handle) {
-          if (typeof handle === 'object') {
-            await handle.close(handle.resource)
-          } else {
-            await Promise.resolve()
-          }
-        })
-      )
-    )
-  }
-
-  ipcMain.handle('handle:free', ipcHandle(freeHandle))
-  ipcMain.handle('handle:clean', ipcHandle(freeAllHandle))
-
-  return {
-    kNullHandle,
-    isValidHandle,
-    createHandle,
-    openHandle,
-    freeHandle,
-    freeAllHandle,
-    shutDown
-  }
-})
-
-export default useHandles
diff --git a/src/main/services/level.js b/src/main/services/level.js
index c6efdc7..83df748 100644
--- a/src/main/services/level.js
+++ b/src/main/services/level.js
@@ -25,8 +25,7 @@ export const useLevelDb = memo(function useLevelDb() {
     function leveldown(name) {
       const path = resolvePath(app.getPath('userData'), name)
       const db = levelDown(path)
-
-      app.on('before-quit', () => {
+      app.on('will-quit', () => {
         db.close((err) => {
           if (err != null) console.error(err)
         })
diff --git a/src/main/services/locale.ts b/src/main/services/locale.ts
new file mode 100644
index 0000000..cd01773
--- /dev/null
+++ b/src/main/services/locale.ts
@@ -0,0 +1,2 @@
+/** Supported API locales. */
+export type ApiLocales = 'en'
diff --git a/src/main/services/sonyRs485.ts b/src/main/services/support/sonyRs485.ts
similarity index 100%
rename from src/main/services/sonyRs485.ts
rename to src/main/services/support/sonyRs485.ts
diff --git a/src/preload/api.d.ts b/src/preload/api.d.ts
index b0dfa82..0882e79 100644
--- a/src/preload/api.d.ts
+++ b/src/preload/api.d.ts
@@ -12,11 +12,23 @@ export type { UserInfo } from '../main/info/user'
 export type { AppConfig } from '../main/info/config'
 export type { AppRouter } from '../main/routes/router'
 export type { DocumentId } from '../main/services/database'
+export type { ApiLocales } from '../main/services/locale'
 export type { UserStore } from '../main/dao/storage'
 export type { PortEntry } from '../main/services/ports'
 export type { Source, NewSource, SourceUpdate } from '../main/dao/sources'
 export type { Switch, NewSwitch, SwitchUpdate } from '../main/dao/switches'
 export type { Tie, NewTie, TieUpdate } from '../main/dao/ties'
+export type { ApiLocales } from '../main/locale'
+
+export type {
+  Driver,
+  DriverData,
+  LocalizedDriverDescriptor,
+  // Cannot be exported as values, but they are literals.
+  kDeviceCanDecoupleAudioOutput,
+  kDeviceHasNoExtraCapabilities,
+  kDeviceSupportsMultipleOutputs
+} from '../main/services/drivers'
 
 //
 // Internal parts
@@ -41,78 +53,11 @@ export type IpcResponse<T> = IpcReturnedValue<T> | IpcThrownError
 // Common parts
 //
 
-/** Supported API locales. */
-export type ApiLocales = 'en'
-
-/** Opaque handles. */
-export type Handle = Tagged<number, 'Handle'>
-
 /** Event listener attachment options */
 export interface ListenerOptions {
   once?: boolean | undefined
 }
 
-//
-// Driver API
-//
-
-/** Defines the localized metadata about a driver. */
-export interface LocalizedDriverDescriptor {
-  /** Defines the title for the driver in a specific locale. */
-  readonly title: string
-  /** Defines the company for the driver in a specific locale. */
-  readonly company: string
-  /** Defines the provider for the driver in a specific locale. */
-  readonly provider: string
-}
-
-/** Defines basic metadata about a device and driver. */
-export interface DriverData {
-  /**
-   * Indicates whether the driver is enabled, this is to allow partially coded drivers to be
-   * commited, but not usable to the UI or other code.
-   */
-  readonly enable: boolean
-  /** A unique identifier for the driver. */
-  readonly guid: string
-  /** Defines the localized driver information in all supported locales. */
-  readonly localized: {
-    /** Defines the localized driver information in a specific locale. */
-    readonly [locale in ApiLocales]: LocalizedDriverDescriptor
-  }
-  /** Defines the capabilities of the device driven by the driver. */
-  readonly capabilities: number
-}
-
-export interface DriverApi {
-  readonly capabilities: {
-    readonly kDeviceHasNoExtraCapabilities: 0
-    readonly kDeviceSupportsMultipleOutputs: 1
-    readonly kDeviceCanDecoupleAudioOutput: 2
-  }
-  /** Lists registered drivers. */
-  readonly list: () => Promise<DriverData[]>
-  /** Loads a driver. */
-  readonly open: (guid: string, uri: string) => Promise<Handle>
-  /** Powers on the switch or monitor. */
-  readonly powerOn: (h: Handle) => Promise<void>
-  /** Closes the device to which the driver is attached. */
-  readonly powerOff: (h: Handle) => Promise<void>
-  /**
-   * Sets input and output ties.
-   *
-   * @param inputChannel The input channel to tie.
-   * @param videoOutputChannel The output video channel to tie.
-   * @param audioOutputChannel The output audio channel to tie.
-   */
-  readonly activate: (
-    h: Handle,
-    inputChannel: number,
-    videoOutputChannel: number,
-    audioOutputChannel: number
-  ) => Promise<void>
-}
-
 //
 // Session control API
 //
@@ -155,13 +100,8 @@ export interface ProcessData {
 /** Functional APIs */
 export interface MainProcessServices {
   readonly process: ProcessData
-  readonly driver: DriverApi
   readonly system: SystemApi
   readonly updates: AppUpdates
-  /** Closes a handle, freeing its resources. */
-  readonly freeHandle: (h: Handle) => Promise<void>
-  /** Closes all handles for a page. */
-  readonly freeAllHandles: () => Promise<void>
 }
 
 export interface AppUpdater {
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 4385dc9..11ad616 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -10,12 +10,7 @@ if (!process.contextIsolated) {
 
 try {
   // Register services and setup to free all handles when the window closes.
-  const services = useServices()
-  globalThis.addEventListener('beforeunload', () => {
-    services.freeAllHandles().catch((e: unknown) => {
-      console.error(e)
-    })
-  })
+  useServices()
 
   useAppConfig()
 } catch (e) {
diff --git a/src/preload/plugins/services.ts b/src/preload/plugins/services.ts
index 8ccb297..b47813d 100644
--- a/src/preload/plugins/services.ts
+++ b/src/preload/plugins/services.ts
@@ -1,22 +1,15 @@
 import { contextBridge } from 'electron'
 import { memo } from 'radash'
-import { useIpc } from '../support'
-import useDriverApi from './services/driver'
 import useProcessData from './services/process'
 import useSystemApi from './services/system'
 import useAppUpdates from './services/updates'
 import type { MainProcessServices } from '../api'
 
 const useServices = memo(function useServices() {
-  const ipc = useIpc()
-
   const services = {
     process: useProcessData(),
-    driver: useDriverApi(),
     system: useSystemApi(),
-    updates: useAppUpdates(),
-    freeHandle: ipc.useInvoke('handle:free'),
-    freeAllHandles: ipc.useInvoke('handle:clean')
+    updates: useAppUpdates()
   } satisfies MainProcessServices
 
   contextBridge.exposeInMainWorld('services', services)
diff --git a/src/preload/plugins/services/driver.ts b/src/preload/plugins/services/driver.ts
deleted file mode 100644
index 41404a1..0000000
--- a/src/preload/plugins/services/driver.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { memo } from 'radash'
-import { useIpc } from '../../support'
-import type { DriverApi } from '../../api'
-
-const useDriverApi = memo(function useDriverApi() {
-  const ipc = useIpc()
-
-  return {
-    capabilities: Object.freeze({
-      kDeviceHasNoExtraCapabilities: 0,
-      kDeviceSupportsMultipleOutputs: 1,
-      kDeviceCanDecoupleAudioOutput: 2
-    }),
-    list: ipc.useInvoke('driver:list'),
-    open: ipc.useInvoke('driver:open'),
-    activate: ipc.useInvoke('driver:activate'),
-    powerOn: ipc.useInvoke('driver:powerOn'),
-    powerOff: ipc.useInvoke('driver:powerOff')
-  } satisfies DriverApi
-})
-
-export default useDriverApi
diff --git a/src/renderer/modals/SwitchDialog.vue b/src/renderer/modals/SwitchDialog.vue
index 2e47760..809f06d 100644
--- a/src/renderer/modals/SwitchDialog.vue
+++ b/src/renderer/modals/SwitchDialog.vue
@@ -5,7 +5,7 @@ import { computed, ref, reactive, onBeforeMount } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { useLocation } from '../helpers/location'
 import { useRules, useValidation } from '../helpers/validation'
-import { useDrivers } from '../services/driver'
+import useDrivers from '../services/driver'
 import usePorts from '../services/ports'
 import { useDialogs, useSwitchDialog } from './dialogs'
 import type { I18nSchema } from '../locales/locales'
diff --git a/src/renderer/modals/TieDialog.vue b/src/renderer/modals/TieDialog.vue
index 2c45b9e..0e18340 100644
--- a/src/renderer/modals/TieDialog.vue
+++ b/src/renderer/modals/TieDialog.vue
@@ -5,7 +5,7 @@ import { computed, ref, reactive, watch, onBeforeMount } from 'vue'
 import { useI18n } from 'vue-i18n'
 import NumberInput from '../components/NumberInput.vue'
 import { useRules, useValidation } from '../helpers/validation'
-import { kDeviceCanDecoupleAudioOutput, kDeviceSupportsMultipleOutputs, useDrivers } from '../services/driver'
+import useDrivers, { kDeviceCanDecoupleAudioOutput, kDeviceSupportsMultipleOutputs } from '../services/driver'
 import { useSources } from '../services/sources'
 import { useSwitches } from '../services/switches'
 import { useDialogs, useTieDialog } from './dialogs'
diff --git a/src/renderer/pages/SourcePage.vue b/src/renderer/pages/SourcePage.vue
index 9e6c9c5..676e124 100644
--- a/src/renderer/pages/SourcePage.vue
+++ b/src/renderer/pages/SourcePage.vue
@@ -10,7 +10,7 @@ import { filesToAttachment, toFiles } from '../helpers/attachment'
 import { useGuardedAsyncOp } from '../helpers/utilities'
 import TieDialog from '../modals/TieDialog.vue'
 import { useDialogs, useTieDialog } from '../modals/dialogs'
-import { useDrivers } from '../services/driver'
+import useDrivers from '../services/driver'
 import { useSources } from '../services/sources'
 import { useSwitches } from '../services/switches'
 import { useTies } from '../services/ties'
@@ -113,6 +113,7 @@ const loadTies = useGuardedAsyncOp(async function loadTies() {
 })
 
 onBeforeMount(loadTies)
+onBeforeMount(drivers.all)
 
 async function addTie(target: NewTie) {
   try {
diff --git a/src/renderer/pages/SwitchList.vue b/src/renderer/pages/SwitchList.vue
index f3b47f2..110d168 100644
--- a/src/renderer/pages/SwitchList.vue
+++ b/src/renderer/pages/SwitchList.vue
@@ -7,7 +7,7 @@ import Page from '../components/Page.vue'
 import { useGuardedAsyncOp } from '../helpers/utilities'
 import SwitchDialog from '../modals/SwitchDialog.vue'
 import { useDialogs, useSwitchDialog } from '../modals/dialogs'
-import { useDrivers } from '../services/driver'
+import useDrivers from '../services/driver'
 import { useSwitches } from '../services/switches'
 import type { I18nSchema } from '../locales/locales'
 import type { DriverInformation } from '../services/driver'
@@ -40,6 +40,7 @@ const items = computed(() =>
 )
 
 const refresh = useGuardedAsyncOp(async () => {
+  // TODO: await drivers.all(), with busy tracking
   await switches.all()
 })
 
diff --git a/src/renderer/services/backup/import.ts b/src/renderer/services/backup/import.ts
index 2ca7a4c..d0fa602 100644
--- a/src/renderer/services/backup/import.ts
+++ b/src/renderer/services/backup/import.ts
@@ -2,7 +2,7 @@ import { BlobReader, BlobWriter, TextWriter, ZipReader } from '@zip.js/zip.js'
 import mime from 'mime'
 import { z } from 'zod'
 import { fileToAttachment } from '../../helpers/attachment'
-import { useDrivers } from '../driver'
+import useDrivers from '../driver'
 import useSettings from '../settings'
 import { useSources } from '../sources'
 import { useSwitches } from '../switches'
diff --git a/src/renderer/services/dashboard.ts b/src/renderer/services/dashboard.ts
index 4f1530d..cbfd1af 100644
--- a/src/renderer/services/dashboard.ts
+++ b/src/renderer/services/dashboard.ts
@@ -1,7 +1,7 @@
 import { defineStore } from 'pinia'
 import { computed, readonly, ref } from 'vue'
 import { toFiles, useImages } from '../helpers/attachment'
-import { useDrivers } from './driver'
+import useDrivers from './driver'
 import useSettings from './settings'
 import { useSources } from './sources'
 import { useSwitches } from './switches'
@@ -58,27 +58,17 @@ export const useDashboard = defineStore('dashboard', function defineDashboard()
       }
     }
 
-    warnPromiseFailures(
-      'driver close failure',
-      await Promise.allSettled(
-        closing.map(async (driver) => {
-          await driver.close()
-        })
-      )
-    )
-
     // Load any drivers that are new or are being replaced.
-    const loading: Promise<[DocumentId, Driver]>[] = []
+    const loading = new Array<[string, Driver]>()
     for (const switcher of switches.items) {
       const driver = loadedDrivers.get(switcher._id)
       if (driver == null || driver.uri !== switcher.path) {
-        loading.push(drivers.load(switcher.driverId, switcher.path).then((loaded) => [switcher._id, loaded]))
+        loading.push([switcher._id, drivers.load(switcher.driverId, switcher.path)])
       }
     }
 
     // Add the replaced and new driver to the loaded registry.
-    const loaded = await Promise.all(loading)
-    for (const [guid, driver] of loaded) {
+    for (const [guid, driver] of loading) {
       loadedDrivers.set(guid, driver)
     }
   }
diff --git a/src/renderer/services/driver.ts b/src/renderer/services/driver.ts
index 05ab39a..5228049 100644
--- a/src/renderer/services/driver.ts
+++ b/src/renderer/services/driver.ts
@@ -1,68 +1,72 @@
-import { createSharedComposable } from '@vueuse/core'
-import { readonly, computed, reactive } from 'vue'
+import { memo } from 'radash'
+import { computed, reactive, readonly, ref, shallowReadonly } from 'vue'
 import i18n from '../plugins/i18n'
+import { useClient } from './rpc'
 import { trackBusy } from './tracking'
+import type {
+  kDeviceHasNoExtraCapabilities as HasNoExtraCapabilities,
+  kDeviceSupportsMultipleOutputs as SupportsMultipleOutputs,
+  kDeviceCanDecoupleAudioOutput as CanDecoupleAudioOutput,
+  LocalizedDriverDescriptor
+} from '../../preload/api'
 
 /** The device has no extended capabilities. */
-export const kDeviceHasNoExtraCapabilities = services.driver.capabilities.kDeviceHasNoExtraCapabilities
+export type kDeviceHasNoExtraCapabilities = HasNoExtraCapabilities
+export const kDeviceHasNoExtraCapabilities: HasNoExtraCapabilities = 0
 /** The device has multiple output channels. */
-export const kDeviceSupportsMultipleOutputs = services.driver.capabilities.kDeviceSupportsMultipleOutputs
+export type kDeviceSupportsMultipleOutputs = SupportsMultipleOutputs
+export const kDeviceSupportsMultipleOutputs: SupportsMultipleOutputs = 1
 /** The device support sending the audio output to a different channel. */
-export const kDeviceCanDecoupleAudioOutput = services.driver.capabilities.kDeviceCanDecoupleAudioOutput
+export type kDeviceCanDecoupleAudioOutput = CanDecoupleAudioOutput
+export const kDeviceCanDecoupleAudioOutput: CanDecoupleAudioOutput = 2
 
 /** Informational metadata about a device and driver. */
-export interface DriverInformation {
+export interface DriverInformation extends LocalizedDriverDescriptor {
   /** A unique identifier for the driver. */
   readonly guid: string
-  /** Defines the title for the driver in a specific locale. */
-  readonly title: string
-  /** Defines the company for the driver in a specific locale. */
-  readonly company: string
-  /** Defines the provider for the driver in a specific locale. */
-  readonly provider: string
   /** Defines the capabilities of the device driven by the driver. */
   readonly capabilities: number
 }
 
 /** Driver to interact with switching devices. */
-export interface Driver {
+export interface Driver extends DriverInformation {
   /**
    * Sets input and output ties.
    *
-   * @param inputChannel The input channel to tie.
-   * @param videoOutputChannel The output video channel to tie.
-   * @param audioOutputChannel The output audio channel to tie.
+   * @param input The input channel to tie.
+   * @param videoOutput The output video channel to tie.
+   * @param audioOutput The output audio channel to tie.
    */
-  readonly activate: (inputChannel: number, videoOutputChannel: number, audioOutputChannel: number) => Promise<void>
+  readonly activate: (input: number, videoOutput: number, audioOutput: number) => Promise<void>
   /** Powers on the switch or monitor. */
   readonly powerOn: () => Promise<void>
   /** Powers on the switch or monitor and closes the driver. */
   readonly powerOff: () => Promise<void>
-  /** Closes the driver. */
-  readonly close: () => Promise<void>
   /** The URI to the device being driven. */
   readonly uri: string
 }
 
-/** Core parts of the drive system, so we don't hold any component's effect scope in memory. */
-const useDriverCore = createSharedComposable(function useDriverCore() {
-  const registry = reactive(new Map<string, DriverInformation>())
+const useDrivers = memo(function useDrivers() {
+  const client = useClient()
 
-  /** Loads the drivers, using the first i18n we can get. */
-  async function loadList() {
-    if (registry.size > 0) {
-      // Already loaded...
-      return
-    }
+  /** Busy tracking. */
+  const tracker = trackBusy()
+
+  /** The registered drivers. */
+  const items = ref(new Array<DriverInformation>())
+
+  const registry = new Map<string, DriverInformation>()
 
-    const drivers = await services.driver.list()
-    for (const { guid, localized, capabilities } of drivers) {
+  async function all() {
+    if (items.value.length > 0) return items.value
+    const drivers = await tracker.wait(client.drivers.all.query())
+    for (const {
+      metadata: { guid, localized, capabilities }
+    } of drivers) {
       /** The localized driver information made i18n compatible. */
       for (const [locale, description] of Object.entries(localized)) {
         i18n.global.mergeLocaleMessage(locale as never, {
-          $driver: {
-            [guid]: { ...description }
-          }
+          $driver: { [guid]: { ...description } }
         })
       }
 
@@ -83,66 +87,49 @@ const useDriverCore = createSharedComposable(function useDriverCore() {
         })
       )
     }
-  }
-
-  return { registry, loadList }
-})
 
-/** Use drivers. */
-export function useDrivers() {
-  const { registry, loadList } = useDriverCore()
-
-  /** Busy tracking. */
-  const tracker = trackBusy()
-
-  /** The registered drivers. */
-  const items = computed(() => Array.from(registry.values()))
-
-  /** Loads information about all the drivers. */
-  const all = tracker.track(async function all() {
-    await loadList()
-  })
+    items.value = Array.from(registry.values())
+    return items.value
+  }
 
-  /** Loads a driver registered in the registry. */
-  async function load(guid: string, path: string): Promise<Driver> {
-    await loadList()
-    if (!registry.has(guid)) {
-      throw new Error(`No such driver registered as "${guid}"`)
+  function load(guid: string, uri: string): Driver {
+    if (items.value.length === 0) {
+      throw new ReferenceError(`No driver "${guid}"`)
     }
 
-    const h = await services.driver.open(guid, path)
+    const driver = registry.get(guid)
+    if (driver == null) {
+      throw new ReferenceError(`No driver "${guid}"`)
+    }
 
-    async function activate(inputChannel: number, videoOutputChannel: number, audioOutputChannel: number) {
-      await services.driver.activate(h, inputChannel, videoOutputChannel, audioOutputChannel)
+    async function activate(input: number, videoOutput: number, audioOutput: number) {
+      await client.drivers.activate.mutate([guid, uri, input, videoOutput, audioOutput])
     }
 
     async function powerOn() {
-      await services.driver.powerOn(h)
+      await client.drivers.powerOn.mutate([guid, uri])
     }
 
     async function powerOff() {
-      await services.driver.powerOff(h)
-      await services.freeHandle(h)
-    }
-
-    async function close() {
-      await services.freeHandle(h)
+      await client.drivers.powerOff.mutate([guid, uri])
     }
 
     return readonly({
+      ...driver,
       activate,
       powerOn,
       powerOff,
-      close,
-      uri: path
+      uri
     })
   }
 
   return reactive({
     isBusy: tracker.isBusy,
     error: tracker.error,
-    items: computed(() => readonly(items.value)),
+    items: computed(() => shallowReadonly(items.value)),
     all,
     load
   })
-}
+})
+
+export default useDrivers
diff --git a/src/tests/drivers/extron/sis.test.ts b/src/tests/drivers/extron/sis.test.ts
index 41bc564..3a0769b 100644
--- a/src/tests/drivers/extron/sis.test.ts
+++ b/src/tests/drivers/extron/sis.test.ts
@@ -6,63 +6,28 @@ const port = await vi.hoisted(async () => await import('../../support/serial'))
 const stream = await vi.hoisted(async () => await import('../../support/stream'))
 
 beforeEach<MockStreamContext>(async (context) => {
-  vi.mock(import('electron'), mock.electronModule)
   vi.mock(import('electron-log'))
   vi.mock(import('serialport'), port.serialPortModule)
   vi.doMock(import('../../../main/services/stream'), stream.commandStreamModule(context))
-  await mock.bridgeCmdrBasics()
   await port.createMockPorts()
-  const { default: useDrivers } = await import('../../../main/services/driver')
-  const { default: registerDrivers } = await import('../../../main/plugins/drivers')
-  useDrivers()
-  registerDrivers()
 })
 
 afterEach(async () => {
-  await globalThis.services.freeAllHandles()
   await port.resetMockPorts()
+  vi.restoreAllMocks()
   vi.resetModules()
 })
 
 const kDriverGuid = '4C8F2838-C91D-431E-84DD-3666D14A6E2C'
 
 test('available', async () => {
-  const { kDeviceSupportsMultipleOutputs, kDeviceCanDecoupleAudioOutput } = await import(
-    '../../../main/services/driver'
-  )
-
-  // Raw list.
-  await expect(globalThis.services.driver.list()).resolves.toContainEqual({
-    enable: true,
-    guid: kDriverGuid,
-    localized: {
-      en: {
-        title: 'Extron SIS-compatible matrix switch',
-        company: 'Extron Electronics',
-        provider: 'BridgeCmdr contributors'
-      }
-    },
-    capabilities: kDeviceSupportsMultipleOutputs | kDeviceCanDecoupleAudioOutput
-  })
-
-  // Localized list.
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const drivers = useDrivers()
-  await expect(drivers.all()).resolves.toBeUndefined()
-  expect(drivers.items).toContainEqual({
-    guid: kDriverGuid,
-    title: 'Extron SIS-compatible matrix switch',
-    company: 'Extron Electronics',
-    provider: 'BridgeCmdr contributors',
-    capabilities: kDeviceSupportsMultipleOutputs | kDeviceCanDecoupleAudioOutput
-  })
-})
+  const { default: driver } = await import('../../../main/drivers/extron/sis')
+  const { default: useDrivers } = await import('../../../main/services/drivers')
 
-test('connect', async () => {
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const drivers = useDrivers()
 
-  await expect(load(kDriverGuid, 'port:/dev/ttyS0')).resolves.not.toBeNull()
+  await expect(drivers.all()).resolves.toContainEqual(driver)
+  await expect(drivers.get(kDriverGuid)).resolves.toEqual(driver)
 })
 
 test<MockStreamContext>('power on', async (context) => {
@@ -70,15 +35,14 @@ test<MockStreamContext>('power on', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power on is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOn()).resolves.toBeUndefined()
+  await expect(drivers.powerOn(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -87,15 +51,14 @@ test<MockStreamContext>('power off', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power off is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOff()).resolves.toBeUndefined()
+  await expect(drivers.powerOff(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -104,8 +67,8 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const input = 1
   const video = 2
@@ -118,7 +81,6 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream.withSequence().on(Buffer.from(command, 'ascii'), () => Buffer.from(response))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.activate(input, video, audio)).resolves.toBeUndefined()
+  await expect(drivers.activate(kDriverGuid, 'port:/dev/ttyS0', input, video, audio)).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
diff --git a/src/tests/drivers/sony/rs485.test.ts b/src/tests/drivers/sony/rs485.test.ts
index f2e5419..ba9870c 100644
--- a/src/tests/drivers/sony/rs485.test.ts
+++ b/src/tests/drivers/sony/rs485.test.ts
@@ -6,61 +6,28 @@ const port = await vi.hoisted(async () => await import('../../support/serial'))
 const stream = await vi.hoisted(async () => await import('../../support/stream'))
 
 beforeEach<MockStreamContext>(async (context) => {
-  vi.mock(import('electron'), mock.electronModule)
   vi.mock(import('electron-log'))
   vi.mock(import('serialport'), port.serialPortModule)
   vi.doMock(import('../../../main/services/stream'), stream.commandStreamModule(context))
-  await mock.bridgeCmdrBasics()
   await port.createMockPorts()
-  const { default: useDrivers } = await import('../../../main/services/driver')
-  const { default: registerDrivers } = await import('../../../main/plugins/drivers')
-  useDrivers()
-  registerDrivers()
 })
 
 afterEach(async () => {
-  await globalThis.services.freeAllHandles()
   await port.resetMockPorts()
+  vi.restoreAllMocks()
   vi.resetModules()
 })
 
 const kDriverGuid = '8626D6D3-C211-4D21-B5CC-F5E3B50D9FF0'
 
 test('available', async () => {
-  const { kDeviceHasNoExtraCapabilities } = await import('../../../main/services/driver')
-
-  // Raw list.
-  await expect(globalThis.services.driver.list()).resolves.toContainEqual({
-    enable: true,
-    guid: kDriverGuid,
-    localized: {
-      en: {
-        title: 'Sony RS-485 controllable monitor',
-        company: 'Sony Corporation',
-        provider: 'BridgeCmdr contributors'
-      }
-    },
-    capabilities: kDeviceHasNoExtraCapabilities
-  })
-
-  // Localized list.
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const drivers = useDrivers()
-  await expect(drivers.all()).resolves.toBeUndefined()
-  expect(drivers.items).toContainEqual({
-    guid: kDriverGuid,
-    title: 'Sony RS-485 controllable monitor',
-    company: 'Sony Corporation',
-    provider: 'BridgeCmdr contributors',
-    capabilities: kDeviceHasNoExtraCapabilities
-  })
-})
+  const { default: driver } = await import('../../../main/drivers/sony/rs485')
+  const { default: useDrivers } = await import('../../../main/services/drivers')
 
-test('connect', async () => {
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const drivers = useDrivers()
 
-  await expect(load(kDriverGuid, 'port:/dev/ttyS0')).resolves.not.toBeNull()
+  await expect(drivers.all()).resolves.toContainEqual(driver)
+  await expect(drivers.get(kDriverGuid)).resolves.toEqual(driver)
 })
 
 test<MockStreamContext>('power on', async (context) => {
@@ -68,8 +35,8 @@ test<MockStreamContext>('power on', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const command = Buffer.of(0x02, 4, 0xc0, 0xc0, 0x29, 0x3e, 0x15)
   // Packet type: Command == 0x02
@@ -82,8 +49,7 @@ test<MockStreamContext>('power on', async (context) => {
 
   context.stream.withSequence().on(command, () => Buffer.from('Good'))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOn()).resolves.toBeUndefined()
+  await expect(drivers.powerOn(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -92,8 +58,8 @@ test<MockStreamContext>('power off', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const commnad = Buffer.of(0x02, 4, 0xc0, 0xc0, 0x2a, 0x3e, 0x14)
   // Packet type: Command == 0x02
@@ -106,8 +72,7 @@ test<MockStreamContext>('power off', async (context) => {
 
   context.stream.withSequence().on(commnad, () => Buffer.from('Good'))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOff()).resolves.toBeUndefined()
+  await expect(drivers.powerOff(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -116,8 +81,8 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const input = 1
   const command = Buffer.of(0x02, 6, 0xc0, 0xc0, 0x21, 0x00, input, 0x01, 0x57)
@@ -133,7 +98,6 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream.withSequence().on(command, () => Buffer.from('Good'))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.activate(input, 0, 0)).resolves.toBeUndefined()
+  await expect(drivers.activate(kDriverGuid, 'port:/dev/ttyS0', input, 0, 0)).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
diff --git a/src/tests/drivers/tesla-smart/kvm.test.ts b/src/tests/drivers/tesla-smart/kvm.test.ts
index 0c458e9..37336b7 100644
--- a/src/tests/drivers/tesla-smart/kvm.test.ts
+++ b/src/tests/drivers/tesla-smart/kvm.test.ts
@@ -6,61 +6,28 @@ const port = await vi.hoisted(async () => await import('../../support/serial'))
 const stream = await vi.hoisted(async () => await import('../../support/stream'))
 
 beforeEach<MockStreamContext>(async (context) => {
-  vi.mock(import('electron'), mock.electronModule)
   vi.mock(import('electron-log'))
   vi.mock(import('serialport'), port.serialPortModule)
   vi.doMock(import('../../../main/services/stream'), stream.commandStreamModule(context))
-  await mock.bridgeCmdrBasics()
   await port.createMockPorts()
-  const { default: useDrivers } = await import('../../../main/services/driver')
-  const { default: registerDrivers } = await import('../../../main/plugins/drivers')
-  useDrivers()
-  registerDrivers()
 })
 
 afterEach(async () => {
-  await globalThis.services.freeAllHandles()
   await port.resetMockPorts()
+  vi.restoreAllMocks()
   vi.resetModules()
 })
 
 const kDriverGuid = '91D5BC95-A8E2-4F58-BCAC-A77BA1054D61'
 
 test('available', async () => {
-  const { kDeviceHasNoExtraCapabilities } = await import('../../../main/services/driver')
-
-  // Raw list.
-  await expect(globalThis.services.driver.list()).resolves.toContainEqual({
-    enable: true,
-    guid: kDriverGuid,
-    localized: {
-      en: {
-        title: 'Tesla smart KVM-compatible switch',
-        company: 'Tesla Elec Technology Co.,Ltd',
-        provider: 'BridgeCmdr contributors'
-      }
-    },
-    capabilities: kDeviceHasNoExtraCapabilities
-  })
-
-  // Localized list.
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const drivers = useDrivers()
-  await expect(drivers.all()).resolves.toBeUndefined()
-  expect(drivers.items).toContainEqual({
-    guid: kDriverGuid,
-    title: 'Tesla smart KVM-compatible switch',
-    company: 'Tesla Elec Technology Co.,Ltd',
-    provider: 'BridgeCmdr contributors',
-    capabilities: kDeviceHasNoExtraCapabilities
-  })
-})
+  const { default: driver } = await import('../../../main/drivers/tesla-smart/kvm')
+  const { default: useDrivers } = await import('../../../main/services/drivers')
 
-test('connect', async () => {
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const drivers = useDrivers()
 
-  await expect(load(kDriverGuid, 'port:/dev/ttyS0')).resolves.not.toBeNull()
+  await expect(drivers.all()).resolves.toContainEqual(driver)
+  await expect(drivers.get(kDriverGuid)).resolves.toEqual(driver)
 })
 
 test<MockStreamContext>('power on', async (context) => {
@@ -68,15 +35,14 @@ test<MockStreamContext>('power on', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power on is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOn()).resolves.toBeUndefined()
+  await expect(drivers.powerOn(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -85,15 +51,14 @@ test<MockStreamContext>('power off', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power off is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOff()).resolves.toBeUndefined()
+  await expect(drivers.powerOff(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -102,15 +67,14 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const input = 1
   const command = Buffer.of(0xaa, 0xbb, 0x03, 0x01, input, 0xee)
 
   context.stream.withSequence().on(Buffer.from(command), () => Buffer.from('Good'))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.activate(input, 0, 0)).resolves.toBeUndefined()
+  await expect(drivers.activate(kDriverGuid, 'port:/dev/ttyS0', input, 0, 0)).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
diff --git a/src/tests/drivers/tesla-smart/matrix.test.ts b/src/tests/drivers/tesla-smart/matrix.test.ts
index 888cd5a..413c646 100644
--- a/src/tests/drivers/tesla-smart/matrix.test.ts
+++ b/src/tests/drivers/tesla-smart/matrix.test.ts
@@ -6,61 +6,28 @@ const port = await vi.hoisted(async () => await import('../../support/serial'))
 const stream = await vi.hoisted(async () => await import('../../support/stream'))
 
 beforeEach<MockStreamContext>(async (context) => {
-  vi.mock(import('electron'), mock.electronModule)
   vi.mock(import('electron-log'))
   vi.mock(import('serialport'), port.serialPortModule)
   vi.doMock(import('../../../main/services/stream'), stream.commandStreamModule(context))
-  await mock.bridgeCmdrBasics()
   await port.createMockPorts()
-  const { default: useDrivers } = await import('../../../main/services/driver')
-  const { default: registerDrivers } = await import('../../../main/plugins/drivers')
-  useDrivers()
-  registerDrivers()
 })
 
 afterEach(async () => {
-  await globalThis.services.freeAllHandles()
   await port.resetMockPorts()
+  vi.restoreAllMocks()
   vi.resetModules()
 })
 
 const kDriverGuid = '671824ED-0BC4-43A6-85CC-4877890A7722'
 
 test('available', async () => {
-  const { kDeviceSupportsMultipleOutputs } = await import('../../../main/services/driver')
-
-  // Raw list.
-  await expect(globalThis.services.driver.list()).resolves.toContainEqual({
-    enable: true,
-    guid: kDriverGuid,
-    localized: {
-      en: {
-        title: 'Tesla smart matrix-compatible switch',
-        company: 'Tesla Elec Technology Co.,Ltd',
-        provider: 'BridgeCmdr contributors'
-      }
-    },
-    capabilities: kDeviceSupportsMultipleOutputs
-  })
-
-  // Localized list.
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const drivers = useDrivers()
-  await expect(drivers.all()).resolves.toBeUndefined()
-  expect(drivers.items).toContainEqual({
-    guid: kDriverGuid,
-    title: 'Tesla smart matrix-compatible switch',
-    company: 'Tesla Elec Technology Co.,Ltd',
-    provider: 'BridgeCmdr contributors',
-    capabilities: kDeviceSupportsMultipleOutputs
-  })
-})
+  const { default: driver } = await import('../../../main/drivers/tesla-smart/matrix')
+  const { default: useDrivers } = await import('../../../main/services/drivers')
 
-test('connect', async () => {
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const drivers = useDrivers()
 
-  await expect(load(kDriverGuid, 'port:/dev/ttyS0')).resolves.not.toBeNull()
+  await expect(drivers.all()).resolves.toContainEqual(driver)
+  await expect(drivers.get(kDriverGuid)).resolves.toEqual(driver)
 })
 
 test<MockStreamContext>('power on', async (context) => {
@@ -68,15 +35,14 @@ test<MockStreamContext>('power on', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power on is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOn()).resolves.toBeUndefined()
+  await expect(drivers.powerOn(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -85,15 +51,14 @@ test<MockStreamContext>('power off', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power off is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOff()).resolves.toBeUndefined()
+  await expect(drivers.powerOff(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -102,8 +67,8 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const input = 1
   const output = 2
@@ -113,7 +78,6 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream.withSequence().on(Buffer.from(command, 'ascii'), () => Buffer.from('Good'))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.activate(input, output, 0)).resolves.toBeUndefined()
+  await expect(drivers.activate(kDriverGuid, 'port:/dev/ttyS0', input, output, 0)).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
diff --git a/src/tests/drivers/tesla-smart/sdi.test.ts b/src/tests/drivers/tesla-smart/sdi.test.ts
index 8820907..7eac0a7 100644
--- a/src/tests/drivers/tesla-smart/sdi.test.ts
+++ b/src/tests/drivers/tesla-smart/sdi.test.ts
@@ -6,61 +6,28 @@ const port = await vi.hoisted(async () => await import('../../support/serial'))
 const stream = await vi.hoisted(async () => await import('../../support/stream'))
 
 beforeEach<MockStreamContext>(async (context) => {
-  vi.mock(import('electron'), mock.electronModule)
   vi.mock(import('electron-log'))
   vi.mock(import('serialport'), port.serialPortModule)
   vi.doMock(import('../../../main/services/stream'), stream.commandStreamModule(context))
-  await mock.bridgeCmdrBasics()
   await port.createMockPorts()
-  const { default: useDrivers } = await import('../../../main/services/driver')
-  const { default: registerDrivers } = await import('../../../main/plugins/drivers')
-  useDrivers()
-  registerDrivers()
 })
 
 afterEach(async () => {
-  await globalThis.services.freeAllHandles()
   await port.resetMockPorts()
+  vi.restoreAllMocks()
   vi.resetModules()
 })
 
 const kDriverGuid = 'DDB13CBC-ABFC-405E-9EA6-4A999F9A16BD'
 
 test('available', async () => {
-  const { kDeviceHasNoExtraCapabilities } = await import('../../../main/services/driver')
-
-  // Raw list.
-  await expect(globalThis.services.driver.list()).resolves.toContainEqual({
-    enable: true,
-    guid: kDriverGuid,
-    localized: {
-      en: {
-        title: 'Tesla smart SDI-compatible switch',
-        company: 'Tesla Elec Technology Co.,Ltd',
-        provider: 'BridgeCmdr contributors'
-      }
-    },
-    capabilities: kDeviceHasNoExtraCapabilities
-  })
-
-  // Localized list.
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const drivers = useDrivers()
-  await expect(drivers.all()).resolves.toBeUndefined()
-  expect(drivers.items).toContainEqual({
-    guid: kDriverGuid,
-    title: 'Tesla smart SDI-compatible switch',
-    company: 'Tesla Elec Technology Co.,Ltd',
-    provider: 'BridgeCmdr contributors',
-    capabilities: kDeviceHasNoExtraCapabilities
-  })
-})
+  const { default: driver } = await import('../../../main/drivers/tesla-smart/sdi')
+  const { default: useDrivers } = await import('../../../main/services/drivers')
 
-test('connect', async () => {
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const drivers = useDrivers()
 
-  await expect(load(kDriverGuid, 'port:/dev/ttyS0')).resolves.not.toBeNull()
+  await expect(drivers.all()).resolves.toContainEqual(driver)
+  await expect(drivers.get(kDriverGuid)).resolves.toEqual(driver)
 })
 
 test<MockStreamContext>('power on', async (context) => {
@@ -68,15 +35,14 @@ test<MockStreamContext>('power on', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power on is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOn()).resolves.toBeUndefined()
+  await expect(drivers.powerOn(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -85,15 +51,14 @@ test<MockStreamContext>('power off', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   // Power off is a no-op
 
   context.stream.withSequence()
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.powerOff()).resolves.toBeUndefined()
+  await expect(drivers.powerOff(kDriverGuid, 'port:/dev/ttyS0')).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
 
@@ -102,15 +67,14 @@ test<MockStreamContext>('activate tie', async (context) => {
 
   context.stream = new stream.MockCommandStream()
 
-  const { useDrivers } = await import('../../../renderer/services/driver')
-  const { load } = useDrivers()
+  const { default: useDrivers } = await import('../../../main/services/drivers')
+  const drivers = useDrivers()
 
   const input = 1
   const command = Buffer.of(0xaa, 0xcc, 0x01, input)
 
   context.stream.withSequence().on(Buffer.from(command), () => Buffer.from('Good'))
 
-  const driver = await load(kDriverGuid, 'port:/dev/ttyS0')
-  await expect(driver.activate(input, 0, 0)).resolves.toBeUndefined()
+  await expect(drivers.activate(kDriverGuid, 'port:/dev/ttyS0', input, 0, 0)).resolves.toBeUndefined()
   context.stream.sequence.expectDone()
 })
diff --git a/src/tests/level.test.ts b/src/tests/level.test.ts
index c9bd4b5..7f9b44e 100644
--- a/src/tests/level.test.ts
+++ b/src/tests/level.test.ts
@@ -2,14 +2,13 @@ import { test, expect, vi, beforeEach, afterEach } from 'vitest'
 
 const mock = await vi.hoisted(async () => await import('./support/mock'))
 
-beforeEach(async () => {
+beforeEach(() => {
   vi.mock(import('electron'), mock.electronModule)
   vi.mock(import('electron-log'))
-  await mock.bridgeCmdrBasics()
 })
 
-afterEach(async () => {
-  await globalThis.services.freeAllHandles()
+afterEach(() => {
+  vi.restoreAllMocks()
   vi.resetModules()
 })
 
diff --git a/src/tests/support/mock.ts b/src/tests/support/mock.ts
index 0161142..51db37c 100644
--- a/src/tests/support/mock.ts
+++ b/src/tests/support/mock.ts
@@ -67,24 +67,6 @@ export async function electronModule(original: () => Promise<typeof import('elec
 //   }
 // }
 
-export function electronProcess() {
-  vi.stubGlobal('process', {
-    ...globalThis.process,
-    get browser() {
-      return true
-    },
-    get contextIsolated() {
-      return true
-    },
-    get versions() {
-      return {
-        chrome: '126.0.6478.185',
-        electron: '31.3.1'
-      }
-    }
-  })
-}
-
 export function console() {
   const error = vi.spyOn(globalThis.console, 'error').mockReturnValue()
   const warn = vi.spyOn(globalThis.console, 'warn').mockReturnValue()
@@ -100,27 +82,3 @@ export function console() {
     debug
   }
 }
-
-/** Mocks DOM global EventTarget. */
-export async function globalEventTarget() {
-  const { default: autoBind } = await import('auto-bind')
-
-  for (const [prop, value] of Object.entries(autoBind(new EventTarget()))) {
-    vi.stubGlobal(prop, value)
-  }
-}
-
-export function bridgeCmdrEnv() {
-  vi.stubEnv('rpc_url_', 'http://127.0.0.1:7180')
-}
-
-export async function bridgeCmdrBasics() {
-  electronProcess()
-  await globalEventTarget()
-  bridgeCmdrEnv()
-
-  await import('../../preload/index')
-
-  const { default: useHandles } = await import('../../main/services/handle')
-  useHandles()
-}

From 0730caf2897c8f4f2a19688197844eddabcc25fc Mon Sep 17 00:00:00 2001
From: Matthew Holder <sixxgate@hotmail.com>
Date: Sat, 2 Nov 2024 01:18:49 -0500
Subject: [PATCH 2/3] Added websocket support for tRPC and ported updated
 system over

---
 PLAN.md                                 |  3 +-
 package.json                            |  4 ++
 src/core/url.ts                         | 25 +++++++
 src/main/dao/storage.ts                 |  1 -
 src/main/info/config.ts                 |  2 +-
 src/main/main.ts                        |  2 -
 src/main/routes/router.ts               |  4 +-
 src/main/routes/updater.ts              | 46 +++++++++++++
 src/main/server.ts                      | 67 ++++++++++++-------
 src/main/services/updater.ts            | 87 ++++++++++++-------------
 src/preload/api.d.ts                    | 22 ++-----
 src/preload/plugins/services.ts         |  4 +-
 src/preload/plugins/services/updates.ts | 20 ------
 src/renderer/services/appUpdates.ts     | 41 ++++++++++--
 src/renderer/services/rpc.ts            | 30 +++++++--
 yarn.lock                               | 26 ++++++++
 16 files changed, 255 insertions(+), 129 deletions(-)
 create mode 100644 src/core/url.ts
 create mode 100644 src/main/routes/updater.ts
 delete mode 100644 src/preload/plugins/services/updates.ts

diff --git a/PLAN.md b/PLAN.md
index 41658be..a5fd4f1 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -3,10 +3,9 @@
     - Switch the majority of the IPC using tRPC.
       - Updater will require websocket for subscriptions.
       - System will require moving the open and save file support to DOM APIs.
-      - Drivers will require an overhaul to no longer need handles.
+      - Wrap some Electron APIs as services for easier mocking without electron itself.
     - More drivers.
     - Move more modules to core.
-    - Wrap some Electron APIs as services for easier mocking without electron itself.
 - Drivers
   - Shinybow
   - Monoprice Blackbird
diff --git a/package.json b/package.json
index 54434a1..937c1d5 100644
--- a/package.json
+++ b/package.json
@@ -80,6 +80,7 @@
     "@types/pouchdb-find": "^7.3.3",
     "@types/setimmediate": "^1.0.4",
     "@types/uuid": "^10.0.0",
+    "@types/ws": "^8.5.12",
     "@typescript-eslint/eslint-plugin": "^8.12.2",
     "@typescript-eslint/parser": "^8.12.2",
     "@vitejs/plugin-vue": "^5.1.4",
@@ -96,6 +97,7 @@
     "assert": "^2.1.0",
     "auto-bind": "^5.0.1",
     "buffer": "^6.0.3",
+    "bufferutil": "^4.0.8",
     "duplexify": "^4.1.3",
     "electron": "^31.7.3",
     "electron-builder": "^24.13.3",
@@ -134,6 +136,7 @@
     "type-fest": "^4.26.1",
     "typescript": "^5.6.3",
     "typescript-eslint-parser-for-extra-files": "^0.7.0",
+    "utf-8-validate": "^6.0.5",
     "util": "^0.12.5",
     "uuid": "^11.0.2",
     "vite": "^5.4.10",
@@ -147,6 +150,7 @@
     "vue-router": "^4.4.5",
     "vue-tsc": "^2.1.10",
     "vuetify": "^3.7.3",
+    "ws": "^8.18.0",
     "xdg-basedir": "^5.1.0",
     "zod": "^3.23.8"
   },
diff --git a/src/core/url.ts b/src/core/url.ts
new file mode 100644
index 0000000..bf1aafe
--- /dev/null
+++ b/src/core/url.ts
@@ -0,0 +1,25 @@
+const protocol = ['http:', 'ws:'] as const
+const support = protocol.join(',')
+export type Protocol = (typeof protocol)[number]
+
+function isSupportedProtocol(value: unknown): value is Protocol {
+  return protocol.includes(value as never)
+}
+
+export type ServerSettings = [host: string, port: number, protocol: Protocol]
+
+export function getServerUrl(url: URL, defaultPort: number) {
+  if (!isSupportedProtocol(url.protocol)) throw new TypeError(`${url.protocol} is not supported; only ${support}`)
+  if (url.pathname.length > 1) throw new TypeError('Server must be at the root')
+  if (url.search.length > 0) throw new TypeError('Query parameters mean nothing')
+  if (url.hash.length > 0) throw new TypeError('Query parameters mean nothing')
+  if (url.username.length > 0) throw new TypeError('Username currently unsupported')
+  if (url.password.length > 0) throw new TypeError('Password currently unsupported')
+  if (url.hostname.length === 0) return ['127.0.0.1', defaultPort, url.protocol] satisfies ServerSettings
+  if (url.port.length === 0) return [url.hostname, defaultPort, url.protocol] satisfies ServerSettings
+
+  const port = Number(url.port)
+  if (Number.isNaN(port)) throw new TypeError(`${url.port} is not a valid port`)
+
+  return [url.hostname, port, url.protocol] satisfies ServerSettings
+}
diff --git a/src/main/dao/storage.ts b/src/main/dao/storage.ts
index 5e84a9a..5d76638 100644
--- a/src/main/dao/storage.ts
+++ b/src/main/dao/storage.ts
@@ -1,7 +1,6 @@
 import { memo } from 'radash'
 import { useLevelDb } from '../services/level'
 
-export type UserStore = ReturnType<typeof useUserStore>
 const useUserStore = memo(function useUserStore() {
   const { levelup } = useLevelDb()
 
diff --git a/src/main/info/config.ts b/src/main/info/config.ts
index 19bb043..f54aebf 100644
--- a/src/main/info/config.ts
+++ b/src/main/info/config.ts
@@ -4,7 +4,7 @@ import type { ReadonlyDeep } from 'type-fest'
 export type AppConfig = ReadonlyDeep<ReturnType<typeof useAppConfig>>
 const useAppConfig = memo(function useAppConfig() {
   const config = {
-    rpcUrl: 'http://127.0.0.1:7180'
+    rpcUrl: 'ws://127.0.0.1:7180'
   }
 
   process.env['rpc_url_'] = config.rpcUrl
diff --git a/src/main/main.ts b/src/main/main.ts
index ac1637e..fe9e057 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -8,7 +8,6 @@ import useAppConfig from './info/config'
 import useCrypto from './plugins/webcrypto'
 import useApiServer from './server'
 import useSystem from './services/system'
-import useUpdater from './services/updater'
 import { logError } from './utilities'
 import { waitTill } from '@/basics'
 import { toError } from '@/error-handling'
@@ -131,7 +130,6 @@ useAppConfig()
 
 useApiServer()
 useCrypto()
-useUpdater()
 useSystem()
 
 await createWindow()
diff --git a/src/main/routes/router.ts b/src/main/routes/router.ts
index 1329e95..fde0e64 100644
--- a/src/main/routes/router.ts
+++ b/src/main/routes/router.ts
@@ -9,6 +9,7 @@ import useTiesRouter from './data/ties'
 import useDriversRouter from './drivers'
 import useSerialPortRouter from './ports'
 import useStartupRouter from './startup'
+import useUpdaterRouter from './updater'
 
 export const useAppRouter = memo(() =>
   router({
@@ -23,7 +24,8 @@ export const useAppRouter = memo(() =>
     storage: useUserStoreRouter(),
     ties: useTiesRouter(),
     switches: useSwitchesRouter(),
-    sources: useSourcesRouter()
+    sources: useSourcesRouter(),
+    updates: useUpdaterRouter()
   })
 )
 
diff --git a/src/main/routes/updater.ts b/src/main/routes/updater.ts
new file mode 100644
index 0000000..ab8e23e
--- /dev/null
+++ b/src/main/routes/updater.ts
@@ -0,0 +1,46 @@
+import { observable } from '@trpc/server/observable'
+import { memo } from 'radash'
+import { procedure, router } from '../services/trpc'
+import useUpdater from '../services/updater'
+import type { AppUpdaterEventMap } from '../services/updater'
+
+const useUpdaterRouter = memo(function useUpdaterRouter() {
+  const updater = useUpdater()
+
+  function defineEvent<Name extends keyof AppUpdaterEventMap>(name: Name) {
+    type Args = AppUpdaterEventMap[Name]
+    // TODO: tRPC 11, use "for await...of on" with AbortSignal
+    return () =>
+      observable<Args>((emit) => {
+        const proxy = (...args: Args) => {
+          emit.next(args)
+        }
+
+        updater.on(name, proxy as never)
+
+        return () => {
+          updater.off(name, proxy as never)
+        }
+      })
+  }
+
+  return router({
+    onChecking: procedure.subscription(defineEvent('checking')),
+    onAvailable: procedure.subscription(defineEvent('available')),
+    onProgress: procedure.subscription(defineEvent('progress')),
+    onDownloaded: procedure.subscription(defineEvent('downloaded')),
+    onCancelled: procedure.subscription(defineEvent('cancelled')),
+    checkForUpdates: procedure.query(async () => await updater.checkForUpdates()),
+    downloadUpdate: procedure.query(async () => {
+      await updater.downloadUpdate()
+    }),
+    cancelUpdate: procedure.query(async () => {
+      await updater.cancelUpdate()
+    }),
+    installUpdate: procedure.query(async () => {
+      await updater.installUpdate()
+    })
+  })
+})
+
+export default useUpdaterRouter
diff --git a/src/main/server.ts b/src/main/server.ts
index f9a8784..a558b38 100644
--- a/src/main/server.ts
+++ b/src/main/server.ts
@@ -1,42 +1,61 @@
 import { createHTTPServer } from '@trpc/server/adapters/standalone'
+import { applyWSSHandler } from '@trpc/server/adapters/ws'
 import Logger from 'electron-log'
+import { WebSocketServer } from 'ws'
 import useAppConfig from './info/config'
 import { useAppRouter } from './routes/router'
+import { getServerUrl } from '@/url'
 
-function getServerUrl(url: URL): [host: string, port: number] {
-  Logger.log(url.pathname)
-  if (url.protocol !== 'http:') throw new TypeError('Only HTTP is supported')
-  if (url.pathname.length > 1) throw new TypeError('Server must be at the root')
-  if (url.search.length > 0) throw new TypeError('Query parameters mean nothing')
-  if (url.hash.length > 0) throw new TypeError('Query parameters mean nothing')
-  if (url.username.length > 0) throw new TypeError('Username currently unsupported')
-  if (url.password.length > 0) throw new TypeError('Password currently unsupported')
-  if (url.hostname.length === 0) return ['127.0.0.1', 7180]
-  if (url.port.length === 0) return [url.hostname, 7180]
-
-  const port = Number(url.port)
-  if (Number.isNaN(port)) throw new TypeError(`${url.port} is not a valid port`)
-
-  return [url.hostname, port]
-}
+function startWebSocketServer(url: URL, host: string, port: number) {
+  // TODO: Authentication via the IPC, later we'll implement a proper authentication model.
 
-export default function useApiServer() {
-  const config = useAppConfig()
-  const url = new URL(config.rpcUrl)
-  const [host, port] = getServerUrl(url)
+  process.env['WS_NO_UTF_8_VALIDATE'] = '1'
+
+  const wss = new WebSocketServer({ host, port })
+
+  wss.on('listening', () => {
+    Logger.info(`RPC server at ${url}`)
+  })
+
+  const handler = applyWSSHandler({ wss, router: useAppRouter() })
+
+  process.on('exit', () => {
+    handler.broadcastReconnectNotification()
+    wss.close()
+  })
+
+  process.on('SIGTERM', () => {
+    handler.broadcastReconnectNotification()
+    wss.close()
+  })
+}
 
+function startHttpServer(url: URL, host: string, port: number) {
   // TODO: Authentication via the IPC, later we'll implement a proper authentication model.
 
-  const httpServer = createHTTPServer({
+  const server = createHTTPServer({
     router: useAppRouter()
   })
 
-  httpServer.listen(port, host)
-  httpServer.server.on('listening', () => {
+  server.server.on('listening', () => {
     Logger.info(`RPC server at ${url}`)
   })
 
+  server.listen(port, host)
   process.on('exit', () => {
-    httpServer.server.close()
+    server.server.close()
+  })
+
+  process.on('SIGTERM', () => {
+    server.server.close()
   })
 }
+
+export default function useApiServer() {
+  const config = useAppConfig()
+  const url = new URL(config.rpcUrl)
+  const [host, port, protocol] = getServerUrl(url, 7180)
+
+  if (protocol === 'http:') startHttpServer(url, host, port)
+  if (protocol === 'ws:') startWebSocketServer(url, host, port)
+}
diff --git a/src/main/services/updater.ts b/src/main/services/updater.ts
index feb196d..2f5591d 100644
--- a/src/main/services/updater.ts
+++ b/src/main/services/updater.ts
@@ -2,19 +2,23 @@ import EventEmitter from 'node:events'
 import { writeFile } from 'node:fs/promises'
 import { resolve as resolvePath } from 'node:path'
 import autoBind from 'auto-bind'
-import { app, ipcMain } from 'electron'
+import { app } from 'electron'
 import { autoUpdater } from 'electron-updater'
 import { memo } from 'radash'
-import { ipcHandle, ipcProxy, isNodeError, logError } from '../utilities'
-import type { AppUpdater } from '../../preload/api'
-import type { WebContents } from 'electron'
-import type { UpdateCheckResult, ProgressInfo, CancellationToken } from 'electron-updater'
+import { isNodeError, logError } from '../utilities'
+import type { UpdateCheckResult, ProgressInfo, CancellationToken, UpdateInfo } from 'electron-updater'
 
-interface AppAutoUpdaterEventMap {
+export type { UpdateInfo, ProgressInfo } from 'electron-updater'
+
+export interface AppUpdaterEventMap {
+  checking: []
+  available: [info: UpdateInfo | null]
   progress: [progress: ProgressInfo]
+  downloaded: [info: UpdateInfo]
+  cancelled: []
 }
 
-type ProgressHandler = (...args: AppAutoUpdaterEventMap['progress']) => void
+export type AppUpdater = ReturnType<typeof useUpdater>
 
 const useUpdater = memo(function useUpdater() {
   /** The internal application updater for AppImage. */
@@ -28,14 +32,33 @@ const useUpdater = memo(function useUpdater() {
   /**
    * Application auto update.
    *
-   * We are using a class for EventEmitter's sake, it wants to return this which must be compatible with the API.
+   * We are using a class for EventEmitter's sake.
    */
-  class AppAutoUpdater extends EventEmitter<AppAutoUpdaterEventMap> implements AppUpdater {
+  class AppUpdater extends EventEmitter<AppUpdaterEventMap> {
     #checkPromise: Promise<UpdateCheckResult | null> | undefined = undefined
     #cancelToken: CancellationToken | undefined = undefined
     #donwloadPromise: Promise<string[]> | undefined = undefined
 
-    async #getUpdateInfo() {
+    constructor() {
+      super()
+      autoUpdater.on('checking-for-update', () => {
+        this.emit('checking')
+      })
+      autoUpdater.on('update-available', (info) => {
+        this.emit('available', info)
+      })
+      autoUpdater.on('update-not-available', () => {
+        this.emit('available', null)
+      })
+      autoUpdater.on('update-downloaded', (info) => {
+        this.emit('downloaded', info)
+      })
+      autoUpdater.on('update-cancelled', () => {
+        this.emit('cancelled')
+      })
+    }
+
+    private async getUpdateInfo() {
       if (this.#checkPromise == null) {
         throw logError(new ReferenceError('Cannot get update information, no check in progress'))
       }
@@ -45,7 +68,7 @@ const useUpdater = memo(function useUpdater() {
         result = await this.#checkPromise
       } catch (cause) {
         if (isNodeError(cause) && cause.code === 'ENOENT') {
-          return undefined
+          return null
         }
 
         throw cause
@@ -53,7 +76,7 @@ const useUpdater = memo(function useUpdater() {
 
       // If the result is null or the cancel token is null, no update is avilable.
       if (result?.cancellationToken == null) {
-        return undefined
+        return null
       }
 
       this.#cancelToken = result.cancellationToken
@@ -61,7 +84,7 @@ const useUpdater = memo(function useUpdater() {
       return result.updateInfo
     }
 
-    async #checkForUpdates() {
+    private async attemptCheckForUpdates() {
       if (this.#donwloadPromise != null) {
         throw logError(new ReferenceError('Update download already in progress'))
       }
@@ -76,7 +99,7 @@ const useUpdater = memo(function useUpdater() {
 
         this.#checkPromise = autoUpdater.checkForUpdates()
 
-        return await this.#getUpdateInfo()
+        return await this.getUpdateInfo()
       } finally {
         this.#checkPromise = undefined
       }
@@ -84,13 +107,13 @@ const useUpdater = memo(function useUpdater() {
 
     async checkForUpdates() {
       if (this.#checkPromise != null) {
-        return await this.#getUpdateInfo()
+        return await this.getUpdateInfo()
       }
 
-      return await this.#checkForUpdates()
+      return await this.attemptCheckForUpdates()
     }
 
-    async #downloadUpdate() {
+    private async attemptDownloadUpdate() {
       if (this.#cancelToken == null) {
         throw logError(new ReferenceError('No update available for download, check first'))
       }
@@ -112,7 +135,7 @@ const useUpdater = memo(function useUpdater() {
       if (this.#donwloadPromise != null) {
         await this.#donwloadPromise
       } else {
-        await this.#downloadUpdate()
+        await this.attemptDownloadUpdate()
       }
     }
 
@@ -133,33 +156,7 @@ const useUpdater = memo(function useUpdater() {
     }
   }
 
-  const updater = autoBind(new AppAutoUpdater())
-
-  const downloadWaiters = new WeakMap<WebContents, ProgressHandler>()
-  const remoteDownloadUpdate = ipcHandle(async function remoteDownloadUpdate(ev) {
-    let handler = downloadWaiters.get(ev.sender)
-    if (handler == null) {
-      handler = (progress) => {
-        ev.sender.send('update:download:progress', progress)
-      }
-      downloadWaiters.set(ev.sender, handler)
-    }
-
-    try {
-      updater.on('progress', handler)
-      await updater.downloadUpdate()
-    } finally {
-      updater.off('progress', handler)
-      downloadWaiters.delete(ev.sender)
-    }
-  })
-
-  ipcMain.handle('update:check', ipcProxy(updater.checkForUpdates.bind(updater)))
-  ipcMain.handle('update:download', remoteDownloadUpdate)
-  ipcMain.handle('update:cancel', ipcProxy(updater.cancelUpdate.bind(updater)))
-  ipcMain.handle('update:install', ipcProxy(updater.installUpdate.bind(updater)))
-
-  return updater
+  return autoBind(new AppUpdater())
 })
 
 export default useUpdater
diff --git a/src/preload/api.d.ts b/src/preload/api.d.ts
index 0882e79..86aae18 100644
--- a/src/preload/api.d.ts
+++ b/src/preload/api.d.ts
@@ -4,24 +4,25 @@ import type { ArrayTail, ReadonlyDeep, Tagged } from 'type-fest'
 import type { AppConfig } from '../main/info/config'
 
 //
-// Exposed from main
+// Exposed via tRPC
 //
 
 export type { AppInfo } from '../main/info/app'
-export type { UserInfo } from '../main/info/user'
 export type { AppConfig } from '../main/info/config'
+export type { UserInfo } from '../main/info/user'
+
 export type { AppRouter } from '../main/routes/router'
 export type { DocumentId } from '../main/services/database'
 export type { ApiLocales } from '../main/services/locale'
-export type { UserStore } from '../main/dao/storage'
 export type { PortEntry } from '../main/services/ports'
+export type { UpdateInfo, ProgressInfo } from '../main/services/updater'
+
 export type { Source, NewSource, SourceUpdate } from '../main/dao/sources'
 export type { Switch, NewSwitch, SwitchUpdate } from '../main/dao/switches'
 export type { Tie, NewTie, TieUpdate } from '../main/dao/ties'
 export type { ApiLocales } from '../main/locale'
 
 export type {
-  Driver,
   DriverData,
   LocalizedDriverDescriptor,
   // Cannot be exported as values, but they are literals.
@@ -101,19 +102,6 @@ export interface ProcessData {
 export interface MainProcessServices {
   readonly process: ProcessData
   readonly system: SystemApi
-  readonly updates: AppUpdates
-}
-
-export interface AppUpdater {
-  readonly checkForUpdates: () => Promise<UpdateInfo | undefined>
-  readonly downloadUpdate: () => Promise<void>
-  readonly cancelUpdate: () => Promise<void>
-  readonly installUpdate: () => Promise<void>
-}
-
-export interface AppUpdates extends AppUpdater {
-  readonly onDownloadProgress: (fn: (progress: ProgressInfo) => void) => void
-  readonly offDownloadProgress: (fn: (progress: ProgressInfo) => void) => void
 }
 
 // The exposed API global structure
diff --git a/src/preload/plugins/services.ts b/src/preload/plugins/services.ts
index b47813d..3626d75 100644
--- a/src/preload/plugins/services.ts
+++ b/src/preload/plugins/services.ts
@@ -2,14 +2,12 @@ import { contextBridge } from 'electron'
 import { memo } from 'radash'
 import useProcessData from './services/process'
 import useSystemApi from './services/system'
-import useAppUpdates from './services/updates'
 import type { MainProcessServices } from '../api'
 
 const useServices = memo(function useServices() {
   const services = {
     process: useProcessData(),
-    system: useSystemApi(),
-    updates: useAppUpdates()
+    system: useSystemApi()
   } satisfies MainProcessServices
 
   contextBridge.exposeInMainWorld('services', services)
diff --git a/src/preload/plugins/services/updates.ts b/src/preload/plugins/services/updates.ts
deleted file mode 100644
index c8ef6ab..0000000
--- a/src/preload/plugins/services/updates.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { memo } from 'radash'
-import { useIpc } from '../../support'
-import type { AppUpdates } from '../../api'
-
-const useAppUpdates = memo(function useAppUpdates(): AppUpdates {
-  const ipc = useIpc()
-
-  const appUpdates = {
-    checkForUpdates: ipc.useInvoke('update:check'),
-    downloadUpdate: ipc.useInvoke('update:download'),
-    cancelUpdate: ipc.useInvoke('update:cancel'),
-    installUpdate: ipc.useInvoke('update:install'),
-    onDownloadProgress: ipc.useAddListener('update:download:progress'),
-    offDownloadProgress: ipc.useRemoveListener('update:download:progress')
-  } satisfies AppUpdates
-
-  return appUpdates
-})
-
-export default useAppUpdates
diff --git a/src/renderer/services/appUpdates.ts b/src/renderer/services/appUpdates.ts
index 969f6b1..26e4a5f 100644
--- a/src/renderer/services/appUpdates.ts
+++ b/src/renderer/services/appUpdates.ts
@@ -1,5 +1,6 @@
 import { tryOnScopeDispose } from '@vueuse/core'
 import useTypedEventTarget from '../support/events'
+import { useClient } from './rpc'
 import type { ProgressInfo } from 'electron-updater'
 
 export class UpdateProgressEvent extends Event implements ProgressInfo {
@@ -16,6 +17,8 @@ export class UpdateProgressEvent extends Event implements ProgressInfo {
 }
 
 function useAppUpdates() {
+  const client = useClient()
+
   // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- Inapropriate, type !== interface
   type Events = {
     progress: (ev: UpdateProgressEvent) => void
@@ -25,19 +28,45 @@ function useAppUpdates() {
 
   const appUpdater = {
     ...target,
-    checkForUpdates: globalThis.services.updates.checkForUpdates,
-    downloadUpdate: globalThis.services.updates.downloadUpdate,
-    cancelUpdate: globalThis.services.updates.cancelUpdate,
-    installUpdate: globalThis.services.updates.installUpdate
+    checkForUpdates: async () => await client.updates.checkForUpdates.query(),
+    downloadUpdate: async () => {
+      await client.updates.downloadUpdate.query()
+    },
+    cancelUpdate: async () => {
+      await client.updates.cancelUpdate.query()
+    },
+    installUpdate: async () => {
+      await client.updates.installUpdate.query()
+    }
   }
 
   function progressProxy(info: ProgressInfo) {
     appUpdater.dispatchEvent(new UpdateProgressEvent(info))
   }
 
-  globalThis.services.updates.onDownloadProgress(progressProxy)
+  const checking = client.updates.onChecking.subscribe(undefined, {
+    onData() {
+      console.log('Checking for updates...')
+    }
+  })
+
+  const available = client.updates.onAvailable.subscribe(undefined, {
+    onData([info]) {
+      if (info != null) console.log(`Update available to ${info.version}`)
+      else console.log('No updates available.')
+    }
+  })
+
+  const progres = client.updates.onProgress.subscribe(undefined, {
+    onData([info]) {
+      progressProxy(info)
+    }
+  })
+
   tryOnScopeDispose(() => {
-    globalThis.services.updates.offDownloadProgress(progressProxy)
+    checking.unsubscribe()
+    available.unsubscribe()
+    progres.unsubscribe()
   })
 
   return appUpdater
diff --git a/src/renderer/services/rpc.ts b/src/renderer/services/rpc.ts
index e17094e..74f5456 100644
--- a/src/renderer/services/rpc.ts
+++ b/src/renderer/services/rpc.ts
@@ -1,11 +1,27 @@
-import { createTRPCProxyClient, httpLink } from '@trpc/client'
+import { createTRPCProxyClient, createWSClient, httpLink, wsLink } from '@trpc/client'
 import { memo } from 'radash'
 import type { AppRouter } from '../../preload/api'
 import useSuperJson from '@/rpc'
+import { getServerUrl } from '@/url'
 
-export const useClient = memo(() =>
-  createTRPCProxyClient<AppRouter>({
-    transformer: useSuperJson(),
-    links: [httpLink({ url: globalThis.configuration.rpcUrl })]
-  })
-)
+export const useClient = memo(function useClient() {
+  function useHttpClient() {
+    return createTRPCProxyClient<AppRouter>({
+      transformer: useSuperJson(),
+      links: [httpLink({ url: globalThis.configuration.rpcUrl })]
+    })
+  }
+
+  function useWsClient() {
+    const client = createWSClient({ url: globalThis.configuration.rpcUrl })
+    return createTRPCProxyClient<AppRouter>({
+      transformer: useSuperJson(),
+      links: [wsLink({ client })]
+    })
+  }
+
+  const url = new URL(globalThis.configuration.rpcUrl)
+  const [, , protocol] = getServerUrl(url, 7180)
+
+  return protocol === 'http:' ? useHttpClient() : useWsClient()
+})
diff --git a/yarn.lock b/yarn.lock
index 9a7d3a5..bf06161 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1241,6 +1241,13 @@
   resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
   integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
 
+"@types/ws@^8.5.12":
+  version "8.5.12"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
+  integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/yauzl@^2.9.1":
   version "2.10.3"
   resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
@@ -2127,6 +2134,13 @@ buffer@^6.0.3:
     base64-js "^1.3.1"
     ieee754 "^1.2.1"
 
+bufferutil@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea"
+  integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==
+  dependencies:
+    node-gyp-build "^4.3.0"
+
 builder-util-runtime@9.2.10:
   version "9.2.10"
   resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz#a0f7d9e214158402e78b74a745c8d9f870c604bc"
@@ -6191,6 +6205,13 @@ url-parse@^1.5.3:
     querystringify "^2.1.1"
     requires-port "^1.0.0"
 
+utf-8-validate@^6.0.5:
+  version "6.0.5"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.5.tgz#8087d39902be2cc15bdb21a426697ff256d65aab"
+  integrity sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==
+  dependencies:
+    node-gyp-build "^4.3.0"
+
 utf8-byte-length@^1.0.1:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e"
@@ -6525,6 +6546,11 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
+ws@^8.18.0:
+  version "8.18.0"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
+  integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
+
 xdg-basedir@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9"

From cb96896ae682810ea2fc99a0070ab4da8e347abc Mon Sep 17 00:00:00 2001
From: Matthew Holder <sixxgate@hotmail.com>
Date: Sat, 2 Nov 2024 11:47:45 -0500
Subject: [PATCH 3/3] Moved last remaining IPC APIs to tRPC

---
 PLAN.md                                       |  23 ++-
 package.json                                  |  16 +-
 src/core/basics.ts                            |  39 ++++-
 src/core/struct.ts                            |   5 -
 src/main/main.ts                              |   8 +-
 src/main/plugins/webcrypto.ts                 |  25 ---
 src/main/routes/router.ts                     |   2 +
 src/main/routes/system.ts                     |  16 ++
 src/main/services/database.ts                 |   4 +-
 src/main/services/system.ts                   |  49 +-----
 src/main/utilities.ts                         |  24 ---
 src/preload/api.d.ts                          |  73 ---------
 src/preload/index.ts                          |   4 -
 src/preload/plugins/services.ts               |  18 ---
 src/preload/plugins/services/process.ts       |  24 ---
 src/preload/plugins/services/system.ts        |  40 -----
 src/preload/support.ts                        |  54 -------
 src/renderer/components/DropMenu.vue          |   2 +-
 src/renderer/components/InputDialog.vue       |   2 +-
 src/renderer/components/OptionDialog.vue      |   4 +-
 src/renderer/components/ReplacableImage.vue   |  12 +-
 .../attachment.ts => hooks/assets.ts}         |  31 +---
 src/renderer/{helpers => hooks}/element.ts    |   3 +-
 src/renderer/{helpers => hooks}/errors.ts     |   0
 src/renderer/{helpers => hooks}/location.ts   |   6 +-
 src/renderer/{services => hooks}/tracking.ts  |   5 +-
 src/renderer/{helpers => hooks}/utilities.ts  |   0
 src/renderer/{helpers => hooks}/validation.ts |   0
 src/renderer/{helpers => hooks}/vue.ts        |   0
 src/renderer/{helpers => hooks}/vuetify.ts    |   6 +-
 src/renderer/index.ts                         |  28 ----
 src/renderer/locales/locales.ts               |   2 +-
 src/renderer/modals/SourceDialog.vue          |   7 +-
 src/renderer/modals/SwitchDialog.vue          |   4 +-
 src/renderer/modals/TieDialog.vue             |   2 +-
 src/renderer/modals/dialogs.ts                |   4 +-
 src/renderer/pages/FirstRunLogic.vue          |   2 +-
 src/renderer/pages/GeneralPage.vue            |   4 +-
 src/renderer/pages/MainDashboard.vue          |   7 +-
 src/renderer/pages/SettingsBackupPage.vue     |  17 +-
 src/renderer/pages/SettingsPage.vue           |   4 +-
 src/renderer/pages/SourceList.vue             |   5 +-
 src/renderer/pages/SourcePage.vue             |   8 +-
 src/renderer/pages/SwitchList.vue             |   2 +-
 src/renderer/services/appUpdates.ts           |   6 +-
 src/renderer/services/backup/export.ts        |   2 +-
 src/renderer/services/backup/import.ts        |   6 +-
 src/renderer/services/dashboard.ts            |   5 +-
 src/renderer/services/driver.ts               |   6 +-
 src/renderer/services/ports.ts                |   2 +-
 src/renderer/services/rpc.ts                  |   4 +-
 src/renderer/services/sources.ts              |   2 +-
 src/renderer/services/startup.ts              |   4 +-
 src/renderer/services/storage.ts              |   8 +-
 src/renderer/services/store.ts                |   2 +-
 src/renderer/services/switches.ts             |   2 +-
 src/renderer/services/ties.ts                 |   2 +-
 src/renderer/support/files.ts                 | 102 ++++++++++++
 src/renderer/{helpers => support}/i18n.ts     |   0
 yarn.lock                                     | 146 ++++++++----------
 60 files changed, 318 insertions(+), 572 deletions(-)
 delete mode 100644 src/core/struct.ts
 delete mode 100644 src/main/plugins/webcrypto.ts
 create mode 100644 src/main/routes/system.ts
 delete mode 100644 src/preload/plugins/services.ts
 delete mode 100644 src/preload/plugins/services/process.ts
 delete mode 100644 src/preload/plugins/services/system.ts
 delete mode 100644 src/preload/support.ts
 rename src/renderer/{helpers/attachment.ts => hooks/assets.ts} (56%)
 rename src/renderer/{helpers => hooks}/element.ts (94%)
 rename src/renderer/{helpers => hooks}/errors.ts (100%)
 rename src/renderer/{helpers => hooks}/location.ts (96%)
 rename src/renderer/{services => hooks}/tracking.ts (89%)
 rename src/renderer/{helpers => hooks}/utilities.ts (100%)
 rename src/renderer/{helpers => hooks}/validation.ts (100%)
 rename src/renderer/{helpers => hooks}/vue.ts (100%)
 rename src/renderer/{helpers => hooks}/vuetify.ts (92%)
 create mode 100644 src/renderer/support/files.ts
 rename src/renderer/{helpers => support}/i18n.ts (100%)

diff --git a/PLAN.md b/PLAN.md
index a5fd4f1..35bb915 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -1,11 +1,26 @@
 - Milestones
   - v2.1
-    - Switch the majority of the IPC using tRPC.
-      - Updater will require websocket for subscriptions.
-      - System will require moving the open and save file support to DOM APIs.
-      - Wrap some Electron APIs as services for easier mocking without electron itself.
+    - (#86) Add a means to select a not-in-use port for the tRPC channel.
+    - (#85) Attempt to secure `serialport` interactione.
+    - (#78) Switch the majority of the IPC using tRPC.
+    - (#87) Attempt to secure local RPC channel.
     - More drivers.
+      - (#88) Add means to mark and label experimental drivers.
+      - (#83) Shinybow
+      - (#84) TESmart
+  - v2.2
     - Move more modules to core.
+    - tRPC over Electron IPC.
+    - Wrap some Electron APIs as services for easier mocking without electron itself.
+    - Rearrangeable dashboard icons.
+    - More drivers.
+      - Monoprice Blackbird
+  - v3.0
+    - Remote UI support
+      - Need settings toggle to control it's activation.
+      - Need security or authentication method, preferrably just a PIN code.
+      - Need a means to identify it's URL via the local UI.
+      - May need a way to disable the power-off button in the remote UI.
 - Drivers
   - Shinybow
   - Monoprice Blackbird
diff --git a/package.json b/package.json
index 937c1d5..45fe349 100644
--- a/package.json
+++ b/package.json
@@ -65,12 +65,11 @@
     "@mdi/js": "^7.4.47",
     "@mdi/svg": "^7.4.47",
     "@sindresorhus/is": "^7.0.1",
-    "@sixxgate/lint": "^3.2.1",
+    "@sixxgate/lint": "^3.3.0",
     "@trpc/client": "^10.45.2",
     "@trpc/server": "^10.45.2",
     "@tsconfig/node20": "^20.1.4",
     "@tsconfig/strictest": "^2.0.5",
-    "@types/duplexify": "^3.6.4",
     "@types/eslint": "^8.56.12",
     "@types/ini": "^4.1.1",
     "@types/leveldown": "^4.0.6",
@@ -78,9 +77,7 @@
     "@types/node": "^20.17.5",
     "@types/pouchdb-core": "^7.0.15",
     "@types/pouchdb-find": "^7.3.3",
-    "@types/setimmediate": "^1.0.4",
-    "@types/uuid": "^10.0.0",
-    "@types/ws": "^8.5.12",
+    "@types/ws": "^8.5.13",
     "@typescript-eslint/eslint-plugin": "^8.12.2",
     "@typescript-eslint/parser": "^8.12.2",
     "@vitejs/plugin-vue": "^5.1.4",
@@ -96,9 +93,7 @@
     "@zip.js/zip.js": "^2.7.53",
     "assert": "^2.1.0",
     "auto-bind": "^5.0.1",
-    "buffer": "^6.0.3",
     "bufferutil": "^4.0.8",
-    "duplexify": "^4.1.3",
     "electron": "^31.7.3",
     "electron-builder": "^24.13.3",
     "electron-unhandled": "^5.0.0",
@@ -113,7 +108,6 @@
     "eslint-plugin-prettier": "^5.2.1",
     "eslint-plugin-promise": "^7.1.0",
     "eslint-plugin-vue": "^9.30.0",
-    "events": "^3.3.0",
     "execa": "^9.5.1",
     "husky": "^9.1.6",
     "ini": "^5.0.0",
@@ -128,17 +122,13 @@
     "pouchdb-find": "^9.0.0",
     "prettier": "^3.3.3",
     "radash": "^12.1.0",
-    "sass": "^1.80.5",
-    "setimmediate": "^1.0.5",
-    "stream-browserify": "^3.0.0",
+    "sass": "^1.80.6",
     "superjson": "^2.2.1",
     "tslib": "^2.8.1",
     "type-fest": "^4.26.1",
     "typescript": "^5.6.3",
     "typescript-eslint-parser-for-extra-files": "^0.7.0",
     "utf-8-validate": "^6.0.5",
-    "util": "^0.12.5",
-    "uuid": "^11.0.2",
     "vite": "^5.4.10",
     "vite-plugin-vue-devtools": "^7.6.2",
     "vite-plugin-vuetify": "^2.0.4",
diff --git a/src/core/basics.ts b/src/core/basics.ts
index b20db6e..59fc708 100644
--- a/src/core/basics.ts
+++ b/src/core/basics.ts
@@ -17,11 +17,40 @@ export function toArray<T>(value: T): T extends unknown[] ? T : T[] {
 }
 
 /**
- * Wait a specified amount of time.
- * @param timeout - The amount of time to wait in milliseconds.
+ * Creates a new promise with externally accessible fulfillment operations.
+ *
+ * This is a polyfill for
+ * [Promise.withResolver](https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers).
+ *
+ * @returns An object with a Promise and its fulfillment operations.
  */
-export async function waitTill(timeout: number) {
-  await new Promise<void>((resolve) => {
-    setTimeout(resolve, timeout)
+export function withResolvers<T>() {
+  let resolve: (value: T | PromiseLike<T>) => void = () => undefined
+  let reject: (reason?: unknown) => void = () => undefined
+
+  const promise = new Promise<T>((_resolve, _reject) => {
+    resolve = _resolve
+    reject = _reject
+  })
+
+  return { resolve, reject, promise }
+}
+
+/**
+ * Run an operation asynchronously as a microtask.
+ * @param op - The operation to run as a micro-task.
+ * @returns The result of the operation.
+ */
+export async function asMicrotask<Result>(op: () => MaybePromise<Result>) {
+  return await new Promise<Result>((resolve, reject) => {
+    queueMicrotask(() => {
+      try {
+        // eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable -- Proxied
+        Promise.resolve(op()).then(resolve).catch(reject)
+      } catch (cause) {
+        // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- Proxied
+        reject(cause)
+      }
+    })
   })
 }
diff --git a/src/core/struct.ts b/src/core/struct.ts
deleted file mode 100644
index 082749c..0000000
--- a/src/core/struct.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface FileData {
-  path: string
-  buffer: Uint8Array
-  type: string
-}
diff --git a/src/main/main.ts b/src/main/main.ts
index fe9e057..ca2c0c4 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -3,13 +3,11 @@ import process from 'node:process'
 import { electronApp, optimizer, is } from '@electron-toolkit/utils'
 import { app, shell, BrowserWindow, nativeTheme } from 'electron'
 import Logger from 'electron-log'
+import { sleep } from 'radash'
 import appIcon from '../../resources/icon.png?asset&asarUnpack'
 import useAppConfig from './info/config'
-import useCrypto from './plugins/webcrypto'
 import useApiServer from './server'
-import useSystem from './services/system'
 import { logError } from './utilities'
-import { waitTill } from '@/basics'
 import { toError } from '@/error-handling'
 
 // In this file you can include the rest of your app"s specific main process
@@ -74,7 +72,7 @@ async function createWindow() {
       Logger.warn(e)
 
       // eslint-disable-next-line no-await-in-loop -- Retry loop must be serial.
-      await waitTill(kWait)
+      await sleep(kWait)
     }
   }
 
@@ -129,7 +127,5 @@ electronApp.setAppUserModelId('org.sleepingcats.BridgeCmdr')
 useAppConfig()
 
 useApiServer()
-useCrypto()
-useSystem()
 
 await createWindow()
diff --git a/src/main/plugins/webcrypto.ts b/src/main/plugins/webcrypto.ts
deleted file mode 100644
index 6f9fab4..0000000
--- a/src/main/plugins/webcrypto.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/// <reference lib="DOM" />
-
-import { webcrypto } from 'node:crypto'
-import { memo } from 'radash'
-
-/* eslint-disable n/no-unsupported-features/node-builtins -- Some modules required this. */
-
-declare global {
-  // eslint-disable-next-line no-var -- Required to augment global.
-  var crypto: Crypto
-}
-
-/**
- * Add Node's webcrypto to the global this.
- *
- * Needed for any modules that required the Web Crypto API.
- */
-const useCrypto = memo(function useCrypto() {
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Ensuring it is Polyfilled.
-  if (globalThis.crypto == null) {
-    globalThis.crypto = webcrypto as never
-  }
-})
-
-export default useCrypto
diff --git a/src/main/routes/router.ts b/src/main/routes/router.ts
index fde0e64..1647c41 100644
--- a/src/main/routes/router.ts
+++ b/src/main/routes/router.ts
@@ -9,6 +9,7 @@ import useTiesRouter from './data/ties'
 import useDriversRouter from './drivers'
 import useSerialPortRouter from './ports'
 import useStartupRouter from './startup'
+import useSystemRouter from './system'
 import useUpdaterRouter from './updater'
 
 export const useAppRouter = memo(() =>
@@ -19,6 +20,7 @@ export const useAppRouter = memo(() =>
     // Functional service routes
     ports: useSerialPortRouter(),
     startup: useStartupRouter(),
+    system: useSystemRouter(),
     drivers: useDriversRouter(),
     // Data service routes
     storage: useUserStoreRouter(),
diff --git a/src/main/routes/system.ts b/src/main/routes/system.ts
new file mode 100644
index 0000000..a7c9317
--- /dev/null
+++ b/src/main/routes/system.ts
@@ -0,0 +1,16 @@
+import { memo } from 'radash'
+import { z } from 'zod'
+import useSystem from '../services/system'
+import { procedure, router } from '../services/trpc'
+
+const useSystemRouter = memo(function useSystemRouter() {
+  const system = useSystem()
+
+  return router({
+    powerOff: procedure.input(z.boolean().optional()).mutation(async ({ input }) => {
+      await system.powerOff(input)
+    })
+  })
+})
+
+export default useSystemRouter
diff --git a/src/main/services/database.ts b/src/main/services/database.ts
index 487cde7..00523d9 100644
--- a/src/main/services/database.ts
+++ b/src/main/services/database.ts
@@ -1,7 +1,7 @@
+import { randomUUID } from 'node:crypto'
 import PouchDb from 'pouchdb-core'
 import find from 'pouchdb-find'
 import { map, memo } from 'radash'
-import { v4 as uuid } from 'uuid'
 import { z } from 'zod'
 import { useLevelAdapter } from './level'
 import type { IterableElement, Simplify } from 'type-fest'
@@ -181,7 +181,7 @@ function defineDatabaseCore<RawSchema extends z.AnyZodObject>(
    * Adds a document to the database.
    */
   const add = defineOperation(async function add(db, document: Insertable, ...attachments: Attachment[]) {
-    const doc = { ...document, _id: uuid().toUpperCase() }
+    const doc = { ...document, _id: randomUUID().toUpperCase() }
     await db.put(doc)
     if (attachments.length > 0) {
       await addAttachments(doc._id, attachments)
diff --git a/src/main/services/system.ts b/src/main/services/system.ts
index baa8c32..cdc3f8e 100644
--- a/src/main/services/system.ts
+++ b/src/main/services/system.ts
@@ -1,13 +1,5 @@
-import { open } from 'node:fs/promises'
-import { BrowserWindow, dialog, ipcMain } from 'electron'
-import mime from 'mime'
 import { memo } from 'radash'
-import { ipcHandle, ipcProxy } from '../utilities'
 import useDbus from './dbus'
-import type { SystemApi } from '../../preload/api'
-import type { FileData } from '@/struct'
-import type { OpenDialogOptions, SaveDialogOptions } from 'electron'
-import { raiseError } from '@/error-handling'
 
 const useSystem = memo(function useSystem() {
   const { dbusBind } = useDbus()
@@ -25,48 +17,9 @@ const useSystem = memo(function useSystem() {
     await powerOffByDbus(interactive)
   }
 
-  const openFile = ipcHandle(async function openFile(ev, options: OpenDialogOptions) {
-    const result = await dialog.showOpenDialog(
-      BrowserWindow.fromWebContents(ev.sender) ?? raiseError(() => new ReferenceError('Invalid window')),
-      options
-    )
-
-    if (result.canceled) {
-      return null
-    }
-
-    return await Promise.all(
-      result.filePaths.map(async (path) => {
-        await using file = await open(path, 'r')
-        const buffer = await file.readFile()
-        const type = mime.getType(path) ?? 'application/octet-stream'
-        return { path, buffer, type } satisfies FileData
-      })
-    )
-  })
-
-  const saveFile = ipcHandle(async function saveFile(ev, source: FileData, options: SaveDialogOptions) {
-    options.defaultPath = options.defaultPath ?? source.path
-
-    const result = await dialog.showSaveDialog(
-      BrowserWindow.fromWebContents(ev.sender) ?? raiseError(() => new ReferenceError('Invalid window')),
-      options
-    )
-
-    if (result.canceled) return false
-
-    await using file = await open(result.filePath, 'w')
-    await file.writeFile(source.buffer)
-    return true
-  })
-
-  ipcMain.handle('system:powerOff', ipcProxy(powerOff))
-  ipcMain.handle('system:openFile', openFile)
-  ipcMain.handle('system:saveFile', saveFile)
-
   return {
     powerOff
-  } satisfies Omit<SystemApi, 'openFile' | 'saveFile'>
+  }
 })
 
 export default useSystem
diff --git a/src/main/utilities.ts b/src/main/utilities.ts
index 313c376..bff8c4b 100644
--- a/src/main/utilities.ts
+++ b/src/main/utilities.ts
@@ -1,28 +1,4 @@
 import Logger from 'electron-log'
-import type { IpcResponse } from '../preload/api'
-import type { MaybePromise } from '@/basics'
-import type { IpcMainInvokeEvent } from 'electron'
-import { toError } from '@/error-handling'
-
-export const ipcProxy = <Args extends unknown[], Result>(fn: (...args: Args) => MaybePromise<Result>) =>
-  async function ipcProxied(...[, ...args]: [IpcMainInvokeEvent, ...Args]): Promise<IpcResponse<Result>> {
-    try {
-      return { value: await fn(...args) }
-    } catch (e) {
-      return { error: toError(e) }
-    }
-  }
-
-export const ipcHandle = <Args extends unknown[], Result>(
-  fn: (ev: IpcMainInvokeEvent, ...args: Args) => MaybePromise<Result>
-) =>
-  async function ipcHandled(ev: IpcMainInvokeEvent, ...args: Args): Promise<IpcResponse<Result>> {
-    try {
-      return { value: await fn(ev, ...args) }
-    } catch (e) {
-      return { error: toError(e) }
-    }
-  }
 
 export function logError<E extends Error>(e: E) {
   Logger.error(e)
diff --git a/src/preload/api.d.ts b/src/preload/api.d.ts
index 86aae18..18b55d1 100644
--- a/src/preload/api.d.ts
+++ b/src/preload/api.d.ts
@@ -1,6 +1,3 @@
-import type { Dialog, IpcRendererEvent, OpenDialogOptions, SaveDialogOptions } from 'electron'
-import type { ProgressInfo, UpdateInfo } from 'electron-updater'
-import type { ArrayTail, ReadonlyDeep, Tagged } from 'type-fest'
 import type { AppConfig } from '../main/info/config'
 
 //
@@ -31,81 +28,11 @@ export type {
   kDeviceSupportsMultipleOutputs
 } from '../main/services/drivers'
 
-//
-// Internal parts
-//
-
-/** Internal IPC response structure */
-export interface IpcReturnedValue<T> {
-  error?: undefined
-  value: T
-}
-
-/** Internal IPC error structure */
-export interface IpcThrownError {
-  error: Error
-  value?: undefined
-}
-
-/** Internal IPC response structure */
-export type IpcResponse<T> = IpcReturnedValue<T> | IpcThrownError
-
-//
-// Common parts
-//
-
-/** Event listener attachment options */
-export interface ListenerOptions {
-  once?: boolean | undefined
-}
-
-//
-// Session control API
-//
-
-/** Exposed session control APIs. */
-export interface SystemApi {
-  /** Powers off the system. */
-  readonly powerOff: (interactive?: boolean) => Promise<void>
-  /** Shows the open file dialog. */
-  readonly openFile: (options: OpenDialogOptions) => Promise<File[] | null>
-  /** Shows the save file dialog to save a file. */
-  readonly saveFile: (file: File, options: SaveDialogOptions) => Promise<boolean>
-}
-
-//
-// Process data
-//
-
-type ProcessType = 'browser' | 'renderer' | 'worker' | 'utility'
-
-export interface ProcessData {
-  readonly appleStore: true | undefined
-  readonly arch: NodeJS.Architecture
-  readonly argv: readonly string[]
-  readonly argv0: string
-  readonly env: ReadonlyDeep<Record<string, string | undefined>>
-  readonly execPath: string
-  readonly platform: NodeJS.Platform
-  readonly resourcesPath: string
-  readonly sandboxed: true | undefined
-  readonly type: ProcessType
-  readonly version: string
-  readonly versions: NodeJS.ProcessVersions
-  readonly windowsStore: true | undefined
-}
 //
 // Exposed APIs
 //
 
-/** Functional APIs */
-export interface MainProcessServices {
-  readonly process: ProcessData
-  readonly system: SystemApi
-}
-
 // The exposed API global structure
 declare global {
-  var services: MainProcessServices
   var configuration: AppConfig
 }
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 11ad616..49d5baf 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -1,7 +1,6 @@
 /* eslint-disable n/no-process-exit -- No real way to do this otherwise */
 
 import useAppConfig from './plugins/info/config'
-import useServices from './plugins/services'
 
 if (!process.contextIsolated) {
   console.error('Context isolation is not enabled')
@@ -9,9 +8,6 @@ if (!process.contextIsolated) {
 }
 
 try {
-  // Register services and setup to free all handles when the window closes.
-  useServices()
-
   useAppConfig()
 } catch (e) {
   console.error('Preload error', e)
diff --git a/src/preload/plugins/services.ts b/src/preload/plugins/services.ts
deleted file mode 100644
index 3626d75..0000000
--- a/src/preload/plugins/services.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { contextBridge } from 'electron'
-import { memo } from 'radash'
-import useProcessData from './services/process'
-import useSystemApi from './services/system'
-import type { MainProcessServices } from '../api'
-
-const useServices = memo(function useServices() {
-  const services = {
-    process: useProcessData(),
-    system: useSystemApi()
-  } satisfies MainProcessServices
-
-  contextBridge.exposeInMainWorld('services', services)
-
-  return services
-})
-
-export default useServices
diff --git a/src/preload/plugins/services/process.ts b/src/preload/plugins/services/process.ts
deleted file mode 100644
index a3e54fd..0000000
--- a/src/preload/plugins/services/process.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { memo } from 'radash'
-import type { ProcessData } from '../../api'
-
-const useProcessData = memo(function useProcessData() {
-  const data = {
-    appleStore: process.mas || undefined,
-    arch: process.arch,
-    argv: Object.freeze(process.argv),
-    argv0: process.argv[0] ?? '',
-    env: { ...process.env },
-    execPath: process.execPath,
-    platform: process.platform,
-    resourcesPath: process.resourcesPath,
-    sandboxed: process.sandboxed || undefined,
-    type: process.type,
-    version: process.version,
-    versions: process.versions,
-    windowsStore: process.windowsStore || undefined
-  } satisfies ProcessData
-
-  return data
-})
-
-export default useProcessData
diff --git a/src/preload/plugins/services/system.ts b/src/preload/plugins/services/system.ts
deleted file mode 100644
index d39efc2..0000000
--- a/src/preload/plugins/services/system.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { basename } from 'node:path'
-import { memo } from 'radash'
-import { useIpc } from '../../support'
-import type { SystemApi } from '../../api'
-import type { FileData } from '@/struct'
-import type { OpenDialogOptions, SaveDialogOptions } from 'electron'
-
-type ShowOpenDialogMain = (options: OpenDialogOptions) => Promise<FileData[] | null>
-type SaveFileMain = (file: FileData, options: SaveDialogOptions) => Promise<boolean>
-
-const useSystemApi = memo(function useSystemApi() {
-  const ipc = useIpc()
-
-  const openFileMain: ShowOpenDialogMain = ipc.useInvoke('system:openFile')
-  async function openFile(options: OpenDialogOptions) {
-    const files = await openFileMain(options)
-    if (files == null) return null
-
-    return files.map(({ path, buffer, type }) => new File([buffer], basename(path), { type }))
-  }
-
-  const saveFileMain: SaveFileMain = ipc.useInvoke('system:saveFile')
-  async function saveFile(file: File, options: SaveDialogOptions) {
-    const source = {
-      path: file.name,
-      buffer: new Uint8Array(await file.arrayBuffer()),
-      type: file.type
-    } satisfies FileData
-
-    return await saveFileMain(source, options)
-  }
-
-  return {
-    powerOff: ipc.useInvoke('system:powerOff'),
-    openFile,
-    saveFile
-  } satisfies SystemApi
-})
-
-export default useSystemApi
diff --git a/src/preload/support.ts b/src/preload/support.ts
deleted file mode 100644
index cb928fb..0000000
--- a/src/preload/support.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { ipcRenderer } from 'electron'
-import { memo } from 'radash'
-import type { IpcResponse, ListenerOptions } from './api'
-import type { IpcRendererEvent } from 'electron'
-
-export const useIpc = memo(function useIpc() {
-  function useInvoke<Id extends string, Args extends unknown[], Result>(id: Id) {
-    return async (...args: Args) => {
-      const response = (await ipcRenderer.invoke(id, ...args)) as IpcResponse<Result>
-      if (response.error != null) throw response.error
-      return response.value
-    }
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- Supports any function type
-  const listeners = new WeakMap<Function, Function>()
-
-  function useAddListener<Id extends string, Args extends unknown[]>(id: Id) {
-    return (fn: (...args: Args) => unknown, options?: ListenerOptions) => {
-      const listener = listeners.get(fn)
-      if (listener != null) {
-        // Already added.
-        return
-      }
-
-      const wrapper = (...[, ...args]: [ev: IpcRendererEvent, ...args: Args]) => {
-        fn(...args)
-      }
-
-      listeners.set(fn, wrapper)
-
-      if (options?.once === true) ipcRenderer.once(id, wrapper as never)
-      else ipcRenderer.on(id, wrapper as never)
-    }
-  }
-
-  function useRemoveListener<Id extends string, Args extends unknown[]>(id: Id) {
-    return (fn: (...args: Args) => unknown) => {
-      const listener = listeners.get(fn)
-      if (listener == null) {
-        // Already removed or never added.
-        return
-      }
-
-      ipcRenderer.off(id, listener as never)
-    }
-  }
-
-  return {
-    useInvoke,
-    useAddListener,
-    useRemoveListener
-  }
-})
diff --git a/src/renderer/components/DropMenu.vue b/src/renderer/components/DropMenu.vue
index a1e8eb6..93890e3 100644
--- a/src/renderer/components/DropMenu.vue
+++ b/src/renderer/components/DropMenu.vue
@@ -2,7 +2,7 @@
 import { useVModel } from '@vueuse/core'
 import { get } from 'radash'
 import { computed, ref, watch, unref } from 'vue'
-import type { Anchor, Origin, SelectItemKey } from '../helpers/vuetify'
+import type { Anchor, Origin, SelectItemKey } from '../hooks/vuetify'
 import type { ComponentPublicInstance } from 'vue'
 
 interface Props {
diff --git a/src/renderer/components/InputDialog.vue b/src/renderer/components/InputDialog.vue
index 2656421..b9a9feb 100644
--- a/src/renderer/components/InputDialog.vue
+++ b/src/renderer/components/InputDialog.vue
@@ -3,7 +3,7 @@ import is from '@sindresorhus/is'
 import { useVModel } from '@vueuse/core'
 import { ref, computed, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
-import type { Anchor, Origin } from '../helpers/vuetify'
+import type { Anchor, Origin } from '../hooks/vuetify'
 import type { I18nSchema } from '../locales/locales'
 import type { ComponentPublicInstance } from 'vue'
 
diff --git a/src/renderer/components/OptionDialog.vue b/src/renderer/components/OptionDialog.vue
index 09e7d1b..7c4a017 100644
--- a/src/renderer/components/OptionDialog.vue
+++ b/src/renderer/components/OptionDialog.vue
@@ -4,8 +4,8 @@ import { useVModel } from '@vueuse/core'
 import { get } from 'radash'
 import { watch, ref, computed } from 'vue'
 import { useI18n } from 'vue-i18n'
-import { useElementScrollingBounds } from '../helpers/element'
-import type { Anchor, Origin, SelectItemKey } from '../helpers/vuetify'
+import { useElementScrollingBounds } from '../hooks/element'
+import type { Anchor, Origin, SelectItemKey } from '../hooks/vuetify'
 import type { I18nSchema } from '../locales/locales'
 import type { ComponentPublicInstance } from 'vue'
 import { toArray } from '@/basics'
diff --git a/src/renderer/components/ReplacableImage.vue b/src/renderer/components/ReplacableImage.vue
index 49532d6..f53af57 100644
--- a/src/renderer/components/ReplacableImage.vue
+++ b/src/renderer/components/ReplacableImage.vue
@@ -4,6 +4,7 @@ import videoInputHdmiIcon from '@mdi/svg/svg/video-input-hdmi.svg'
 import { useObjectUrl } from '@vueuse/core'
 import { computed } from 'vue'
 import { useI18n } from 'vue-i18n'
+import { openFile } from '../support/files'
 
 const props = defineProps<{
   image?: File | undefined
@@ -20,16 +21,7 @@ const file = computed(() => props.image)
 const url = useObjectUrl(file)
 
 async function selectImage() {
-  const results = await globalThis.services.system.openFile({
-    title: t('label.select'),
-    filters: [
-      { extensions: ['svg', 'png', 'gif', 'jpg', 'jpeg'], name: t('filter.all') },
-      { extensions: ['jpg', 'jpeg'], name: t('filter.jpg') },
-      { extensions: ['svg'], name: t('filter.svg') },
-      { extensions: ['png'], name: t('filter.png') },
-      { extensions: ['gif'], name: t('filter.gif') }
-    ]
-  })
+  const results = await openFile({ accepts: 'image/*' })
 
   const newFile = results?.at(0) ?? null
   if (newFile != null) {
diff --git a/src/renderer/helpers/attachment.ts b/src/renderer/hooks/assets.ts
similarity index 56%
rename from src/renderer/helpers/attachment.ts
rename to src/renderer/hooks/assets.ts
index ec8fc10..aea0873 100644
--- a/src/renderer/helpers/attachment.ts
+++ b/src/renderer/hooks/assets.ts
@@ -1,32 +1,9 @@
 import { tryOnScopeDispose } from '@vueuse/core'
-import { map } from 'radash'
-import { computed, readonly, ref, unref, watch } from 'vue'
-import type { MaybeRef } from '@vueuse/core'
-import { Attachment } from '@/attachments'
-import { isNotNullish } from '@/basics'
+import { computed, readonly, ref, toValue, unref, watch } from 'vue'
+import type { MaybeRefOrGetter } from 'vue'
 
 /* eslint-disable n/no-unsupported-features/node-builtins -- In browser environment. */
 
-export function toFile(attachment: Attachment) {
-  return new File([attachment], attachment.name, { type: attachment.type })
-}
-
-export function toFiles(attachments?: Attachment[] | null) {
-  if (attachments == null) {
-    return []
-  }
-
-  return attachments.map(toFile).filter(isNotNullish)
-}
-
-export async function fileToAttachment(file: File) {
-  return new Attachment(file.name, file.type, await file.arrayBuffer())
-}
-
-export async function filesToAttachment(files: File[]) {
-  return await map(files, fileToAttachment)
-}
-
 export function useImages() {
   const images = ref<(string | undefined)[]>([])
 
@@ -48,7 +25,7 @@ export function useImages() {
   return { images, loadImages }
 }
 
-export function useObjectUrls(sources: MaybeRef<(Blob | MediaSource | undefined)[]>) {
+export function useObjectUrls(sources: MaybeRefOrGetter<(Blob | MediaSource | undefined)[]>) {
   const urls = ref<(string | undefined)[]>([])
   function release() {
     for (const url of urls.value) {
@@ -64,7 +41,7 @@ export function useObjectUrls(sources: MaybeRef<(Blob | MediaSource | undefined)
     () => unref(sources),
     function handleSourceChange(files) {
       release()
-      for (const file of files) {
+      for (const file of toValue(files)) {
         urls.value.push(file != null ? URL.createObjectURL(file) : undefined)
       }
     },
diff --git a/src/renderer/helpers/element.ts b/src/renderer/hooks/element.ts
similarity index 94%
rename from src/renderer/helpers/element.ts
rename to src/renderer/hooks/element.ts
index e8191ed..96e2a45 100644
--- a/src/renderer/helpers/element.ts
+++ b/src/renderer/hooks/element.ts
@@ -1,6 +1,5 @@
 import { useScroll, useResizeObserver } from '@vueuse/core'
-import { toValue } from '@vueuse/shared'
-import { watch, computed, ref } from 'vue'
+import { toValue, watch, computed, ref } from 'vue'
 import type { MaybeComputedElementRef } from '@vueuse/core'
 
 interface ScrollingBoundsEntry {
diff --git a/src/renderer/helpers/errors.ts b/src/renderer/hooks/errors.ts
similarity index 100%
rename from src/renderer/helpers/errors.ts
rename to src/renderer/hooks/errors.ts
diff --git a/src/renderer/helpers/location.ts b/src/renderer/hooks/location.ts
similarity index 96%
rename from src/renderer/helpers/location.ts
rename to src/renderer/hooks/location.ts
index 2f05977..b793534 100644
--- a/src/renderer/helpers/location.ts
+++ b/src/renderer/hooks/location.ts
@@ -1,6 +1,5 @@
 import { helpers } from '@vuelidate/validators'
-import { toValue } from '@vueuse/shared'
-import { computed, readonly } from 'vue'
+import { toValue, computed, readonly } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { isHostWithOptionalPort } from './validation'
 import type { I18nSchema } from '../locales/locales'
@@ -8,8 +7,7 @@ import type { PortEntry } from '../services/ports'
 import type { NewSwitch } from '../services/switches'
 import type { Fixed } from '@/basics'
 import type { MessageProps } from '@vuelidate/validators'
-import type { MaybeRefOrGetter } from '@vueuse/shared'
-import type { Ref } from 'vue'
+import type { MaybeRefOrGetter, Ref } from 'vue'
 
 export type PathType = 'port' | 'ip' | 'path'
 
diff --git a/src/renderer/services/tracking.ts b/src/renderer/hooks/tracking.ts
similarity index 89%
rename from src/renderer/services/tracking.ts
rename to src/renderer/hooks/tracking.ts
index 8bfd31a..420b791 100644
--- a/src/renderer/services/tracking.ts
+++ b/src/renderer/hooks/tracking.ts
@@ -1,6 +1,5 @@
-import { toValue } from '@vueuse/core'
-import { computed, ref, shallowRef } from 'vue'
-import type { MaybeRefOrGetter } from '@vueuse/core'
+import { toValue, computed, ref, shallowRef } from 'vue'
+import type { MaybeRefOrGetter } from 'vue'
 import { toError } from '@/error-handling'
 
 type Trackable<T> = (() => PromiseLike<T>) | (() => Promise<T>) | (() => T) | PromiseLike<T> | Promise<T>
diff --git a/src/renderer/helpers/utilities.ts b/src/renderer/hooks/utilities.ts
similarity index 100%
rename from src/renderer/helpers/utilities.ts
rename to src/renderer/hooks/utilities.ts
diff --git a/src/renderer/helpers/validation.ts b/src/renderer/hooks/validation.ts
similarity index 100%
rename from src/renderer/helpers/validation.ts
rename to src/renderer/hooks/validation.ts
diff --git a/src/renderer/helpers/vue.ts b/src/renderer/hooks/vue.ts
similarity index 100%
rename from src/renderer/helpers/vue.ts
rename to src/renderer/hooks/vue.ts
diff --git a/src/renderer/helpers/vuetify.ts b/src/renderer/hooks/vuetify.ts
similarity index 92%
rename from src/renderer/helpers/vuetify.ts
rename to src/renderer/hooks/vuetify.ts
index d20ee39..0278bff 100644
--- a/src/renderer/helpers/vuetify.ts
+++ b/src/renderer/hooks/vuetify.ts
@@ -1,9 +1,7 @@
-import { toValue } from '@vueuse/shared'
-import { computed, ref } from 'vue'
+import { toValue, computed, ref } from 'vue'
 import { z } from 'zod'
 import { useElementScrollingBounds } from './element'
-import type { MaybeRefOrGetter } from '@vueuse/shared'
-import type { ComponentPublicInstance } from 'vue'
+import type { MaybeRefOrGetter, ComponentPublicInstance } from 'vue'
 
 const Block = ['top', 'bottom'] as const
 type Block = (typeof Block)[number]
diff --git a/src/renderer/index.ts b/src/renderer/index.ts
index 7b3f7d1..56ec1d0 100644
--- a/src/renderer/index.ts
+++ b/src/renderer/index.ts
@@ -1,6 +1,4 @@
-import { Buffer } from 'buffer'
 import log from 'electron-log'
-import 'setimmediate'
 
 //
 // Earliest to enable logger.
@@ -10,32 +8,6 @@ if (import.meta.env.PROD) {
   log.errorHandler.startCatching()
 }
 
-//
-// Polyfills
-//
-// These polyfills will allow some dependencies we use to function as
-// if under node due to needing certain things in the globalThis.
-//
-
-globalThis.global = globalThis
-globalThis.Buffer = Buffer
-Object.defineProperties((globalThis.process = {} as never), {
-  arch: { value: globalThis.services.process.arch },
-  argv: { value: globalThis.services.process.argv },
-  argv0: { value: globalThis.services.process.argv0 },
-  env: { value: globalThis.services.process.env },
-  execPath: { value: globalThis.services.process.execPath },
-  mas: { value: globalThis.services.process.appleStore },
-  platform: { value: globalThis.services.process.platform },
-  resourcesPath: { value: globalThis.services.process.resourcesPath },
-  sandboxed: { value: globalThis.services.process.sandboxed },
-  type: { value: globalThis.services.process.type },
-  version: { value: globalThis.services.process.version },
-  versions: { value: globalThis.services.process.versions },
-  windowsStore: { value: globalThis.services.process.windowsStore },
-  nextTick: { value: setImmediate }
-})
-
 //
 // Dynamic load the main module.
 //
diff --git a/src/renderer/locales/locales.ts b/src/renderer/locales/locales.ts
index 0437c86..309505d 100644
--- a/src/renderer/locales/locales.ts
+++ b/src/renderer/locales/locales.ts
@@ -1,7 +1,7 @@
 import type enDateTimes from './en/datetimes'
 import type en from './en/messages.json'
 import type enNumbers from './en/numbers'
-import type { DefineDateTimeFormatSchema, DefineMessageSchema, DefineNumberFormatSchema } from '../helpers/i18n'
+import type { DefineDateTimeFormatSchema, DefineMessageSchema, DefineNumberFormatSchema } from '../support/i18n'
 
 export type Locales = 'en'
 export type MessageSchema = DefineMessageSchema<typeof en>
diff --git a/src/renderer/modals/SourceDialog.vue b/src/renderer/modals/SourceDialog.vue
index 7a90ab2..02dcdc0 100644
--- a/src/renderer/modals/SourceDialog.vue
+++ b/src/renderer/modals/SourceDialog.vue
@@ -4,13 +4,12 @@ import { useVModel } from '@vueuse/core'
 import { reactive, ref, watch } from 'vue'
 import { useI18n } from 'vue-i18n'
 import ReplacableImage from '../components/ReplacableImage.vue'
-import { filesToAttachment } from '../helpers/attachment'
-import { useRules, useValidation } from '../helpers/validation'
+import { useRules, useValidation } from '../hooks/validation'
 import { useSources } from '../services/sources'
+import { toAttachments } from '../support/files'
 import { useDialogs, useSourceDialog } from './dialogs'
 import type { I18nSchema } from '../locales/locales'
 import type { NewSource, Source } from '../services/sources'
-import { isNotNullish } from '@/basics'
 import { toError } from '@/error-handling'
 
 const props = defineProps<{
@@ -42,7 +41,7 @@ const isVisible = useVModel(props, 'visible', emit)
 
 async function confirm() {
   try {
-    const result = await sources.add(source.value, ...(await filesToAttachment([file.value].filter(isNotNullish))))
+    const result = await sources.add(source.value, ...(await toAttachments([file.value])))
     isVisible.value = false
     emit('confirm', result)
   } catch (e) {
diff --git a/src/renderer/modals/SwitchDialog.vue b/src/renderer/modals/SwitchDialog.vue
index 809f06d..804ab4f 100644
--- a/src/renderer/modals/SwitchDialog.vue
+++ b/src/renderer/modals/SwitchDialog.vue
@@ -3,8 +3,8 @@ import { mdiClose } from '@mdi/js'
 import { useVModel } from '@vueuse/core'
 import { computed, ref, reactive, onBeforeMount } from 'vue'
 import { useI18n } from 'vue-i18n'
-import { useLocation } from '../helpers/location'
-import { useRules, useValidation } from '../helpers/validation'
+import { useLocation } from '../hooks/location'
+import { useRules, useValidation } from '../hooks/validation'
 import useDrivers from '../services/driver'
 import usePorts from '../services/ports'
 import { useDialogs, useSwitchDialog } from './dialogs'
diff --git a/src/renderer/modals/TieDialog.vue b/src/renderer/modals/TieDialog.vue
index 0e18340..1eb4f7b 100644
--- a/src/renderer/modals/TieDialog.vue
+++ b/src/renderer/modals/TieDialog.vue
@@ -4,7 +4,7 @@ import { useVModel } from '@vueuse/core'
 import { computed, ref, reactive, watch, onBeforeMount } from 'vue'
 import { useI18n } from 'vue-i18n'
 import NumberInput from '../components/NumberInput.vue'
-import { useRules, useValidation } from '../helpers/validation'
+import { useRules, useValidation } from '../hooks/validation'
 import useDrivers, { kDeviceCanDecoupleAudioOutput, kDeviceSupportsMultipleOutputs } from '../services/driver'
 import { useSources } from '../services/sources'
 import { useSwitches } from '../services/switches'
diff --git a/src/renderer/modals/dialogs.ts b/src/renderer/modals/dialogs.ts
index 157e0ab..c992915 100644
--- a/src/renderer/modals/dialogs.ts
+++ b/src/renderer/modals/dialogs.ts
@@ -2,8 +2,8 @@ import { createSharedComposable, useConfirmDialog } from '@vueuse/core'
 import { computed } from 'vue'
 import { useDisplay } from 'vuetify'
 import { z } from 'zod'
-import { useErrors } from '../helpers/errors'
-import { useResponsiveModal } from '../helpers/vuetify'
+import { useErrors } from '../hooks/errors'
+import { useResponsiveModal } from '../hooks/vuetify'
 
 export const AlertModalOptions = z
   .string()
diff --git a/src/renderer/pages/FirstRunLogic.vue b/src/renderer/pages/FirstRunLogic.vue
index 48e4b25..fc789db 100644
--- a/src/renderer/pages/FirstRunLogic.vue
+++ b/src/renderer/pages/FirstRunLogic.vue
@@ -1,10 +1,10 @@
 <script setup lang="ts">
 import { useAsyncState, useLocalStorage, watchOnce } from '@vueuse/core'
 import { useI18n } from 'vue-i18n'
+import { trackBusy } from '../hooks/tracking'
 import { useDialogs } from '../modals/dialogs'
 import { useClient } from '../services/rpc'
 import useStartup from '../services/startup'
-import { trackBusy } from '../services/tracking'
 import type { I18nSchema } from '../locales/locales'
 
 const { t } = useI18n<I18nSchema>()
diff --git a/src/renderer/pages/GeneralPage.vue b/src/renderer/pages/GeneralPage.vue
index fa86111..d96efa1 100644
--- a/src/renderer/pages/GeneralPage.vue
+++ b/src/renderer/pages/GeneralPage.vue
@@ -14,10 +14,10 @@ import { useI18n } from 'vue-i18n'
 import { useRouter } from 'vue-router'
 import OptionDialog from '../components/OptionDialog.vue'
 import Page from '../components/Page.vue'
-import { useGuardedAsyncOp } from '../helpers/utilities'
+import { trackBusy } from '../hooks/tracking'
+import { useGuardedAsyncOp } from '../hooks/utilities'
 import { useClient } from '../services/rpc'
 import useSettings from '../services/settings'
-import { trackBusy } from '../services/tracking'
 import type { I18nSchema } from '../locales/locales'
 
 const { t } = useI18n<I18nSchema>()
diff --git a/src/renderer/pages/MainDashboard.vue b/src/renderer/pages/MainDashboard.vue
index e1e776b..a049a24 100644
--- a/src/renderer/pages/MainDashboard.vue
+++ b/src/renderer/pages/MainDashboard.vue
@@ -2,9 +2,10 @@
 import { mdiPower, mdiVideoInputHdmi, mdiWrench } from '@mdi/js'
 import { computed, onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
-import { useGuardedAsyncOp } from '../helpers/utilities'
+import { useGuardedAsyncOp } from '../hooks/utilities'
 import { useDialogs } from '../modals/dialogs'
 import { useDashboard } from '../services/dashboard'
+import { useClient } from '../services/rpc'
 import useSettings from '../services/settings'
 import FirstRunLogic from './FirstRunLogic.vue'
 import type { I18nSchema } from '../locales/locales'
@@ -23,10 +24,12 @@ const dialogs = useDialogs()
 
 const dashboard = useDashboard()
 
+const client = useClient()
+
 async function powerOff() {
   try {
     await dashboard.powerOff()
-    await services.system.powerOff()
+    await client.system.powerOff.mutate()
   } catch (e) {
     await dialogs.error(e)
   }
diff --git a/src/renderer/pages/SettingsBackupPage.vue b/src/renderer/pages/SettingsBackupPage.vue
index 748be6c..69bb275 100644
--- a/src/renderer/pages/SettingsBackupPage.vue
+++ b/src/renderer/pages/SettingsBackupPage.vue
@@ -3,10 +3,11 @@ import { mdiArrowLeft, mdiExport, mdiImport } from '@mdi/js'
 import { useI18n } from 'vue-i18n'
 import { useRouter } from 'vue-router'
 import Page from '../components/Page.vue'
+import { trackBusy } from '../hooks/tracking'
 import { useDialogs } from '../modals/dialogs'
 import { exportSettings } from '../services/backup/export'
 import { importSettings } from '../services/backup/import'
-import { trackBusy } from '../services/tracking'
+import { openFile, saveFile } from '../support/files'
 import type { I18nSchema } from '../locales/locales'
 
 //
@@ -22,16 +23,10 @@ const router = useRouter()
 // Functionality.
 //
 
-const kFilters = [{ extensions: ['zip'], name: t('description.archive') }]
-
 async function exportToFile() {
   try {
     const file = await wait(exportSettings())
-    await globalThis.services.system.saveFile(file, {
-      title: t('label.export'),
-      filters: kFilters,
-      properties: ['showOverwriteConfirmation']
-    })
+    await saveFile(file)
   } catch (e) {
     await dialogs.error(e, {
       title: t('error.export')
@@ -41,11 +36,7 @@ async function exportToFile() {
 
 async function importFromFile() {
   try {
-    const files = await globalThis.services.system.openFile({
-      title: t('label.import'),
-      filters: kFilters,
-      properties: ['openFile', 'dontAddToRecent']
-    })
+    const files = await openFile({ accepts: '.zip' })
 
     const file = files?.at(0)
     if (file == null) return
diff --git a/src/renderer/pages/SettingsPage.vue b/src/renderer/pages/SettingsPage.vue
index 1a9b47e..a63cfaa 100644
--- a/src/renderer/pages/SettingsPage.vue
+++ b/src/renderer/pages/SettingsPage.vue
@@ -12,11 +12,11 @@ import { ref, onMounted, onUnmounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { useRouter } from 'vue-router'
 import Page from '../components/Page.vue'
-import { useGuardedAsyncOp } from '../helpers/utilities'
+import { trackBusy } from '../hooks/tracking'
+import { useGuardedAsyncOp } from '../hooks/utilities'
 import { useClient } from '../services/rpc'
 import { useSources } from '../services/sources'
 import { useSwitches } from '../services/switches'
-import { trackBusy } from '../services/tracking'
 import type { I18nSchema } from '../locales/locales'
 
 //
diff --git a/src/renderer/pages/SourceList.vue b/src/renderer/pages/SourceList.vue
index 2d5a230..01f4e41 100644
--- a/src/renderer/pages/SourceList.vue
+++ b/src/renderer/pages/SourceList.vue
@@ -4,11 +4,12 @@ import { computed, onBeforeMount } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { useRouter } from 'vue-router'
 import Page from '../components/Page.vue'
-import { toFiles, useObjectUrls } from '../helpers/attachment'
-import { useGuardedAsyncOp } from '../helpers/utilities'
+import { useObjectUrls } from '../hooks/assets'
+import { useGuardedAsyncOp } from '../hooks/utilities'
 import SourceDialog from '../modals/SourceDialog.vue'
 import { useDialogs, useSourceDialog } from '../modals/dialogs'
 import { useSources } from '../services/sources'
+import { toFiles } from '../support/files'
 import type { I18nSchema } from '../locales/locales'
 import type { Source } from '../services/sources'
 import type { DocumentId } from '../services/store'
diff --git a/src/renderer/pages/SourcePage.vue b/src/renderer/pages/SourcePage.vue
index 676e124..a25c03f 100644
--- a/src/renderer/pages/SourcePage.vue
+++ b/src/renderer/pages/SourcePage.vue
@@ -6,15 +6,15 @@ import { useRouter } from 'vue-router'
 import InputDialog from '../components/InputDialog.vue'
 import Page from '../components/Page.vue'
 import ReplacableImage from '../components/ReplacableImage.vue'
-import { filesToAttachment, toFiles } from '../helpers/attachment'
-import { useGuardedAsyncOp } from '../helpers/utilities'
+import { trackBusy } from '../hooks/tracking'
+import { useGuardedAsyncOp } from '../hooks/utilities'
 import TieDialog from '../modals/TieDialog.vue'
 import { useDialogs, useTieDialog } from '../modals/dialogs'
 import useDrivers from '../services/driver'
 import { useSources } from '../services/sources'
 import { useSwitches } from '../services/switches'
 import { useTies } from '../services/ties'
-import { trackBusy } from '../services/tracking'
+import { toAttachments, toFiles } from '../support/files'
 import type { I18nSchema } from '../locales/locales'
 import type { DriverInformation } from '../services/driver'
 import type { Source } from '../services/sources'
@@ -65,7 +65,7 @@ async function save() {
   try {
     source.value.image = file.value.name
     source.value = {
-      ...(await sources.update(source.value, ...(await filesToAttachment([file.value].filter(isNotNullish)))))
+      ...(await sources.update(source.value, ...(await toAttachments([file.value]))))
     }
     file.value = toFiles(source.value._attachments).find(
       (f) => source.value?.image != null && f.name === source.value.image
diff --git a/src/renderer/pages/SwitchList.vue b/src/renderer/pages/SwitchList.vue
index 110d168..da52c70 100644
--- a/src/renderer/pages/SwitchList.vue
+++ b/src/renderer/pages/SwitchList.vue
@@ -4,7 +4,7 @@ import { computed, onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { useRouter } from 'vue-router'
 import Page from '../components/Page.vue'
-import { useGuardedAsyncOp } from '../helpers/utilities'
+import { useGuardedAsyncOp } from '../hooks/utilities'
 import SwitchDialog from '../modals/SwitchDialog.vue'
 import { useDialogs, useSwitchDialog } from '../modals/dialogs'
 import useDrivers from '../services/driver'
diff --git a/src/renderer/services/appUpdates.ts b/src/renderer/services/appUpdates.ts
index 26e4a5f..0f14734 100644
--- a/src/renderer/services/appUpdates.ts
+++ b/src/renderer/services/appUpdates.ts
@@ -1,4 +1,4 @@
-import { tryOnScopeDispose } from '@vueuse/core'
+import { createSharedComposable, tryOnScopeDispose } from '@vueuse/core'
 import useTypedEventTarget from '../support/events'
 import { useClient } from './rpc'
 import type { ProgressInfo } from 'electron-updater'
@@ -16,7 +16,7 @@ export class UpdateProgressEvent extends Event implements ProgressInfo {
   }
 }
 
-function useAppUpdates() {
+const useAppUpdates = createSharedComposable(function useAppUpdates() {
   const client = useClient()
 
   // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- Inapropriate, type !== interface
@@ -70,6 +70,6 @@ function useAppUpdates() {
   })
 
   return appUpdater
-}
+})
 
 export default useAppUpdates
diff --git a/src/renderer/services/backup/export.ts b/src/renderer/services/backup/export.ts
index bb57c5e..3f4d486 100644
--- a/src/renderer/services/backup/export.ts
+++ b/src/renderer/services/backup/export.ts
@@ -1,6 +1,6 @@
 import { BlobReader, BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js'
 import { pick } from 'radash'
-import { toFiles } from '../../helpers/attachment'
+import { toFiles } from '../../support/files'
 import useSettings from '../settings'
 import { useSources } from '../sources'
 import { useSwitches } from '../switches'
diff --git a/src/renderer/services/backup/import.ts b/src/renderer/services/backup/import.ts
index d0fa602..9346777 100644
--- a/src/renderer/services/backup/import.ts
+++ b/src/renderer/services/backup/import.ts
@@ -1,7 +1,7 @@
 import { BlobReader, BlobWriter, TextWriter, ZipReader } from '@zip.js/zip.js'
 import mime from 'mime'
 import { z } from 'zod'
-import { fileToAttachment } from '../../helpers/attachment'
+import { toAttachment } from '../../support/files'
 import useDrivers from '../driver'
 import useSettings from '../settings'
 import { useSources } from '../sources'
@@ -58,7 +58,7 @@ export async function importSettings(file: File) {
         const type = mime.getType(item.image) ?? 'application/octet-stream'
         let imageAttachment = imageCache.get(item.image)
         if (imageAttachment != null) {
-          await sources.add(item, await fileToAttachment(imageAttachment))
+          await sources.add(item, await toAttachment(imageAttachment))
           return
         }
 
@@ -75,7 +75,7 @@ export async function importSettings(file: File) {
         const imageData = await imageFile.getData()
         imageAttachment = new File([imageData], item.image, { type })
         imageCache.set(item.image, imageAttachment)
-        await sources.add(item, await fileToAttachment(imageAttachment))
+        await sources.add(item, await toAttachment(imageAttachment))
       })
     ),
     Promise.all(
diff --git a/src/renderer/services/dashboard.ts b/src/renderer/services/dashboard.ts
index cbfd1af..9b24b90 100644
--- a/src/renderer/services/dashboard.ts
+++ b/src/renderer/services/dashboard.ts
@@ -1,12 +1,13 @@
 import { defineStore } from 'pinia'
 import { computed, readonly, ref } from 'vue'
-import { toFiles, useImages } from '../helpers/attachment'
+import { useImages } from '../hooks/assets'
+import { trackBusy } from '../hooks/tracking'
+import { toFiles } from '../support/files'
 import useDrivers from './driver'
 import useSettings from './settings'
 import { useSources } from './sources'
 import { useSwitches } from './switches'
 import { useTies } from './ties'
-import { trackBusy } from './tracking'
 import type { Driver } from './driver'
 import type { Source } from './sources'
 import type { DocumentId } from './store'
diff --git a/src/renderer/services/driver.ts b/src/renderer/services/driver.ts
index 5228049..9ec3cb4 100644
--- a/src/renderer/services/driver.ts
+++ b/src/renderer/services/driver.ts
@@ -1,8 +1,8 @@
-import { memo } from 'radash'
+import { createSharedComposable } from '@vueuse/shared'
 import { computed, reactive, readonly, ref, shallowReadonly } from 'vue'
+import { trackBusy } from '../hooks/tracking'
 import i18n from '../plugins/i18n'
 import { useClient } from './rpc'
-import { trackBusy } from './tracking'
 import type {
   kDeviceHasNoExtraCapabilities as HasNoExtraCapabilities,
   kDeviceSupportsMultipleOutputs as SupportsMultipleOutputs,
@@ -46,7 +46,7 @@ export interface Driver extends DriverInformation {
   readonly uri: string
 }
 
-const useDrivers = memo(function useDrivers() {
+const useDrivers = createSharedComposable(function useDrivers() {
   const client = useClient()
 
   /** Busy tracking. */
diff --git a/src/renderer/services/ports.ts b/src/renderer/services/ports.ts
index 22c44f8..f278617 100644
--- a/src/renderer/services/ports.ts
+++ b/src/renderer/services/ports.ts
@@ -1,7 +1,7 @@
 import { createSharedComposable } from '@vueuse/shared'
 import { ref, computed, readonly, reactive } from 'vue'
+import { trackBusy } from '../hooks/tracking'
 import { useClient } from './rpc'
-import { trackBusy } from './tracking'
 import type { PortEntry } from '../../preload/api'
 
 export type { PortEntry } from '../../preload/api'
diff --git a/src/renderer/services/rpc.ts b/src/renderer/services/rpc.ts
index 74f5456..3f18f26 100644
--- a/src/renderer/services/rpc.ts
+++ b/src/renderer/services/rpc.ts
@@ -1,10 +1,10 @@
 import { createTRPCProxyClient, createWSClient, httpLink, wsLink } from '@trpc/client'
-import { memo } from 'radash'
+import { createGlobalState } from '@vueuse/shared'
 import type { AppRouter } from '../../preload/api'
 import useSuperJson from '@/rpc'
 import { getServerUrl } from '@/url'
 
-export const useClient = memo(function useClient() {
+export const useClient = createGlobalState(function useClient() {
   function useHttpClient() {
     return createTRPCProxyClient<AppRouter>({
       transformer: useSuperJson(),
diff --git a/src/renderer/services/sources.ts b/src/renderer/services/sources.ts
index 736ce7f..f703289 100644
--- a/src/renderer/services/sources.ts
+++ b/src/renderer/services/sources.ts
@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { forceUndefined } from '../helpers/utilities'
+import { forceUndefined } from '../hooks/utilities'
 import { useClient } from './rpc'
 import { useDataStore } from './store'
 import type { DocumentId } from './store'
diff --git a/src/renderer/services/startup.ts b/src/renderer/services/startup.ts
index 2285232..83c3ab5 100644
--- a/src/renderer/services/startup.ts
+++ b/src/renderer/services/startup.ts
@@ -1,7 +1,7 @@
-import { memo } from 'radash'
+import { createSharedComposable } from '@vueuse/shared'
 import { useClient } from './rpc'
 
-const useStartup = memo(function useStartup() {
+const useStartup = createSharedComposable(function useStartup() {
   const client = useClient()
   const checkEnabled = async () => await client.startup.checkEnabled.query()
   const enable = async () => {
diff --git a/src/renderer/services/storage.ts b/src/renderer/services/storage.ts
index 7caf1ef..2a6b3ca 100644
--- a/src/renderer/services/storage.ts
+++ b/src/renderer/services/storage.ts
@@ -1,7 +1,7 @@
-import { useStorageAsync } from '@vueuse/core'
-import { memo } from 'radash'
+import { createSharedComposable, useStorageAsync } from '@vueuse/core'
 import { useClient } from './rpc'
-import type { MaybeRefOrGetter, UseStorageAsyncOptions, RemovableRef } from '@vueuse/core'
+import type { RemovableRef, UseStorageAsyncOptions } from '@vueuse/core'
+import type { MaybeRefOrGetter } from 'vue'
 
 // To make this work well, we will require async methods.
 export interface StorageLikeAsync {
@@ -21,7 +21,7 @@ interface AsyncStorageEventInitDict {
   readonly storageArea: StorageLikeAsync | null
 }
 
-const useUserStore = memo(function useUserStore() {
+const useUserStore = createSharedComposable(function useUserStore() {
   const client = useClient()
 
   function emit(data: AsyncStorageEventInitDict) {
diff --git a/src/renderer/services/store.ts b/src/renderer/services/store.ts
index 326b171..7bd93f2 100644
--- a/src/renderer/services/store.ts
+++ b/src/renderer/services/store.ts
@@ -1,5 +1,5 @@
 import { computed, nextTick, ref, shallowReadonly } from 'vue'
-import { trackBusy } from './tracking'
+import { trackBusy } from '../hooks/tracking'
 import type { DocumentId } from '../../preload/api'
 import type { Ref } from 'vue'
 
diff --git a/src/renderer/services/switches.ts b/src/renderer/services/switches.ts
index dbe475d..ef68500 100644
--- a/src/renderer/services/switches.ts
+++ b/src/renderer/services/switches.ts
@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { forceUndefined } from '../helpers/utilities'
+import { forceUndefined } from '../hooks/utilities'
 import { useClient } from './rpc'
 import { useDataStore } from './store'
 import type { DocumentId } from './store'
diff --git a/src/renderer/services/ties.ts b/src/renderer/services/ties.ts
index 2d96e23..dc85689 100644
--- a/src/renderer/services/ties.ts
+++ b/src/renderer/services/ties.ts
@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { forceUndefined } from '../helpers/utilities'
+import { forceUndefined } from '../hooks/utilities'
 import { useClient } from './rpc'
 import { useDataStore } from './store'
 import type { DocumentId } from './store'
diff --git a/src/renderer/support/files.ts b/src/renderer/support/files.ts
new file mode 100644
index 0000000..9494268
--- /dev/null
+++ b/src/renderer/support/files.ts
@@ -0,0 +1,102 @@
+import { defer } from 'radash'
+import { Attachment } from '@/attachments'
+import { asMicrotask, isNotNullish, withResolvers } from '@/basics'
+import { raiseError } from '@/error-handling'
+
+/* eslint-disable n/no-unsupported-features/node-builtins -- In browser environment. */
+
+export async function saveFile(source: File): Promise<void>
+export async function saveFile(source: Blob, defaultName: string): Promise<void>
+export async function saveFile(source: File | (Blob & { name?: undefined }), defaultName?: string) {
+  await defer(async (cleanup) => {
+    const saver = globalThis.document.createElement('a')
+    saver.style.display = 'none'
+    saver.href = URL.createObjectURL(source)
+    saver.download = source.name ?? defaultName ?? raiseError(() => new TypeError('No file name specified'))
+    cleanup(() => {
+      URL.revokeObjectURL(saver.href)
+    })
+
+    await asMicrotask(() => {
+      globalThis.document.body.appendChild(saver)
+      cleanup(() => {
+        globalThis.document.body.removeChild(saver)
+      })
+    })
+
+    await asMicrotask(() => {
+      saver.click()
+    })
+  })
+}
+
+interface OpenFileOptions {
+  accepts?: string | undefined
+  multiple?: true | undefined
+}
+
+export async function openFile(options: OpenFileOptions = {}) {
+  return await defer(async (cleanup) => {
+    const opener = globalThis.document.createElement('input')
+    opener.style.display = 'none'
+    opener.type = 'file'
+    opener.accept = options.accepts ?? ''
+    opener.multiple = options.multiple ?? false
+
+    await asMicrotask(() => {
+      globalThis.document.body.appendChild(opener)
+      cleanup(() => {
+        globalThis.document.body.removeChild(opener)
+      })
+    })
+
+    return await asMicrotask(async () => {
+      const { resolve, promise } = withResolvers<File[] | null>()
+      opener.addEventListener(
+        'cancel',
+        () => {
+          resolve(null)
+        },
+        { once: true }
+      )
+      opener.addEventListener(
+        'change',
+        () => {
+          resolve([...(opener.files ?? [])].filter(isNotNullish))
+        },
+        { once: true }
+      )
+
+      opener.click()
+
+      return await promise
+    })
+  })
+}
+
+type MaybeAttachment = Attachment | null | undefined
+
+export function toFile(attachment: Attachment): File
+export function toFile(attachment: MaybeAttachment): File | null
+export function toFile(attachment: MaybeAttachment) {
+  return attachment != null ? new File([attachment], attachment.name, { type: attachment.type }) : null
+}
+
+export function toFiles(attachments: MaybeAttachment[] | null | undefined) {
+  if (attachments == null) return []
+  return attachments.map(toFile).filter(isNotNullish)
+}
+
+type MaybeFile = File | null | undefined
+
+export function toAttachment(file: File): Promise<Attachment>
+export function toAttachment(file: MaybeFile): Promise<Attachment | null>
+export async function toAttachment(file: MaybeFile) {
+  return file != null ? new Attachment(file.name, file.type, await file.arrayBuffer()) : null
+}
+
+export async function toAttachments(files: MaybeFile[] | null | undefined) {
+  if (files == null) return []
+  const attachments = await Promise.all(files.map(toAttachment))
+  return attachments.filter(isNotNullish)
+}
diff --git a/src/renderer/helpers/i18n.ts b/src/renderer/support/i18n.ts
similarity index 100%
rename from src/renderer/helpers/i18n.ts
rename to src/renderer/support/i18n.ts
diff --git a/yarn.lock b/yarn.lock
index bf06161..7fb3556 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -491,7 +491,22 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@8.57.1", "@eslint/js@^8.57.0":
+"@eslint/eslintrc@^3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6"
+  integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==
+  dependencies:
+    ajv "^6.12.4"
+    debug "^4.3.2"
+    espree "^10.0.1"
+    globals "^14.0.0"
+    ignore "^5.2.0"
+    import-fresh "^3.2.1"
+    js-yaml "^4.1.0"
+    minimatch "^3.1.2"
+    strip-json-comments "^3.1.1"
+
+"@eslint/js@8.57.1", "@eslint/js@^8.57.1":
   version "8.57.1"
   resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
   integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
@@ -1016,25 +1031,25 @@
   resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339"
   integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==
 
-"@sixxgate/lint@^3.2.1":
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/@sixxgate/lint/-/lint-3.2.1.tgz#95b797593d3e1958c2e613c8cb75e01d9e0c9eef"
-  integrity sha512-M74rx8kFJ9CvNIpC23rx3Si6HBJHRDtAUm3WfbiDhXc4IPNJs1b85tlzLlj5jJXAvNdXrWx+K1/P0aKV+GmEZQ==
+"@sixxgate/lint@^3.3.0":
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/@sixxgate/lint/-/lint-3.3.0.tgz#3936f31f24a0ef9791c9a64c77b85cb6a7ec699c"
+  integrity sha512-MCqIVWd/EJTG/GKE+hh2K4RsmHrf1GGYSvpPxRQWN6PSUop8QNDDvUX4/3H8+9A6e51CSDV6Jh3mb/p4az44ow==
   dependencies:
-    "@eslint/eslintrc" "^2.1.4"
-    "@eslint/js" "^8.57.0"
-    "@types/eslint" "^8.56.11"
+    "@eslint/eslintrc" "^3.1.0"
+    "@eslint/js" "^8.57.1"
+    "@types/eslint" "^8.56.12"
     chalk "^4.1.2"
-    debug "^4.3.6"
-    eslint "^8.57.0"
+    debug "^4.3.7"
+    eslint "^8.57.1"
     execa "^5.1.1"
     is-interactive "^1.0.0"
     lodash "^4.17.21"
     pkg-dir "^5.0.0"
     radash "^12.1.0"
     read-pkg "^5.2.0"
-    tslib "^2.6.3"
-    type-fest "^4.24.0"
+    tslib "^2.8.1"
+    type-fest "^4.26.1"
     zod "^3.23.8"
 
 "@szmarczak/http-timer@^4.0.5":
@@ -1091,14 +1106,7 @@
   dependencies:
     "@types/ms" "*"
 
-"@types/duplexify@^3.6.4":
-  version "3.6.4"
-  resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.4.tgz#aa7e916c33fcc05be8769546fd0441d9b368613e"
-  integrity sha512-2eahVPsd+dy3CL6FugAzJcxoraWhUghZGEQJns1kTKfCXWKJ5iG/VkaB05wRVrDKHfOFKqb0X0kXh91eE99RZg==
-  dependencies:
-    "@types/node" "*"
-
-"@types/eslint@^8.56.11", "@types/eslint@^8.56.12":
+"@types/eslint@^8.56.12":
   version "8.56.12"
   resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.12.tgz#1657c814ffeba4d2f84c0d4ba0f44ca7ea1ca53a"
   integrity sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==
@@ -1221,16 +1229,6 @@
   dependencies:
     "@types/node" "*"
 
-"@types/setimmediate@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@types/setimmediate/-/setimmediate-1.0.4.tgz#c863c763c284ceaf8d31ec1b29362a889864da4f"
-  integrity sha512-rWPw1drMVf5zInxNpgH3nn/h6KkWqwgLT2y/ciAYQ16RAsbXOXe0AmtZ/HyzwPNw+r4GMJuI7IV7YNKO7Fs/xA==
-
-"@types/uuid@^10.0.0":
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
-  integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==
-
 "@types/verror@^1.10.3":
   version "1.10.10"
   resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.10.tgz#d5a4b56abac169bfbc8b23d291363a682e6fa087"
@@ -1241,10 +1239,10 @@
   resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
   integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
 
-"@types/ws@^8.5.12":
-  version "8.5.12"
-  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
-  integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
+"@types/ws@^8.5.13":
+  version "8.5.13"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20"
+  integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==
   dependencies:
     "@types/node" "*"
 
@@ -2621,16 +2619,6 @@ double-ended-queue@2.1.0-0:
   resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
   integrity sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==
 
-duplexify@^4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f"
-  integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==
-  dependencies:
-    end-of-stream "^1.4.1"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-    stream-shift "^1.0.2"
-
 eastasianwidth@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@@ -2744,7 +2732,7 @@ emoji-regex@^9.2.2:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
   integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
 
-end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+end-of-stream@^1.1.0:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
@@ -3084,7 +3072,12 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
 
-eslint@^8.57.0, eslint@^8.57.1:
+eslint-visitor-keys@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
+  integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
+
+eslint@^8.57.1:
   version "8.57.1"
   resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
   integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@@ -3128,6 +3121,15 @@ eslint@^8.57.0, eslint@^8.57.1:
     strip-ansi "^6.0.1"
     text-table "^0.2.0"
 
+espree@^10.0.1:
+  version "10.3.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a"
+  integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==
+  dependencies:
+    acorn "^8.14.0"
+    acorn-jsx "^5.3.2"
+    eslint-visitor-keys "^4.2.0"
+
 espree@^9.0.0, espree@^9.3.1, espree@^9.6.0, espree@^9.6.1:
   version "9.6.1"
   resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -3178,11 +3180,6 @@ esutils@^2.0.2:
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-events@^3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
-  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
-
 execa@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -3577,6 +3574,11 @@ globals@^13.19.0, globals@^13.24.0:
   dependencies:
     type-fest "^0.20.2"
 
+globals@^14.0.0:
+  version "14.0.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
 globals@^15.11.0:
   version "15.11.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-15.11.0.tgz#b96ed4c6998540c6fb824b24b5499216d2438d6e"
@@ -3820,7 +3822,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.4:
+inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -5283,7 +5285,7 @@ readable-stream@1.1.14:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0:
+"readable-stream@2 || 3", readable-stream@^3.4.0:
   version "3.6.2"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
   integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
@@ -5457,15 +5459,16 @@ sanitize-filename@^1.6.3:
   dependencies:
     truncate-utf8-bytes "^1.0.0"
 
-sass@^1.80.5:
-  version "1.80.5"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.5.tgz#0ba965223d44df22497f2966b498cf5c453fae8f"
-  integrity sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==
+sass@^1.80.6:
+  version "1.80.6"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.6.tgz#5d0aa55763984effe41e40019c9571ab73e6851f"
+  integrity sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==
   dependencies:
-    "@parcel/watcher" "^2.4.1"
     chokidar "^4.0.0"
     immutable "^4.0.0"
     source-map-js ">=0.6.2 <2.0.0"
+  optionalDependencies:
+    "@parcel/watcher" "^2.4.1"
 
 sax@^1.2.4:
   version "1.4.1"
@@ -5553,11 +5556,6 @@ set-function-name@^2.0.2:
     functions-have-names "^1.2.3"
     has-property-descriptors "^1.0.2"
 
-setimmediate@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
-  integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
-
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -5718,19 +5716,6 @@ std-env@^3.7.0:
   resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
   integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
 
-stream-browserify@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f"
-  integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==
-  dependencies:
-    inherits "~2.0.4"
-    readable-stream "^3.5.0"
-
-stream-shift@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b"
-  integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==
-
 "string-width-cjs@npm:string-width@^4.2.0":
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -6035,7 +6020,7 @@ tsconfig-paths@^3.15.0:
     minimist "^1.2.6"
     strip-bom "^3.0.0"
 
-tslib@^2.6.2, tslib@^2.6.3, tslib@^2.8.1:
+tslib@^2.6.2, tslib@^2.8.1:
   version "2.8.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -6067,7 +6052,7 @@ type-fest@^2.12.2:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
   integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
 
-type-fest@^4.24.0, type-fest@^4.26.1:
+type-fest@^4.26.1:
   version "4.26.1"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e"
   integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==
@@ -6238,11 +6223,6 @@ uuid@8.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
-uuid@^11.0.2:
-  version "11.0.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.2.tgz#a8d68ba7347d051e7ea716cc8dcbbab634d66875"
-  integrity sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==
-
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"