oriel

Concepts

How an Oriel app is put together: one native process, its windows and web views, the bridge between the page and Zig, modules, the build and the platform layer.

An Oriel app is one native program written in Zig. It opens windows whose content is a web page: HTML, CSS and JavaScript from the app’s frontend/, drawn by the platform’s own web view, or by Oriel’s native renderer with -Dnative_ui. The page calls Zig through a small bridge (window.oriel), and Zig pushes events back. Everything else, the tray, notifications, a database, a local model, is a module the Zig side uses and may expose to the page.

This page is the map. Each part links to the page that documents it in full.

The pieces

flowchart LR
  subgraph Process["One process: your Zig app"]
    Main["src/main.zig<br/>Commands, Events, setup"]
    Core["Oriel core<br/>App, ipc, security"]
    Mods["Modules<br/>oriel.tray, oriel.sql, ..."]
    Shell["Platform shell<br/>src/platform/OS/"]
  end
  Page["Page in a window<br/>(web view or native renderer)"]
  OS["Operating system<br/>windows, tray, files, ..."]
  Page -->|"invoke(cmd, args)"| Shell
  Shell -->|"checked request"| Core
  Core -->|"calls"| Main
  Main -->|"uses"| Mods
  Core -->|"events, replies"| Page
  Shell -->|"native API"| OS
  Mods -->|"native API"| OS
PieceWhat it isSourceDocumented in
Your appsrc/main.zig: the Commands and Events structs and main, which calls oriel.mainyour projectCommands and events
oriel_appA module the build generates from build.zig: the app id, embedded assets, dev server, icon, URL schemes, permissions, isolation hookbuild.zig (appConfigModule)The build
CoreWindows, IPC dispatch, events, asset lookup, the worker poolsrc/core/App.zig, ipc.zig, window_commands.zigWindows
SecurityNavigation policy, IPC scopes (capabilities), CSP, the isolation patternsrc/core/security.zig, csp.zig, isolation.zigSecurity
The bridgeThe window.oriel script every page gets, one script for every platformsrc/core/bridge_js.zigThe bridge
Platform shellThe event loop, native windows, the web view, the app scheme, the bridge transportsrc/platform/<os>/The platform layer
Modulesoriel.tray, oriel.sql, oriel.notification, …: compiled only when onsrc/modules/, src/plugins/Modules
Native codeApp extensions (your Kotlin, Objective-C, Swift or Zig as a service) and platform hooks (your code in the platform’s own events)src/core/extensions.zig, src/core/hooks.zigExtensions, Platform hooks
CLIoriel init, dev, build, package, …: wrappers around zig build plus tool managementcli/CLI

The entry point

const std = @import("std");
const oriel = @import("oriel");
const app = @import("oriel_app"); // generated from build.zig

pub const Commands = struct {
    pub fn greet(gpa: std.mem.Allocator, args: struct { name: []const u8 }) ![]const u8 {
        return std.fmt.allocPrint(gpa, "Hello, {s}!", .{args.name});
    }
};
pub const Events = struct { saved: struct { id: u64 } };

pub fn main(init: std.process.Init) !u8 {
    return oriel.main(init, .{ .commands = Commands, .events = Events }, .{
        .app = app.build, // id, assets, dev server, icon, url_schemes, permissions, isolation
        .window = .{ .title = "My App", .width = 800, .height = 600 }, // the main window
        .setup = setup, // optional: runs on the main thread once the main window exists
    });
}

fn setup() !void {
    std.log.info("main window is up", .{});
}

oriel.main(init, api, config) (in src/oriel.zig):

  1. initializes the updater when it is compiled in;
  2. answers --emit-types <path> by writing the page’s TypeScript bindings and exiting (the build uses this, see Commands);
  3. keeps the process arguments for the platform (deep links, a second launch), then runs the app on the calling (main) thread and returns the exit code when it quits.

It is the only entry point. An app that reads its own arguments does so before calling it (examples/smoke handles --check that way); a config chosen at runtime is a choice between two oriel.main calls, each with its own comptime config.

