Velox is a port of Tauri to Swift.
See the codeBringing Tauri to Swift developers.
I love Rust as much as the next security paranoid person, but I do not love it to write apps, it gets in my way, and I am too old to develop an appreciation for poetry or Rust. So this is a port of Tauri to Swift so I can both build desktop apps using HTML with Swift backends and fill me with joy.
Discord: invite
The Swift package declares a build-tool plugin that automatically compiles the Rust FFI crate
whenever VeloxRuntimeWryFFI is built. Simply run:
swift build
The plugin will invoke cargo build with the correct configuration (debug or release) and
emit libraries into runtime-wry-ffi/target. If you prefer to build the Rust crate manually you
can still run cargo build or cargo build --release inside runtime-wry-ffi/.
By default the plugin runs Cargo in offline mode to avoid sandboxed network access and ensures
velox/.cargo/config.toml patch overrides are picked up. If you need Cargo to fetch from the
network, set VELOX_CARGO_ONLINE=1 when building.
Use the create-velox-app command to create a new blank project, starting from one of the built-in templates.
Velox includes a CLI tool for development workflow, similar to Tauri's CLI.
swift build --product velox
The CLI binary will be available at .build/debug/velox.
velox init - Initialize a New ProjectInitialize Velox in a new or existing directory:
# Initialize with defaults (derives name from directory)
velox init
# Specify product name and identifier
velox init --name "MyApp" --identifier "com.example.myapp"
# Overwrite existing files
velox init --force
This creates:
your-project/
├── Package.swift # Swift package manifest
├── Sources/
│ └── YourApp/
│ └── main.swift # App entry point with IPC handlers
├── assets/
│ └── index.html # Frontend UI template
└── velox.json # Velox configuration
velox dev - Development ModeRun the app in development mode with hot reloading:
# Run with auto-detected target
velox dev
# Specify a target explicitly
velox dev MyApp
# Run in release mode
velox dev --release
# Disable file watching
velox dev --no-watch
# Override dev server port
velox dev --port 3000
Features:
beforeDevCommand from velox.json (e.g., npm run dev)devUrl if configuredVELOX_DEV_URL setdevUrl is set, the app:// protocol proxies requests to your dev server, enabling HMR from tools like Vitevelox build - Production BuildBuild the app for production:
# Release build (default)
velox build
# Debug build
velox build --debug
# Create macOS app bundle (.app)
velox build --bundle
# Specify target
velox build MyApp
# Debug build with app bundle
velox build --debug --bundle
The --bundle flag creates a complete macOS app bundle:
.build/release/MyApp.app/
├── Contents/
│ ├── Info.plist # Generated from velox.json
│ ├── MacOS/
│ │ └── MyApp # Executable
│ └── Resources/
│ └── assets/ # Frontend files (from frontendDist)
You can also set bundle.active: true in velox.json to enable bundling without the CLI flag.
For full macOS bundling details (signing, DMG, notarization), see Sources/VeloxRuntime/VeloxRuntime.docc/Articles/Bundling.md.
The CLI uses settings from velox.json:
{
"productName": "MyApp",
"version": "1.0.0",
"identifier": "com.example.myapp",
"build": {
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"beforeBundleCommand": "npm run prepare-bundle",
"frontendDist": "dist",
"env": {
"API_URL": "https://api.example.com",
"DEBUG": "true"
}
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"icon": "icons/AppIcon.icns",
"resources": ["extra-assets"],
"macos": {
"minimumSystemVersion": "13.0",
"infoPlist": "Info.plist",
"entitlements": "entitlements.plist",
"signingIdentity": "Developer ID Application: Example (ABCDE12345)",
"hardenedRuntime": true,
"dmg": {
"enabled": true,
"name": "MyApp",
"volumeName": "MyApp"
},
"notarization": {
"keychainProfile": "AC_NOTARY",
"wait": true,
"staple": true
}
}
}
}
| Field | Description |
|---|---|
devUrl | Dev server URL; enables proxy mode (see below) |
beforeDevCommand | Command to run before velox dev (e.g., start Vite) |
beforeBuildCommand | Command to run before velox build (e.g., build frontend) |
beforeBundleCommand | Command to run before creating app bundle |
frontendDist | Directory containing frontend assets for bundling |
env | Environment variables to inject into build and dev processes |
Velox supports environment variable injection from multiple sources:
.env - Base environment file.env.development or .env.production - Mode-specific overrides.env.local - Local overrides (gitignored)velox.json build.env - Configuration-defined variablesPriority (highest to lowest): system env > velox.json > .env.local > .env.[mode] > .env
Example .env file:
API_URL=https://api.example.com
DEBUG=true
# Comments are supported
MULTILINE="line1\nline2"
Velox offers two approaches for serving frontend assets during development. Choose based on your project's complexity and tooling preferences.
When to use: Static HTML/CSS/JS without a build step, simple projects, or when you want the fastest possible reload cycle.
{
"build": {
"frontendDist": "assets"
}
}
How it works:
velox dev serves files directly from the frontendDist directory (e.g., assets/)index.html, styles.css, or app.js, the app restarts instantly (no rebuild)Pros:
assets/Cons:
When to use: Projects using Vite, webpack, or other modern frontend toolchains that provide HMR.
{
"build": {
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"frontendDist": "dist"
}
}
How it works:
beforeDevCommand starts your frontend dev server (e.g., Vite)devUrl to respond before launching the appVELOX_DEV_URL environment variable is passed to your Swift appapp:// protocol proxies all requests to the dev serverPros:
app:// protocol between dev and productionCons:
| Feature | Local Assets | Dev Server Proxy |
|---|---|---|
| Setup complexity | Minimal | Requires npm project |
| Frontend tooling | None (vanilla JS) | Full (Vite, webpack, etc.) |
| TypeScript/JSX | Not supported | Fully supported |
| Hot Module Replacement | No (app restart) | Yes (instant) |
| Page reload on change | Full restart | Partial/none (HMR) |
| Swift change handling | Rebuild + restart | Rebuild + restart |
| Production build | Copy files | Run build command |
To switch from proxy mode to local asset serving, simply remove or comment out devUrl:
{
"build": {
// "devUrl": "http://localhost:5173", // Commented out = local mode
// "beforeDevCommand": "npm run dev", // Not needed without devUrl
"frontendDist": "assets"
}
}
The same frontendDist directory is used for both development (local mode) and production builds.
The repository includes several example applications demonstrating Velox capabilities. Examples are located in the Examples/ directory.
Build and run any example using Swift Package Manager:
# Build all examples
swift build
# Run a specific example
swift run HelloWorld
swift run HelloWorld2
swift run MultiWindow
swift run State
swift run Splashscreen
swift run Streaming
swift run RunReturn
let loop = VeloxRuntimeWry.EventLoop()
let proxy = loop?.makeProxy()
let window = loop?.makeWindow(configuration: .init(width: 800, height: 600, title: "Velox"))
let webview = window?.makeWebview(configuration: .init(url: "https://tauri.app"))
loop?.pump { event in
switch event {
case .loopDestroyed, .userExit:
return .exit
default:
return .poll
}
}
proxy?.requestExit()
This demonstrates the bridging between Swift and the underlying Tao/Wry event loop, window, and
webview primitives exposed by the Rust shim. Event callbacks now deliver structured metadata (via
JSON) which the Swift layer normalises into strongly-typed VeloxRuntimeWry.Event values.
VeloxRuntimeWry.Event exposes rich keyboard, pointer, focus, DPI, and file-drop information so
Swift applications can respond to Tao/Wry input without having to touch the underlying JSON payloads.
The Swift API now includes helpers to:
Velox now ships a nascent VeloxRuntime module that defines the Swift-first protocols mirroring Tauri's runtime traits. VeloxRuntimeWry.Runtime remains a stub while the native implementation is completed; the event-loop based APIs remain the primary entry point until the dedicated Swift runtime is feature-complete.
The repository includes several example applications demonstrating Velox capabilities. Examples are located in the Examples/ directory.
Build and run any example using Swift Package Manager:
# Build all examples
swift build
# Run a specific example
swift run HelloWorld
swift run HelloWorld2
swift run MultiWindow
swift run State
swift run Splashscreen
swift run Streaming
swift run RunReturn
swift run Commands
swift run CommandsManual
swift run CommandsManualRegistry
swift run Resources
swift run WindowControls
swift run MultiWebView
swift run DynamicHTML
swift run Events
swift run Tray
Velox supports two approaches for loading web content, mirroring Tauri's flexibility:
The simplest approach embeds HTML directly in Swift code. This is ideal for simple UIs or when you want a single-binary deployment with no external dependencies.
Example: HelloWorld
let html = """
<!doctype html>
<html>
<body>
<h1>Hello from Velox!</h1>
</body>
</html>
"""
let appProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "app") { _ in
VeloxRuntimeWry.CustomProtocol.Response(
status: 200,
headers: ["Content-Type": "text/html"],
body: Data(html.utf8)
)
}
Pros:
Cons:
For larger applications, keep HTML, CSS, and JavaScript as separate files loaded at runtime. This mirrors Tauri's asset bundling approach.
Example: HelloWorld2
Examples/HelloWorld2/
├── main.swift # Swift entry point with AssetBundle
└── assets/
├── index.html # HTML markup
├── styles.css # Stylesheet
└── app.js # JavaScript
The AssetBundle struct discovers and serves files from the assets directory:
struct AssetBundle {
let basePath: String
func loadAsset(path: String) -> (data: Data, mimeType: String)? {
// Load file and detect MIME type
}
}
let appProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "app") { request in
guard let url = URL(string: request.url),
let asset = assets.loadAsset(path: url.path) else {
return notFoundResponse()
}
return VeloxRuntimeWry.CustomProtocol.Response(
status: 200,
headers: ["Content-Type": asset.mimeType],
body: asset.data
)
}
Pros:
Cons:
| Example | Description | Asset Approach |
|---|---|---|
| HelloWorld | Basic window with inline HTML and IPC | Self-contained |
| HelloWorld2 | Same functionality with external assets | Bundled assets |
| MultiWindow | Multiple windows running simultaneously | Self-contained |
| State | Shared state across IPC calls | Self-contained |
| Splashscreen | Splash window before main window | Self-contained |
| Streaming | Server-sent events from Swift to webview | Self-contained |
| RunReturn | Manual event loop control | Self-contained |
| Commands | @VeloxCommand macro for cleanest command definitions | Bundled assets |
| CommandsManual | Manual IPC routing with switch statement | Bundled assets |
| CommandsManualRegistry | Type-safe command DSL with automatic JSON decoding | Bundled assets |
| Resources | Resource bundling and path resolution | Bundled assets |
| WindowControls | Comprehensive window/webview API demonstration | Self-contained |
| MultiWebView | Multiple child webviews: local app + GitHub, tauri.app, Twitter | Mixed |
| DynamicHTML | Swift-rendered dynamic HTML with counter, todos, and themes | Self-contained |
| Events | Event system: backend-to-frontend and frontend-to-backend events | Self-contained |
| Tray | System tray icon with context menu (macOS) | Self-contained |
Velox supports a configuration file (velox.json) similar to Tauri's tauri.conf.json. This allows declarative app configuration:
{
"$schema": "https://velox.dev/schema/velox.schema.json",
"productName": "MyApp",
"version": "1.0.0",
"identifier": "com.example.myapp",
"app": {
"windows": [
{
"label": "main",
"title": "My Application",
"width": 800,
"height": 600,
"url": "app://localhost/",
"create": true,
"visible": true,
"resizable": true,
"devtools": true,
"customProtocols": ["app", "ipc"]
}
],
"macOS": {
"activationPolicy": "regular"
}
},
"build": {
"frontendDist": "assets"
}
}
Devtools: devtools defaults to true in debug builds and false in release. On macOS, enabling devtools uses private WebKit APIs, so avoid setting it to true for release builds.
Use VeloxAppBuilder to create your app from configuration:
import VeloxRuntime
import VeloxRuntimeWry
let config = try VeloxConfig.load(from: URL(fileURLWithPath: "path/to/app"))
let eventLoop = VeloxRuntimeWry.EventLoop()!
let app = VeloxAppBuilder(config: config)
.registerProtocol("app") { request in
// Serve assets
}
.registerProtocol("ipc") { request in
// Handle IPC commands
}
.build(eventLoop: eventLoop)
Platform-Specific Overrides: Create velox.macos.json, velox.ios.json, etc. to override settings per platform using RFC 7396 JSON Merge Patch.
Both approaches use custom protocols for IPC between Swift and the webview. Velox injects a
window.Velox.invoke helper that supports both immediate and deferred command responses:
// JavaScript: invoke a Swift command (preferred)
const message = await window.Velox.invoke('greet', { name: 'World' });
If you need a custom helper, you can still use fetch for immediate responses:
async function invoke(command, args = {}) {
const response = await fetch(`ipc://localhost/${command}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args)
});
return (await response.json()).result;
}
To return after the IPC handler completes (for modal dialogs or long tasks), defer the response
and resolve it later. window.Velox.invoke will await the final result automatically:
struct DelayedEchoArgs: Codable, Sendable { let message: String; let delayMs: Int? }
commands.register("delayed_echo", args: DelayedEchoArgs.self, returning: DeferredCommandResponse.self) { args, ctx in
let deferred = try ctx.deferResponse()
let delay = max(0, args.delayMs ?? 500)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
deferred.responder.resolve(args.message)
}
return deferred.pending
}
const reply = await window.Velox.invoke('delayed_echo', { message: 'Hello later', delayMs: 800 });
Velox provides the @VeloxCommand macro for the cleanest command definitions, similar to Tauri's #[tauri::command]:
import VeloxMacros
import VeloxRuntimeWry
// Define response types
struct GreetResponse: Codable, Sendable {
let message: String
}
// Commands must be in a container (enum/struct) for macro expansion
enum Commands {
@VeloxCommand
static func greet(name: String) -> GreetResponse {
GreetResponse(message: "Hello, \(name)!")
}
@VeloxCommand
static func divide(numerator: Double, denominator: Double) throws -> MathResponse {
guard denominator != 0 else {
throw CommandError(code: "DivisionByZero", message: "Cannot divide by zero")
}
return MathResponse(result: numerator / denominator)
}
// Access state via CommandContext parameter
@VeloxCommand
static func increment(context: CommandContext) -> CounterResponse {
let state: AppState = context.requireState()
return CounterResponse(value: state.increment())
}
// Custom command name
@VeloxCommand("get_counter")
static func getCounter(context: CommandContext) -> CounterResponse {
let state: AppState = context.requireState()
return CounterResponse(value: state.counter)
}
}
// Register macro-generated commands
let registry = commands {
Commands.greetCommand // Generated by @VeloxCommand
Commands.divideCommand
Commands.incrementCommand
Commands.getCounterCommand
}
See Examples/Commands for a complete demonstration of the @VeloxCommand macro.
For cases where you prefer explicit control, use the command DSL directly:
import VeloxRuntime
import VeloxRuntimeWry
// Define typed arguments and responses
struct GreetArgs: Codable, Sendable {
let name: String
}
struct GreetResponse: Codable, Sendable {
let message: String
}
// Register commands using the DSL
let registry = commands {
command("greet", args: GreetArgs.self, returning: GreetResponse.self) { args, _ in
GreetResponse(message: "Hello, \(args.name)!")
}
command("divide", args: DivideArgs.self, returning: MathResponse.self) { args, _ in
guard args.denominator != 0 else {
throw CommandError(code: "DivisionByZero", message: "Cannot divide by zero")
}
return MathResponse(result: args.numerator / args.denominator)
}
command("increment", returning: CounterResponse.self) { context in
let state: AppState = context.requireState()
return CounterResponse(value: state.increment())
}
}
// Create IPC handler and register protocol
let stateContainer = StateContainer().manage(AppState())
let ipcHandler = createCommandHandler(registry: registry, stateContainer: stateContainer)
let ipcProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "ipc", handler: ipcHandler)
See Examples/CommandsManualRegistry for a complete demonstration of the type-safe command DSL.
For simpler cases, you can handle IPC requests manually:
// Swift: handle IPC requests manually
let ipcProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "ipc") { request in
let command = URL(string: request.url)?.path.trimmingCharacters(in: .init(charactersIn: "/"))
switch command {
case "greet":
let name = parseArgs(request.body)["name"] as? String ?? "World"
return jsonResponse(["result": "Hello \(name)!"])
default:
return errorResponse("Unknown command")
}
}
Package.swift: Swift Package definition exposing the VeloxRuntimeWry library target.Sources/VeloxRuntimeWry: Swift surface area that mirrors the Tauri runtime concepts with
Velox naming.Sources/VeloxRuntimeWryFFI: Lightweight C target that bridges into the Rust static library.runtime-wry-ffi: Rust crate producing a velox_runtime_wry_ffi static/dynamic library that
re-exports selected pieces of tao, wry, and tauri-runtime-wry.Velox supports two build modes for the Rust FFI crate:
Uses published versions of tao and wry from crates.io. This is the default for clean checkouts and CI:
# Remove local patches if present
rm -f .cargo/config.toml
swift build
Uses locally patched tao and wry with additional testing features. Requires sibling checkouts of these repositories with the velox-testing feature added to tao's default features.
# Ensure .cargo/config.toml exists with patches (at package root, not runtime-wry-ffi)
# Then build with local-dev feature:
VELOX_LOCAL_DEV=1 swift build
The .cargo/config.toml (in the package root) patches crates.io dependencies with local paths:
[patch.crates-io]
tao = { path = "../tao" }
wry = { path = "../wry" }
Requirements for Local Dev:
tao version must match crates.io (currently 0.34.5)tao must have velox-testing in its default features in Cargo.tomlwry version must match crates.io (currently 0.53.5)Velox is licensed under either of:
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Velox by you shall be dual licensed as above, without any additional terms or conditions.
Swift
78.5%
Rust
15.5%
C
3.0%
JavaScript
2.1%
Velox is a port of Tauri to Swift.
See the codeBringing Tauri to Swift developers.
I love Rust as much as the next security paranoid person, but I do not love it to write apps, it gets in my way, and I am too old to develop an appreciation for poetry or Rust. So this is a port of Tauri to Swift so I can both build desktop apps using HTML with Swift backends and fill me with joy.
Discord: invite
The Swift package declares a build-tool plugin that automatically compiles the Rust FFI crate
whenever VeloxRuntimeWryFFI is built. Simply run:
swift build
The plugin will invoke cargo build with the correct configuration (debug or release) and
emit libraries into runtime-wry-ffi/target. If you prefer to build the Rust crate manually you
can still run cargo build or cargo build --release inside runtime-wry-ffi/.
By default the plugin runs Cargo in offline mode to avoid sandboxed network access and ensures
velox/.cargo/config.toml patch overrides are picked up. If you need Cargo to fetch from the
network, set VELOX_CARGO_ONLINE=1 when building.
Use the create-velox-app command to create a new blank project, starting from one of the built-in templates.
Velox includes a CLI tool for development workflow, similar to Tauri's CLI.
swift build --product velox
The CLI binary will be available at .build/debug/velox.
velox init - Initialize a New ProjectInitialize Velox in a new or existing directory:
# Initialize with defaults (derives name from directory)
velox init
# Specify product name and identifier
velox init --name "MyApp" --identifier "com.example.myapp"
# Overwrite existing files
velox init --force
This creates:
your-project/
├── Package.swift # Swift package manifest
├── Sources/
│ └── YourApp/
│ └── main.swift # App entry point with IPC handlers
├── assets/
│ └── index.html # Frontend UI template
└── velox.json # Velox configuration
velox dev - Development ModeRun the app in development mode with hot reloading:
# Run with auto-detected target
velox dev
# Specify a target explicitly
velox dev MyApp
# Run in release mode
velox dev --release
# Disable file watching
velox dev --no-watch
# Override dev server port
velox dev --port 3000
Features:
beforeDevCommand from velox.json (e.g., npm run dev)devUrl if configuredVELOX_DEV_URL setdevUrl is set, the app:// protocol proxies requests to your dev server, enabling HMR from tools like Vitevelox build - Production BuildBuild the app for production:
# Release build (default)
velox build
# Debug build
velox build --debug
# Create macOS app bundle (.app)
velox build --bundle
# Specify target
velox build MyApp
# Debug build with app bundle
velox build --debug --bundle
The --bundle flag creates a complete macOS app bundle:
.build/release/MyApp.app/
├── Contents/
│ ├── Info.plist # Generated from velox.json
│ ├── MacOS/
│ │ └── MyApp # Executable
│ └── Resources/
│ └── assets/ # Frontend files (from frontendDist)
You can also set bundle.active: true in velox.json to enable bundling without the CLI flag.
For full macOS bundling details (signing, DMG, notarization), see Sources/VeloxRuntime/VeloxRuntime.docc/Articles/Bundling.md.
The CLI uses settings from velox.json:
{
"productName": "MyApp",
"version": "1.0.0",
"identifier": "com.example.myapp",
"build": {
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"beforeBundleCommand": "npm run prepare-bundle",
"frontendDist": "dist",
"env": {
"API_URL": "https://api.example.com",
"DEBUG": "true"
}
},
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"icon": "icons/AppIcon.icns",
"resources": ["extra-assets"],
"macos": {
"minimumSystemVersion": "13.0",
"infoPlist": "Info.plist",
"entitlements": "entitlements.plist",
"signingIdentity": "Developer ID Application: Example (ABCDE12345)",
"hardenedRuntime": true,
"dmg": {
"enabled": true,
"name": "MyApp",
"volumeName": "MyApp"
},
"notarization": {
"keychainProfile": "AC_NOTARY",
"wait": true,
"staple": true
}
}
}
}
| Field | Description |
|---|---|
devUrl | Dev server URL; enables proxy mode (see below) |
beforeDevCommand | Command to run before velox dev (e.g., start Vite) |
beforeBuildCommand | Command to run before velox build (e.g., build frontend) |
beforeBundleCommand | Command to run before creating app bundle |
frontendDist | Directory containing frontend assets for bundling |
env | Environment variables to inject into build and dev processes |
Velox supports environment variable injection from multiple sources:
.env - Base environment file.env.development or .env.production - Mode-specific overrides.env.local - Local overrides (gitignored)velox.json build.env - Configuration-defined variablesPriority (highest to lowest): system env > velox.json > .env.local > .env.[mode] > .env
Example .env file:
API_URL=https://api.example.com
DEBUG=true
# Comments are supported
MULTILINE="line1\nline2"
Velox offers two approaches for serving frontend assets during development. Choose based on your project's complexity and tooling preferences.
When to use: Static HTML/CSS/JS without a build step, simple projects, or when you want the fastest possible reload cycle.
{
"build": {
"frontendDist": "assets"
}
}
How it works:
velox dev serves files directly from the frontendDist directory (e.g., assets/)index.html, styles.css, or app.js, the app restarts instantly (no rebuild)Pros:
assets/Cons:
When to use: Projects using Vite, webpack, or other modern frontend toolchains that provide HMR.
{
"build": {
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"frontendDist": "dist"
}
}
How it works:
beforeDevCommand starts your frontend dev server (e.g., Vite)devUrl to respond before launching the appVELOX_DEV_URL environment variable is passed to your Swift appapp:// protocol proxies all requests to the dev serverPros:
app:// protocol between dev and productionCons:
| Feature | Local Assets | Dev Server Proxy |
|---|---|---|
| Setup complexity | Minimal | Requires npm project |
| Frontend tooling | None (vanilla JS) | Full (Vite, webpack, etc.) |
| TypeScript/JSX | Not supported | Fully supported |
| Hot Module Replacement | No (app restart) | Yes (instant) |
| Page reload on change | Full restart | Partial/none (HMR) |
| Swift change handling | Rebuild + restart | Rebuild + restart |
| Production build | Copy files | Run build command |
To switch from proxy mode to local asset serving, simply remove or comment out devUrl:
{
"build": {
// "devUrl": "http://localhost:5173", // Commented out = local mode
// "beforeDevCommand": "npm run dev", // Not needed without devUrl
"frontendDist": "assets"
}
}
The same frontendDist directory is used for both development (local mode) and production builds.
The repository includes several example applications demonstrating Velox capabilities. Examples are located in the Examples/ directory.
Build and run any example using Swift Package Manager:
# Build all examples
swift build
# Run a specific example
swift run HelloWorld
swift run HelloWorld2
swift run MultiWindow
swift run State
swift run Splashscreen
swift run Streaming
swift run RunReturn
let loop = VeloxRuntimeWry.EventLoop()
let proxy = loop?.makeProxy()
let window = loop?.makeWindow(configuration: .init(width: 800, height: 600, title: "Velox"))
let webview = window?.makeWebview(configuration: .init(url: "https://tauri.app"))
loop?.pump { event in
switch event {
case .loopDestroyed, .userExit:
return .exit
default:
return .poll
}
}
proxy?.requestExit()
This demonstrates the bridging between Swift and the underlying Tao/Wry event loop, window, and
webview primitives exposed by the Rust shim. Event callbacks now deliver structured metadata (via
JSON) which the Swift layer normalises into strongly-typed VeloxRuntimeWry.Event values.
VeloxRuntimeWry.Event exposes rich keyboard, pointer, focus, DPI, and file-drop information so
Swift applications can respond to Tao/Wry input without having to touch the underlying JSON payloads.
The Swift API now includes helpers to:
Velox now ships a nascent VeloxRuntime module that defines the Swift-first protocols mirroring Tauri's runtime traits. VeloxRuntimeWry.Runtime remains a stub while the native implementation is completed; the event-loop based APIs remain the primary entry point until the dedicated Swift runtime is feature-complete.
The repository includes several example applications demonstrating Velox capabilities. Examples are located in the Examples/ directory.
Build and run any example using Swift Package Manager:
# Build all examples
swift build
# Run a specific example
swift run HelloWorld
swift run HelloWorld2
swift run MultiWindow
swift run State
swift run Splashscreen
swift run Streaming
swift run RunReturn
swift run Commands
swift run CommandsManual
swift run CommandsManualRegistry
swift run Resources
swift run WindowControls
swift run MultiWebView
swift run DynamicHTML
swift run Events
swift run Tray
Velox supports two approaches for loading web content, mirroring Tauri's flexibility:
The simplest approach embeds HTML directly in Swift code. This is ideal for simple UIs or when you want a single-binary deployment with no external dependencies.
Example: HelloWorld
let html = """
<!doctype html>
<html>
<body>
<h1>Hello from Velox!</h1>
</body>
</html>
"""
let appProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "app") { _ in
VeloxRuntimeWry.CustomProtocol.Response(
status: 200,
headers: ["Content-Type": "text/html"],
body: Data(html.utf8)
)
}
Pros:
Cons:
For larger applications, keep HTML, CSS, and JavaScript as separate files loaded at runtime. This mirrors Tauri's asset bundling approach.
Example: HelloWorld2
Examples/HelloWorld2/
├── main.swift # Swift entry point with AssetBundle
└── assets/
├── index.html # HTML markup
├── styles.css # Stylesheet
└── app.js # JavaScript
The AssetBundle struct discovers and serves files from the assets directory:
struct AssetBundle {
let basePath: String
func loadAsset(path: String) -> (data: Data, mimeType: String)? {
// Load file and detect MIME type
}
}
let appProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "app") { request in
guard let url = URL(string: request.url),
let asset = assets.loadAsset(path: url.path) else {
return notFoundResponse()
}
return VeloxRuntimeWry.CustomProtocol.Response(
status: 200,
headers: ["Content-Type": asset.mimeType],
body: asset.data
)
}
Pros:
Cons:
| Example | Description | Asset Approach |
|---|---|---|
| HelloWorld | Basic window with inline HTML and IPC | Self-contained |
| HelloWorld2 | Same functionality with external assets | Bundled assets |
| MultiWindow | Multiple windows running simultaneously | Self-contained |
| State | Shared state across IPC calls | Self-contained |
| Splashscreen | Splash window before main window | Self-contained |
| Streaming | Server-sent events from Swift to webview | Self-contained |
| RunReturn | Manual event loop control | Self-contained |
| Commands | @VeloxCommand macro for cleanest command definitions | Bundled assets |
| CommandsManual | Manual IPC routing with switch statement | Bundled assets |
| CommandsManualRegistry | Type-safe command DSL with automatic JSON decoding | Bundled assets |
| Resources | Resource bundling and path resolution | Bundled assets |
| WindowControls | Comprehensive window/webview API demonstration | Self-contained |
| MultiWebView | Multiple child webviews: local app + GitHub, tauri.app, Twitter | Mixed |
| DynamicHTML | Swift-rendered dynamic HTML with counter, todos, and themes | Self-contained |
| Events | Event system: backend-to-frontend and frontend-to-backend events | Self-contained |
| Tray | System tray icon with context menu (macOS) | Self-contained |
Velox supports a configuration file (velox.json) similar to Tauri's tauri.conf.json. This allows declarative app configuration:
{
"$schema": "https://velox.dev/schema/velox.schema.json",
"productName": "MyApp",
"version": "1.0.0",
"identifier": "com.example.myapp",
"app": {
"windows": [
{
"label": "main",
"title": "My Application",
"width": 800,
"height": 600,
"url": "app://localhost/",
"create": true,
"visible": true,
"resizable": true,
"devtools": true,
"customProtocols": ["app", "ipc"]
}
],
"macOS": {
"activationPolicy": "regular"
}
},
"build": {
"frontendDist": "assets"
}
}
Devtools: devtools defaults to true in debug builds and false in release. On macOS, enabling devtools uses private WebKit APIs, so avoid setting it to true for release builds.
Use VeloxAppBuilder to create your app from configuration:
import VeloxRuntime
import VeloxRuntimeWry
let config = try VeloxConfig.load(from: URL(fileURLWithPath: "path/to/app"))
let eventLoop = VeloxRuntimeWry.EventLoop()!
let app = VeloxAppBuilder(config: config)
.registerProtocol("app") { request in
// Serve assets
}
.registerProtocol("ipc") { request in
// Handle IPC commands
}
.build(eventLoop: eventLoop)
Platform-Specific Overrides: Create velox.macos.json, velox.ios.json, etc. to override settings per platform using RFC 7396 JSON Merge Patch.
Both approaches use custom protocols for IPC between Swift and the webview. Velox injects a
window.Velox.invoke helper that supports both immediate and deferred command responses:
// JavaScript: invoke a Swift command (preferred)
const message = await window.Velox.invoke('greet', { name: 'World' });
If you need a custom helper, you can still use fetch for immediate responses:
async function invoke(command, args = {}) {
const response = await fetch(`ipc://localhost/${command}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args)
});
return (await response.json()).result;
}
To return after the IPC handler completes (for modal dialogs or long tasks), defer the response
and resolve it later. window.Velox.invoke will await the final result automatically:
struct DelayedEchoArgs: Codable, Sendable { let message: String; let delayMs: Int? }
commands.register("delayed_echo", args: DelayedEchoArgs.self, returning: DeferredCommandResponse.self) { args, ctx in
let deferred = try ctx.deferResponse()
let delay = max(0, args.delayMs ?? 500)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
deferred.responder.resolve(args.message)
}
return deferred.pending
}
const reply = await window.Velox.invoke('delayed_echo', { message: 'Hello later', delayMs: 800 });
Velox provides the @VeloxCommand macro for the cleanest command definitions, similar to Tauri's #[tauri::command]:
import VeloxMacros
import VeloxRuntimeWry
// Define response types
struct GreetResponse: Codable, Sendable {
let message: String
}
// Commands must be in a container (enum/struct) for macro expansion
enum Commands {
@VeloxCommand
static func greet(name: String) -> GreetResponse {
GreetResponse(message: "Hello, \(name)!")
}
@VeloxCommand
static func divide(numerator: Double, denominator: Double) throws -> MathResponse {
guard denominator != 0 else {
throw CommandError(code: "DivisionByZero", message: "Cannot divide by zero")
}
return MathResponse(result: numerator / denominator)
}
// Access state via CommandContext parameter
@VeloxCommand
static func increment(context: CommandContext) -> CounterResponse {
let state: AppState = context.requireState()
return CounterResponse(value: state.increment())
}
// Custom command name
@VeloxCommand("get_counter")
static func getCounter(context: CommandContext) -> CounterResponse {
let state: AppState = context.requireState()
return CounterResponse(value: state.counter)
}
}
// Register macro-generated commands
let registry = commands {
Commands.greetCommand // Generated by @VeloxCommand
Commands.divideCommand
Commands.incrementCommand
Commands.getCounterCommand
}
See Examples/Commands for a complete demonstration of the @VeloxCommand macro.
For cases where you prefer explicit control, use the command DSL directly:
import VeloxRuntime
import VeloxRuntimeWry
// Define typed arguments and responses
struct GreetArgs: Codable, Sendable {
let name: String
}
struct GreetResponse: Codable, Sendable {
let message: String
}
// Register commands using the DSL
let registry = commands {
command("greet", args: GreetArgs.self, returning: GreetResponse.self) { args, _ in
GreetResponse(message: "Hello, \(args.name)!")
}
command("divide", args: DivideArgs.self, returning: MathResponse.self) { args, _ in
guard args.denominator != 0 else {
throw CommandError(code: "DivisionByZero", message: "Cannot divide by zero")
}
return MathResponse(result: args.numerator / args.denominator)
}
command("increment", returning: CounterResponse.self) { context in
let state: AppState = context.requireState()
return CounterResponse(value: state.increment())
}
}
// Create IPC handler and register protocol
let stateContainer = StateContainer().manage(AppState())
let ipcHandler = createCommandHandler(registry: registry, stateContainer: stateContainer)
let ipcProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "ipc", handler: ipcHandler)
See Examples/CommandsManualRegistry for a complete demonstration of the type-safe command DSL.
For simpler cases, you can handle IPC requests manually:
// Swift: handle IPC requests manually
let ipcProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "ipc") { request in
let command = URL(string: request.url)?.path.trimmingCharacters(in: .init(charactersIn: "/"))
switch command {
case "greet":
let name = parseArgs(request.body)["name"] as? String ?? "World"
return jsonResponse(["result": "Hello \(name)!"])
default:
return errorResponse("Unknown command")
}
}
Package.swift: Swift Package definition exposing the VeloxRuntimeWry library target.Sources/VeloxRuntimeWry: Swift surface area that mirrors the Tauri runtime concepts with
Velox naming.Sources/VeloxRuntimeWryFFI: Lightweight C target that bridges into the Rust static library.runtime-wry-ffi: Rust crate producing a velox_runtime_wry_ffi static/dynamic library that
re-exports selected pieces of tao, wry, and tauri-runtime-wry.Velox supports two build modes for the Rust FFI crate:
Uses published versions of tao and wry from crates.io. This is the default for clean checkouts and CI:
# Remove local patches if present
rm -f .cargo/config.toml
swift build
Uses locally patched tao and wry with additional testing features. Requires sibling checkouts of these repositories with the velox-testing feature added to tao's default features.
# Ensure .cargo/config.toml exists with patches (at package root, not runtime-wry-ffi)
# Then build with local-dev feature:
VELOX_LOCAL_DEV=1 swift build
The .cargo/config.toml (in the package root) patches crates.io dependencies with local paths:
[patch.crates-io]
tao = { path = "../tao" }
wry = { path = "../wry" }
Requirements for Local Dev:
tao version must match crates.io (currently 0.34.5)tao must have velox-testing in its default features in Cargo.tomlwry version must match crates.io (currently 0.53.5)Velox is licensed under either of:
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Velox by you shall be dual licensed as above, without any additional terms or conditions.
Swift
78.5%
Rust
15.5%
C
3.0%
JavaScript
2.1%