docs/lua-api.md (new, 380 lines):
- Section 1-2: Why Lua + file location (owlry.lua, NOT init.lua per D23)
- Section 3: Quick reference (one self-contained example covering every
surface)
- Section 4: API reference (owlry.set / providers / tabs / provider /
theme) with per-field tables and rules
- Section 5: Host API in scope (full stdlib + owlry.util convenience
helpers); no sandbox in 2.1
- Section 6: How providers/tabs/provider{} compose at runtime — the
three orthogonal axes (compiled in / enabled / shown as tab) made
explicit with a worked example and a what-if table
- Section 7: Hot reload via notify crate (re-added in Phase 3)
- Section 8: Validation via 'owlry config validate' / 'config show'
- Section 9: Migration via 'owlry migrate-config' with full TOML→Lua
mapping table
- Section 10: Open questions resolved before Phase 3 ships
- Section 11: Version compatibility roadmap (2.0 -> 2.1 -> 2.2 -> 3.0)
- Section 12: Implementation outline (handoff to engineering)
docs/RESTRUCTURE-V2.md:
- D23: config file named owlry.lua (brand identity over init.lua
convention; file is loaded explicitly, not via Lua's require)
- D24: owlry.providers vs owlry.tabs distinction made explicit — three
orthogonal axes (compiled in / enabled / shown as tab), full
composition spec lives in lua-api.md §6
The Lua API doc is intended as both the design spec we're committing to
AND the user-facing reference once Phase 3 ships.
26 KiB
Owlry Lua API
The owlry Lua configuration system. Lands in 2.1 as an opt-in preview alongside TOML; becomes the canonical config format in 3.0 when TOML support is removed.
Status: design spec for Phase 3 of the v2 rewrite plan. Becomes the user-facing reference doc once Phase 3 ships.
1. Why Lua
Owlry's 2.0 config is ~/.config/owlry/config.toml. TOML is fine for flat key/value settings, but it can't express the things people actually want from a launcher config:
- "Use a different SSH command for
:ssh host-foothan for everything else" - "Add a Hyprland shutdown menu as a
:hsprovider" - "Build the bookmarks list from
~/Documents/links.mdinstead of Firefox" - "Conditionally enable systemd-units only on machines where
$HOSTNAMEmatches a regex"
These are all one-liners in a real programming language. They're impossible in TOML.
Owlry 2.1 ships a Lua-driven config: the file is real Lua 5.4, evaluated at startup, and the same file holds:
- Global settings (theme, dimensions, terminal command)
- Enabled providers (which built-ins run)
- Tab layout (which providers appear as tab buttons)
- User-defined providers (custom search sources)
- Theme overrides
This is the same model Hyprland adopted (hyprlang) and that AwesomeWM / wezterm / xmonad have always used: the config language IS the extension language. No second-class scripting bolted on alongside a static config.
2. File location & loading order
The config file is ~/.config/owlry/owlry.lua.
Not
init.lua— the nameowlry.luais intentional brand identity.init.luais Lua'srequireentry-point convention; this file isn't loaded byrequire, it's loaded explicitly by owlry, so the convention doesn't apply.
Resolution
On startup the daemon resolves config in this order:
$XDG_CONFIG_HOME/owlry/owlry.lua(default:~/.config/owlry/owlry.lua) — if this file exists, it's the only source of truth. TOML is ignored entirely when Lua is present.$XDG_CONFIG_HOME/owlry/config.toml— fallback for 2.x users who haven't migrated. Behavior unchanged from 2.0.- Shipped defaults —
/usr/share/owlry/default.lua(a stub that callsowlry.set {}with reasonable defaults). Used when neither user file exists.
In 3.0, step 2 is removed. Users who haven't migrated get a clear error from owlry config validate and a pointer to owlry migrate-config.
Why Lua "wins" over TOML when both exist
To avoid the "did you remember to delete config.toml?" footgun. If you ever write an owlry.lua, that's your config — the TOML next to it doesn't quietly partial-override anything. Clear winner: predictable behavior.
3. Quick reference
-- ~/.config/owlry/owlry.lua
local owlry = require("owlry")
-- ──────────────────────────────────────────────────────────────────────────
-- Global settings
-- ──────────────────────────────────────────────────────────────────────────
owlry.set {
theme = "owl",
width = 850,
height = 650,
font_size = 14,
terminal = "kitty",
use_uwsm = false,
show_icons = true,
max_results = 100,
}
-- ──────────────────────────────────────────────────────────────────────────
-- Which providers run (the "enabled set")
-- ──────────────────────────────────────────────────────────────────────────
owlry.providers {
"app", "cmd", "calc", "conv", "power", -- always-on built-ins
"systemd", "ssh", "websearch", "filesearch", "emoji", "clipboard",
}
-- ──────────────────────────────────────────────────────────────────────────
-- Which providers get a tab button in the UI header bar (Ctrl+1..N)
-- Must be a subset of the enabled set. If omitted, defaults to all providers.
-- ──────────────────────────────────────────────────────────────────────────
owlry.tabs { "app", "cmd", "uuctl" }
-- ──────────────────────────────────────────────────────────────────────────
-- A user-defined provider — automatically joins the enabled set.
-- ──────────────────────────────────────────────────────────────────────────
owlry.provider {
id = "hs",
prefix = ":hs",
tab_label = "Shutdown",
icon = "system-shutdown",
items = function()
return {
{ name = "Lock", command = "hyprlock" },
{ name = "Shutdown", command = "systemctl poweroff" },
{ name = "Reboot", command = "systemctl reboot" },
}
end,
}
-- ──────────────────────────────────────────────────────────────────────────
-- Theme selection (or inline definition; see §4.5)
-- ──────────────────────────────────────────────────────────────────────────
owlry.theme("catppuccin-mocha")
4. API reference
The owlry module is the only thing in scope. require("owlry") returns it. All functions are callable in any order, multiple times — last-write-wins on most settings; some accumulate (see each function's note).
4.1 owlry.set { ... } — global settings
Sets top-level config values. Takes a table of key-value pairs. Calling owlry.set multiple times merges — later calls override earlier values for the same key. Keys not mentioned keep their default.
| Key | Type | Default | What it does |
|---|---|---|---|
theme |
string |
nil (system theme) |
Theme name (see §4.5 for options) |
width |
integer |
850 |
Launcher window width in pixels |
height |
integer |
650 |
Launcher window height in pixels |
font_size |
integer |
14 |
Base font size in points |
border_radius |
integer |
12 |
Window corner radius in pixels |
terminal |
string | nil |
autodetected | Terminal command for items marked terminal=true. Falls back to $TERMINAL → xdg-terminal-exec → common terminals |
use_uwsm |
boolean |
false |
Launch apps via uwsm app -- for systemd session integration |
show_icons |
boolean |
true |
Show provider icons in results |
max_results |
integer |
100 |
Cap on results returned per query |
frecency |
boolean |
true |
Boost frequently/recently used items |
frecency_weight |
number |
0.3 |
Frecency boost weight (0.0 = off, 1.0 = strong) |
search_engine |
string |
"duckduckgo" |
Engine for :web / ? queries (see §6) |
Example:
owlry.set { theme = "owl", width = 900 }
owlry.set { font_size = 16 } -- merged: theme + width still applied
Unknown keys produce a owlry config validate warning, not an error — forward-compat for 2.2+ keys.
4.2 owlry.providers { ... } — enabled set
Lists which providers run. Takes a sequence (array) of provider IDs. Order doesn't matter.
owlry.providers { "app", "cmd", "calc", "conv", "power" }
Rules:
- A provider must be in this list to produce any results. If it's not enabled, neither typing its prefix (
:foo) nor including it inowlry.tabswill make it active. - Built-in IDs are:
app,cmd,dmenu,calc,conv,power,systemd(alias:uuctl),ssh,clipboard,emoji,websearch,filesearch. Pre-2.0 aliases (sys,system→power;uuctl→systemd) still work. - User providers defined via
owlry.provider {}auto-join the enabled set — you don't list them here unless you want to. - Calling
owlry.providersmultiple times replaces the list (it's not additive). Use one call. - If a feature isn't compiled into the binary (e.g. someone built with
--no-default-features), the provider is silently ignored at runtime —owlry doctorreports the mismatch.
Default if omitted: all compiled-in providers are enabled. (The AUR build has --features full, so this defaults to everything.)
4.3 owlry.tabs { ... } — UI tab buttons
Lists which providers appear as tab buttons in the header bar. Tabs cycle with Tab / Shift+Tab and can be jumped to with Ctrl+1..9.
owlry.tabs { "app", "cmd", "uuctl" }
Rules:
- Each entry must be in
owlry.providers(or be aowlry.provider {}user provider). Listing an unknown ID produces aowlry config validatewarning and is silently dropped at runtime. - Order is preserved —
Ctrl+1targets the first entry,Ctrl+2the second, etc. - The implicit "All" tab is always present at position 0 (
Ctrl+0). - Default if omitted: all enabled providers get tabs in the order they were registered.
owlry.tabs {}(empty) is valid: hides all tab buttons. The "All" tab is still there.
The tabs ⊆ providers rule, made obvious:
provider IS enabled ┌──────────────────────────────────────────┐
(runs, contributes results) │ app cmd power calc conv systemd ssh emoji│ <- owlry.providers
└─────┬─────┬───────────────┬──────────────┘
│ │ │
┌─────▼─────▼───────────────▼──────────────┐
│ app cmd uuctl │ <- owlry.tabs
│ ↑1 ↑2 ↑3 │ (subset; Ctrl+N order)
└──────────────────────────────────────────┘
A provider in providers but not in tabs is fully searchable (auto mode + :prefix works), it just doesn't get a permanent tab button. This is the right place for calc/conv/websearch/filesearch — they're triggered with = / > / ? / / and don't need to take up tab-bar real estate.
4.4 owlry.provider { ... } — user-defined provider
Defines a custom provider that runs alongside the built-ins. The minimum useful shape:
owlry.provider {
id = "hs", -- type_id, used internally
prefix = ":hs", -- search prefix (optional)
items = function(query) -- called to populate results
return {
{ name = "Lock", command = "hyprlock" },
{ name = "Shutdown", command = "systemctl poweroff" },
}
end,
}
Full field reference:
| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
id |
string |
yes | — | Unique provider ID. Used for filter matching (-m <id>) and tab listings. Must be lowercase, alphanumeric + -/_. |
items |
function(query: string) -> table |
yes | — | Returns the list of items. Called on every query if dynamic = true, otherwise cached after the first call (see refresh below). |
prefix |
string | nil |
no | nil |
Search prefix (e.g. ":hs "). Typing the prefix narrows results to this provider. |
name |
string |
no | id capitalised |
Display name shown in owlry providers. |
tab_label |
string |
no | name |
Tab button text. |
icon |
string |
no | "application-x-addon" |
XDG icon name shown next to items. |
search_noun |
string |
no | id |
Search placeholder noun (e.g. "Search SSH hosts…"). |
dynamic |
boolean |
no | false |
If true, items is called on every keystroke. If false, items is called once at startup and cached. 2.1 ships only false (static); true arrives in 2.2. |
priority |
integer |
no | 0 |
Tiebreaker for ordering when multiple providers match. Higher = first. |
The item table:
Each entry in the table returned by items is itself a table:
| Field | Type | Required | What it does |
|---|---|---|---|
name |
string |
yes | Item title shown in the result row |
command |
string |
yes | Shell command executed on launch |
description |
string |
no | Secondary text below the title |
icon |
string |
no | Per-item icon (overrides provider icon) |
terminal |
boolean |
no | If true, launches inside a terminal |
tags |
table |
no | List of tag strings for :tag:X filtering |
Multiple owlry.provider calls accumulate — each call registers an additional provider. Calling twice with the same id overrides the previous registration (warning emitted).
Lifecycle: the provider's items function is called by the daemon. It runs in the Lua state with the full host API available (see §5). It must return within a reasonable time — slow items functions block the UI. If you need to do work that takes >100ms, cache it externally and refresh on demand (see dynamic = true for the 2.2 path).
4.5 owlry.theme(...) — theme selection
Two forms:
-- Built-in or user theme by name
owlry.theme("catppuccin-mocha")
Looks up the theme in order: ~/.config/owlry/themes/{name}.css → bundled theme. Built-in themes: owl, catppuccin-mocha, nord, rose-pine, dracula, gruvbox-dark, tokyo-night, solarized-dark, one-dark, apex-neon.
-- Inline color overrides (merges on top of selected theme or system defaults)
owlry.theme {
background = "#1e1e2e",
background_secondary = "#313244",
border = "#45475a",
text = "#cdd6f4",
text_secondary = "#a6adc8",
accent = "#cba6f7",
accent_bright = "#f5c2e7",
-- Per-provider badge colors (optional)
badge_app = "#a6e3a1",
badge_cmd = "#fab387",
badge_power = "#f38ba8",
badge_uuctl = "#9ece6a",
}
The two forms can be combined — call owlry.theme(name) first, then owlry.theme { ... } to layer overrides on top.
5. The host API in Lua scope
User code runs in a Lua 5.4 state with the mlua runtime. Beyond the standard library, owlry exposes a small host API for things provider code is likely to need.
5.1 What's available in owlry.lua
- Full Lua 5.4 stdlib —
math,string,table,os,io,coroutine,package. No sandboxing. It's your config file on your machine; deal with it. - The
owlrymodule —set,providers,tabs,provider,theme. (Documented above.) - Convenience helpers under
owlry.util(added incrementally; ship in 2.1):owlry.util.shell(cmd) -> string— run a shell command, return stdout. Blocking; use sparingly.owlry.util.shell_lines(cmd) -> table— same, split into a list of lines.owlry.util.read_file(path) -> string|nil— read a file's contents, nil if missing.owlry.util.glob(pattern) -> table— list paths matching a glob.owlry.util.env(name, default?) -> string— read an env var with a fallback.owlry.util.hostname() -> string— current hostname.
5.2 What's NOT in scope
- No process spawning at config-load time for non-trivial setup. Provider
itemsfunctions can shell out viaowlry.util.shell, but the top-level config eval should be fast. If the daemon spends >500ms runningowlry.luaat startup, something's wrong. - No network helpers in 2.1. If a user provider needs HTTP, they can
os.execute("curl ...")for now. A first-classowlry.util.http_getlands in 2.2. - No reactive state. Each provider's
itemsfunction should be stateless (or use local upvalues for caching). Cross-call state via shared mutable globals will work but isn't a supported pattern.
5.3 Sandbox stance
None in 2.1. This is the user's config file on the user's machine, by analogy to bashrc or init.vim. If a future "share your config" community emerges, sandbox concerns kick in then. For now: assume the user wrote the file they're running.
6. How providers + tabs + provider {} compose at runtime
The single most-asked question this section pre-empts.
6.1 The three axes
- Compiled in — cargo features at build time. AUR build = all of them. Users on
cargo install --no-default-featuresonly have a minimal set. Cannot be enabled at runtime. - Enabled —
owlry.providers { ... }decides which compiled-in providers actually run. Plus allowlry.provider {}user definitions are auto-enabled. - Shown as tab —
owlry.tabs { ... }decides which enabled providers get a button in the header bar.
A provider must be compiled in AND enabled to do anything. Being shown as a tab is purely a UI convenience.
6.2 Selection at use time
On top of the three axes, the user picks at launch time:
owlry # auto mode — every enabled provider contributes, tabs cycle through
owlry -m auto # explicit form of above
owlry -m app # single-mode — only `app` runs, even though others are enabled
owlry --profile dev # profile (see §6.5) — pre-baked subset of enabled providers
Plus the prefix override inside the UI: typing :uuctl foo narrows the current query to the systemd provider regardless of the mode.
6.3 Worked example
Given this config:
owlry.providers { "app", "cmd", "calc", "conv", "power", "systemd", "websearch" }
owlry.tabs { "app", "cmd", "systemd" }
owlry.provider { id = "hs", prefix = ":hs", items = function() ... end }
Behavior:
| Invocation / typed prefix | What runs | Tabs shown |
|---|---|---|
owlry (no flags), empty query |
every enabled provider scored by frecency. app, cmd, calc, conv, power, systemd, websearch, hs all contribute |
app, cmd, systemd, hs (hs auto-added because it wasn't in owlry.tabs → defaults policy kicks in for user providers; see note below) |
owlry then user types :hs |
only the hs provider |
tabs unchanged |
owlry -m app |
only app |
only the app tab is highlighted |
owlry, user types = 2+3 |
calculator catches the = trigger, returns 5 |
tabs unchanged |
owlry, user types :foo bar |
nothing (no provider with id foo) |
tabs unchanged |
owlry, user types :bookmarks rust |
nothing (bookmarks not in providers → not enabled, even though it's compiled in) |
warning surfaces in owlry doctor |
Default tab policy when
owlry.tabsis omitted: all enabled providers get tabs. Whenowlry.tabsIS present: user providers fromowlry.provider {}are NOT auto-added — the user explicitly chose their tab list. To pin a user provider, include itsidinowlry.tabs. (Open: do we want auto-add behavior here? See §10.)
6.4 What happens if a config refers to something missing
| Situation | Behavior | Where surfaced |
|---|---|---|
owlry.providers lists a provider not compiled in |
Silently dropped at runtime; warning logged | owlry doctor lists it under "Not compiled in" |
owlry.providers lists an unknown id |
Warning at config-validate time | owlry config validate prints the offending line |
owlry.tabs lists an id not in owlry.providers |
Warning, dropped from tab bar | owlry config validate |
owlry.provider { items = function() ... end } errors |
Provider returns 0 items for that query; full traceback in daemon log | owlry doctor shows the provider as "errored" with the last exception |
Two owlry.provider {} calls with same id |
Second one wins; warning emitted | owlry config validate |
6.5 Profiles
Pre-baked alternate enabled sets (a la 2.0's --profile). Inline in Lua:
owlry.profiles {
dev = { "app", "cmd", "ssh" },
media = { "emoji", "clipboard" },
minimal = { "app" },
}
owlry --profile dev # uses { app, cmd, ssh } instead of owlry.providers's full set
Profiles override owlry.providers for the launch but inherit owlry.set / owlry.theme / owlry.tabs.
7. Hot reload
The daemon watches owlry.lua and any files it requires. On save:
- Re-evaluate the Lua state in an isolated context.
- If eval succeeds → swap the new config into the running daemon. Providers reload. No window flicker; no socket disconnect.
- If eval fails → keep the old config alive. The error is logged to the daemon journal and surfaced on the next
owlry doctorinvocation.
There is no systemctl reload required. Edit the file, save, the next query reflects the change.
Hot reload uses the notify filesystem watcher (re-added to deps in Phase 3 — it was removed in v2 demolition).
8. Validation & error reporting
owlry config validate
Runs the Lua file in dry-run mode (no side effects, no daemon swap). Reports:
- Syntax errors with line numbers.
- Unknown keys in
owlry.set. - Unknown IDs in
owlry.providers/owlry.tabs. owlry.tabsentries not inowlry.providers.- Duplicate
idinowlry.provider {}calls. - Providers compiled out (warning, not error).
Exit code: 0 on clean, 1 on any error, 2 on warnings only (configurable).
owlry config show
Prints the resolved config as TOML (yes, TOML — for diffability and consistency with how owlry config show works today). Useful for debugging "what does owlry actually see?"
owlry config show --lua
Prints the resolved config as a Lua file (round-trippable). Useful for owlry migrate-config diff comparison.
9. Migration from TOML
owlry migrate-config
Reads ~/.config/owlry/config.toml, writes ~/.config/owlry/owlry.lua with the equivalent settings. Refuses to overwrite an existing owlry.lua unless --force is passed.
Mapping:
| TOML | Lua |
|---|---|
[general] theme = "owl" |
owlry.set { theme = "owl" } |
[general] tabs = ["app", "cmd"] |
owlry.tabs { "app", "cmd" } |
[providers] app = true |
included in owlry.providers { ... } |
[providers] app = false |
excluded from owlry.providers { ... } |
[appearance.colors] badge_app = "..." |
merged into owlry.theme { badge_app = "..." } |
[profiles.dev] modes = [...] |
owlry.profiles { dev = { ... } } |
Edge cases:
- Pre-v2 aliases (
systemconfig key,badge_syscolor) are normalized to v2 names (power,badge_power) in the emitted Lua. - Comments in the TOML are translated to Lua comments where they survive serialization round-trips. Order is preserved as much as possible.
- Unknown TOML keys (e.g. removed config options) are commented out in the output with a
-- DROPPED:prefix.
The migrator is deterministic — running it twice on the same input produces byte-identical output.
10. Open questions (resolved before Phase 3 ships)
| Q | Default proposed | Notes |
|---|---|---|
Auto-add user providers to owlry.tabs when tabs is non-empty? |
No — explicit tabs list is explicit | Easy to revisit; user feedback after 2.1 will decide |
owlry.theme separate vs folded into owlry.set? |
Separate — themes are big enough to deserve their own verb | Matches Hyprland's general / decoration split |
| Profile keybinds (per-key Lua functions)? | Deferred to 2.2 | Needs careful design re: which thread runs the function |
| Error reporting depth: traceback in UI banner? | No in 2.1 — log to journal, surface via owlry doctor |
UI banner gets noisy fast |
Allow owlry.lua to be a directory of files merged together? |
No — single file. Users wanting modularity use Lua require |
|
Multi-file config via require? |
Yes, supported transparently — same hot-reload watcher follows required files |
Standard Lua semantics |
11. Version & compatibility roadmap
| Owlry version | Lua config | TOML config | Notes |
|---|---|---|---|
| 2.0 (shipped) | not supported | canonical | owlry migrate-config exists as a stub |
| 2.1 (Phase 3) | opt-in preview | still works | owlry migrate-config becomes functional |
| 2.2 (Phase 3 polish) | preferred; documented | works but warning emitted on load | dynamic providers, owlry.bind, owlry.util.http_get |
| 3.0 (D18) | canonical | removed | owlry config validate errors loudly when TOML is found without owlry.lua |
12. Implementation outline (engineering, not user-facing)
Tracked in docs/RESTRUCTURE-V2.md Phase 3. High level:
- Add
mluadep (Lua 5.4, vendored, send, serialize) - New
crates/owlry/src/lua/module: runtime, api surface, error types, host API utilities crates/owlry/src/config/loader.rs: resolve order Lua → TOML → defaults; both paths produce the sameConfigstructLuaProviderimplProvider/DynamicProviderfrom a Lua closurenotifywatcher re-added; daemon listens forowlry.luachanges, re-evaluates in an isolated state, hot-swapsowlry migrate-configfunctional; reads TOML, emits Lua- Characterization tests for every section of this doc; integration test that loads each example end-to-end
- README + CLAUDE.md +
owlry(1)updated
Target release: 2.1.0.