api and config are comptime: the commands, the events, the security policy and the main window’s options are known when the app compiles, so the bridge script, the CSP and the dispatch code are generated for exactly this app.

The process and its threads

flowchart TB
  subgraph UI["Main (UI) thread: the platform event loop"]
    Loop["GTK / Win32 / AppKit / UIKit / Android looper"]
    Sync["Sync commands, window commands,<br/>setup, tray and menu callbacks"]
  end
  subgraph Pool["Worker pool (2 to 16 threads)"]
    Async["async_commands"]
    Spawned["App.spawn jobs"]
  end
  Other["Module threads<br/>(fs_watch, extension workers, dictation, ...)"]
  Loop --> Sync
  Loop -->|"async command"| Async
  Async -->|"reply queued to main"| Loop
  Spawned -->|"App.runOnMain"| Loop
  Other -->|"events.emit (any thread)"| Loop
  • The main thread runs the platform’s event loop. Windows, web views, the tray and menus may only be touched there. Commands run there unless they are listed in async_commands, so a slow sync command freezes the UI.
  • The worker pool (src/core/ThreadPool.zig) has one thread per CPU, at least 2 and at most 16, and at most 1024 queued tasks. It runs async commands and App.spawn jobs; replies go back to the main thread.
  • From any thread, events.emit, events.emitTo, App.quit and App.runOnMain are safe. Use App.runOnMain(ctx, func) to touch a window from a worker.

The rules for each command and event are on Commands and events.

Windows and the page

A window is a native window plus what draws its page:

PlatformWindowWeb viewApp origin
LinuxGtkApplicationWindow (GTK 4)WebKitGTK 6.0app://app
WindowsWin32 HWNDWebView2https://app.localhost
macOSNSWindow (AppKit)WKWebViewapp://app
AndroidAn Activity per window (Kotlin, over JNI)android.webkit.WebViewhttps://app.localhost
iOSA view controller in a UIWindowSceneWKWebViewapp://app

With -Dnative_ui the same page runs in QuickJS and is drawn with native views instead of a web view, on every platform; see Native renderer.

In a production build the frontend’s dist/ is embedded in the executable and served from the app origin by the platform’s scheme handler (src/platform/<os>/scheme.zig), with the app’s CSP on every page. In a development build the window loads the frontend’s dev server instead (Development). Each window has a label; the first one, from App.Config.window, is "main". Windows, their options and the page’s oriel.window API are on Windows.

The bridge

Every page the app trusts gets one script before its own scripts run: window.oriel, built from src/core/bridge_js.zig around the page API in src/core/bridge_api.js (which the native renderer bundles too). It is the same script on every platform; each platform only supplies how a message travels:

PlatformPage to ZigZig to page
Linux, macOS, iOSWebKit script message handler oriel (postMessage with a reply)evaluateJavascript("window.oriel.__emit(...)")
Windows, AndroidA message channel (WebView2 chrome.webview, Android WebMessageListener); requests carry an idEvents and replies on the same channel, in order

sequenceDiagram
  participant P as Page
  participant B as window.oriel
  participant S as Platform bridge
  participant C as Oriel core
  participant Z as Your command
  P->>B: invoke("greet", { name })
  B->>S: { cmd, args, token }
  S->>S: size, token, isolation and capability checks
  S->>C: dispatch
  C->>Z: greet(arena, args)
  Z-->>C: result
  C-->>S: JSON
  S-->>B: reply
  B-->>P: Promise resolves

What the bridge carries is checked before anything runs: the request size (32 MiB at most), the page’s IPC token (only the top frame of an allowed origin has it), the isolation hook’s signature when the isolation pattern is on, and the calling origin’s capabilities. The checks are on Security.

window.oriel holds invoke, listen, openExternal, platform, features, window, permissions, lifecycle, deepLink, share, camera, extensions and systemAi. The generated frontend/src/oriel.ts gives them types (Commands).

Commands and events

