Build an app
An app is a component that opens in its own window. OmniWM tiles it, focuses it and closes it like any other window, and draws its Luau views directly.
Layout
Section titled “Layout”Put each app in its own folder:
~/.config/omniwm/apps/<id>/ component.toml init.luauThe folder name is the app’s ID. OmniWM notices new apps and edits on its own. Windows already open keep the version they started with.
Declare the windows
Section titled “Declare the windows”A [window] table makes a component an app. Sizes are content points, and all four are required:
title = "Notes"icon = "app"views = ["window", "confirm"]
[window]width = 640height = 420minWidth = 280minHeight = 240view picks the main view and defaults to window. Each child window is a table under [window.children]. kind is sheet, attached to the main window and blocking it, or utility, a floating window attached to it:
[window.children.confirm]kind = "sheet"view = "confirm"title = "Confirm deletion"width = 360height = 200minWidth = 280minHeight = 160Write the views
Section titled “Write the views”init.luau exports a view for each name in views. Use ctx.view.w and ctx.view.h so the content follows the window size.
return { views = { window = function(ctx) return ui.column{ w = ctx.view.w, h = ctx.view.h, pad = 20, gap = 12, center = false, ui.text{ text = ctx.instance.state.result or "Ready", color = "foreground" }, ui.stack{ key = "delete", w = 120, h = 36, fill = "accent", radius = 8, onClick = function() omniwm.windows.open("confirm") end, ui.text{ text = "Delete", color = "foreground" } }, } end, confirm = function(ctx) return ui.column{ w = ctx.view.w, h = ctx.view.h, pad = 16, gap = 12, ui.text{ text = "Delete this note?", color = "foreground" }, ui.stack{ key = "confirm", w = 120, h = 36, fill = "accent", radius = 8, onClick = function(next) next.instance.state.result = "Deleted" omniwm.windows.close("confirm") end, ui.text{ text = "Delete", color = "foreground" } }, } end, },}ctx.instance.state is shared by every window of one app instance, so a sheet can change what the main window shows. ctx.state belongs to one window. omniwm.windows.open and close work from that app’s UI handlers.
Open it
Section titled “Open it”The command palette lists Open title and New title Window for each app, and you can bind them to keys. With IPC turned on, use:
omniwmctl command app-open <id>omniwmctl command app-new-window <id>Open reuses the most recently used window. New Window starts a separate instance.
If an app has a mistake, such as a missing view or size, it isn’t listed, and the error appears in Settings → Styles & Widgets. When two folders use the same ID, the first keeps it and the other shows an error.
Add a provider
Section titled “Add a provider”A provider is a separate program for work Luau can’t do. It uses the same JSON Lines channel as any other component and gets no window API. It can’t open windows: it sends an event, and your Luau decides what to show.
This example asks its provider to check for work. The provider answers with an event, and the app shows a Review button that opens the sheet.
Download apps/provider-review/component.toml
title = "Provider Review"icon = "app"views = ["window", "confirm"]capabilities = { provider = true }
[provider]command = ["provider"]
[window]width = 520height = 280minWidth = 320minHeight = 220
[window.children.confirm]kind = "sheet"view = "confirm"title = "Confirm deletion"width = 360height = 180minWidth = 280minHeight = 160Download apps/provider-review/init.luau
local function button(key, label, onClick) return ui.stack{ key = key, w = 150, h = 36, fill = "elevatedSurface", radius = 8, onClick = onClick, ui.text{ text = label, color = "foreground", size = 14 } }end
return { receive = function(instance, event) if event.name == "confirm-delete" then instance.state.itemId = event.value.itemId end end, views = { window = function(ctx) local provider = ctx.instance.provider local itemId = ctx.instance.state.itemId return ui.column{ w = ctx.view.w, h = ctx.view.h, pad = 20, gap = 12, center = false, fill = "background", ui.text{ text = provider.state.summary or provider.status, size = 15, color = "foreground" }, ui.text{ text = ctx.instance.state.result or "Nothing deleted yet.", color = "secondaryForeground" }, button("check", "Check for work", function() omniwm.provider.send("check") end), if itemId then button("review", "Review " .. itemId, function() omniwm.windows.open("confirm") end) else nil, } end, confirm = function(ctx) return ui.column{ w = ctx.view.w, h = ctx.view.h, pad = 16, gap = 12, fill = "background", ui.text{ text = "Delete " .. (ctx.instance.state.itemId or "this item") .. "?", size = 18, color = "foreground" }, ui.row{ gap = 12, button("confirm", "Delete", function(next) next.instance.state.result = "Deleted " .. next.instance.state.itemId .. "." next.instance.state.itemId = nil omniwm.windows.close("confirm") end), button("cancel", "Cancel", function() omniwm.windows.close("confirm") end), }, } end, },}Download apps/provider-review/provider.swift
// SPDX-License-Identifier: GPL-2.0-only// Copyright (C) 2026 OmniNull — https://github.com/OmniNull/OmniWM
import Foundation
func emit(_ frame: [Any]) throws { var data = try JSONSerialization.data(withJSONObject: frame) data.append(10) try FileHandle.standardOutput.write(contentsOf: data)}
try emit(["state", "summary", "Ready"])while let line = readLine() { guard let frame = try? JSONSerialization.jsonObject(with: Data(line.utf8)) as? [Any], frame.count == 4, frame[0] as? String == "action", let request = frame[1] as? String, let action = frame[2] as? String else { continue } try emit(["result", request, true, [:] as [String: Any]]) if action == "check" { try emit(["event", "confirm-delete", ["itemId": "selected-item"]]) }}Download the three files into ~/.config/omniwm/apps/provider-review/, then compile the Swift source into the program named in the manifest:
cd ~/.config/omniwm/apps/provider-reviewswiftc provider.swift -o providerOpen Provider Review and approve the program when prompted. Nothing runs until you do. The provider can be written in any language.
Apps ask for the network hosts they declare the same way. Review or change your answers in Settings → Network & Permissions.
Limits
Section titled “Limits”Apps run inside OmniWM’s own process and use its identity. They don’t come back after a restart, and they aren’t packaged as separate macOS apps. Only an approved provider runs outside it.