Skip to content

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.

Put each app in its own folder:

~/.config/omniwm/apps/<id>/
component.toml
init.luau

The 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.

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 = 640
height = 420
minWidth = 280
minHeight = 240

view 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 = 360
height = 200
minWidth = 280
minHeight = 160

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.

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:

Terminal window
omniwmctl command app-open <id>
Terminal window
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.

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

apps/provider-review/component.toml
title = "Provider Review"
icon = "app"
views = ["window", "confirm"]
capabilities = { provider = true }
[provider]
command = ["provider"]
[window]
width = 520
height = 280
minWidth = 320
minHeight = 220
[window.children.confirm]
kind = "sheet"
view = "confirm"
title = "Confirm deletion"
width = 360
height = 180
minWidth = 280
minHeight = 160

Download apps/provider-review/init.luau

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

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:

Terminal window
cd ~/.config/omniwm/apps/provider-review
swiftc provider.swift -o provider

Open 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.

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.