The app’s API to the page is two plain Zig structs:

  • Commands: every pub fn is a command the page calls with invoke(name, args). Arguments arrive as JSON and are parsed into the function’s parameter type; the result goes back as JSON.
  • Events: every field is an event Zig pushes with events.emit(.name, payload); the page subscribes with listen(name, callback).

Both sides are typed: the build generates TypeScript from the structs. Commands and events has the rules.

Modules

Oriel’s features beyond windows and IPC are modules: oriel.tray, oriel.menu, oriel.dialog, oriel.notification, oriel.store, oriel.sql, oriel.fs_watch, oriel.media_server, oriel.deep_link, oriel.share, oriel.camera, the local AI modules (oriel.llama, oriel.whisper, oriel.tts, …) and the plugins oriel.clipboard, oriel.global_shortcut and oriel.input.

  • Each one is a build flag on the oriel dependency (.tray = false). A module that is off is not compiled or linked.
  • oriel.<module>.available is a comptime bool: false where the build leaves the module out or the target has none (a tray on a phone). Guard calls with if (oriel.tray.available).
  • The page sees the same flags as oriel.features (features.tray, features.fsWatch, camelCase).

The list, flags and per-platform availability are on Modules.

Native code

Two ways in, for code the modules don’t cover:

  • App extensions: your Kotlin, Objective-C, Swift or Zig as a named service; Zig calls it, and the page only through what Zig exposes.
  • Platform hooks: your code inside the platform’s own events (an Activity starting, a web view being created, a URL opening).

The build

flowchart LR
  BZ["build.zig<br/>oriel.addApp(...)"] -->|"options"| Gen["oriel_app module<br/>(oriel_app.build)"]
  BZ -->|"npm run build"| Dist["frontend/dist"]
  Dist -->|"embed_assets<br/>(+ CSP hashes)"| Gen
  Gen --> Prod["Production exe<br/>zig-out/bin/NAME"]
  BZ -->|"dev settings"| Dev["Dev exe NAME-dev<br/>(loads the dev server)"]
  Dev -->|"--emit-types"| TS["frontend/src/oriel.ts"]
  Prod -->|"zig build package"| Pkg["deb, rpm, AppImage,<br/>setup.exe, .app, .dmg"]

An app’s build.zig calls oriel.addApp(b, dep, .{ ... }) with its name, root file, frontend, icon, package metadata, URL schemes, permissions and platform options. From that the build makes:

  • oriel_app, a generated module the app imports. oriel_app.build is an App.Build (below) to pass as App.Config.app; oriel_app.update_public_key is the updater’s key.
  • A production executable with the frontend’s dist/ embedded (tools/embed_assets.zig, which also hashes each HTML file’s inline scripts and styles for the CSP), optimized ReleaseSafe unless -Doptimize says otherwise.
  • A development executable <name>-dev (Debug) whose assets are empty and whose dev points at the frontend’s dev server.
  • Steps: zig build (production), run, dev, build-dev, types, check, package and the per-format package steps, desktop-entry; Android and iOS targets have their own (android-dev, ios-dev, …).

The oriel CLI runs these steps for you from anywhere in the project. The build options are on Building, packaging on Packaging, the dev loop on Development.

The platform layer

src/platform/platform.zig picks one backend at compile time and checks, with comptime assertions, that it exports the whole contract: window operations (createWindow, showWindow, setWindowPlacement, startWindowDrag, …), the event loop (run, quit), evalJs, openExternal, dispatchWithCleanup (queue work on the UI thread) and setMenu. The core never calls an OS API directly.

