Main process
The main process is an optional Node program (the "sidecar") that the kernel
spawns when OW_APP_MAIN is set. ow dev and ow build compile app/main.ts
and set that variable for you. Its SDK, @owear/core, is intentionally close to
Electron.
import { app, BrowserWindow, Menu, Tray } from '@owear/core'
app.whenReady().then(() => {
const win = new BrowserWindow({ width: 1024, height: 700, url: 'app://index.html' })
win.on('closed', () => app.quit())
})
app
Lifecycle and identity
await app.whenReady() // connects to the kernel control socket
app.quit(exitCode = 0)
app.info() // { pid, version, socket }
app.getName() / app.setName(name)
app.getVersion() // package.json version, else kernel version
app.isPackaged() // true in a packaged app
app.getAppPath() // root of the app bundle
Paths (Electron conventions)
app.getPath(name) // home, appData, userData, sessionData, cache, temp, logs,
// downloads, documents, desktop, pictures, music, videos,
// exe, appPath
app.setPath(name, value)
Events
app.on('before-quit', () => {})
app.on('will-quit', () => {})
app.on('window-all-closed', () => {})
app.on('second-instance', (argv) => {})
app.on('activate', () => {})
app.on('child-process-gone', () => {})
Single instance
const gotLock = await invokeNative<boolean>('app', 'requestSingleInstanceLock')
if (!gotLock) {
app.quit()
} else {
app.on('second-instance', (payload) => { /* focus your window */ })
}
The first instance binds a per-app socket; a second instance connects, forwards
its argv, and exits. The first instance receives second-instance.
Exposing Node to the renderer
app.handle(fn, handler) // (…args) => result
app.handleContext(fn, handler) // (ctx, …args) => result, ctx = { windowId }
app.send(name, payload?, windowId?)
See IPC.
Browser flags
app.commandLine.appendSwitch('disable-gpu')
app.commandLine.appendArgument('--enable-features=Foo')
app.commandLine.hasSwitch('disable-gpu')
app.commandLine.getSwitchValue('disable-gpu')
Applied per WebView when windows are created (Windows: AdditionalBrowserArguments; Linux: best effort).
Custom protocols
app.protocol('scrakk-ext', {
privileged: { secure: true, cors: true },
handler: async (req) => new Response('<h1>hello</h1>', { headers: { 'content-type': 'text/html' } }),
})
app.protocol('assets', { serve: '/path/to/dir' }) // kernel serves the directory
The handler runs in the main process and may return a Response, a
{ status, headers, body } object, a string, or null (→ 404). See
protocol reference.
Node runtime and workers
await app.ensureNodeRuntime('lts') // { path, version, source }
app.forkWorker('heavy-worker.js')
app.workersDir() // OW_APP_WORKERS, if defined
ensureNodeRuntime resolves a Node binary with the priority
OW_NODE_BIN → system Node → Owear cache → official download. source tells you
which path won (env | system | cache | downloaded).
App icon
app.setIcon('/path/to/icon.png') // default icon for windows created afterwards
Workers
Workers are child processes with an IPC channel — the replacement for Electron's
utilityProcess.fork.
const w = app.forkWorker('tree-sitter-worker.js') // or app/workers/tree-sitter-worker.ts
w.on('message', (m) => console.log(m))
w.postMessage({ op: 'tokenize', text })
w.on('exit', (code) => console.log('worker exited', code))
w.kill()
The entry may be absolute, relative to app.workersDir(), or start with ./.
ow dev/ow build compile app/workers/** and point OW_APP_WORKERS at them.
Because the worker runs on the same real Node, it can require native addons
(see Native addons).
Tips
- Keep the main process thin. Window orchestration, menus, tray, and Node-only integrations belong here; everything else can go in the renderer.
app.whenReady()is idempotent and returns the same promise, so calling it from several modules is fine.- The control socket can drop (kernel crash). Listen for
disconnectedon windows to surface it.