From 1075eefbf388a950253fea1f9de780e3853e953e Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 03:41:11 +0200 Subject: [PATCH] docs(phase-3): full Lua API spec + D23/D24 decisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/lua-api.md | 501 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 docs/lua-api.md diff --git a/docs/lua-api.md b/docs/lua-api.md new file mode 100644 index 0000000..c0bea31 --- /dev/null +++ b/docs/lua-api.md @@ -0,0 +1,501 @@ +# 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-foo` than for everything else" +- "Add a Hyprland shutdown menu as a `:hs` provider" +- "Build the bookmarks list from `~/Documents/links.md` instead of Firefox" +- "Conditionally enable systemd-units only on machines where `$HOSTNAME` matches 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: + +1. **Global settings** (theme, dimensions, terminal command) +2. **Enabled providers** (which built-ins run) +3. **Tab layout** (which providers appear as tab buttons) +4. **User-defined providers** (custom search sources) +5. **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 name `owlry.lua` is intentional brand identity. `init.lua` is Lua's `require` entry-point convention; this file isn't loaded by `require`, it's loaded explicitly by owlry, so the convention doesn't apply. + +### Resolution + +On startup the daemon resolves config in this order: + +1. **`$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. +2. **`$XDG_CONFIG_HOME/owlry/config.toml`** — fallback for 2.x users who haven't migrated. Behavior unchanged from 2.0. +3. **Shipped defaults** — `/usr/share/owlry/default.lua` (a stub that calls `owlry.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 + +```lua +-- ~/.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:** + +```lua +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.** + +```lua +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 in `owlry.tabs` will 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.providers` multiple 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 doctor` reports 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`. + +```lua +owlry.tabs { "app", "cmd", "uuctl" } +``` + +**Rules:** + +- Each entry must be in `owlry.providers` (or be a `owlry.provider {}` user provider). Listing an unknown ID produces a `owlry config validate` warning and is silently dropped at runtime. +- Order is preserved — `Ctrl+1` targets the first entry, `Ctrl+2` the 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: + +```lua +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 `) 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: + +```lua +-- 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`. + +```lua +-- 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 `owlry` module** — `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 `items` functions can shell out via `owlry.util.shell`, but the top-level config eval should be fast. If the daemon spends >500ms running `owlry.lua` at 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-class `owlry.util.http_get` lands in 2.2. +- **No reactive state.** Each provider's `items` function 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 + +1. **Compiled in** — cargo features at build time. AUR build = all of them. Users on `cargo install --no-default-features` only have a minimal set. Cannot be enabled at runtime. +2. **Enabled** — `owlry.providers { ... }` decides which compiled-in providers actually run. Plus all `owlry.provider {}` user definitions are auto-enabled. +3. **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: + +```bash +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: + +```lua +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.tabs` is omitted:** all enabled providers get tabs. +> **When `owlry.tabs` IS present:** user providers from `owlry.provider {}` are NOT auto-added — the user explicitly chose their tab list. To pin a user provider, include its `id` in `owlry.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: + +```lua +owlry.profiles { + dev = { "app", "cmd", "ssh" }, + media = { "emoji", "clipboard" }, + minimal = { "app" }, +} +``` + +```bash +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 `require`s. On save: + +1. Re-evaluate the Lua state in an isolated context. +2. If eval succeeds → swap the new config into the running daemon. Providers reload. No window flicker; no socket disconnect. +3. If eval fails → keep the old config alive. The error is logged to the daemon journal and surfaced on the next `owlry doctor` invocation. + +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 + +```bash +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.tabs` entries not in `owlry.providers`. +- Duplicate `id` in `owlry.provider {}` calls. +- Providers compiled out (warning, not error). + +Exit code: `0` on clean, `1` on any error, `2` on warnings only (configurable). + +```bash +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?" + +```bash +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 + +```bash +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 (`system` config key, `badge_sys` color) 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 `require`d 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: + +1. Add `mlua` dep (Lua 5.4, vendored, send, serialize) +2. New `crates/owlry/src/lua/` module: runtime, api surface, error types, host API utilities +3. `crates/owlry/src/config/loader.rs`: resolve order Lua → TOML → defaults; both paths produce the same `Config` struct +4. `LuaProvider` impl `Provider`/`DynamicProvider` from a Lua closure +5. `notify` watcher re-added; daemon listens for `owlry.lua` changes, re-evaluates in an isolated state, hot-swaps +6. `owlry migrate-config` functional; reads TOML, emits Lua +7. Characterization tests for every section of this doc; integration test that loads each example end-to-end +8. README + CLAUDE.md + `owlry(1)` updated + +Target release: **2.1.0**.