DirectoryUnderneathNotes
src/platform/linux/GTK 4, WebKitGTK 6.0, GIOShell.zig (GtkApplication, signals), window.zig, scheme.zig (app://), bridge.zig, overlay.zig (layer-shell, X11 hints), dev_server.zig
src/platform/windows/Win32, WebView2, WinRTCross-builds from Linux; WebView2Loader.dll comes from oriel webview2
src/platform/macos/AppKit, WebKit, through the Objective-C runtime from ZigNo Xcode project; .app and .dmg from zig build package
src/platform/android/A Kotlin Activity and android.webkit.WebView, driven over JNIThe Gradle project lives in the app’s android/ (oriel android init)
src/platform/ios/UIKit and WKWebView from ZigNo Xcode project; oriel ios build
src/platform/apple/Shared by macOS and iOSPlatform hooks, extensions

Which modules exist on which platform, and what each platform needs to build, is on Desktop, Android, iOS and Modules.

Reference

oriel.main

DeclarationMeaning
oriel.main(init: std.process.Init, comptime api: App.Api, comptime config: App.Config) !u8The entry point: updater init, --emit-types, process arguments, then the app runs on the calling (main) thread until it quits. Returns the exit code.
App.Apistruct { commands: type, events: type = struct {} }
oriel.writeTypes(io, gpa, comptime api, path) !voidWrites the TypeScript bindings to path, leaving the file untouched when nothing changed.

App.Config

FieldTypeDefaultMeaning
appApp.BuildrequiredWhat build.zig declares: pass oriel_app.build.
windowWindowOptions.{}The main window; its label is always "main" (another label is a compile error). Every field: Windows.
user_agent?[:0]const u8nullThe web view’s User-Agent.
start[:0]const u8"index.html"Path and query loaded at startup, relative to the app origin, when the window has no url.
spa_fallbackbooltrueAn unknown path whose last segment has no dot serves index.html (client-side routing).
devtoolsbooltrue in Debug buildsThe web view’s inspector (Development).
securitysecurity.Security.{}CSP, allowed origins, capabilities, external links, window API policy, headers (Security).
window_open.main_view or .new_window.main_viewWhere window.open() and target="_blank" to an app page go (Windows).
setup?*const fn () anyerror!voidnullRuns once on the main thread after the main window exists: create the tray, register shortcuts, start background work. An error is logged.
on_second_instance?*const fn (args: []const []const u8) voidnullApps are single-instance on the desktop: a second launch forwards its arguments (without argv[0]) to the running instance and exits. This runs on the running instance’s main thread with them. Null keeps the platform default (the main window comes back).

App.Build

Generated into oriel_app.build; an app doesn’t write it by hand.

FieldTypeFrom build.zig
id[:0]const u8.package.id, else "dev.oriel.<name>"
assets[]const App.AssetThe embedded frontend/dist (empty in dev builds)
dev?App.DevThe dev server (null in production builds)
icon?[]const u8.icon, PNG bytes (Oriel’s icon by default)
url_schemes[]const []const u8.url_schemes (Deep links)
permissionspermissions.Declared.permissions (Permissions)
isolation?security.Isolation.isolation (Security)

Source map

FileWhat’s in it
src/oriel.zigThe oriel module: main, writeTypes, every module by name
src/core/App.zigThe app’s API: Config, Build, WindowOptions, Window, events, spawn, runOnMain
src/core/internal.zigOriel’s own state, not exported: the window list and its lock, the worker pool, the untyped emit modules use, run
src/core/ipc.zigCommand dispatch, async dispatch, built-in commands, IPC tokens, TypeScript generation
src/core/window_commands.zigThe page’s oriel.window commands
src/core/bridge_js.zig, bridge_api.jsThe window.oriel script; the page API itself, shared with the native renderer
src/core/security.zig, csp.zig, isolation.zigThe security policy
src/core/ThreadPool.zigThe worker pool
src/platform/platform.zigThe backend contract and selection
build.zigaddApp, AppOptions, Frontend, the generated oriel_app
tools/dev_runner.zigoriel dev’s rebuild-and-restart loop
cli/The oriel command

Migrating from 0.11

  • App.run(io, api, config) and App.setProcessArgs are gone: call oriel.main(init, api, config), after parsing any arguments of your own.
  • The platform layer’s state on App (windows_list, windows_mutex, getWorkerPool, current_security, gtk_app, main_window, …) moved to an internal file apps can’t reach. Native handles come from platform hooks.

Related

Search documentation

Type to search the documentation.