Context and data
A first widget uses this context directly. Component views add ctx.instance and ctx.view; ordinary bar widgets and roots do not have a component instance.
Widget lifecycle and dependencies
Section titled “Widget lifecycle and dependencies”A widget is a folder with an init.luau. omniwmctl shell init <name> creates a starter one:
interval = 60
function render(ctx) return ui.row{ gap = 6, onClick = ui.popout, ui.symbol{ name = "clock" }, ui.text{ text = ctx.data.clock } }end
function popout(ctx) return ui.text{ text = ctx.data.date, pad = 12 }endrender(ctx)draws the bar slot or configured island root; returnui.box{ w = 0, h = 0 }to draw nothing there.popout(ctx)draws the popout, opened byonClick = ui.popout,shell toggle <widget>, or a drawer.interval(seconds) sets how often timed data such asseconds,cpuandnetworkupdates this widget.trigger(ctx, event)runs foromniwmctl shell trigger <widget> <name> [value]and host lifecycle events.require("./helpers")andrequire("@self/helpers")load other files of the widget.require("../lib/panel")loadslib/panel.luaushared by every widget, from the shell folder, else the profile, elsedefault/.ctx.statekeeps values between renders;ctx.formatformats numbers, percentages and byte sizes for the user’s locale.ctx.format.date(seconds, template, zone)formats a time from a date template such as"jmm"or"EEEdMMM", following the user’s 12- or 24-hour clock, andctx.format.offset(seconds, zone)gives that zone’s offset from UTC in seconds;zoneis an identifier such as"Asia/Tokyo", the local zone when omitted, and an unknown zone givesnil.onClick,onRightClickandonScrolltake a command string or a function(ctx, event);eventhas the button, modifier keys, scrolldxanddy, and a slider’svalue.omniwm.run(command)runs an action, andomniwm.log(...)writes to the macOS log under theshellcategory.spanIslands = trueshows the widget on both islands of a bar split around the notch, instead of only its group’s island.
A widget only receives the data it reads, and OmniWM only gathers what some widget reads.
Use ctx.select("media", "title") when a view needs one field of an object. The view updates when that field changes, so playback-position updates can leave a title-only widget alone. Paths can contain strings and integer array indices, for example ctx.select("bluetooth", "devices", 1, "battery"); a missing path returns nil. Bar and popout dependencies are tracked separately. Reading the same object through ctx.data.media, or selecting a table rather than a scalar, subscribes to the whole object. Event handlers still receive current data and rerender after changing widget state.
render and popout are view construction hooks. Keep service requests and persistent-state mutations in event/lifecycle callbacks; do not start repeating work from a render. Services preserve the current owner when their callbacks run.
| Context field | Use |
|---|---|
data |
Immutable host facts. Optional feeds can be nil; show an unavailable state before reading nested fields. |
select(key, ...) |
Subscribe to a scalar path rather than all updates of a parent object. |
state |
Mutable, in-memory state for this display and placement; it is not automatically saved. |
format |
Locale-aware formatting functions below. |
instance |
A component’s shared identity, options, state, and provider snapshot; absent outside component callbacks/views. |
view |
Requested component view name, offered dimensions, and value-only props. |
Context
export type Context = { data: Data, select: (key: string, ...(string | number)) -> any, state: { [string]: any }, format: Format, instance: ComponentInstance?, view: { name: string, w: number?, h: number?, props: any? }?,}Fields: data, select, state, format, instance, view
Format
export type Format = { number: (value: number, digits: number?) -> string, percent: (fraction: number, digits: number?) -> string, bytes: (count: number, style: ("memory" | "file")?) -> string, date: (seconds: number, template: string, zone: string?) -> string?, offset: (seconds: number, zone: string?) -> number?,}Fields: number, percent, bytes, date, offset
| Formatter | Behavior |
|---|---|
number(value, digits?) |
Localized number; digits defaults to 0 and is rounded/clamped to 0–6 fractional digits. |
percent(fraction, digits?) |
Localized percentage: pass 0.5 for 50%. Same digits policy as number. |
bytes(count, style?) |
Localized byte count; style defaults to "memory", or use "file". Invalid/out-of-range counts display "—". |
date(seconds, template, zone?) |
Localized date from Unix seconds and a nonempty template such as "jmm" or "EEEdMMM". Zone defaults to local; an unknown zone returns nil. |
offset(seconds, zone?) |
Zone offset from UTC in seconds at that time, including daylight-saving rules; unknown zones return nil. |
ui.text{ text = ctx.format.percent((ctx.data.battery or 0) / 100) }ui.text{ text = ctx.format.bytes(ctx.data.system and ctx.data.system.memory.used or 0) }TriggerEvent
export type TriggerEvent = { source: "host"?, name: string, value: string?,}Fields: source, name, value
Data feeds
Section titled “Data feeds”ctx.data |
Contents |
|---|---|
clock, date, seconds |
Localized time and date, and the current time in seconds |
island |
Persistent island geometry and display state; nil for a bar instance |
bar, workspaces, workspace, app, title |
The bar’s workspaces, windows, scratchpads and style; the focused app |
cpu, memory, gpu, disk, network, system |
System statistics and host details, including uptime |
networkHistory |
The last 30 seconds of download and upload speed, and the totals since the Mac started |
thermal |
CPU and GPU temperatures in °C |
battery, charging |
Battery level |
power |
Power source, minutes until empty or full, Low Power Mode, and battery health, cycles, temperature and watts |
inputSource |
The keyboard input source and the enabled ones, each with a short label |
privacy |
Whether any app is using a microphone or a camera |
wifi |
Wi-Fi power, signal, band, channel and security, and the network name once Location access is allowed |
bluetooth |
Bluetooth power and paired devices with their battery levels, once Bluetooth access is allowed |
volume, muted, outputs |
Output volume and the audio outputs |
audioApps |
Apps with audio and their own volume |
media |
What is playing, with artwork, shuffle and repeat |
lyrics |
Synced lyrics for what is playing, from LRCLIB; the title, artist and album are sent there only while a widget reads lyrics |
spectrum |
60 bars of what the Mac is playing, from 50 Hz to 10 kHz, and its tempo, about four times a second; needs System Audio Recording, like per-app volume |
tempo |
Just the spectrum tempo in beats per minute, which changes far less often than the bars |
calendar |
Upcoming events and a month to draw |
reminders |
Access state, incomplete reminders, total count and read/action errors |
badges |
Dock badge labels keyed by lowercase bundle ID for apps with managed windows; available to bars, islands and popouts |
weather |
Current conditions with humidity, sunrise and sunset, 24 hours and 7 days |
user |
Your name and account picture |
appearance |
"light" or "dark" |
hiddenBar |
Hidden Bar items |
menuApp |
App-menu target with process ID, name, and bundle ID; absent if no target is available |
components, catalog, settingsSchema |
Configured instance metadata, installed component catalog, and root option schema; see components and configuration |
vars, settings |
Values from omniwmctl shell set and from [widgets.<name>] |
omniwm.d.luau in the shell folder lists every field with its type.
seconds is Unix time in seconds. A root island receives Island geometry and navigation data; ordinary bar placements receive nil for island. Reading a feed requests it; the host does not poll integrations that no widget uses. Library helpers obey the same dependency tracking.
Data
export type Data = { clock: string, date: string, seconds: number, cpu: number?, memory: number, battery: number?, charging: boolean, gpu: number?, disk: number?, network: { down: number, up: number }?, volume: number?, muted: boolean?, vars: { [string]: string }, weather: Weather?, workspace: string, app: string, bundle: string, title: string, workspaces: { Workspace }, bar: Bar, settings: { [string]: any }?, hiddenBar: HiddenBar, system: System?, outputs: { AudioOutput }?, audioApps: AudioApps?, media: Media?, calendar: Calendar?, power: Power?, inputSource: InputSource?, privacy: { microphone: boolean, camera: boolean }?, wifi: WiFi?, bluetooth: Bluetooth?, user: { name: string, account: string, avatar: string? }?, thermal: { cpu: number?, gpu: number?, gpuName: string? }?, networkHistory: { down: { number }, up: { number }, received: number, sent: number }?, appearance: ("light" | "dark")?, lyrics: { status: "loading" | "synced" | "none", lines: { { time: number, text: string } } }?, spectrum: { access: "unknown" | "granted" | "denied", bars: { number }, bpm: number? }?, tempo: number?, island: Island?, components: { ComponentInfo }?, catalog: { ComponentCatalogEntry }?, settingsSchema: { [string]: ComponentOption }?, reminders: Reminders?, badges: { [string]: string }, menuApp: { pid: number, name: string, bundle: string }?,}Fields: clock, date, seconds, cpu, memory, battery, charging, gpu, disk, network, volume, muted, vars, weather, workspace, app, bundle, title, workspaces, bar, settings, , system, outputs, audioApps, media, calendar, power, inputSource, privacy, wifi, bluetooth, user, thermal, networkHistory, appearance, lyrics, spectrum, tempo, island, components, catalog, settingsSchema, reminders, badges,
Reminders data
Section titled “Reminders data”ctx.data.reminders is a cached snapshot with access, items, total, truncated and optional error. Access is "notDetermined", "restricted", "denied", "writeOnly" or "full", independently of Calendar access. Reading this data does not request permission.
With full access, items contains at most 100 incomplete reminders. Each Reminder has id, title, list, color, optional due (Unix seconds), allDay, priority and canComplete. Items are ordered by due date, then priority and identifier; undated items come last, and priority 0 means unset. total counts all fetched incomplete reminders; truncated is true when the snapshot contains fewer. Custom views can display any of the supplied items.
Use reminders authorize to request access, reminders settings to open Reminders privacy settings, and reminders complete <id> to complete a selected item. Completion checks current access and whether its list is writable. These are commands, not callback-based service requests: read ctx.data.reminders.error for read or action failures. A successful authorization or completion clears the previous action error; a background refresh does not conceal it. Cached data updates on changes without a reminder polling timer.
Commands and popouts
Section titled “Commands and popouts”omniwm.run(command) sends a host command rather than invoking a shell process. It has no result callback. ui.popout is a host action marker when used as an element handler; it opens/closes the widget’s panel and closes a containing drawer when invoked from its section. Calling it as an ordinary Luau function does not open a native window.
omniwm.run
run: (command: string) -> (),ui.popout
popout: (ctx: Context) -> (),Actions
Section titled “Actions”onClick, onRightClick and omniwm.run take any omniwmctl command (for example "switch-workspace 2") and these shell commands:
| Command | Effect |
|---|---|
volume up [percent], volume down [percent], volume set <0-100>, volume mute |
Output volume |
volume output <id> |
Switch the output |
volume app <bundle> set <0-100>, volume app <bundle> mute, volume app <bundle> reset |
One app’s volume |
volume permission |
Open the System Audio Recording privacy settings |
media play, media pause, media toggle, media next, media previous, media seek <seconds>, media shuffle, media repeat, media open |
Playback; shuffle turns shuffle on or off and repeat steps through off, one and all, as media.shuffle and media.repeatMode show; when no widget reads media, play, pause and track changes send the media keys |
session lock, session sleep, session logout, session restart, session shutdown |
Lock the screen, sleep, or ask macOS to log out, restart or shut down; macOS shows its usual confirmation for the last three, and asks once for permission to control loginwindow |
calendar authorize, calendar settings, calendar open [event], calendar month next|previous|today |
Calendar; settings opens the Calendars privacy settings |
reminders authorize, reminders settings, reminders complete <id> |
Request Reminders access, open its privacy settings, or complete a selected writable reminder; failures appear in ctx.data.reminders.error |
battery settings |
Open the Battery settings |
input-source select <id>, input-source settings |
Switch the keyboard input source; open the Keyboard settings |
wifi authorize, wifi settings |
Ask for Location access, or open its privacy settings once asked; open the Wi-Fi settings |
bluetooth authorize, bluetooth settings |
Ask for Bluetooth access, or open its privacy settings once asked; open the Bluetooth settings |
shell toggle <name> |
Open or close a drawer or a widget’s popout |
shell menu |
Open the OmniWM menu at the widget |
hidden-bar activate <id> |
Click a Hidden Bar item |
window <action> <id> |
Any omniwmctl window action on a window |
Logging and localization
Section titled “Logging and localization”omniwm.log(...) sends diagnostic values to the macOS unified log’s shell category. Use it from a handler or lifecycle hook when diagnosing a widget. omniwm.l10n(key, ...) uses OmniWM’s localization catalog and optional format arguments. It accepts up to eight formatting arguments. For your own widget text, provide the string directly unless it corresponds to an existing catalog key.
omniwm.log
log: (...any) -> (),omniwm.l10n
l10n: (key: string, ...any) -> string,Workspace and bar snapshots
Section titled “Workspace and bar snapshots”IDs are host-issued identities. Use a current snapshot for actions; titles and labels are presentation data. BarStyle describes the effective native bar settings, while bar.island indicates which split-bar group is rendering. Hidden Bar menu row IDs are valid only for the displayed native-menu page.
Workspace
export type Workspace = { id: string, name: string, focused: boolean, visible: boolean, windows: number,}Fields: id, name, focused, visible, windows
Bar
export type Bar = { monitor: string, island: "all" | "active" | "secondary", drag: BarDrag?, badges: "off" | "dot" | "text", style: BarStyle, workspaces: { BarWorkspace }, scratchpads: { BarScratchpad },}Fields: monitor, island, drag, badges, style, workspaces, scratchpads
BarStyle
export type BarStyle = { height: number, labels: boolean, backgrounds: boolean, accents: boolean, inactive: number, position: "overlappingMenuBar" | "belowMenuBar" | "bottom" | "left" | "right" | "top", vertical: boolean, notch: string, notchWidth: number, level: string, offsetX: number, offsetY: number, autoHide: boolean, reveal: string, opacity: number, transparent: boolean, solidBlack: boolean, accentColor: string?, textColor: string?, statsButton: boolean, secureInput: boolean, previews: boolean, hideInFullscreen: boolean,}Fields: height, labels, backgrounds, accents, inactive, position, vertical, notch, notchWidth, level, offsetX, offsetY, autoHide, reveal, opacity, transparent, solidBlack, accentColor, textColor, , secureInput, previews, hideInFullscreen
BarWindow
export type BarWindow = { id: string, title: string, focused: boolean, hidden: boolean,}Fields: id, title, focused,
BarItem
export type BarItem = { id: string, bundle: string, name: string, focused: boolean, count: number, hidden: number, badge: string, floating: boolean, windows: { BarWindow },}Fields: id, bundle, name, focused, count, , badge, floating, windows
BarWorkspace
export type BarWorkspace = { id: string, name: string, focused: boolean, windows: { BarItem },}Fields: id, name, focused, windows
BarScratchpad
export type BarScratchpad = { compact: boolean, id: number, name: string, focused: boolean, revealed: boolean, visible: boolean, windows: { BarItem },}Fields: compact, id, name, focused, revealed, visible, windows
BarDrag
export type BarDrag = { sources: { string }, workspaces: { string }, icons: { string }, gaps: { { workspace: string, before: number } },}Fields: sources, workspaces, icons, gaps
HiddenBar
export type HiddenBar = { enabled: boolean, available: boolean, rehide: number, items: { HiddenBarItem }, maxWidth: number, maxHeight: number, cell: number, vertical: boolean, menu: HiddenBarMenu?,}Fields: , , , , , , , ,
HiddenBarItem
export type HiddenBarItem = { id: string, name: string, image: string?, w: number, h: number,}Fields: , , , ,
HiddenBarMenu
export type HiddenBarMenu = { title: string, backID: string?, rows: { HiddenBarMenuRow },}Fields: , ,
HiddenBarMenuRow
export type HiddenBarMenuRow = { id: string, title: string, enabled: boolean, mark: string, shortcut: string, submenu: boolean, separator: boolean,}Fields: , , , , , ,
Audio and media snapshots
Section titled “Audio and media snapshots”Optional artwork is an image reference. Media elapsed time is sampled with sampledAt and rate; a missing duration/elapsed value must not be replaced with a claimed playback position. Spectrum and per-app volume require System Audio Recording permission; Now Playing does not.
AudioOutput
export type AudioOutput = { id: string, name: string, transport: "builtin" | "usb" | "bluetooth" | "hdmi" | "displayport" | "airplay" | "thunderbolt" | "virtual" | "aggregate" | "other", default: boolean,}Fields: id, name, transport, default
AudioApp
export type AudioApp = { id: string, name: string, playing: boolean, volume: number, muted: boolean,}Fields: id, name, playing, volume, muted
AudioApps
export type AudioApps = { access: "unknown" | "granted" | "denied", apps: { AudioApp },}Fields: access, apps
Media
export type Media = { title: string, artist: string, album: string, app: string, bundle: string, pid: number, playing: boolean, duration: number?, elapsed: number?, rate: number, sampledAt: number, artwork: string?, shuffle: boolean?, repeatMode: ("off" | "one" | "all")?,}Fields: title, artist, album, app, bundle, pid, playing, duration, elapsed, rate, sampledAt, artwork, shuffle, repeatMode
Calendar and reminders snapshots
Section titled “Calendar and reminders snapshots”Read access before rendering protected contents. Timestamps are Unix seconds. Calendar’s end field is accessed as event["end"] because it is a Luau keyword.
Calendar
export type Calendar = { access: "full" | "writeOnly" | "denied" | "restricted" | "notDetermined", events: { CalendarEvent }, month: CalendarMonth?,}Fields: access, events, month
CalendarEvent
export type CalendarEvent = { id: string, title: string, calendar: string, color: string, start: number, ["end"]: number, allDay: boolean, location: string?, time: string, day: string,}Fields: id, title, calendar, color, start, end, allDay, location, time, day
CalendarMonth
export type CalendarMonth = { title: string, year: number, month: number, days: number, offset: number, today: number, weekdays: { string }, marked: { number },}Fields: title, year, month, days, offset, today, weekdays, marked
Reminders
export type Reminders = { access: "full" | "writeOnly" | "denied" | "restricted" | "notDetermined", items: { Reminder }, total: number, truncated: boolean, error: string?,}Fields: access, items, total, truncated, error
Reminder
export type Reminder = { id: string, title: string, list: string, color: string, due: number?, allDay: boolean, priority: number, canComplete: boolean,}Fields: id, title, list, color, due, allDay, priority, canComplete
Weather and system snapshots
Section titled “Weather and system snapshots”Weather supplies current conditions, 24 hourly entries, and seven daily entries; status = "stale" marks retained old data. Temperatures follow the configured unit. System and hardware fields can be absent when unavailable; show that state explicitly.
Weather
export type Weather = { temp: number, feels: number?, high: number?, low: number?, rain: number?, wind: number?, humidity: number?, sunrise: number?, sunset: number?, code: number, day: boolean, unit: string, condition: string, symbol: string, place: string?, status: "ok" | "stale", hours: { WeatherHour }, days: { WeatherDay },}Fields: temp, feels, high, low, rain, wind, humidity, sunrise, sunset, code, day, unit, condition, symbol, place, status, hours, days
WeatherHour
export type WeatherHour = { time: number, code: number, label: string, temp: number, condition: string, symbol: string, day: boolean, rain: number?,}Fields: time, code, label, temp, condition, symbol, day, rain
WeatherDay
export type WeatherDay = { time: number, code: number, label: string, high: number, low: number, condition: string, symbol: string, rain: number?,}Fields: time, code, label, high, low, condition, symbol, rain
System
export type System = { memory: { used: number, total: number, pressure: ("normal" | "warning" | "critical")? }, disk: { used: number, total: number }, uptime: number, host: { name: string, chip: string, model: string, os: string }, displays: { SystemDisplay },}Fields: memory, disk, uptime, host, displays
SystemDisplay
export type SystemDisplay = { w: number, h: number, hz: number,}Fields: w, h, hz
Power
export type Power = { source: "ac" | "battery" | "ups", remaining: number?, full: number?, charged: boolean, lowPower: boolean, cycles: number?, health: number?, temperature: number?, watts: number?, adapter: number?,}Fields: source, remaining, full, charged, lowPower, cycles, health, temperature, watts, adapter
InputSource
export type InputSource = { id: string, name: string, short: string, sources: { InputSourceEntry },}Fields: id, name, short, sources
InputSourceEntry
export type InputSourceEntry = { id: string, name: string, short: string }Fields: id, name, short
WiFi
export type WiFi = { power: boolean, connected: boolean, ssid: string?, rssi: number?, band: string?, channel: number?, security: string?, location: "authorized" | "denied" | "restricted" | "notDetermined",}Fields: power, connected, ssid, rssi, band, channel, security, location
Bluetooth
export type Bluetooth = { power: boolean, access: "granted" | "denied" | "restricted" | "notDetermined", devices: { BluetoothDevice },}Fields: power, access, devices
BluetoothDevice
export type BluetoothDevice = { id: string, name: string, kind: "headphones" | "keyboard" | "mouse" | "gamepad" | "computer" | "phone" | "other", connected: boolean, battery: number?, left: number?, right: number?, case: number?,}Fields: id, name, kind, connected, battery, left, right, case
Island snapshots
Section titled “Island snapshots”The panel envelope stays fixed while the Luau content expands or collapses. Coordinates begin at its top-left; size and notch dimensions are in points. navigation.windows follows Niri column/row order, then floating windows. column is one-based for Niri, zero otherwise. Titles use cached metadata and can be empty; fall back to name. Use omniwm.run("window focus " .. id) to focus a window.
Island
export type Island = { display: string, size: { w: number, h: number }, notch: { x: number, w: number, h: number }?, menuBar: number, scale: number, main: boolean, visible: boolean, fullscreen: boolean, focused: boolean, drag: { active: boolean, types: { "file" | "url" | "text" | "image" } }, navigation: IslandNavigation?,}Fields: display, size, notch, , scale, main, visible, fullscreen, focused, drag,
IslandNavigation
export type IslandNavigation = { workspace: string, name: string, layout: "niri" | "dwindle", focused: string?, windows: { { id: string, bundle: string, name: string, title: string, column: number, } },}Fields: , , , ,