319 Commits

Author SHA1 Message Date
vikingowl 5ae5bb8b87 chore(aur): update owlry to 2.1.0 2026-05-13 14:23:06 +02:00
vikingowl bee64dc082 chore(owlry): bump version to 2.1.0 owlry-v2.1.0 2026-05-13 14:23:01 +02:00
vikingowl 52833c1a33 Merge pull request 'owlry 2.1.0: Lua config layer (Phase 3)' (#8) from feat/lua-config into main 2026-05-13 14:21:52 +02:00
vikingowl 02c3281afe feat(doctor): surface config source + migrate hint + legacy artifacts
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.
2026-05-13 14:11:22 +02:00
vikingowl aeafce157b style(migrate): fix '1 file(s)' stutter in scripts/ artifact report
Trivial: pluralise 'file' based on entries.len() rather than the lazy '(s)' suffix. The current output says '1 file(s)' which reads awkwardly.
2026-05-13 14:04:58 +02:00
vikingowl 3e374e70f4 fix(migrate): emit valid Lua in the scripts/ legacy artifact snippet
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.
2026-05-13 14:03:41 +02:00
vikingowl 2ba06a7d2b feat(migrate): detect pre-v2 plugins + scripts/ and surface to user
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.
2026-05-13 14:01:38 +02:00
vikingowl 30713f065f fix(tests): add missing prefix field to Request::Query call sites
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.
2026-05-13 13:33:25 +02:00
vikingowl ad4e538a7a fix(aur): export RUSTFLAGS in PKGBUILD to actually pin ld.bfd
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.
2026-05-13 13:24:51 +02:00
vikingowl 8383746a72 fix(cargo): pin ld.bfd to dodge LLD's arg-order strict mode (chroot fix)
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.
2026-05-13 13:19:46 +02:00
vikingowl b3764b308e fix(cargo): drop optional on mlua/glob/notify to dodge chroot resolver bug
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.
2026-05-13 13:04:19 +02:00
vikingowl a4d4f30d1e fix(cargo): hoist mlua features to lua cargo feature row
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".
2026-05-13 12:46:40 +02:00
vikingowl 35f6501de2 docs(phase 3.10): owlry.example.lua + refresh user-facing docs
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
2026-05-13 12:39:48 +02:00
vikingowl aa1a38bbcf feat(lua): owlry config validate covers the Lua surface (Phase 3.9)
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.
2026-05-13 05:12:52 +02:00
vikingowl c200b1447c feat(lua): migrate-config writes owlry.lua from config.toml (Phase 3.8)
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.
2026-05-13 05:07:29 +02:00
vikingowl eba590bad8 feat(lua): owlry.lua hot reload with desktop-notification errors (Phase 3.7)
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.
2026-05-13 04:59:30 +02:00
vikingowl 82133edade feat(lua): owlry.util.* host helpers (Phase 3.6)
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.
2026-05-13 04:47:59 +02:00
vikingowl 32c6972a9e feat(lua): owlry.theme(...) + owlry.profiles {} (Phase 3.5)
Implements the theme selection and named-profile surface per
docs/lua-api.md §4.5 and §6.5:

  owlry.theme("catppuccin-mocha")          -- string form: theme name
  owlry.theme { background = "#1e1e2e",    -- table form: colour overrides
                accent = "#cba6f7",
                badge_app = "#a6e3a1" }
  owlry.profiles { dev = { "app", "cmd", "ssh" }, media = {...} }

LuaConfig (lua/config.rs):
- theme_name: Option<String>  (set by string form, last-write-wins)
- theme_colors: ThemeColors   (table form, per-key last-write-wins)
- unknown_theme_keys: Vec<String> (forward-compat for 2.2+ palette)
- profiles: Option<HashMap<String, Vec<String>>>  (replace-on-call)
- New KNOWN_THEME_KEYS const lists every recognised colour name.
- merge_into() applies theme name + colour overlay (Some-only fields)
  and replaces base profiles when Lua sets them. Omitting `owlry.profiles`
  leaves existing TOML/default profiles intact.

api.rs:
- owlry.theme dispatches on mlua::Value: Value::String → set name,
  Value::Table → read colour keys. Other types raise a clean Lua
  runtime error.
- Pre-v2 `badge_sys` alias normalises to `badge_power` (mirrors the
  serde alias on ThemeColors).
- owlry.profiles: every value must be a table of strings; non-table
  values produce a clear "list of provider ids" error message.

Tests: 19 new
- lua::config (6): theme name override, colours overlay only Some
  fields, name+colours compose, profiles replace base, empty profiles
  clears, omitted profiles preserves base.
- lua::runtime (13): string form, full colour table, name + table
  compose, per-key merge across calls, badge_sys alias, unknown
  forward-compat keys, wrong-arg-type error, multi-profile capture,
  profiles replace, non-list value error, numeric coercion docs.

Smoke test (release build, isolated XDG): `owlry config show` reports
`theme = "nord"`, the four colour overrides under [appearance.colors],
and all three named profiles. 300/300 lib tests with --features full,
no new clippy warnings.
2026-05-13 04:43:33 +02:00
vikingowl bfbce42eab fix(ipc): thread active_prefix through Query so daemon honours it (Phase 3.4.5)
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.
2026-05-13 04:36:59 +02:00
vikingowl 270f02dd69 feat(lua): wire owlry.lua into Config::load + daemon (Phase 3.4)
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.
2026-05-13 04:28:10 +02:00
vikingowl cfcd201c35 chore(providers): clear standing clippy warnings
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.
2026-05-13 04:19:11 +02:00
vikingowl 76524739e9 feat(lua): owlry.provider {} registers a static LuaProvider (Phase 3.3)
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.
2026-05-13 04:16:08 +02:00
vikingowl fabe60a7e0 feat(lua): set / providers / tabs read into LuaConfig (Phase 3.2)
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.
2026-05-13 04:09:27 +02:00
vikingowl 5b8bc8bac9 feat(lua): scaffold mlua-based config module (Phase 3.1)
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.
2026-05-13 04:03:29 +02:00
vikingowl d9cde0b3a4 chore(aur): update PKGBUILD 2026-05-13 03:55:16 +02:00
vikingowl 9921c5f280 Merge pull request 'owlry 2.0.1: doctor visibility + proactive migration hints' (#7) from fix/dynamic-providers-doctor into main
Reviewed-on: #7
owlry-v2.0.1
2026-05-13 03:54:34 +02:00
vikingowl 0242e48707 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 <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.
2026-05-13 03:52:23 +02:00
vikingowl 048c446b26 feat(aur/install-hook): proactive legacy-cruft detection
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).
2026-05-13 03:52:20 +02:00
vikingowl e88525fa19 fix(providers): surface dynamic providers in doctor + providers list
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).
2026-05-13 03:52:06 +02:00
vikingowl 1075eefbf3 docs(phase-3): full Lua API spec + D23/D24 decisions
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.
2026-05-13 03:41:11 +02:00
vikingowl 60780f2fdd chore(aur): finalize owlry 2.0.0 PKGBUILD (real b2sum) 2026-05-13 03:29:17 +02:00
vikingowl 645a4ce637 Merge pull request 'owlry 2.0: single-binary rewrite' (#6) from v2 into main
Reviewed-on: #6
owlry-v2.0.0
2026-05-13 03:28:06 +02:00
vikingowl 2cac6556f3 chore(owlry): bump version to 2.0.0 2026-05-13 03:24:08 +02:00
vikingowl 41e794f4d5 build(aur): disable debug subpackage (options=!debug)
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.
2026-05-13 03:19:14 +02:00
vikingowl a7af0e5d46 feat(v2): defer bookmarks provider (D22) — drop rusqlite dep
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.
2026-05-13 03:13:16 +02:00
vikingowl 7569b2d7f0 fix(bookmarks): force libsqlite3-sys bundled via explicit feature gate
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.
2026-05-13 03:06:39 +02:00
vikingowl fa3f04e3fc build(justfile): rewrite for single-repo, single-package v2 reality
The pre-v2 justfile drove a 5-crate workspace and 15 AUR packages, with
machinery to bump-all crates, iterate over aur/*/ subdirs, manage meta-
package versions, and run a release-crate pipeline parameterised by
crate name. None of that applies anymore — there is one crate and one
PKGBUILD.

Replaced with:
- build / release / release-minimal — release defaults to --features
  full so dev builds match the AUR binary's feature set.
- run / run-daemon / run-debug — run-debug uses dev-logging feature.
- test — runs both feature axes (no-default-features and --features full)
  so contributors can't silently break either build.
- check — cargo check on both axes + clippy on --features full.
- install-local — installs the man page too now.
- version / bump — single-crate operations, no 'crate' parameter.
- tag / push-tags — tags owlry-v<version> directly.
- aur-stage / aur-update / aur-publish / aur-status / aur-commit — all
  hardcoded to aur/owlry/, no pkg parameter.
- aur-update fetches the tagged tarball, recomputes b2sum, regenerates
  .SRCINFO. Errors with a clear message if the tag isn't pushed yet.
- aur-publish errors with a clone hint if aur/owlry/.git is missing.
- release-owlry — full pipeline: bump -> push -> tag -> push-tags ->
  aur-update -> aur-commit -> push -> aur-publish. Drop-in replacement
  for the old release-crate recipe.

Removed:
- build-ui / build-daemon / release-daemon — no separate daemon crate.
- show-versions / crate-version / bump-crate / bump-all — single crate.
- tag-crate — there's no per-crate concept anymore.
- aur-update-pkg / aur-update-all / aur-publish-pkg / aur-publish-all /
  aur-test-pkg-by-name / release-crate — collapsed since only aur/owlry
  exists.
- bump-meta — meta-bundles dropped in the v2 collapse.

aur-local-test now defaults its args to 'owlry' so 'just aur-local-test'
without arguments does the right thing.
2026-05-13 03:00:24 +02:00
vikingowl 1487a12c65 feat(man): ship owlry(1) man page
Hand-written groff man page documenting the v2 CLI surface, environment
variables, files, and examples. Sections:

  NAME / SYNOPSIS / DESCRIPTION
  OPTIONS                -d, -m, --profile, -p, -h, -V
  COMMANDS               daemon, dmenu, doctor, providers, config, migrate-config
  ENVIRONMENT            XDG_RUNTIME_DIR, OWLRY_SOCKET, XDG_CONFIG_HOME,
                         XDG_DATA_HOME, TERMINAL
  FILES                  config, themes, frecency, socket, docs, units
  EXAMPLES               UI launch, daemon background, dmenu pipeline,
                         doctor, side-by-side dev daemon via OWLRY_SOCKET
  SEE ALSO / BUGS / AUTHORS

aur/owlry/PKGBUILD: install -Dm644 data/owlry.1 -> /usr/share/man/man1/owlry.1.
makepkg auto-gzips to owlry.1.gz in the final package.

Verified with groff -Tutf8 -man: no warnings or errors. Tested locally
via makepkg; man page lands at /usr/share/man/man1/owlry.1.gz in the
2.7 MB .pkg.tar.zst.
2026-05-13 02:57:46 +02:00
vikingowl b22e1a52fb docs(v2): refresh README, ROADMAP, CLAUDE.md + expand replaces array
User-facing docs that lagged behind the v2 rewrite. Replaces the 1.x
package-mosaic narrative everywhere with the single-binary 2.0 reality.

README.md (rewrite):
- Highlights single binary + cargo-feature-gated providers
- Drops the 11-plugin Available Packages table (single package now)
- Drops Settings Editor section (config_editor was deleted)
- Drops Plugin Management CLI section (entire 'owlry plugin ...' tree
  is gone; commands.rs handles doctor / providers / config / migrate)
- New cargo features table (matches owlry/Cargo.toml [features])
- Build-from-source uses cargo build --release --features full
- All owlryd refs -> owlry -d / owlry.service
- Search prefixes table updated for sys->power rename
- Architecture diagram redrawn for single binary
- New 'Roadmap' subsection pointing at upcoming Lua config + widgets

ROADMAP.md (revise):
- Drops 'Plugin hot-reload' (compiled-in providers can't hot-reload)
- Drops 'Plugin settings UI' (config_editor was the v1 equivalent; gone)
- Drops 'Plugin marketplace' / Lua plugin install via CLI (the install
  surface itself was removed in v2; user extensions return via Phase 3
  Lua config, not via a registry)
- Drops 'Split monorepo' / 'Plugin API backwards compatibility' /
  'Per-plugin config' — all resolved by the v2 collapse
- Adds 'Lua-driven configuration' and 'Widget providers return' as the
  next-up bets
- Notes Phase 5 hygiene items (file splits, submenu protocol redesign,
  double-spawn) so they're not lost

CLAUDE.md (rewrite for v2):
- Project shape diagram = single crate, single binary
- Build commands use --features full
- OWLRY_SOCKET env var documented (smoke-test enabler)
- Naming-rules table covers the sys->power and owlryd->owlry transitions
  so future AI assistants don't reinvent the old vocabulary
- Drops references to owlry-core, owlry-plugin-api, owlry-lua, owlry-rune
- Drops plugin CLI documentation (subcommand tree is gone)

aur/owlry/PKGBUILD:
- replaces / conflicts / provides arrays grow from 18 -> 21 entries.
- Adds three plugin packages I'd missed in the first pass:
    owlry-plugin-calculator  (pre-v2 transitional stub, pkgrel -99)
    owlry-plugin-converter   (same)
    owlry-plugin-system      (same — distinct from owlry-plugin-systemd!)
- Arrays now organised by reason (folded plugins / deferred widgets /
  Lua-replaced / pre-v2 stubs / meta-bundles) with inline comments.
- .SRCINFO regenerated.

Local rebuild verified: makepkg produces owlry-2.0.0-1-x86_64.pkg.tar.zst
with 21 conflict= entries in .PKGINFO.
2026-05-13 02:55:01 +02:00
vikingowl 38057b36e3 build(v2): Phase 2 local prep — PKGBUILD, units rename, .install hook
Stages everything needed for the AUR 2.0.0 republish, without pushing
or publishing. The next checkpoint is a local makepkg test (task #2.5);
push/publish actions wait for explicit go-ahead (task #2.6).

aur/owlry/PKGBUILD:
- pkgver 1.0.10 -> 2.0.0; pkgrel 1
- depends drops owlry-core (now folded into owlry)
- optdepends cleaned: just cliphist, wl-clipboard, fd, mlocate — the
  external tools providers shell out to. No more 11 plugin packages.
- build uses --features full (AUR ships everything compiled in; cargo
  install consumers still get the minimal default)
- check runs cargo test --features full
- package installs single binary + renamed systemd units + docs/themes
- replaces/conflicts/provides cover 18 dropped packages: owlry-core,
  owlry-lua, owlry-rune, 11 owlry-plugin-* (including the deferred
  widgets per D20), 4 owlry-meta-*

aur/owlry/owlry.install (new):
- post_install message: how to start the daemon
- post_upgrade from 1.x: announce the systemd unit rename and tell the
  user to disable old owlryd.service / enable new owlry.service. Includes
  a banner with the v2 breaking changes (widgets gone, plugins built in)
- post_remove note: config stays

systemd/:
- owlryd.service -> owlry.service (per D15)
- owlryd.socket -> owlry.socket

crates/owlry/src/client.rs:
- connect_or_start invokes 'systemctl --user start owlry.service'

justfile:
- install-local installs renamed units

aur/owlry-{core,lua,rune}/:
- Tracked files (PKGBUILD, .SRCINFO, .gitignore) removed from main repo
- .gitignore entries added so the leftover local checkouts (still on
  disk with their AUR-remote .git dirs) don't keep showing as untracked
- AUR remotes themselves unaffected; orphaning on aur.archlinux.org is
  a separate manual step

aur/owlry/.SRCINFO regenerated via makepkg --printsrcinfo.
2026-05-13 02:43:22 +02:00
vikingowl 1dd945d0b5 docs(v2): record live smoke results across all 13 provider entry points
Updates Phase 1 acceptance section with the per-provider live
verification done after the OWLRY_SOCKET env var landed. Replaces
the earlier 'unit-tests-imply-correctness' note for issue #5 with
the actual live-daemon evidence.
2026-05-13 02:35:28 +02:00
vikingowl 0376abddae feat(paths): OWLRY_SOCKET env var to override the IPC socket path
Adds a one-shot override before XDG_RUNTIME_DIR resolution so smoke
tests (and side-by-side daemon instances during dev) can run without
disturbing a production daemon listening on the default socket.

  - OWLRY_SOCKET=/tmp/foo.sock owlry -d
  - OWLRY_SOCKET=/tmp/foo.sock owlry providers

Test added (owlry_socket_env_overrides_xdg_runtime) verifying the
env var takes precedence and restoring the previous value so it
doesn't leak across other tests in the module.

Used to verify Phase 1 live behavior across all 13 provider entry
points — each returned expected results, including uuctl resolving
to real systemd unit names ('dbus' -> dbus-broker, at-spi-dbus-bus).
Issue #5 confirmed fixed end-to-end.
2026-05-13 02:35:13 +02:00
vikingowl a3e134e6b7 docs(v2): Phase 1 acceptance results + close-out
Phase 1 (repo collapse) of the v2 restructure is complete. All 11 tasks
landed; the acceptance checklist in section 9 passes end-to-end.

Captured in section 10:
- Per-task commit log (a4a903 -> c48efaa)
- Each acceptance check (build, test, clippy, fmt, file layout, runtime
  smoke) with the result
- Three deferred follow-ups that don't block 2.0.0 ship: dynamic-provider
  visibility in doctor, clippy style nits, refresh_widgets stub

Next: Phase 2 (AUR republish as 2.0.0). The single owlry PKGBUILD
declares replaces/conflicts/provides for the 14 dropped packages and
builds with --features full.
2026-05-13 02:30:39 +02:00
vikingowl c48efaa7a5 style(v2): apply cargo fmt across the workspace
Routine formatting pass after the feature-gate / config / CLI / power
work landed. Import ordering, line wrapping, and trailing-comma cleanup
only — no behavior changes. `cargo fmt --all --check` is now clean.

Part of Phase 1 task #11 (final build + smoke).
2026-05-13 02:30:10 +02:00
vikingowl e9f310d202 test(auto-mode): integration test guarding the no-flag default behavior
Per D7 and task #10 in the v2 plan. Five tests pinning down the
`ProviderFilter` invariants that drive the default 'owlry' launch:

- auto_mode_filter_accepts_every_provider_type: no CLI mode/profile
  yields accept_all=true; every provider type (including ones not in
  general.tabs) is searchable.
- auto_mode_filter_with_empty_tabs_still_accepts_everything: even an
  empty tabs list doesn't silently drop providers from query routing.
- auto_mode_changes_to_filtered_when_cli_mode_is_set: counterpoint —
  -m <provider> flips accept_all off and excludes others.
- auto_mode_prefix_overrides_accept_all_for_routing: :uuctl prefix
  inside the UI narrows even when accept_all is true; clearing the
  prefix restores reach.
- dash_m_auto_explicit_alias_is_equivalent_to_no_flag: 'auto' parses
  to Plugin('auto'); the value lives under user control and won't be
  silently remapped.

252 total tests with --features full. Task #10 complete.
2026-05-13 02:27:16 +02:00
vikingowl 27e2683917 feat(cli): subcommand structure with doctor/providers/config/dmenu/migrate-config
Subcommand surface per docs/RESTRUCTURE-V2.md section 2:

  owlry                          UI, auto mode (default)
  owlry -m <mode> | --profile    UI variants (unchanged)
  owlry -d                       Daemon (alias for 'daemon' subcommand)
  owlry daemon                   Daemon
  owlry dmenu [-p PROMPT]        dmenu mode (canonical entry; -m dmenu still works)
  owlry doctor                   Diagnostics: config + socket + provider list
  owlry providers [<id>]         List providers (or show details for one)
  owlry config validate          Parse config; report errors
  owlry config show              Print resolved effective config as TOML
  owlry migrate-config           Stub; lands with Phase 3 (Lua config)

New module crates/owlry/src/commands.rs holds the dispatchers. Each
returns ! (calls process::exit) so main.rs is a thin router.

doctor and providers connect to the daemon via the local socket and
report what's registered. When the daemon is unreachable, doctor
prints a hint; providers exits 1.

config validate / show use the existing Config loader — no behavioral
change to config parsing.

Tests added (clap-level): 9 new tests in cli::tests covering each
subcommand and the -d-vs-subcommand precedence. 247 total (up from 239)
with --features full. Task #9 complete.
2026-05-13 02:26:23 +02:00
vikingowl 1ba0a97e6d docs(example): update config.example.toml for sys->power rename
Follow-up to d1c3270 — the prior commit missed the example config
because the Edit calls landed before a Read.
2026-05-13 02:23:31 +02:00
vikingowl d1c327002b refactor(power): rename sys provider to power (D13)
Per D13 in the v2 plan: 'sys' collides mentally with 'systemd'. The
power & session provider becomes 'power' everywhere. Pre-v2 names
('sys', 'system', badge_sys) remain accepted as serde aliases so
existing user configs keep parsing.

Also fixes a pre-existing bug: filter.rs mapped :sys/:system/:power
prefixes to the type_id 'system', but the provider exposed itself as
Plugin('sys'). The two never matched, so prefix filtering on this
provider was a no-op. Now everything (filter table, ProviderType
FromStr, provider's own provider_type, config key) agrees on 'power'.

Files renamed: providers/system.rs -> providers/power.rs
Struct renamed: SystemProvider -> PowerProvider
type_id: 'sys' -> 'power'
Item IDs: 'sys:shutdown' -> 'power:shutdown'  (frecency for the 7
  power items resets after upgrade — acceptable for a v2 break)
Config key: providers.system -> providers.power (alias 'system', 'sys')
Theme color: colors.badge_sys -> colors.badge_power (alias 'badge_sys').
  theme.rs emits both --owlry-badge-power and --owlry-badge-sys so
  existing stylesheets keep rendering.
UI provider_meta: 'system' arm becomes 'power' | 'system'
ProviderType::FromStr: 'power', 'sys', 'system' all -> Plugin('power')
  (and 'uuctl', 'systemd' -> Plugin('uuctl') as parallel hygiene)

Tests added (TDD):
- provider_type_from_str_maps_power_aliases
- provider_type_from_str_maps_systemd_aliases
- providers_config_accepts_power_key
- providers_config_accepts_pre_v2_system_alias
- theme_colors_accepts_pre_v2_badge_sys_alias
- all_item_ids_use_power_prefix (in power.rs)

239 tests pass (up from 234) with --features full. Task #8 complete.
2026-05-13 02:23:13 +02:00
vikingowl cb2ea5973b feat(providers): convert remaining 6 plugins from C-ABI to native impls
Closes the v2 plugin conversion. Six providers ported from the
owlry-plugins sibling repo into the single owlry crate as feature-
gated modules. Each follows the same pattern established by systemd:
drop extern "C"/PluginItem/ProviderHandle/owlry_plugin! scaffolding,
implement Provider or DynamicProvider directly on a regular struct.

Static providers (Provider trait, populate via refresh):
- providers/bookmarks.rs — Firefox + Chromium bookmarks via rusqlite,
  favicon cache preserved. dep: rusqlite (bundled), feature: bookmarks
- providers/clipboard.rs — cliphist history. feature: clipboard
- providers/emoji.rs — bundled emoji list with keyword tags.
  feature: emoji
- providers/ssh.rs — ~/.ssh/config host extraction. feature: ssh

Dynamic providers (DynamicProvider trait, generate per query):
- providers/filesearch.rs — fd / mlocate shellout with extract_search_term
  for ':file' and '/' triggers. feature: filesearch
- providers/websearch.rs — URL builder with DuckDuckGo/Google/custom
  engines. TODO: plumb engine through constructor once Lua config lands
  (Phase 3). feature: websearch

Wiring:
- Cargo.toml: 7 per-provider features + 'full' meta-feature. rusqlite
  added as optional dep (only pulled in with feature 'bookmarks').
- config/mod.rs: ProvidersConfig gains 6 new bool fields (defaults true)
- providers/mod.rs: gated module declarations + new_with_config takes a
  config snapshot and registers each provider behind its feature flag

Verification across feature axes:
- --no-default-features: 178 tests pass (feature-gated modules excluded)
- default (systemd only): 186 tests pass
- --features full: 233 tests pass (+55 from the 6 new conversions)

Tasks #6 and #7 complete.
2026-05-13 02:17:42 +02:00
vikingowl eb8a65f1fd feat(systemd): convert systemd provider from C-ABI to native Provider impl
First of 7 plugin conversions (task #6). Establishes the conversion
pattern: drop extern "C" vtable + PluginItem + opaque ProviderHandle
in favor of a regular struct that impls the Provider trait. Submenu
support comes via the new submenu_actions() trait method instead of
the old '?SUBMENU:' string-encoded query convention.

- providers/systemd.rs: new module, type_id 'uuctl' (CLI back-compat)
- Provider impl with prefix(:uuctl), icon(system-run), tab_label(Units),
  search_noun(systemd units), and submenu_actions for service controls
- ProviderManager::new_with_config registers it when config and feature both enabled
- config.providers.systemd added (alias 'uuctl' for back-compat)
- cargo feature 'systemd' (in default and full feature sets)
- 8 unit tests: parse_systemctl_output, provider_type, submenu for
  active/inactive/empty data, terminal flag on status/journal,
  clean_display_name edge cases

Issue #5 fixed locally — 'owlry -m uuctl' will return systemd units
once the binary is rebuilt and installed. 186 tests pass.
2026-05-13 02:10:09 +02:00