connect_row_activated only had the old submenu/launch logic, so double-
clicking an app inside the picker launched it standalone (no file) and
double-clicking an unassociated file hit the notify-only fallback. Mirror
the picker-active and NoAssociation branches from the Enter path so mouse
and keyboard behave identically (double-click = open once).
Add a Shift+Enter application picker so files can be opened with a chosen
app, optionally persisting the choice as the new default.
- Shift+Enter on a file result opens the picker (the full installed-app
list, reusing the submenu UI, searchable by typing).
- A file whose type has no associated application now opens the picker on
plain Enter instead of only showing a notification.
- In the picker: Enter opens the file once; Shift+Enter records the chosen
app as the default for the file's MIME type via `xdg-mime default`
(writes ~/.config/mimeapps.list), then opens.
- Launching with a chosen app reuses the default-handler path, so a
Terminal=true app still opens inside the configured terminal.
The key controller runs in the capture phase so Shift+Enter is intercepted
before the entry's activate signal; non-file results are unaffected
(passed through with Propagation::Proceed). No IPC/daemon changes — the app
list comes from an app-only query. Documents the picker and Shift+Enter in
the README.
Opening a file from filesearch shelled out to 'xdg-open' unconditionally,
which ignores the default handler's Terminal flag. Files whose default app
is a terminal program (nvim, vim, less, ...) launched windowless and
appeared to do nothing.
Resolve the default handler at launch time via the path's MIME type:
- terminal handlers (Terminal=true) run through the configured terminal,
reconstructing the .desktop Exec line with freedesktop field-code
expansion (%f/%F/%u/%U, %% literal, others dropped, path appended when
absent);
- GUI handlers and indeterminate resolutions keep using xdg-open;
- types with no associated application surface a desktop notification
instead of failing silently.
Resolution happens once for the selected item (not per result), keyed off
the item's 'file:<path>' id. Documents the open behavior in the README.
- reframe headline from the 2.0-collapse era to current 2.1 state; drop the
bookmarks provider from the compiled-in lists (it's deferred, not shipped)
- add a Performance section with informational warm-daemon latencies
- add a demo GIF (calc / converter / web / power) and a power-menu screenshot,
captured with synthetic providers only so no user data is shown; embed the
GIF as the hero image
Keeps rustfmt/clippy behaviour reproducible across contributors so the
formatting baseline doesn't drift again. Dev-toolchain pin only; the crate
MSRV (rust-version = 1.90) is unchanged, and the AUR chroot build (no rustup)
is unaffected.
Phase deltas across activation (boot → activate → present → first paint)
in app.rs, and per-query dispatch→IPC→paint timing in the keystroke search
path, to localise launcher latency in the field. Gated behind debug log
level via `log::debug!`; no cost in normal runs.
File search shelled out to `fd`/`locate` on every auto-mode keystroke,
walking $HOME and serialising behind a single daemon connection — observed
0.9–1.3s per keystroke, stalling every other provider's results. Gate it
behind explicit intent and bound the walk.
- DynamicProvider::runs_in_auto_mode() (default true); filesearch returns
false, so it only runs when its `:file` prefix is active — never on a
bare auto-mode query
- route `/` as a `:file` prefix alias in parse_query (identical ParsedQuery)
- drop the colon-less `file `/`find ` auto-triggers; `:file` / `/` cover it
(also fixes `:file find me` searching for "me")
- bound fd: --max-depth, --exclude .git/node_modules/target/.cache,
20 results, 750ms kill-timeout (spawn + drained stdout + reap)
- configurable providers.filesearch_roots (TOML + owlry.set; `~/` expanded,
non-existent entries dropped); empty searches $HOME
Auto-mode typing drops from up to 1.3s to ~2–20ms; file search stays one
keystroke away.
User feedback after running `owlry doctor` against a TOML-only setup:
the report said "[config] OK" with no indication that owlry.lua is the
canonical format from 2.1+ — or that a `migrate-config` command
existed. Doctor is the natural place to nudge.
commands.rs:
- New `print_config_source_info()` runs after the "OK" line on a
successful Config::load(). Cases:
* owlry.lua present → "source: Lua (path)". When config.toml is
also present, an extra "note: …toml is present alongside —
ignored." line surfaces the precedence rule.
* owlry.lua absent, config.toml present → "source: TOML (path)" +
a 3-line hint pointing at `owlry migrate-config` (deterministic,
`--force` to overwrite, TOML works until 3.0).
* Neither file → "source: built-in defaults (no user config found)".
- Also pulls in the same `lua::migrate::scan_legacy_artifacts` used by
`migrate-config` and emits a compact "legacy: N pre-v2 artifact(s)
detected" block listing each plugin / scripts dir with paths. Closing
line points at migrate-config for paste-ready snippets.
- `#[cfg(feature = "lua")]` branch covers the rich output; the non-lua
build emits a simpler note that owlry.lua is ignored without the
feature compiled in.
- Pluralisation: "1 entry"/"2 entries" and "1 artifact"/"2 artifacts"
(no more `(s)` stutter). Same nit also fixed in migrate-config's
"Heads up: detected N pre-v2 artifact(s)..." line for consistency.
Verified against three real scenarios:
1. config.toml + pre-v2 plugins/hyprshutdown + scripts/test.sh:
"OK / source: TOML / hint: migrate-config / legacy: 2 detected"
2. config.toml only, empty tempdir:
"OK / source: TOML / hint: migrate-config" (no legacy section)
3. No user config:
"OK / source: built-in defaults (no user config found)"
Clippy silent across --features full and --no-default-features.
The skeleton I generated for the legacy scripts/ directory used
`:map(function(name) ... end)`, which is JS/Rust idiom — Lua tables
don't have a built-in :map method. Anyone who pasted that into
their owlry.lua would have hit a runtime error on the first refresh.
Rewrite the snippet to use a standard `ipairs` loop with
`table.insert`. Verified by piping the exact rendered snippet
(against /tmp as the script dir) through `owlry config validate`:
exit 0, "OK (Lua, ...)". The hyprshutdown plugin snippet was already
valid Lua and didn't need any change.
User feedback after migrate-config: TOML migration was correct but
silently ignored the legacy `plugins/<name>/` (Rune / C-ABI) and
`scripts/` directories that v2 removed support for. The files still
exist on disk but aren't loaded — easy to miss what got broken.
lua/migrate.rs:
- New LegacyArtifact { path, kind } + LegacyKind { Plugin {..} |
ScriptsDir { entries } } types. Plugin variant carries id, name,
prefix, icon, entry_point pulled from the original plugin.toml so
the user has paste-ready data for the rewrite.
- scan_legacy_artifacts(config_dir) walks `plugins/` and `scripts/`
in alphabetical order:
* plugins/ — recognise a subdir as a plugin ONLY if it has a
plugin.toml. Skip dotfile dirs (.claude, .git, .cache, etc.)
even when they accidentally contain a manifest. This avoids
false positives like the user's `plugins/.claude/` (editor
config) and `plugins/docs/` (spec drafts under superpowers/).
* scripts/ — surfaced as a single ScriptsDir entry with the file
listing so the user sees what's there at a glance.
- parse_plugin_dir does a best-effort toml parse: malformed manifests
fall back to the directory name as id with everything else None.
- MigrateOutcome grows `legacy_artifacts: Vec<LegacyArtifact>`.
Empty when nothing matches.
commands.rs:
- New report_legacy_artifacts() prints a heads-up block per artifact
after the success summary. Each block includes the original
metadata AND a ready-to-paste `owlry.provider {}` skeleton:
* Plugin block fills in id/prefix/icon from the manifest and points
at the Rune entry_point as "this is what you need to translate".
* ScriptsDir block proposes a wrapper provider that iterates the
dir via owlry.util.shell_lines.
- Closing line links to docs/lua-api.md §4.4 (user providers) and
§5.1 (owlry.util.*).
Tests: 7 new (24/24 migrate tests green)
- scan_returns_empty_when_no_legacy_dirs
- scan_detects_plugin_with_full_manifest (id/name/prefix/icon/entry_point)
- scan_skips_directories_without_plugin_toml (was: fallback to dir name)
- scan_skips_dotfile_directories (covers `.claude` false positive)
- scan_tolerates_malformed_manifest
- scan_detects_scripts_directory_with_listing
- scan_reports_both_plugins_and_scripts_when_present
- migrate_outcome_includes_legacy_artifacts_field (end-to-end)
Verified against the real config (~/.config/owlry/ with a Rune-based
hyprshutdown plugin + a scripts/test.sh): output names both, the
hyprshutdown block surfaces its original `:hs` prefix and
system-shutdown icon, the scripts block lists test.sh and proposes a
wrapper provider. The earlier false positives (`.claude`, `docs`)
no longer appear.
359/359 lib tests green with --features full. Clippy silent across
--features full, --no-default-features, --no-default-features --features lua.
ad4e538 fixed the chroot link error, surfacing the next failure:
`cargo test --features full` in check() couldn't compile integration
tests because Request::Query gained an optional `prefix` field in
3.4.5 (commit bfbce42) and I missed three constructors under
crates/owlry/tests/. Unit tests under src/ were already updated.
Locally these tests compiled before because `cargo build --features
full` doesn't run them — only `cargo test` does, and I'd been running
`--lib` only. The chroot's PKGBUILD `check()` runs the full suite.
352 lib + 27 integration tests now green with --features full.
8383746 added .cargo/config.toml with rustflags=link-arg=-fuse-ld=bfd
but the chroot still linked with `-fuse-ld=lld`. cargo's RUSTFLAGS
env var takes precedence over [target.X.rustflags] in any
config.toml, so Arch's `extra/rust` pkg (which apparently sets its
own RUSTFLAGS to default rustc to LLD) was overriding the project
config silently.
Set `export RUSTFLAGS="-C link-arg=-fuse-ld=bfd"` directly in the
PKGBUILD's build() and check() functions. This is the highest-
precedence place — beats Arch's defaults, beats the project config,
beats any inherited env. Confirmed via the build-log chain in
build-logs/aur-test-20260513-132012.log: the link line still
contained `-fuse-ld=lld` despite the .cargo/config.toml landing in
the tarball.
`.cargo/config.toml` is kept (commit 8383746) so the local rustup
toolchain mirrors the chroot's linker. Belt-and-braces.
Re-run `just aur-local-test owlry`. Expected link line should now
contain `-fuse-ld=bfd` and the build should complete.
THIRD attempt at the chroot build. The real root cause: rustc's link
arg ordering plus Arch's `extra/rust` defaulting to `-fuse-ld=lld`.
lua-src emits `cargo:rustc-link-lib=static:-bundle=lua5.4` (negative
`-bundle` = "static, don't pack into the rlib") with a separate
`cargo:rustc-link-search=native=$OUT_DIR/lib`. rustc serialises these
into the final linker command line with `-llua5.4` appearing BEFORE
`-L .../mlua-sys-*/out/lib`. Single-pass linkers (LLD) honour args
left-to-right and fail to locate the archive at the time `-l` is
processed → 60+ undefined symbols out of mlua.
GNU `ld.bfd` does multi-pass resolution and finds the archive
regardless of arg order. rustup's stable toolchain (used locally)
defaults to bfd, which is why the bug never reproduced outside the
chroot despite identical Cargo.toml / Cargo.lock / cargo / rustc
versions.
Add `.cargo/config.toml` at the workspace root pinning bfd for
x86_64-unknown-linux-gnu via `rustflags = ["-C", "link-arg=-fuse-ld=bfd"]`.
binutils (which ships bfd) is already a build-essential dep of the
chroot — no new install cost. Local builds gain identical behaviour
to chroot, which is a nice side effect.
Verified locally: clean + frozen + features full → 84s, no link errors.
Sequence to reproduce the fix:
cargo clean
cargo build --frozen --release --features full
Reverts the previous two attempts to fix this:
- a4d4f30 (hoist mlua features to feature row)
- b3764b3 (drop optional from mlua/glob/notify)
... were both red herrings. mlua-sys's vendored feature WAS active in
the chroot — `Compiling lua-src v550.0.0` and `Compiling mlua-sys v0.10.0`
appear in build-logs/aur-test-20260513-131232.log and the link line
contains `-L .../mlua-sys-*/out/lib`. The static archive existed; LLD
just couldn't find it through the misordered arg list.
Re-run `just aur-local-test owlry` to confirm.
Second chroot test still failed with the same `undefined symbol:
lua_gettop` linker errors after hoisting features to the cargo
feature row in a4d4f30. mlua-sys runs its build script (the search
path `-L .../mlua-sys-*/out/lib` is in the gcc command line) but no
`liblua5.4.a` lands in OUT_DIR. The `vendored` feature isn't
propagating across the `optional = true` boundary even with the
feature-row syntax.
Reproduced on the LOCAL machine: clean `cargo build --frozen
--release --features full` succeeds with Arch's exact CFLAGS,
CXXFLAGS, LDFLAGS, LTOFLAGS. mlua-sys+lua-src+cc all work. So
something specific to the chroot's `extra/rust 1.95.0` package
(or its build environment) breaks the optional-dep feature
propagation for vendored. Couldn't pin the exact root cause
without chroot interactive access.
Pragmatic fix: drop `optional = true` from mlua, glob, and notify.
Inline `features = ["lua54", "vendored", "send", "serialize"]` on
the now-non-optional mlua dep — cargo resolves non-optional dep
features eagerly and consistently, no resolver-boundary surprises.
The `lua` cargo feature stays as a pure marker (gates the
crates/owlry/src/lua module via `#[cfg(feature = "lua")]`). Minimal
builds (`cargo install owlry --no-default-features`) still compile
mlua + glob + notify but skip the lua module — they pay maybe 20-30s
of extra compile time and a few MB of stripped-binary bloat for the
deps, but get a working build matrix that's identical in chroot.
Verified locally:
- `cargo clean && cargo build --frozen --release --features full`
builds clean in 32s with liblua5.4.a in OUT_DIR
- 352/352 lib tests with --features full
- 182/182 lib tests with --no-default-features
- Clippy silent across both configs
Re-run `just aur-local-test owlry` to confirm the chroot builds clean.
Cargo's clean-chroot resolver drops inline `features = [...]` on
optional deps under `--frozen` (the same gotcha that bit rusqlite
during v2). When mlua's `vendored` feature got stripped the chroot
build tried to link a system `liblua5.4` that doesn't exist, with
60+ undefined-symbol errors from `lua_gettop`, `lua_pcallk`, etc.
Move lua54, vendored, send, and serialize out of the inline `mlua`
dep spec and into the `lua` cargo feature row using `mlua/<feat>`
syntax. cargo's resolver propagates these reliably whether or not
the lock-file is frozen.
Verified locally with `cargo clean && cargo build --frozen --release
--features full` — full from-scratch compile of every dep including
mlua + lua-src, links cleanly in 42s. 352/352 lib tests still pass.
Cross-references the same lesson captured in memory under
"Cargo optional-dep features need explicit dep/feature wiring".
Documentation, example config, and one validator-test fix. Version
bump and AUR push intentionally deferred.
data/owlry.example.lua (new):
- Annotated reference config exercising every surface (set / providers
/ tabs / theme / profiles / provider / util). Active section is
minimal and validates clean.
aur/owlry/PKGBUILD:
- Ships data/owlry.example.lua to /usr/share/doc/owlry/owlry.example.lua.
pkgver kept at 2.0.1.
README.md:
- Config table puts owlry.lua first (preferred from 2.1), config.toml
marked legacy/fallback with precedence note linking lua-api.md §2.
- New "Quick Start (Lua config)" section with migrate-config blurb.
- migrate-config row: [--force], deterministic. config validate row:
exit 1 errors / exit 2 warnings.
- Roadmap section flips Lua config from "lands in 2.1/3.0" to
"shipped in 2.1"; lists 2.2 follow-ups (dynamic providers,
owlry.bind, util.http_get).
CLAUDE.md:
- Project shape tree expands lua/ module with per-file descriptions.
- Build section documents the `lua` cargo feature.
- CLI shape line for migrate-config: [--force]. config validate:
exit codes 1/2 mentioned.
- New "Lua config layer (2.1+)" section covers precedence, the
notify-based watcher, desktop-notification errors, and the
Arc<Lua> / LoadedConfig invariants.
data/owlry.1:
- --profile mentions both owlry.profiles (lua) and [profiles.<NAME>]
(toml).
- config validate paragraph describes the categorised report and
lists 0/1/2 exit codes.
- migrate-config description no longer says "stub" — covers
determinism, --force/-f, pre-v2 alias normalisation, links §9.
- FILES adds ~/.config/owlry/owlry.lua and the example .lua;
config.toml labelled legacy.
ROADMAP.md:
- "Lua-driven configuration" reworded as shipped in 2.1; example
uses owlry.lua and the v2 API; 2.2 follow-ups listed.
cli.rs help: migrate-config stub-era text → "TOML → owlry.lua
(--force to overwrite)".
lua/validate.rs: loosen pre_v2_aliases_are_known to assert only
that aliases aren't flagged as unknown ids. is_clean() failed
under --no-default-features --features lua because uuctl
correctly triggered the compiled-out warning (uuctl → systemd,
systemd feature off → silently dropped at runtime).
Test matrix (all green):
- --features full 352 lib tests
- --no-default-features 182 lib tests
- --no-default-features --features lua 305 lib tests
Clippy silent in all three configurations.
Smoke (release build, isolated XDG):
1. config validate on the shipped owlry.example.lua → OK exit 0
2. migrate-config: TOML → owlry.lua with the precedence notice
3. Daemon loads, watcher armed; appending owlry.provider triggers
hot-reload within the debounce window
4. :phase310 prefix routes the empty query to the new provider
5. config validate against the live file still OK
Implements the validator per docs/lua-api.md §8. Replaces the
TOML-only "load and pass/fail" check from 2.0 with categorised
findings: errors (exit 1), warnings (exit 2), clean (exit 0).
lua/config.rs:
- LuaConfig grows `duplicate_user_provider_ids: Vec<String>`. The dedup
in apply_provider would otherwise erase duplicates by the time the
snapshot is read.
lua/api.rs:
- apply_provider records the id on the duplicate list when replacing
an existing entry. The original warn! log is unchanged.
lua/validate.rs (new):
- ValidationReport { errors, warnings } with exit_code() honouring the
§8 codes (0/1/2).
- validate(cfg) is a pure function over a LuaConfig snapshot. Surfaces:
* Unknown keys in owlry.set
* Unknown colour keys in owlry.theme
* Unknown ids in owlry.providers (built-in alias table consulted;
user-provider ids are recognised)
* Providers compiled out — Cargo features checked via cfg! macros;
always-on ids (app, cmd, calc, conv, power, dmenu) skip the check
* Unknown / not-in-providers ids in owlry.tabs (user providers
auto-join the enabled set, so they pass when listed in tabs)
* Duplicate provider ids
* Unknown ids inside owlry.profiles values (per-profile label)
- canonical_provider_id() mirrors the apply_providers_list alias table
in lua/config.rs so an id accepted by the merger is never flagged.
commands.rs:
- run_config_validate now branches: when owlry.lua exists, build the
LoadedConfig, run validate::validate on the snapshot, print the
report with `<n> warning(s):` / `<n> error(s):` headers. Eval
failures print the chained mlua error (file path + line/col).
- TOML fallback path unchanged when no owlry.lua is present.
- Whole feature path is `#[cfg(feature = "lua")]`-gated.
Tests: 13 new in lua::validate::tests
- clean config emits nothing, exit 0
- unknown set key warns
- unknown theme key warns
- unknown provider id warns (and `app` passes through unflagged)
- user_provider ids in owlry.providers list are recognised
- tabs with unknown id warn
- tabs with id not in owlry.providers warn ("dropped from tab bar")
- tabs with user provider id passes (auto-joins enabled set)
- duplicate provider id warns
- profile with unknown id warns and names the profile
- pre-v2 aliases (sys, uuctl) validate cleanly
- always-compiled-in providers never trigger compile-out warning
- multi-warning report keeps every finding
Live smoke (release build, isolated XDG) for each category:
- clean → "config: OK (Lua, ...)", exit 0
- unknown set key → "owlry.set: unknown key `mystery_key` — ignored
(forward-compat for 2.2+ keys)", exit 2
- unknown provider id → "owlry.providers: unknown id `fictional_provider`
— not a built-in and not registered by any owlry.provider call", exit 2
- tabs not in providers → "owlry.tabs: `cmd` is not in owlry.providers
— it will be dropped from the tab bar", exit 2
- duplicate provider id → "owlry.provider: id `hs` was registered more
than once — the last definition wins (earlier registrations are
dropped)", exit 2
- profile with unknown id → "owlry.profiles.dev: unknown id `phantom`",
exit 2
- syntax error → "config: ERROR — Lua evaluation error in /.../
owlry.lua: syntax error: [string]:5: '}' expected (to close '{' at
line 2) near <eof>", exit 1
- 5-warning combo → all five surfaced with stable ordering, exit 2
352/352 lib tests with --features full. Clippy silent across full,
--features lua, and --no-default-features.
Implements the TOML → owlry.lua migrator per docs/lua-api.md §9. The
stub from 2.0 is gone; `owlry migrate-config [--force]` now reads
$XDG_CONFIG_HOME/owlry/config.toml and writes an equivalent owlry.lua.
cli.rs:
- MigrateConfig grew a `--force` / `-f` flag.
lua/migrate.rs (new, 700+ lines):
- MigrateRequest { toml_path, lua_path, force } + MigrateOutcome.
- migrate() validates paths, parses TOML via the existing
Config::load_from_toml pipeline (so pre-v2 aliases like `system` and
`badge_sys` normalise to v2 names through serde's #[serde(alias)]),
then calls generate_lua() and writes the file. Refuses to overwrite
without --force; refuses if no config.toml exists.
- generate_lua(cfg, raw_toml, src) is a pure function emitting Lua
text section-by-section. Properties:
* Deterministic — every list and map is alphabetically sorted, so
running the migrator twice on the same TOML produces byte-identical
output (locked in tests::migration_is_deterministic).
* Minimal — values matching Config::default() are omitted. A
config.toml with no overrides produces a Lua stub of just the
header comment.
* Round-trippable — the generated Lua, evaluated through LuaContext
and merged onto a default Config, matches the source Config's
scalars (tests::round_trip_preserves_scalars).
* Preserves leading `#` comment block from the source TOML by
re-emitting as `--` Lua comments under "Preserved from your
config.toml:". Other comments are intentionally dropped (cleanly,
without DROPPED placeholders).
- emit_table_key() bracket-quotes profile names that aren't valid Lua
identifiers (`my-media`, `media center`) and Lua 5.4 reserved words
(`end`, `for`, etc.), so the generated file always parses.
- lua_string() escapes \", \\, \n, \r, \t. Non-ASCII passes through
(Lua 5.4 strings are 8-bit clean).
commands.rs:
- run_migrate_config(force: bool) lives behind `#[cfg(feature = "lua")]`
with a clean stub on the no-lua build that tells the user how to
rebuild. Exit codes: 0 success, 1 destination-exists or no-TOML, 2
unrecoverable.
- Success output names both paths and bytes written, plus a one-liner
reminder that owlry.lua now takes precedence over config.toml.
Tests: 16 new in lua::migrate::tests
- lua_string escaping (quotes, backslashes, newlines, UTF-8 passthrough).
- header comment extraction (stops at first non-`#` non-empty line).
- default config → header-only output (no spurious blocks).
- emit_set only writes modified scalars.
- providers section omitted when all 11 built-ins are enabled.
- providers section emits alphabetically and excludes disabled ids.
- theme colours emit alphabetically (accent, background, badge_app order).
- profiles emit alphabetically (dev before minimal).
- profile names with hyphens get `["my-dev"]` bracket form.
- Lua reserved word `"end"` as profile name → bracket-quoted.
- determinism: twice on same Config → identical bytes.
- refuses existing destination without --force; preserves existing file.
- overwrites with --force.
- refuses when source TOML is missing.
- pre-v2 aliases (`system`, `badge_sys`) normalise to v2 names.
- round-trip: generate_lua → eval through LuaContext → merge onto
default Config → matches scalar fields of original.
Live smoke (release build, isolated XDG, pre-v2-flavoured TOML with
header comments, `system = false`, `badge_sys = "#f38ba8"`, hyphenated
`profiles."my-media"`):
- migrate-config writes 1044 bytes; output has the header block,
owlry.set with the 4 modified scalars, owlry.tabs, owlry.providers
(alphabetical, excluding power/ssh), owlry.theme with v2-named
badge_power, and owlry.profiles with `["my-media"]` bracket-quoted.
- Re-running without --force reports "already exists. Pass --force..."
and exits 1.
- Daemon launched against the generated file logs "Loaded Lua config"
and `config show` reports the migrated theme/scalars/profiles.
339/339 lib tests with --features full. Clippy silent across full,
--features lua, and --no-default-features.
Implements the hot-reload pipeline per docs/lua-api.md §7: the daemon
watches the user's owlry.lua and, on save, re-evaluates in a fresh
LuaContext. On success the new state hot-swaps atomically; on failure
the previous state is preserved and the user is told what broke via
BOTH the daemon log AND a desktop notification — no need to tail the
journal to discover the config is dead.
Cargo.toml:
- New optional dep `notify = "6"` gated by the `lua` feature alongside
mlua and glob. inotify backend on Linux; no platform-specific feature
flags needed.
lua/watcher.rs (new):
- ConfigWatcher::spawn(lua_path, config, pm, lua_ctx) wires a
notify::RecommendedWatcher on the parent directory (atomic-rename
saves from vim/JetBrains/etc rebind to a new inode, so watching the
file directly misses them) and spawns the event-pump thread.
- Debounce: 200ms after the first event, drain follow-ups, then reload
once. Coalesces burst saves into a single re-eval.
- reload() builds a fresh state in isolation; on Err keeps the old
Config/PM/LuaContext untouched.
- Swap order: config → ProviderManager → LuaContext. Old user-provider
Boxes inside the dropped PM retain the old Arc<Lua> until they
themselves drop, so the OLD Lua state survives the swap until the
OLD PM finishes dropping. New LuaProviders in new_pm hold the new
Arc<Lua>.
- report_reload_failure() formats the full error chain and fires a
notify_rust notification with summary/body/icon/urgency so the user
sees "config reload failed — /path/to/owlry.lua — <exact line>" the
moment a save breaks the file.
- Success path also emits a low-noise notification ("config reloaded").
lua/error.rs:
- LuaConfigError::Eval and ::Read no longer embed `{source}` in their
Display strings — error_chain() walks `.source()` already, and the
doubled rendering produced duplicated text in notifications and
logs. Comment explains why for the next reader.
server.rs:
- _lua_ctx changed from `Option<LuaContext>` to
`Arc<Mutex<Option<LuaContext>>>` so the watcher thread can atomically
replace it without taking the daemon down.
- New `_lua_watcher: Option<ConfigWatcher>` field, populated only when
loaded.lua_path is Some. Failure to spawn the watcher logs at warn
but doesn't fail Server::bind — the daemon stays usable even if
inotify is exhausted or perms are wrong.
- Mutex import feature-gated so --no-default-features stays
warning-free.
Tests: 5 new in lua::watcher::tests
- reload_swaps_in_new_config_state: change max_results in the file,
call reload() directly, verify Config got the new value.
- reload_failure_preserves_previous_state: write broken Lua, verify
Config is untouched.
- reload_replaces_user_providers_with_new_definitions: replace user
provider v1 with v2, verify a search for the v1 item returns nothing
and v2 is found (the websearch dynamic provider initially false-
positived on substring "v1 unique" via "Search: v1 unique" — locked
the test to the exact name "v1 unique item").
- event_touches_matches_only_watched_path: paths outside the watched
file are ignored.
- error_chain_renders_nested_causes: confirms chain walking works.
Live smoke (release build, isolated XDG, file edits via shell):
- v1 → save v2 → daemon log "hot-reload: applied"; :test prefix now
returns 2 v2 items.
- v2 → save broken Lua → daemon log "hot-reload: re-eval of /.../
owlry.lua failed, keeping previous config — Lua evaluation error in
/.../owlry.lua: syntax error: [string]:2: '}' expected (to close
'{' at line 1) near <eof>". Notification body shows the same
detail (file path + precise error). :test prefix still returns v2
items.
- Invalid provider id ("BadId With Spaces"): "runtime error: owlry.
provider: id 'BadId With Spaces' invalid — must be lowercase
alphanumeric with `-`/`_`". Old state preserved.
322/322 lib tests with --features full. Clippy silent across full,
--features lua, and --no-default-features.
Implements the six host helpers per docs/lua-api.md §5.1:
owlry.util.shell(cmd) -> string (stdout, trim_end)
owlry.util.shell_lines(cmd) -> table (split, no trailing empty)
owlry.util.read_file(path) -> string|nil (nil on any I/O error)
owlry.util.glob(pattern) -> table (tilde-expanded; PatternError → Lua err)
owlry.util.env(name, def?) -> string|nil
owlry.util.hostname() -> string
Cargo.toml:
- New optional dep `glob = "0.3"` gated by the `lua` feature alongside
mlua. Pure-Rust, no system Lua/C deps.
lua/util.rs:
- `build(lua) -> mlua::Result<Table>` constructs the owlry.util sub-table
with every helper registered. Stateless; no Arc<Mutex<LuaConfig>> needed.
- shell uses `sh -c`; stderr is logged at warn on non-zero exit but
stdout is still returned (don't crash user configs on missing commands).
- shell_lines drops the trailing empty element so a terminating newline
doesn't produce a phantom "" item.
- read_file extends "nil if missing" (per spec) to "nil on any I/O
failure" — saves users wrapping every read in pcall.
- glob expands a leading `~/` via `dirs::home_dir()`. `~user/`, embedded
`~`, and env-var refs are intentionally left as-is (predictable surface).
- hostname uses libc::gethostname directly (libc is already a hard dep).
lua/api.rs:
- install() now attaches `owlry.util = util::build(lua)?` to the parent
owlry table.
Tests: 17 new
- util.rs (15): shell trim, interior newlines preserved, shell_lines
split/empty cases, read_file present/missing, glob match/empty/error,
env present/missing/default, hostname non-empty, tilde expansion
positive/negative.
- runtime.rs (2): owlry.util surface exists with all 6 functions after
api::install; util helpers feed into a user provider's items()
end-to-end (hostname-as-launch-item, via LuaProvider::refresh).
Smoke (release build, isolated XDG, user provider :host_info using
hostname/shell/env): daemon loads 4 items per the user provider —
"host: cn-arch", "kernel: 7.0.5-zen1-1.1-zen", "home: /home/...",
"user: cnachtigall". 317/317 lib tests with --features full. Clippy
silent across all three feature configurations.
The smoke test in Phase 3.4 surfaced a long-standing gap: parse_query
already routes `:smoke foo` → Plugin("smoke") via the dynamic-prefix
fallback (filter.rs:319-347), but the UI never told the daemon. The
UI's filter.active_prefix was set locally yet build_modes_param only
serialised filter.enabled, so the daemon rebuilt a fresh filter with
no active_prefix and searched across every provider. Same gap also
affected hardcoded plugin prefixes (`:emoji heart` etc.) — they only
appeared to work because their items outranked others on fuzzy score.
ipc.rs:
- Request::Query gains optional `prefix: Option<String>`, default None,
skip_serializing_if = "Option::is_none". Wire-format omits the field
when None so existing 2.0.x clients keep deserialising on a 2.1
daemon and vice-versa.
filter.rs:
- New test parse_query_routes_unknown_prefix_to_plugin_type_id locks
in the existing dynamic-fallback behaviour for `:smoke item`.
- New test build_prefix_param_round_trips_through_provider_type
proves Display + FromStr agree on the wire format.
client.rs:
- `query(text, modes, prefix)` — third arg added; 2 existing tests
updated. Two new tests with a capturing mock_server assert the
prefix field is serialised when set and elided when None.
backend.rs:
- QueryParams grows a `prefix: Option<String>`. New helper
build_prefix_param(filter) -> Option<String> mirrors set_prefix's
active_prefix to the wire. All three client.query call sites updated
(search, search_with_tag, query_async).
server.rs:
- Query handler destructures the new prefix field and applies it via
filter.set_prefix(ProviderType::from_str(prefix)). Unknown ids fall
through to ProviderType::Plugin(_) per the existing FromStr impl —
symmetric with the UI's dynamic-fallback path.
Live smoke (release build, isolated XDG + socket):
- prefix=smoke + text="item" → 3 items, all smoke (was: 100, mixed)
- prefix=smoke + text="one" → 1 item: "smoke one item"
- prefix=smoke + text="" → 3 smoke items, frecency-sorted
- prefix=app + text="firefox" → 1 firefox app item (regression
check; hardcoded prefix path still works)
7 new tests (ipc:3, client:2, filter:2). 283/283 lib tests green with
--features full. Both clippy configurations silent. Closes task #28.
Resolution order per docs/lua-api.md §2:
1. $XDG_CONFIG_HOME/owlry/owlry.lua -- defaults + Lua overlay; TOML ignored
2. $XDG_CONFIG_HOME/owlry/config.toml -- back-compat
3. Built-in defaults
When owlry.lua is present alongside config.toml, an info log notes that
TOML is being ignored ("Lua takes precedence"). 2.2 will upgrade this to
a warning; 3.0 removes TOML entirely.
paths.rs:
- New lua_config_file() -> $XDG_CONFIG_HOME/owlry/owlry.lua.
config/mod.rs:
- New LoadedConfig { config, lua, lua_path, user_providers } gated by
feature = "lua". Keeps the LuaContext alive so user-provider items
closures remain callable. Drop the context and mlua::Function refs
inside user_providers go invalid — that was the lifetime bug 3.3
uncovered.
- LoadedConfig::load() does the full resolution; load_or_default()
falls back to LoadedConfig::defaults() on error.
- Config::load() now consults owlry.lua first when feature enabled,
then falls through to the existing TOML path. The Lua context is
dropped after merge (read-only callers don't need user providers).
- Extracted load_from_toml() and detect_and_apply_terminal() so all
three load paths share the same terminal-detection tail.
- Manual Debug impl on LoadedConfig that doesn't try to format the
LuaContext (mlua::Lua doesn't impl Debug).
providers/mod.rs:
- new_with_config now takes a user_providers: Vec<Box<dyn Provider>>
parameter (callers pre-build them). Appended after the built-in set;
info-logs the count when non-empty.
server.rs:
- Server::bind switches to LoadedConfig::load_or_default() under the
lua feature, builds LuaProvider boxes from the captured specs using
the context's lua_handle(), and stores the LuaContext in a new
_lua_ctx field to keep it alive for the daemon's lifetime. Hot
reload (Phase 3.7) will use this field.
- Non-lua build keeps the existing Config::load_or_default() path
with an empty user_providers vec.
Tests: 7 new in config::loaded_config_tests
- defaults_apply_when_no_files_exist
- lua_path_overrides_defaults_with_set_values
- lua_providers_list_disables_unlisted_built_ins
- lua_user_provider_is_captured
- lua_provider_remains_callable_after_load (end-to-end: load → snapshot
→ build LuaProvider → refresh → assert items, with the LuaContext
field outliving the helper scope)
- lua_syntax_error_is_surfaced
- toml_only_load_returns_defaults_when_path_missing
- toml_only_load_parses_existing_file
Live smoke test (release build, $XDG_CONFIG_HOME=tempdir, custom
owlry.lua with one user provider): daemon logs "Loaded Lua config",
"Registering 1 user-defined provider(s)", "Provider 'smoke' loaded 2
items". IPC Providers returns the smoke provider; auto-mode fuzzy
query against the user-defined items succeeds.
Known limitation surfaced during smoke (task #28): UI prefix routing
via ProviderFilter::parse_query uses a hardcoded prefix table, so
typing `:smoke foo` in the UI doesn't narrow to the user provider.
Auto-mode is unaffected. Separate task tracks the parse_query
generalisation; not in 3.4 scope.
276/276 lib tests green with --features full. 174/174 with
--no-default-features. Clippy silent in both configurations.
Pre-existing nits surfaced by recent clippy runs. No behavior change:
- application/command/systemd: sort_by(|a,b| a.name.to_lowercase().cmp(...))
→ sort_by_key(|i| i.name.to_lowercase()). The key form computes
to_lowercase() once per element instead of twice per comparison.
- providers/mod.rs (6 sites): descending score sort_by(|a,b| b.1.cmp(&a.1))
→ sort_by_key(|x| std::cmp::Reverse(x.1)).
- dmenu: add impl Default for DmenuProvider delegating to ::new().
cargo clippy now silent under --features full, --features lua, and
--no-default-features. 268/268 lib tests still green.
Implements the user-defined provider surface per docs/lua-api.md §4.4:
owlry.provider {
id = "hs",
prefix = ":hs",
items = function() return { { name = "Lock", command = "hyprlock" } } end,
}
Spec capture (lua/config.rs):
- New LuaProviderSpec carrying id / name / prefix / tab_label / icon /
search_noun / priority / dynamic + the mlua::Function `items` callback.
- LuaConfig grows a `user_providers: Vec<LuaProviderSpec>` accumulator.
- is_valid_provider_id() enforces the §4.4 grammar (lowercase a-z 0-9 - _).
Registration validation (lua/api.rs):
- `id` and `items` are required; missing field → clean Lua runtime error.
- Invalid id format rejected with a precise message.
- `dynamic = true` rejected in 2.1 (per spec: "lands in 2.2"). dynamic=false
explicit is accepted.
- Duplicate id: second wins, prior entry replaced, warning logged (§6.4).
- Multiple distinct ids accumulate in registration order.
Provider bridge (lua/provider.rs, new):
- LuaProvider impl Provider (Send + Sync, verified by static assertion).
- refresh() calls the Lua function with an empty query, parses each row,
drops items missing required name/command (warning per drop), caches.
- Errors from the user's items() are logged and result in an empty cache
for that refresh — daemon never crashes (§6.4).
- LaunchItem.source = ItemSource::ScriptPlugin (already reserved for
exactly this case in v2 demolition).
Lua-state lifetime fix (lua/runtime.rs):
- LuaContext now holds `Arc<Lua>`. mlua::Function references don't bump
the Lua refcount on their own, so providers built from a dropped context
would panic at call time. lua_handle() exposes the Arc so LuaProvider
can keep the state alive independently.
Tests: 18 new
- lua::config: 2 (id-validation positive/negative)
- lua::provider::tests: 9 (defaults, full spec, refresh happy/sad paths,
item field parsing, error handling, Send+Sync compile-time check)
- lua::provider::registration_tests: 8 (accumulation, duplicate replace,
missing-required errors, invalid-id, dynamic gate)
46/46 lua tests green. 267/267 lib tests green with --features full. No
new clippy warnings. ProviderManager wiring is Phase 3.4.
Implements the three core owlry.* config-collection functions per
docs/lua-api.md §4.1-§4.3:
owlry.set { theme = "owl", width = 900, ... }
owlry.providers { "app", "cmd", "calc", "conv", "power" }
owlry.tabs { "app", "cmd", "uuctl" }
Accumulator: Arc<Mutex<LuaConfig>> shared between the three registered
Lua closures and the host. LuaConfig is a strongly-typed Option-fielded
struct mirroring the spec table in §4.1.
- owlry.set merges across calls (last-write-wins per key); unknown keys
are recorded in unknown_settings for `owlry config validate` (3.9).
- owlry.providers replaces the list on each call; v1 aliases
(sys/system/uuctl) honoured at merge time.
- owlry.tabs replaces the tab order list verbatim.
LuaConfig::merge_into(&mut Config) overlays the captured state onto
the existing TOML-derived Config without touching unset fields. This
is the data layer; Phase 3.4 wires it into Config::load().
require("owlry") returns the same table as the global `owlry` (via a
tiny package.preload shim) so both styles in the spec quick-reference
work.
Tests: 28 new (config: 11, runtime: 17) covering happy path, multi-call
merge semantics, unknown-key tolerance, type-error propagation, alias
expansion, empty lists, file reads, and error path reporting. 249/249
lib tests green with --features full. No new clippy warnings.
Adds the lua cargo feature and a stubbed crates/owlry/src/lua/ module
in preparation for Phase 3 (Lua config layer per docs/lua-api.md).
- mlua 0.11 (lua54, vendored, send, serialize) as an optional dep so
AUR clean-chroot builds don't depend on system Lua.
- New `lua` feature gates the module; included in the `full` feature
set so the AUR build ships it. Disabled by default so minimal
`cargo install` consumers don't pay for the C compile.
- Stub submodules: runtime (LuaContext), api (owlry.* surface), error
(LuaConfigError), util (owlry.util.*). Every function is a no-op
placeholder for the sub-phases that will wire them.
- pub mod lua gated behind #[cfg(feature = "lua")] in lib.rs.
cargo check passes with --no-default-features, --features lua, and
--features full. 221/221 lib tests still green. Zero clippy warnings
from the new module.
Patch release covering:
- fix(providers): dynamic providers (calc, conv, websearch, filesearch)
now appear in owlry doctor and owlry providers <id> 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.
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).
ProviderManager::available_providers() and available_provider_types()
iterated only the static `Vec<Box<dyn Provider>>` field, never the
parallel `Vec<Box<dyn DynamicProvider>>`. Result: calc, conv,
websearch, filesearch ran fine but were silently absent from
`owlry doctor` and `owlry providers <id>` 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(<type_id>) — 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).
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.
makepkg in clean chroots defaults to OPTIONS+=(debug), which tries to
split debug symbols into an owlry-debug subpackage. Cargo's release
profile already strips the binary at compile time (strip = true in
workspace Cargo.toml), so the debug split finds nothing and emits a
benign-but-noisy 'No debugging symbols' warning followed by an empty
owlry-debug-2.0.0-1-x86_64.pkg.tar.zst.
Set options=('!debug') so makepkg skips debug splitting entirely. The
released package is unchanged from the user's perspective; we just stop
producing the empty subpackage and the namcap warning that came with it.
Leaves the unrelated namcap warnings about transitively-satisfied deps
(glib2, openssl, pango, glibc, libgcc) untouched — those are normal for
Rust binaries pulled through GTK4 and don't need explicit listing in
depends=. The 'checkpkg: target not found: owlry' warning will auto-
resolve once 2.0.0 is published to AUR.
The bookmarks provider opened Firefox's places.sqlite directly via
rusqlite, which pulled libsqlite3-sys with its bundled feature to
compile sqlite3.c statically. In clean Arch chroots the bundled feature
kept slipping out of the resolved feature graph and the chroot build
failed at link time with every sqlite3_* symbol undefined. Inline
features = ["bundled"] on the optional dep didn't help; neither did
the explicit "rusqlite/bundled" feature wiring (commit 7569b2d).
Rather than fight Cargo's resolver further, defer the bookmarks
provider alongside the widgets per the same pattern (D20 -> D22).
Returns in a later 2.x release with a pure-Rust reader:
- Chromium's Bookmarks file is already JSON.
- Firefox exposes JSON backups under ~/.mozilla/firefox/<profile>/
bookmarkbackups/<dated>.jsonlz4 — needs lz4 + JSON parse, no SQLite.
Removed:
- crates/owlry/src/providers/bookmarks.rs
- bookmarks module + registration in providers/mod.rs
- bookmarks feature + rusqlite dep in Cargo.toml
- ProvidersConfig.bookmarks field (existing user configs that still
have 'bookmarks = true' silently ignore it — serde default behavior)
- :bm prefix from README's search-prefix table
- bookmarks acknowledgement in README + ROADMAP roadmap section
Cargo.lock loses rusqlite, libsqlite3-sys, fallible-iterator,
fallible-streaming-iterator, foldhash, hashlink, rsqlite-vfs,
sqlite-wasm-rs (-80 lines).
PKGBUILD: owlry-plugin-bookmarks stays in replaces= (any user who has
it installed still needs a clean upgrade), but moved from the 'folded'
comment block to the 'deferred (D20+)' block.
Docs:
- README: bookmarks removed from optional-providers list, feature
table, search-prefix table, config example. Added to upcoming
roadmap section alongside widgets.
- ROADMAP: 'Bookmarks return' subsection added next to 'Widget
providers return'.
- CLAUDE.md: provider tree updated.
- docs/RESTRUCTURE-V2.md: new decision D22 with chroot-build
rationale; task #6 plugin count 8 -> 7 -> 6.
245 tests pass with --features full (was 252; the 7 bookmarks unit
tests went with the module). Build verified locally; chroot build
should now succeed since libsqlite3-sys is no longer in the graph
at all.
The chroot build of owlry-2.0.0 failed at link time with every sqlite3_*
symbol undefined — libsqlite3-sys's build.rs was taking the
'build_linked' branch (which links to system libsqlite3) instead of
'build_bundled' (which compiles sqlite3.c statically). The host build
worked only because /usr/lib/libsqlite3.so happens to be installed.
The original Cargo.toml had:
rusqlite = { version = "0.39", features = ["bundled"], optional = true }
In theory this activates rusqlite/bundled which pulls libsqlite3-sys/bundled.
In practice — apparently a Cargo resolver edge case with optional deps in
edition 2024 — the bundled feature did not flow into the resolved feature
graph in a clean chroot build.
Fixed by declaring rusqlite without inline features and wiring the bundled
flag explicitly in the bookmarks feature row:
[dependencies]
rusqlite = { version = "0.39", optional = true }
[features]
bookmarks = ["dep:rusqlite", "rusqlite/bundled"]
Verified via 'cargo tree --features full -i libsqlite3-sys -e features':
the libsqlite3-sys 'bundled' feature now traces back to owlry/full ->
owlry/bookmarks -> rusqlite/bundled.