From 1075eefbf388a950253fea1f9de780e3853e953e Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 03:41:11 +0200 Subject: [PATCH 1/4] 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**. From e88525fa1991d0e5685fe376147ac4625669ea7a Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 03:52:06 +0200 Subject: [PATCH 2/4] fix(providers): surface dynamic providers in doctor + providers list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProviderManager::available_providers() and available_provider_types() iterated only the static `Vec>` field, never the parallel `Vec>`. Result: calc, conv, websearch, filesearch ran fine but were silently absent from `owlry doctor` and `owlry providers ` outputs. Fix: - Add prefix(), icon(), tab_label(), search_noun() as defaulted methods on the DynamicProvider trait (matching the static Provider trait shape). Default values: None / "application-x-addon" / None / None. - Override those methods on the four dynamic provider impls with sensible values (e.g. calculator: ":calc" / "accessories-calculator" / "Calc" / "math expression"). - Extend available_providers() and available_provider_types() to iterate builtin_dynamic too. Each dynamic provider always reports as ProviderType::Plugin() — Application/Command/Dmenu variants are static-only, but the match is defensive. Tests added (TDD characterization for 2.0.1): - available_providers_includes_dynamic_providers — synthetic DynRich provider with every overridable method set; asserts the descriptor comes back with the right id/prefix/icon/position/tab_label/search_noun. - available_provider_types_includes_dynamic_providers — verifies the type list returns both static and dynamic types. - dynamic_provider_trait_defaults_return_documented_values — minimal impl returns the documented defaults. After this fix, `owlry doctor` reports 11 providers instead of 7 on a default --features full build, and `owlry providers calc` returns the calculator's full metadata instead of 'No provider with id'. 248 tests pass with --features full (was 245). --- crates/owlry/src/providers/calculator.rs | 16 +++ crates/owlry/src/providers/converter/mod.rs | 16 +++ crates/owlry/src/providers/filesearch.rs | 16 +++ crates/owlry/src/providers/mod.rs | 149 +++++++++++++++++++- crates/owlry/src/providers/websearch.rs | 16 +++ 5 files changed, 211 insertions(+), 2 deletions(-) diff --git a/crates/owlry/src/providers/calculator.rs b/crates/owlry/src/providers/calculator.rs index 900b0d6..7e19825 100644 --- a/crates/owlry/src/providers/calculator.rs +++ b/crates/owlry/src/providers/calculator.rs @@ -22,6 +22,22 @@ impl DynamicProvider for CalculatorProvider { 10_000 } + fn prefix(&self) -> Option<&str> { + Some(":calc") + } + + fn icon(&self) -> &str { + "accessories-calculator" + } + + fn tab_label(&self) -> Option<&str> { + Some("Calc") + } + + fn search_noun(&self) -> Option<&str> { + Some("math expression") + } + fn query(&self, query: &str) -> Vec { let expr = match extract_expression(query) { Some(e) if !e.is_empty() => e, diff --git a/crates/owlry/src/providers/converter/mod.rs b/crates/owlry/src/providers/converter/mod.rs index 2c36e95..1541704 100644 --- a/crates/owlry/src/providers/converter/mod.rs +++ b/crates/owlry/src/providers/converter/mod.rs @@ -28,6 +28,22 @@ impl DynamicProvider for ConverterProvider { 9_000 } + fn prefix(&self) -> Option<&str> { + Some(":conv") + } + + fn icon(&self) -> &str { + PROVIDER_ICON + } + + fn tab_label(&self) -> Option<&str> { + Some("Convert") + } + + fn search_noun(&self) -> Option<&str> { + Some("unit / currency conversion") + } + fn query(&self, query: &str) -> Vec { let query_str = query.trim(); // Strip prefix diff --git a/crates/owlry/src/providers/filesearch.rs b/crates/owlry/src/providers/filesearch.rs index 602ec36..e2a1114 100644 --- a/crates/owlry/src/providers/filesearch.rs +++ b/crates/owlry/src/providers/filesearch.rs @@ -192,6 +192,22 @@ impl DynamicProvider for FileSearchProvider { 8_000 } + fn prefix(&self) -> Option<&str> { + Some(":file") + } + + fn icon(&self) -> &str { + "system-file-manager" + } + + fn tab_label(&self) -> Option<&str> { + Some("Files") + } + + fn search_noun(&self) -> Option<&str> { + Some("files") + } + fn query(&self, query: &str) -> Vec { self.evaluate(query) } diff --git a/crates/owlry/src/providers/mod.rs b/crates/owlry/src/providers/mod.rs index 1a44cf6..3ae4b4c 100644 --- a/crates/owlry/src/providers/mod.rs +++ b/crates/owlry/src/providers/mod.rs @@ -217,6 +217,23 @@ pub trait DynamicProvider: Send + Sync { fn query(&self, query: &str) -> Vec; fn priority(&self) -> u32; + /// Optional search prefix (e.g. ":calc"). None = no prefix. + fn prefix(&self) -> Option<&str> { + None + } + /// Icon name (XDG icon theme). + fn icon(&self) -> &str { + "application-x-addon" + } + /// Tab button label. + fn tab_label(&self) -> Option<&str> { + None + } + /// Search placeholder noun. + fn search_noun(&self) -> Option<&str> { + None + } + /// Handle a plugin action command. Returns true if handled. fn execute_action(&self, _command: &str) -> bool { false @@ -631,12 +648,19 @@ impl ProviderManager { } /// Get all available provider types (for UI tabs). + /// + /// Includes both static `Provider`s and dynamic `DynamicProvider`s so + /// callers like `owlry doctor` / `owlry providers` see the full picture. #[allow(dead_code)] pub fn available_provider_types(&self) -> Vec { - self.providers.iter().map(|p| p.provider_type()).collect() + self.providers + .iter() + .map(|p| p.provider_type()) + .chain(self.builtin_dynamic.iter().map(|p| p.provider_type())) + .collect() } - /// Get descriptors for all registered providers. + /// Get descriptors for all registered providers (static + dynamic). /// /// Used by the IPC server to report what providers are available to clients. pub fn available_providers(&self) -> Vec { @@ -675,6 +699,27 @@ impl ProviderManager { }); } + // Dynamic providers (calc, conv, websearch, filesearch). They're always + // ProviderType::Plugin() — the only other variants are + // Application/Command/Dmenu which are static-only. + for provider in &self.builtin_dynamic { + let id = match provider.provider_type() { + ProviderType::Plugin(type_id) => type_id, + // Defensive: keep the surface honest even if a future dynamic + // provider claims a non-Plugin type. + other => other.to_string(), + }; + descs.push(ProviderDescriptor { + id, + name: provider.name().to_string(), + prefix: provider.prefix().map(String::from), + icon: provider.icon().to_string(), + position: ProviderPosition::Normal.as_str().to_string(), + tab_label: provider.tab_label().map(String::from), + search_noun: provider.search_noun().map(String::from), + }); + } + descs } @@ -1186,6 +1231,106 @@ mod tests { assert!(pm.execute_plugin_action("DYN:thing")); } + #[test] + fn available_providers_includes_dynamic_providers() { + // Regression guard (2.0.1): dynamic providers must surface in the + // diagnostic list so `owlry doctor` and `owlry providers` can report + // them. Pre-2.0.1, only static providers were enumerated and the + // dynamic ones were silently absent despite running fine. + struct DynRich; + impl DynamicProvider for DynRich { + fn name(&self) -> &str { + "Rich Dynamic" + } + fn provider_type(&self) -> ProviderType { + ProviderType::Plugin("rich-dyn".into()) + } + fn query(&self, _q: &str) -> Vec { + Vec::new() + } + fn priority(&self) -> u32 { + 7_000 + } + fn prefix(&self) -> Option<&str> { + Some(":rdyn") + } + fn icon(&self) -> &str { + "rich-dynamic-icon" + } + fn tab_label(&self) -> Option<&str> { + Some("Rich") + } + fn search_noun(&self) -> Option<&str> { + Some("rich queries") + } + } + + let pm = ProviderManager::new(Vec::new(), vec![Box::new(DynRich)]); + let descs = pm.available_providers(); + assert_eq!(descs.len(), 1); + let d = &descs[0]; + assert_eq!(d.id, "rich-dyn"); + assert_eq!(d.name, "Rich Dynamic"); + assert_eq!(d.prefix.as_deref(), Some(":rdyn")); + assert_eq!(d.icon, "rich-dynamic-icon"); + assert_eq!(d.position, "normal"); + assert_eq!(d.tab_label.as_deref(), Some("Rich")); + assert_eq!(d.search_noun.as_deref(), Some("rich queries")); + } + + #[test] + fn available_provider_types_includes_dynamic_providers() { + struct DynStub; + impl DynamicProvider for DynStub { + fn name(&self) -> &str { + "stub" + } + fn provider_type(&self) -> ProviderType { + ProviderType::Plugin("stub".into()) + } + fn query(&self, _q: &str) -> Vec { + Vec::new() + } + fn priority(&self) -> u32 { + 0 + } + } + + let mock = MockProvider::new("Apps", ProviderType::Application); + let pm = ProviderManager::new(vec![Box::new(mock)], vec![Box::new(DynStub)]); + let types = pm.available_provider_types(); + assert_eq!(types.len(), 2); + assert!(types.contains(&ProviderType::Application)); + assert!(types.contains(&ProviderType::Plugin("stub".into()))); + } + + #[test] + fn dynamic_provider_trait_defaults_return_documented_values() { + // 2.0.1 added prefix/icon/tab_label/search_noun as defaulted methods + // on DynamicProvider so the trait can describe its own UI metadata + // without falling back to a hardcoded match table. + struct Minimal; + impl DynamicProvider for Minimal { + fn name(&self) -> &str { + "m" + } + fn provider_type(&self) -> ProviderType { + ProviderType::Plugin("m".into()) + } + fn query(&self, _: &str) -> Vec { + Vec::new() + } + fn priority(&self) -> u32 { + 0 + } + } + let m = Minimal; + assert_eq!(m.prefix(), None); + assert_eq!(m.icon(), "application-x-addon"); + assert_eq!(m.tab_label(), None); + assert_eq!(m.search_noun(), None); + } + #[test] fn execute_plugin_action_returns_false_when_nothing_handles() { let prov = RichMockProvider { diff --git a/crates/owlry/src/providers/websearch.rs b/crates/owlry/src/providers/websearch.rs index d08fa4d..1998f54 100644 --- a/crates/owlry/src/providers/websearch.rs +++ b/crates/owlry/src/providers/websearch.rs @@ -150,6 +150,22 @@ impl DynamicProvider for WebSearchProvider { 9_000 } + fn prefix(&self) -> Option<&str> { + Some(":web") + } + + fn icon(&self) -> &str { + "applications-internet" + } + + fn tab_label(&self) -> Option<&str> { + Some("Web") + } + + fn search_noun(&self) -> Option<&str> { + Some("the web") + } + fn query(&self, query: &str) -> Vec { match self.evaluate(query) { Some(item) => vec![item], From 048c446b26bbc03475ce236a50987419f94c13c3 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 03:52:20 +0200 Subject: [PATCH 3/4] feat(aur/install-hook): proactive legacy-cruft detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Common upgrade snags from pre-v2 setups that the 2.0.0 hook didn't catch in time. Both detected read-only and reported with precise remediation; the hook never modifies user files. 1. Stale ~/.config/systemd/user/owlry{,d}.{service,socket} overrides. systemd-user gives precedence to ~/.config/systemd/user/* over /usr/lib/systemd/user/*. A leftover dev-time override (e.g. from 'just install-local' in a 1.x tree) silently masks the AUR-shipped unit. We classify them: - references the deleted owlryd binary - points at a dev-time target/debug or target/release path - redundant override of the canonical /usr/bin/owlry - non-standard ExecStart (flagged for review) 2. Compositor autostart referencing 'owlryd': ~/.config/hypr/hyprland.conf and any *.conf / *.hyprlang in hypr/ ~/.config/sway/config ~/.config/i3/config ~/.config/river/init ~/.config/niri/config.kdl Each affected user gets a banner with the exact rm + systemctl --user commands to run (and what to replace 'owlryd' with in compositor configs). The hook reads only — it never executes the cleanup itself. Runs on: - 1.x -> 2.x upgrades (alongside the existing rename banner) - 2.0.0 -> 2.0.1 upgrade (re-runs the detection idempotently for users who missed cleanup the first time around) The 1.x banner copy is touched lightly: bookmarks now appears in the 'deferred' line alongside the widgets (D22). --- aur/owlry/owlry.install | 152 ++++++++++++++++++++++++++++++++++------ 1 file changed, 131 insertions(+), 21 deletions(-) diff --git a/aur/owlry/owlry.install b/aur/owlry/owlry.install index 66108b0..ae8cc6c 100644 --- a/aur/owlry/owlry.install +++ b/aur/owlry/owlry.install @@ -1,8 +1,127 @@ ## owlry .install hook ## ## v2.0 renamed the systemd user units (owlryd.{service,socket} → owlry.{service,socket}) -## and consolidated 14 separate packages into one. Handle the unit migration so users -## upgrading from 1.x don't end up with an enabled-but-missing owlryd.service. +## and consolidated 14 separate packages into one. +## +## 2.0.1 extends this hook with proactive detection of two common upgrade snags: +## 1. Stale ~/.config/systemd/user/owlry{,d}.{service,socket} overrides pointing +## at a deleted dev-time build path or the removed `owlryd` binary. systemd-user +## gives precedence to user-level units over /usr/lib/systemd/user/*, so a +## stale override silently breaks the AUR-shipped unit. +## 2. Compositor configs (Hyprland, Sway, i3, river, niri) with `exec owlryd` / +## `exec-once = owlryd` lines that won't resolve anymore. +## +## The hook only DETECTS and REPORTS — it never touches user homedirs from a root +## pacman context. Each affected user gets a precise remediation command. + +_owlry_for_each_user() { + # Apply $1 (function name) to each logged-in user's home directory. + # Falls back silently if loginctl/getent aren't available. + local fn="$1" + command -v loginctl >/dev/null 2>&1 || return 0 + command -v getent >/dev/null 2>&1 || return 0 + local user home + while read -r user; do + [ -n "$user" ] || continue + home="$(getent passwd "$user" 2>/dev/null | awk -F: '{print $6}')" + [ -d "$home" ] || continue + "$fn" "$user" "$home" + done < <(loginctl list-users --no-legend 2>/dev/null | awk '{print $2}') +} + +_owlry_check_unit() { + # Inspect a user-level systemd unit; emit a remediation hint if it's stale. + # Args: $1=user, $2=home, $3=unit-filename (e.g. owlry.service) + local user="$1" home="$2" unit="$3" + local svc="$home/.config/systemd/user/$unit" + [ -f "$svc" ] || return 1 + local exec_line + exec_line="$(grep -E '^ExecStart=' "$svc" 2>/dev/null | head -1 | sed 's/^ExecStart=//')" + case "$exec_line" in + */owlryd|*/owlryd\ *) + printf ' %s -> references deleted owlryd binary: %s\n' "$svc" "$exec_line" + return 0 + ;; + */target/debug/*|*/target/release/*) + printf ' %s -> points at a dev-time build path: %s\n' "$svc" "$exec_line" + return 0 + ;; + */owlry|*/owlry\ *|/usr/bin/owlry|/usr/bin/owlry\ *) + # If the override matches the canonical /usr/bin/owlry, it's harmless + # but redundant. Flag it gently. + printf ' %s -> user-level override (redundant; AUR ships this unit)\n' "$svc" + return 0 + ;; + *) + # Unknown ExecStart — flag it so the user can review. + printf ' %s -> non-standard ExecStart: %s\n' "$svc" "$exec_line" + return 0 + ;; + esac +} + +_owlry_check_compositors() { + # Args: $1=user, $2=home + local user="$1" home="$2" + local found=0 f + # Single-file configs + for f in \ + "$home/.config/sway/config" \ + "$home/.config/i3/config" \ + "$home/.config/river/init" \ + "$home/.config/niri/config.kdl"; do + if [ -f "$f" ] && grep -qE '\bowlryd\b' "$f" 2>/dev/null; then + printf ' %s -> contains `owlryd` reference\n' "$f" + found=1 + fi + done + # Hyprland — main file + include directory + if [ -d "$home/.config/hypr" ]; then + while IFS= read -r f; do + if grep -qE '\bowlryd\b' "$f" 2>/dev/null; then + printf ' %s -> contains `owlryd` reference\n' "$f" + found=1 + fi + done < <(find "$home/.config/hypr" -type f \( -name '*.conf' -o -name '*.hyprlang' \) 2>/dev/null) + fi + return $((1 - found)) +} + +_owlry_per_user_migration_check() { + local user="$1" home="$2" + # Collect all check output in one subshell so internal newlines are + # preserved (only the trailing newline gets stripped by $()). + local out + out="$({ + _owlry_check_unit "$user" "$home" owlry.service + _owlry_check_unit "$user" "$home" owlry.socket + _owlry_check_unit "$user" "$home" owlryd.service + _owlry_check_unit "$user" "$home" owlryd.socket + _owlry_check_compositors "$user" "$home" + })" + + [ -z "$out" ] && return 0 + + echo + echo " ── user '$user' ─────────────────────────────────────────────" + echo "$out" + echo + echo " Suggested cleanup (run as '$user'):" + echo + cat </dev/null || true + + # 2. Replace any \`owlryd\` autostart line in your compositor config: + # exec-once = owlryd -> exec-once = owlry -d + # exec owlryd -> exec owlry -d + # (Usually unnecessary now — owlry.service / owlry.socket replace autostart.) +EOF +} post_upgrade() { local old_pkgver="$2" @@ -20,31 +139,22 @@ post_upgrade() { │ owlryd.service -> owlry.service │ │ owlryd.socket -> owlry.socket │ │ │ - │ If you had the old service enabled, run: │ - │ systemctl --user disable --now owlryd.service │ - │ systemctl --user enable --now owlry.service │ - │ │ │ Plugin packages (bookmarks, systemd, clipboard, …) are now │ │ built into owlry by default — they were dropped from AUR and │ │ replaced via this package. │ │ │ - │ Widget providers (weather, media, pomodoro) are not in 2.0; │ - │ they return in a later 2.x release. See: │ - │ docs/RESTRUCTURE-V2.md (decisions D20, section 8) │ + │ Widget providers (weather, media, pomodoro) and bookmarks │ + │ are not in 2.0; they return in a later 2.x release. See: │ + │ docs/RESTRUCTURE-V2.md (decisions D20, D22) │ ╰─────────────────────────────────────────────────────────────────╯ - EOF - # Best-effort transition: if the old owlryd.service is enabled or - # active for the invoking user, stop it and disable it so the new - # owlry.service can take over. Errors are non-fatal — pacman runs - # as root, so we can only inspect, not toggle user units here. - if command -v loginctl >/dev/null 2>&1; then - local invoking_user - invoking_user="$(loginctl list-users --no-legend 2>/dev/null | awk 'NR==1 {print $2}')" - if [ -n "$invoking_user" ]; then - echo " (Run the systemctl --user commands above as user '$invoking_user'.)" - fi - fi + _owlry_for_each_user _owlry_per_user_migration_check + ;; + 2.0|2.0.0) + # 2.0.0 → 2.0.1: re-run the migration check; some users may have + # missed cleanup the first time. Idempotent — only prints when + # there's still something to flag. + _owlry_for_each_user _owlry_per_user_migration_check ;; esac } From 0242e4870794c4df2c8535df54dbdc3dd803ab42 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 03:52:23 +0200 Subject: [PATCH 4/4] chore(owlry): bump version to 2.0.1 Patch release covering: - fix(providers): dynamic providers (calc, conv, websearch, filesearch) now appear in owlry doctor and owlry providers output - feat(aur/install-hook): proactive detection of stale owlryd references in user-level systemd units and compositor configs No behavioural changes beyond diagnostic visibility and upgrade-time warnings. No config or API breakage; safe drop-in upgrade. Cargo.toml + PKGBUILD bumped together; .SRCINFO regenerated; b2sum will be refreshed by 'just aur-update' once the tag is pushed. --- Cargo.lock | 2 +- aur/owlry/.SRCINFO | 4 ++-- aur/owlry/PKGBUILD | 2 +- crates/owlry/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1fe159..180f66d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1823,7 +1823,7 @@ dependencies = [ [[package]] name = "owlry" -version = "2.0.0" +version = "2.0.1" dependencies = [ "chrono", "clap", diff --git a/aur/owlry/.SRCINFO b/aur/owlry/.SRCINFO index f854767..32f9fd2 100644 --- a/aur/owlry/.SRCINFO +++ b/aur/owlry/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = owlry pkgdesc = Lightweight Wayland application launcher — UI, daemon, and providers in one binary - pkgver = 2.0.0 + pkgver = 2.0.1 pkgrel = 1 url = https://somegit.dev/Owlibou/owlry install = owlry.install @@ -78,7 +78,7 @@ pkgbase = owlry replaces = owlry-meta-tools replaces = owlry-meta-full options = !debug - source = owlry-2.0.0.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v2.0.0.tar.gz + source = owlry-2.0.1.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v2.0.1.tar.gz b2sums = e943074d2768cd3260248b67298860b00eeadffab0ccde5bcc07dc71356390c09ea6328fb2655cfdf9323de3a490e410be2b36ac1719bfc11361c4911142ee05 pkgname = owlry diff --git a/aur/owlry/PKGBUILD b/aur/owlry/PKGBUILD index dd1e08a..2f61ab9 100644 --- a/aur/owlry/PKGBUILD +++ b/aur/owlry/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: vikingowl pkgname=owlry -pkgver=2.0.0 +pkgver=2.0.1 pkgrel=1 pkgdesc="Lightweight Wayland application launcher — UI, daemon, and providers in one binary" arch=('x86_64') diff --git a/crates/owlry/Cargo.toml b/crates/owlry/Cargo.toml index 203bfa7..00c00ad 100644 --- a/crates/owlry/Cargo.toml +++ b/crates/owlry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "owlry" -version = "2.0.0" +version = "2.0.1" edition = "2024" rust-version = "1.90" description = "A lightweight, owl-themed application launcher for Wayland"