Compare commits
24 Commits
owlry-lua-
...
plugin-api
| Author | SHA1 | Date | |
|---|---|---|---|
| ffe04f3c54 | |||
| 5c0e63f94c | |||
| 5441011d6b | |||
| 317572634f | |||
| 449dc010db | |||
| 7273cd3ba7 | |||
| f8388a4327 | |||
| fa671ebd77 | |||
| d63c7d170b | |||
| 5f14ed2b3b | |||
| 83f551dd7f | |||
| 9b1eada1ee | |||
| 677e6d7fa9 | |||
| f0741f4128 | |||
| 7da8f3c249 | |||
| 38dda8c44c | |||
| ab2d3cfe55 | |||
| e2939e266c | |||
| 651166a9f3 | |||
| a2eb7d1b0d | |||
| 8073d27df2 | |||
| 3349350bf6 | |||
| 3aaeafde8b | |||
| 7ce6de17aa |
28
CLAUDE.md
28
CLAUDE.md
@@ -166,6 +166,23 @@ just bump 0.5.1
|
||||
# Create and push release tag
|
||||
git push && just tag
|
||||
|
||||
# Tagging convention: every crate gets its own tag
|
||||
# Format: {crate-name}-v{version}
|
||||
# Examples:
|
||||
# owlry-v1.0.1
|
||||
# owlry-core-v1.1.0
|
||||
# owlry-lua-v1.1.0
|
||||
# owlry-rune-v1.1.0
|
||||
# plugin-api-v1.0.1
|
||||
#
|
||||
# The plugins repo uses the same convention:
|
||||
# owlry-plugin-bookmarks-v1.0.1
|
||||
# owlry-plugin-calculator-v1.0.1
|
||||
# etc.
|
||||
#
|
||||
# IMPORTANT: After bumping versions, tag EVERY changed crate individually.
|
||||
# The plugin-api tag is referenced by owlry-plugins Cargo.toml as a git dependency.
|
||||
|
||||
# AUR package management
|
||||
just aur-update # Update core UI PKGBUILD
|
||||
just aur-update-pkg NAME # Update specific package (owlry-core, owlry-lua, etc.)
|
||||
@@ -299,10 +316,18 @@ CoreClient ──── IPC ────→ ProviderManager ProviderFi
|
||||
|
||||
All other providers are native plugins in the separate `owlry-plugins` repo (`somegit.dev/Owlibou/owlry-plugins`).
|
||||
|
||||
**User plugins** (script-based, in `~/.config/owlry/plugins/`):
|
||||
- **Lua plugins**: Loaded by `owlry-lua` runtime from `/usr/lib/owlry/runtimes/liblua.so`
|
||||
- **Rune plugins**: Loaded by `owlry-rune` runtime from `/usr/lib/owlry/runtimes/librune.so`
|
||||
- User plugins are **hot-reloaded** automatically when files change (no daemon restart needed)
|
||||
- Custom prefixes (e.g., `:hs`) are resolved dynamically for user plugins
|
||||
|
||||
`ProviderManager` (in `owlry-core`) orchestrates providers and handles:
|
||||
- Fuzzy matching via `SkimMatcherV2`
|
||||
- Frecency score boosting
|
||||
- Native plugin loading from `/usr/lib/owlry/plugins/`
|
||||
- Script runtime loading from `/usr/lib/owlry/runtimes/` for user plugins
|
||||
- Filesystem watching for automatic user plugin hot-reload
|
||||
|
||||
**Submenu System**: Plugins can return items with `SUBMENU:plugin_id:data` commands. When selected, the plugin is queried with `?SUBMENU:data` to get action items (e.g., systemd service actions).
|
||||
|
||||
@@ -340,6 +365,7 @@ Plugins are compiled as `.so` (cdylib) and loaded by the daemon at startup.
|
||||
- Profile-based mode selection (`--profile dev`)
|
||||
- Provider toggling (Ctrl+1/2/3)
|
||||
- Prefix parsing (`:app`, `:cmd`, `:sys`, etc.)
|
||||
- Dynamic prefix fallback for user plugins (any `:word` prefix maps to `Plugin(word)`)
|
||||
|
||||
Query parsing extracts prefix and forwards clean query to providers.
|
||||
|
||||
@@ -395,6 +421,8 @@ Plugins live in a separate repository: `somegit.dev/Owlibou/owlry-plugins`
|
||||
- **Rc<RefCell<T>>** used throughout for GTK signal handlers needing mutable state
|
||||
- **Feature flag `dev-logging`**: Wraps debug!() calls in `#[cfg(feature = "dev-logging")]`
|
||||
- **Feature flag `lua`**: Enables built-in Lua runtime (off by default); enable to embed Lua in core binary
|
||||
- **Script runtimes**: External `.so` runtimes loaded from `/usr/lib/owlry/runtimes/` — Lua and Rune user plugins loaded from `~/.config/owlry/plugins/`
|
||||
- **Hot-reload**: Filesystem watcher (`notify` crate) monitors user plugins dir and reloads runtimes on file changes
|
||||
- **dmenu mode**: Runs locally without daemon. Use `-m dmenu` with piped stdin
|
||||
- **Frecency**: Time-decayed frequency scoring stored in `~/.local/share/owlry/frecency.json`
|
||||
- **ABI stability**: Plugin interface uses `abi_stable` crate for safe Rust dynamic linking
|
||||
|
||||
178
Cargo.lock
generated
178
Cargo.lock
generated
@@ -337,6 +337,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@@ -400,7 +406,7 @@ version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"cairo-sys-rs",
|
||||
"glib 0.21.5",
|
||||
"libc",
|
||||
@@ -699,7 +705,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -851,6 +857,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -909,6 +926,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@@ -1231,7 +1257,7 @@ version = "0.20.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
@@ -1252,7 +1278,7 @@ version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
@@ -1425,7 +1451,7 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1d422cce9367945916b7a5083eedf67b0a5380d326af1943a0b5cef9afb6e48"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"gdk4",
|
||||
"glib 0.21.5",
|
||||
"glib-sys 0.21.5",
|
||||
@@ -1777,6 +1803,35 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -1899,6 +1954,26 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1943,7 +2018,10 @@ version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2102,6 +2180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -2186,7 +2265,7 @@ version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2198,6 +2277,37 @@ version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-mini"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaa5a66d07ed97dce782be94dcf5ab4d1b457f4243f7566c7557f15cabc8c799"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
"notify-types",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.12.0"
|
||||
@@ -2212,6 +2322,15 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -2335,7 +2454,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
@@ -2352,7 +2471,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -2417,7 +2536,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -2437,7 +2556,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-core"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"ctrlc",
|
||||
@@ -2449,6 +2568,8 @@ dependencies = [
|
||||
"log",
|
||||
"meval",
|
||||
"mlua",
|
||||
"notify",
|
||||
"notify-debouncer-mini",
|
||||
"notify-rust",
|
||||
"owlry-plugin-api",
|
||||
"reqwest 0.13.2",
|
||||
@@ -2462,7 +2583,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-lua"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"chrono",
|
||||
@@ -2480,7 +2601,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-api"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"serde",
|
||||
@@ -2488,7 +2609,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-rune"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
@@ -2553,7 +2674,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
@@ -2619,6 +2740,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
@@ -2821,7 +2948,16 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3096,7 +3232,7 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -3227,7 +3363,7 @@ version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -3475,7 +3611,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
@@ -3803,7 +3939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -4161,7 +4297,7 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
@@ -4786,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-core"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
@@ -36,6 +36,10 @@ dirs = "5"
|
||||
# Error handling
|
||||
thiserror = "2"
|
||||
|
||||
# Filesystem watching (plugin hot-reload)
|
||||
notify = "7"
|
||||
notify-debouncer-mini = "0.5"
|
||||
|
||||
# Signal handling
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ impl ProviderFilter {
|
||||
cli_providers: Option<Vec<ProviderType>>,
|
||||
config_providers: &ProvidersConfig,
|
||||
) -> Self {
|
||||
let accept_all = cli_mode.is_none() && cli_providers.is_none();
|
||||
|
||||
let enabled = if let Some(mode) = cli_mode {
|
||||
// --mode overrides everything: single provider
|
||||
HashSet::from([mode])
|
||||
@@ -90,7 +92,7 @@ impl ProviderFilter {
|
||||
let filter = Self {
|
||||
enabled,
|
||||
active_prefix: None,
|
||||
accept_all: false,
|
||||
accept_all,
|
||||
};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
@@ -184,6 +186,11 @@ impl ProviderFilter {
|
||||
self.accept_all || self.enabled.contains(&provider)
|
||||
}
|
||||
|
||||
/// Whether this filter accepts all provider types
|
||||
pub fn is_accept_all(&self) -> bool {
|
||||
self.accept_all
|
||||
}
|
||||
|
||||
/// Get current active prefix if any
|
||||
#[allow(dead_code)]
|
||||
pub fn active_prefix(&self) -> Option<ProviderType> {
|
||||
@@ -353,6 +360,28 @@ impl ProviderFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic plugin prefix fallback: ":word " or ":word" where word is unknown
|
||||
// Maps to Plugin(word) so user plugins with custom prefixes work
|
||||
if let Some(rest) = trimmed.strip_prefix(':') {
|
||||
if let Some(space_idx) = rest.find(' ') {
|
||||
let prefix_word = &rest[..space_idx];
|
||||
if !prefix_word.is_empty() && prefix_word.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
|
||||
return ParsedQuery {
|
||||
prefix: Some(ProviderType::Plugin(prefix_word.to_string())),
|
||||
tag_filter: None,
|
||||
query: rest[space_idx + 1..].to_string(),
|
||||
};
|
||||
}
|
||||
} else if !rest.is_empty() && rest.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
|
||||
// Partial prefix (no space yet)
|
||||
return ParsedQuery {
|
||||
prefix: Some(ProviderType::Plugin(rest.to_string())),
|
||||
tag_filter: None,
|
||||
query: String::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let result = ParsedQuery {
|
||||
prefix: None,
|
||||
tag_filter: None,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
|
||||
use owlry_core::paths;
|
||||
use owlry_core::server::Server;
|
||||
@@ -25,11 +25,12 @@ fn main() {
|
||||
|
||||
// Graceful shutdown on SIGTERM/SIGINT
|
||||
let sock_cleanup = sock.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
if let Err(e) = ctrlc::set_handler(move || {
|
||||
let _ = std::fs::remove_file(&sock_cleanup);
|
||||
std::process::exit(0);
|
||||
})
|
||||
.ok();
|
||||
}) {
|
||||
warn!("Failed to set signal handler: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = server.run() {
|
||||
eprintln!("Server error: {e}");
|
||||
|
||||
@@ -26,6 +26,7 @@ pub mod manifest;
|
||||
pub mod native_loader;
|
||||
pub mod registry;
|
||||
pub mod runtime_loader;
|
||||
pub mod watcher;
|
||||
|
||||
// Lua-specific modules (require mlua)
|
||||
#[cfg(feature = "lua")]
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
//! Note: This module is infrastructure for the runtime architecture. Full integration
|
||||
//! is pending Phase 5 (AUR Packaging) when runtime packages will be available.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -56,7 +54,7 @@ pub struct RuntimeHandle(pub *mut ());
|
||||
#[repr(C)]
|
||||
pub struct ScriptRuntimeVTable {
|
||||
pub info: extern "C" fn() -> RuntimeInfo,
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> RuntimeHandle,
|
||||
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<ScriptProviderInfo>,
|
||||
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(
|
||||
@@ -83,12 +81,13 @@ pub struct LoadedRuntime {
|
||||
|
||||
impl LoadedRuntime {
|
||||
/// Load the Lua runtime from the system directory
|
||||
pub fn load_lua(plugins_dir: &Path) -> PluginResult<Self> {
|
||||
pub fn load_lua(plugins_dir: &Path, owlry_version: &str) -> PluginResult<Self> {
|
||||
Self::load_from_path(
|
||||
"Lua",
|
||||
&PathBuf::from(SYSTEM_RUNTIMES_DIR).join("liblua.so"),
|
||||
b"owlry_lua_runtime_vtable",
|
||||
plugins_dir,
|
||||
owlry_version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,6 +97,7 @@ impl LoadedRuntime {
|
||||
library_path: &Path,
|
||||
vtable_symbol: &[u8],
|
||||
plugins_dir: &Path,
|
||||
owlry_version: &str,
|
||||
) -> PluginResult<Self> {
|
||||
if !library_path.exists() {
|
||||
return Err(PluginError::NotFound(library_path.display().to_string()));
|
||||
@@ -124,7 +124,7 @@ impl LoadedRuntime {
|
||||
|
||||
// Initialize the runtime
|
||||
let plugins_dir_str = plugins_dir.to_string_lossy();
|
||||
let handle = (vtable.init)(RStr::from_str(&plugins_dir_str));
|
||||
let handle = (vtable.init)(RStr::from_str(&plugins_dir_str), RStr::from_str(owlry_version));
|
||||
|
||||
// Get provider information
|
||||
let providers_rvec = (vtable.providers)(handle);
|
||||
@@ -169,6 +169,14 @@ impl Drop for LoadedRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
// LoadedRuntime needs to be Send + Sync because ProviderManager is shared across
|
||||
// threads via Arc<RwLock<ProviderManager>>.
|
||||
// Safety: RuntimeHandle is an opaque FFI handle accessed only through extern "C"
|
||||
// vtable functions. The same safety argument that applies to RuntimeProvider applies
|
||||
// here — all access is mediated by the vtable, and the runtime itself serializes access.
|
||||
unsafe impl Send for LoadedRuntime {}
|
||||
unsafe impl Sync for LoadedRuntime {}
|
||||
|
||||
/// A provider backed by a dynamically loaded runtime
|
||||
pub struct RuntimeProvider {
|
||||
/// Runtime name (for logging)
|
||||
@@ -243,8 +251,12 @@ impl Provider for RuntimeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// RuntimeProvider needs to be Send for the Provider trait
|
||||
// RuntimeProvider needs to be Send + Sync for the Provider trait.
|
||||
// Safety: RuntimeHandle is an opaque FFI handle accessed only through
|
||||
// extern "C" vtable functions. The same safety argument that justifies
|
||||
// Send applies to Sync — all access is mediated by the vtable.
|
||||
unsafe impl Send for RuntimeProvider {}
|
||||
unsafe impl Sync for RuntimeProvider {}
|
||||
|
||||
/// Check if the Lua runtime is available
|
||||
pub fn lua_runtime_available() -> bool {
|
||||
@@ -262,12 +274,13 @@ pub fn rune_runtime_available() -> bool {
|
||||
|
||||
impl LoadedRuntime {
|
||||
/// Load the Rune runtime from the system directory
|
||||
pub fn load_rune(plugins_dir: &Path) -> PluginResult<Self> {
|
||||
pub fn load_rune(plugins_dir: &Path, owlry_version: &str) -> PluginResult<Self> {
|
||||
Self::load_from_path(
|
||||
"Rune",
|
||||
&PathBuf::from(SYSTEM_RUNTIMES_DIR).join("librune.so"),
|
||||
b"owlry_rune_runtime_vtable",
|
||||
plugins_dir,
|
||||
owlry_version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
104
crates/owlry-core/src/plugins/watcher.rs
Normal file
104
crates/owlry-core/src/plugins/watcher.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
//! Filesystem watcher for user plugin hot-reload
|
||||
//!
|
||||
//! Watches `~/.config/owlry/plugins/` for changes and triggers
|
||||
//! runtime reload when plugin files are modified.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{info, warn};
|
||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||
|
||||
use crate::providers::ProviderManager;
|
||||
|
||||
/// Start watching the user plugins directory for changes.
|
||||
///
|
||||
/// Spawns a background thread that monitors the directory and triggers
|
||||
/// a full runtime reload on any file change. Returns immediately.
|
||||
///
|
||||
/// Respects `OWLRY_SKIP_RUNTIMES=1` — returns early if set.
|
||||
pub fn start_watching(pm: Arc<RwLock<ProviderManager>>) {
|
||||
if std::env::var("OWLRY_SKIP_RUNTIMES").is_ok() {
|
||||
info!("OWLRY_SKIP_RUNTIMES set, skipping file watcher");
|
||||
return;
|
||||
}
|
||||
|
||||
let plugins_dir = match crate::paths::plugins_dir() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
info!("No plugins directory configured, skipping file watcher");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !plugins_dir.exists()
|
||||
&& std::fs::create_dir_all(&plugins_dir).is_err()
|
||||
{
|
||||
warn!(
|
||||
"Failed to create plugins directory: {}",
|
||||
plugins_dir.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
info!(
|
||||
"Plugin file watcher started for {}",
|
||||
plugins_dir.display()
|
||||
);
|
||||
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = watch_loop(&plugins_dir, &pm) {
|
||||
warn!("Plugin watcher stopped: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn watch_loop(
|
||||
plugins_dir: &PathBuf,
|
||||
pm: &Arc<RwLock<ProviderManager>>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
let mut debouncer = new_debouncer(Duration::from_millis(500), tx)?;
|
||||
|
||||
debouncer
|
||||
.watcher()
|
||||
.watch(plugins_dir.as_ref(), notify::RecursiveMode::Recursive)?;
|
||||
|
||||
info!("Watching {} for plugin changes", plugins_dir.display());
|
||||
|
||||
// Skip events during initial startup grace period (watcher setup triggers events)
|
||||
let startup = std::time::Instant::now();
|
||||
let grace_period = Duration::from_secs(2);
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(Ok(events)) => {
|
||||
if startup.elapsed() < grace_period {
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_relevant_change = events.iter().any(|e| {
|
||||
matches!(
|
||||
e.kind,
|
||||
DebouncedEventKind::Any | DebouncedEventKind::AnyContinuous
|
||||
)
|
||||
});
|
||||
|
||||
if has_relevant_change {
|
||||
info!("Plugin file change detected, reloading runtimes...");
|
||||
let mut pm_guard = pm.write().unwrap_or_else(|e| e.into_inner());
|
||||
pm_guard.reload_runtimes();
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
warn!("File watcher error: {}", error);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +89,12 @@ impl Provider for LuaProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// LuaProvider needs to be Send for the Provider trait
|
||||
// Since we're using Rc<RefCell<>>, we need to be careful about thread safety
|
||||
// For now, owlry is single-threaded, so this is safe
|
||||
// LuaProvider needs to be Send + Sync for the Provider trait.
|
||||
// Rc<RefCell<>> is !Send and !Sync, but the ProviderManager RwLock ensures
|
||||
// Rc<RefCell<>> is only accessed during refresh() (write lock = exclusive access).
|
||||
// Read-only operations (items(), search) only touch self.items (Vec<LaunchItem>).
|
||||
unsafe impl Send for LuaProvider {}
|
||||
unsafe impl Sync for LuaProvider {}
|
||||
|
||||
/// Create LuaProviders from all registered providers in a plugin
|
||||
pub fn create_providers_from_plugin(plugin: Rc<RefCell<LoadedPlugin>>) -> Vec<Box<dyn Provider>> {
|
||||
|
||||
@@ -25,6 +25,7 @@ use log::debug;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::data::FrecencyStore;
|
||||
use crate::plugins::runtime_loader::LoadedRuntime;
|
||||
|
||||
/// Metadata descriptor for an available provider (used by IPC/daemon API)
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -94,7 +95,7 @@ impl std::fmt::Display for ProviderType {
|
||||
}
|
||||
|
||||
/// Trait for all search providers
|
||||
pub trait Provider: Send {
|
||||
pub trait Provider: Send + Sync {
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str;
|
||||
fn provider_type(&self) -> ProviderType;
|
||||
@@ -116,6 +117,10 @@ pub struct ProviderManager {
|
||||
widget_providers: Vec<NativeProvider>,
|
||||
/// Fuzzy matcher for search
|
||||
matcher: SkimMatcherV2,
|
||||
/// Loaded script runtimes (Lua, Rune) — must stay alive to keep Library handles
|
||||
runtimes: Vec<LoadedRuntime>,
|
||||
/// Type IDs of providers from script runtimes (for hot-reload removal)
|
||||
runtime_type_ids: std::collections::HashSet<String>,
|
||||
}
|
||||
|
||||
impl ProviderManager {
|
||||
@@ -134,6 +139,8 @@ impl ProviderManager {
|
||||
dynamic_providers: Vec::new(),
|
||||
widget_providers: Vec::new(),
|
||||
matcher: SkimMatcherV2::default(),
|
||||
runtimes: Vec::new(),
|
||||
runtime_type_ids: std::collections::HashSet::new(),
|
||||
};
|
||||
|
||||
// Categorize native plugins based on their declared ProviderKind and ProviderPosition
|
||||
@@ -180,7 +187,7 @@ impl ProviderManager {
|
||||
use std::sync::Arc;
|
||||
|
||||
// Create core providers
|
||||
let core_providers: Vec<Box<dyn Provider>> = vec![
|
||||
let mut core_providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(ApplicationProvider::new()),
|
||||
Box::new(CommandProvider::new()),
|
||||
];
|
||||
@@ -220,7 +227,122 @@ impl ProviderManager {
|
||||
}
|
||||
};
|
||||
|
||||
Self::new(core_providers, native_providers)
|
||||
// Load script runtimes (Lua, Rune) for user plugins
|
||||
let mut runtime_providers: Vec<Box<dyn Provider>> = Vec::new();
|
||||
let mut runtimes: Vec<LoadedRuntime> = Vec::new();
|
||||
let mut runtime_type_ids = std::collections::HashSet::new();
|
||||
let owlry_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let skip_runtimes = std::env::var("OWLRY_SKIP_RUNTIMES").is_ok();
|
||||
if !skip_runtimes
|
||||
&& let Some(plugins_dir) = crate::paths::plugins_dir()
|
||||
{
|
||||
// Try Lua runtime
|
||||
match LoadedRuntime::load_lua(&plugins_dir, owlry_version) {
|
||||
Ok(rt) => {
|
||||
info!("Loaded Lua runtime with {} provider(s)", rt.providers().len());
|
||||
for provider in rt.create_providers() {
|
||||
let type_id = format!("{}", provider.provider_type());
|
||||
runtime_type_ids.insert(type_id);
|
||||
runtime_providers.push(provider);
|
||||
}
|
||||
runtimes.push(rt);
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Lua runtime not available: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Try Rune runtime
|
||||
match LoadedRuntime::load_rune(&plugins_dir, owlry_version) {
|
||||
Ok(rt) => {
|
||||
info!("Loaded Rune runtime with {} provider(s)", rt.providers().len());
|
||||
for provider in rt.create_providers() {
|
||||
let type_id = format!("{}", provider.provider_type());
|
||||
runtime_type_ids.insert(type_id);
|
||||
runtime_providers.push(provider);
|
||||
}
|
||||
runtimes.push(rt);
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Rune runtime not available: {}", e);
|
||||
}
|
||||
}
|
||||
} // skip_runtimes
|
||||
|
||||
// Merge runtime providers into core providers
|
||||
for provider in runtime_providers {
|
||||
info!("Registered runtime provider: {}", provider.name());
|
||||
core_providers.push(provider);
|
||||
}
|
||||
|
||||
let mut manager = Self::new(core_providers, native_providers);
|
||||
manager.runtimes = runtimes;
|
||||
manager.runtime_type_ids = runtime_type_ids;
|
||||
manager
|
||||
}
|
||||
|
||||
/// Reload all script runtime providers (called by filesystem watcher)
|
||||
pub fn reload_runtimes(&mut self) {
|
||||
use crate::plugins::runtime_loader::LoadedRuntime;
|
||||
|
||||
// Remove old runtime providers from the core providers list
|
||||
self.providers.retain(|p| {
|
||||
let type_str = format!("{}", p.provider_type());
|
||||
!self.runtime_type_ids.contains(&type_str)
|
||||
});
|
||||
|
||||
// Drop old runtimes (catch panics from runtime cleanup)
|
||||
let old_runtimes = std::mem::take(&mut self.runtimes);
|
||||
drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
drop(old_runtimes);
|
||||
})));
|
||||
self.runtime_type_ids.clear();
|
||||
|
||||
let owlry_version = env!("CARGO_PKG_VERSION");
|
||||
let plugins_dir = match crate::paths::plugins_dir() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Reload Lua runtime
|
||||
match LoadedRuntime::load_lua(&plugins_dir, owlry_version) {
|
||||
Ok(rt) => {
|
||||
info!("Reloaded Lua runtime with {} provider(s)", rt.providers().len());
|
||||
for provider in rt.create_providers() {
|
||||
let type_id = format!("{}", provider.provider_type());
|
||||
self.runtime_type_ids.insert(type_id);
|
||||
self.providers.push(provider);
|
||||
}
|
||||
self.runtimes.push(rt);
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Lua runtime not available on reload: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload Rune runtime
|
||||
match LoadedRuntime::load_rune(&plugins_dir, owlry_version) {
|
||||
Ok(rt) => {
|
||||
info!("Reloaded Rune runtime with {} provider(s)", rt.providers().len());
|
||||
for provider in rt.create_providers() {
|
||||
let type_id = format!("{}", provider.provider_type());
|
||||
self.runtime_type_ids.insert(type_id);
|
||||
self.providers.push(provider);
|
||||
}
|
||||
self.runtimes.push(rt);
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Rune runtime not available on reload: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the newly added providers
|
||||
for provider in &mut self.providers {
|
||||
provider.refresh();
|
||||
}
|
||||
|
||||
info!("Runtime reload complete");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::thread;
|
||||
|
||||
/// Maximum allowed size for a single IPC request line (1 MiB).
|
||||
const MAX_REQUEST_SIZE: usize = 1_048_576;
|
||||
|
||||
use log::{error, info, warn};
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -17,8 +22,8 @@ use crate::providers::{LaunchItem, ProviderManager};
|
||||
pub struct Server {
|
||||
listener: UnixListener,
|
||||
socket_path: PathBuf,
|
||||
provider_manager: Arc<Mutex<ProviderManager>>,
|
||||
frecency: Arc<Mutex<FrecencyStore>>,
|
||||
provider_manager: Arc<RwLock<ProviderManager>>,
|
||||
frecency: Arc<RwLock<FrecencyStore>>,
|
||||
config: Arc<Config>,
|
||||
}
|
||||
|
||||
@@ -34,6 +39,7 @@ impl Server {
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(socket_path)?;
|
||||
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))?;
|
||||
info!("IPC server listening on {:?}", socket_path);
|
||||
|
||||
let config = Config::load_or_default();
|
||||
@@ -43,14 +49,17 @@ impl Server {
|
||||
Ok(Self {
|
||||
listener,
|
||||
socket_path: socket_path.to_path_buf(),
|
||||
provider_manager: Arc::new(Mutex::new(provider_manager)),
|
||||
frecency: Arc::new(Mutex::new(frecency)),
|
||||
provider_manager: Arc::new(RwLock::new(provider_manager)),
|
||||
frecency: Arc::new(RwLock::new(frecency)),
|
||||
config: Arc::new(config),
|
||||
})
|
||||
}
|
||||
|
||||
/// Accept connections in a loop, spawning a thread per client.
|
||||
pub fn run(&self) -> io::Result<()> {
|
||||
// Start filesystem watcher for user plugin hot-reload
|
||||
crate::plugins::watcher::start_watching(Arc::clone(&self.provider_manager));
|
||||
|
||||
info!("Server entering accept loop");
|
||||
for stream in self.listener.incoming() {
|
||||
match stream {
|
||||
@@ -90,15 +99,33 @@ impl Server {
|
||||
/// dispatch each, and write the JSON response back.
|
||||
fn handle_client(
|
||||
stream: UnixStream,
|
||||
pm: Arc<Mutex<ProviderManager>>,
|
||||
frecency: Arc<Mutex<FrecencyStore>>,
|
||||
pm: Arc<RwLock<ProviderManager>>,
|
||||
frecency: Arc<RwLock<FrecencyStore>>,
|
||||
config: Arc<Config>,
|
||||
) -> io::Result<()> {
|
||||
let reader = BufReader::new(stream.try_clone()?);
|
||||
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut writer = stream;
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
let bytes_read = reader.read_line(&mut line)?;
|
||||
if bytes_read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if line.len() > MAX_REQUEST_SIZE {
|
||||
let resp = Response::Error {
|
||||
message: format!(
|
||||
"request too large ({} bytes, max {})",
|
||||
line.len(),
|
||||
MAX_REQUEST_SIZE
|
||||
),
|
||||
};
|
||||
write_response(&mut writer, &resp)?;
|
||||
break;
|
||||
}
|
||||
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
@@ -107,6 +134,7 @@ impl Server {
|
||||
let request: Request = match serde_json::from_str(trimmed) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
warn!("Malformed request from client: {}", e);
|
||||
let resp = Response::Error {
|
||||
message: format!("invalid request JSON: {}", e),
|
||||
};
|
||||
@@ -126,8 +154,8 @@ impl Server {
|
||||
/// the response.
|
||||
fn handle_request(
|
||||
request: &Request,
|
||||
pm: &Arc<Mutex<ProviderManager>>,
|
||||
frecency: &Arc<Mutex<FrecencyStore>>,
|
||||
pm: &Arc<RwLock<ProviderManager>>,
|
||||
frecency: &Arc<RwLock<FrecencyStore>>,
|
||||
config: &Arc<Config>,
|
||||
) -> Response {
|
||||
match request {
|
||||
@@ -139,8 +167,8 @@ impl Server {
|
||||
let max = config.general.max_results;
|
||||
let weight = config.providers.frecency_weight;
|
||||
|
||||
let pm_guard = pm.lock().unwrap();
|
||||
let frecency_guard = frecency.lock().unwrap();
|
||||
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner());
|
||||
let frecency_guard = frecency.read().unwrap_or_else(|e| e.into_inner());
|
||||
let results = pm_guard.search_with_frecency(
|
||||
text,
|
||||
max,
|
||||
@@ -162,13 +190,13 @@ impl Server {
|
||||
item_id,
|
||||
provider: _,
|
||||
} => {
|
||||
let mut frecency_guard = frecency.lock().unwrap();
|
||||
let mut frecency_guard = frecency.write().unwrap_or_else(|e| e.into_inner());
|
||||
frecency_guard.record_launch(item_id);
|
||||
Response::Ack
|
||||
}
|
||||
|
||||
Request::Providers => {
|
||||
let pm_guard = pm.lock().unwrap();
|
||||
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner());
|
||||
let descs = pm_guard.available_providers();
|
||||
Response::Providers {
|
||||
list: descs.into_iter().map(descriptor_to_desc).collect(),
|
||||
@@ -176,7 +204,7 @@ impl Server {
|
||||
}
|
||||
|
||||
Request::Refresh { provider } => {
|
||||
let mut pm_guard = pm.lock().unwrap();
|
||||
let mut pm_guard = pm.write().unwrap_or_else(|e| e.into_inner());
|
||||
pm_guard.refresh_provider(provider);
|
||||
Response::Ack
|
||||
}
|
||||
@@ -187,7 +215,7 @@ impl Server {
|
||||
}
|
||||
|
||||
Request::Submenu { plugin_id, data } => {
|
||||
let pm_guard = pm.lock().unwrap();
|
||||
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner());
|
||||
match pm_guard.query_submenu_actions(plugin_id, data, plugin_id) {
|
||||
Some((_name, actions)) => Response::SubmenuItems {
|
||||
items: actions
|
||||
@@ -202,7 +230,7 @@ impl Server {
|
||||
}
|
||||
|
||||
Request::PluginAction { command } => {
|
||||
let pm_guard = pm.lock().unwrap();
|
||||
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner());
|
||||
if pm_guard.execute_plugin_action(command) {
|
||||
Response::Ack
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-lua"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn register_provider_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
}
|
||||
|
||||
/// Implementation of owlry.provider.register()
|
||||
fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> {
|
||||
fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> {
|
||||
let name: String = config.get("name")?;
|
||||
let display_name: String = config
|
||||
.get::<Option<String>>("display_name")?
|
||||
@@ -47,6 +47,21 @@ fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> {
|
||||
|
||||
let is_dynamic = has_query;
|
||||
|
||||
// Store the config table in owlry.provider._registrations[name]
|
||||
// so call_refresh/call_query can find the callback functions later
|
||||
let globals = lua.globals();
|
||||
let owlry: Table = globals.get("owlry")?;
|
||||
let provider: Table = owlry.get("provider")?;
|
||||
let registrations: Table = match provider.get::<Value>("_registrations")? {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
let t = lua.create_table()?;
|
||||
provider.set("_registrations", t.clone())?;
|
||||
t
|
||||
}
|
||||
};
|
||||
registrations.set(name.as_str(), config)?;
|
||||
|
||||
REGISTRATIONS.with(|regs| {
|
||||
regs.borrow_mut().push(ProviderRegistration {
|
||||
name,
|
||||
|
||||
@@ -27,28 +27,19 @@ mod manifest;
|
||||
mod runtime;
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{PluginItem, ProviderKind};
|
||||
use owlry_plugin_api::PluginItem;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use loader::LoadedPlugin;
|
||||
|
||||
// Runtime metadata
|
||||
const RUNTIME_ID: &str = "lua";
|
||||
const RUNTIME_NAME: &str = "Lua Runtime";
|
||||
const RUNTIME_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const RUNTIME_DESCRIPTION: &str = "Lua 5.4 runtime for user plugins";
|
||||
|
||||
/// API version for compatibility checking
|
||||
pub const LUA_RUNTIME_API_VERSION: u32 = 1;
|
||||
|
||||
/// Runtime vtable - exported interface for the core to use
|
||||
#[repr(C)]
|
||||
pub struct LuaRuntimeVTable {
|
||||
/// Get runtime info
|
||||
pub info: extern "C" fn() -> RuntimeInfo,
|
||||
/// Initialize the runtime with plugins directory
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> RuntimeHandle,
|
||||
/// Get provider infos from all loaded plugins
|
||||
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<LuaProviderInfo>,
|
||||
/// Refresh a provider's items
|
||||
@@ -66,11 +57,8 @@ pub struct LuaRuntimeVTable {
|
||||
/// Runtime info returned by the runtime
|
||||
#[repr(C)]
|
||||
pub struct RuntimeInfo {
|
||||
pub id: RString,
|
||||
pub name: RString,
|
||||
pub version: RString,
|
||||
pub description: RString,
|
||||
pub api_version: u32,
|
||||
}
|
||||
|
||||
/// Opaque handle to the runtime state
|
||||
@@ -106,24 +94,22 @@ impl RuntimeHandle {
|
||||
}
|
||||
|
||||
/// Provider info from a Lua plugin
|
||||
///
|
||||
/// Must match ScriptProviderInfo layout in owlry-core/src/plugins/runtime_loader.rs
|
||||
#[repr(C)]
|
||||
pub struct LuaProviderInfo {
|
||||
/// Full provider ID: "plugin_id:provider_name"
|
||||
pub id: RString,
|
||||
/// Plugin ID this provider belongs to
|
||||
pub plugin_id: RString,
|
||||
/// Provider name within the plugin
|
||||
pub provider_name: RString,
|
||||
/// Provider name (used as vtable refresh/query key: "plugin_id:provider_name")
|
||||
pub name: RString,
|
||||
/// Display name
|
||||
pub display_name: RString,
|
||||
/// Optional prefix trigger
|
||||
pub prefix: ROption<RString>,
|
||||
/// Icon name
|
||||
pub icon: RString,
|
||||
/// Provider type (static/dynamic)
|
||||
pub provider_type: ProviderKind,
|
||||
/// Type ID for filtering
|
||||
pub type_id: RString,
|
||||
/// Icon name
|
||||
pub default_icon: RString,
|
||||
/// Whether this is a static provider (true) or dynamic (false)
|
||||
pub is_static: bool,
|
||||
/// Optional prefix trigger
|
||||
pub prefix: ROption<RString>,
|
||||
}
|
||||
|
||||
/// Internal runtime state
|
||||
@@ -187,21 +173,14 @@ impl LuaRuntimeState {
|
||||
if let Ok(registrations) = plugin.get_provider_registrations() {
|
||||
for reg in registrations {
|
||||
let full_id = format!("{}:{}", plugin_id, reg.name);
|
||||
let provider_type = if reg.is_dynamic {
|
||||
ProviderKind::Dynamic
|
||||
} else {
|
||||
ProviderKind::Static
|
||||
};
|
||||
|
||||
providers.push(LuaProviderInfo {
|
||||
id: RString::from(full_id),
|
||||
plugin_id: RString::from(plugin_id.as_str()),
|
||||
provider_name: RString::from(reg.name.as_str()),
|
||||
name: RString::from(full_id),
|
||||
display_name: RString::from(reg.display_name.as_str()),
|
||||
prefix: reg.prefix.map(RString::from).into(),
|
||||
icon: RString::from(reg.default_icon.as_str()),
|
||||
provider_type,
|
||||
type_id: RString::from(reg.type_id.as_str()),
|
||||
default_icon: RString::from(reg.default_icon.as_str()),
|
||||
is_static: !reg.is_dynamic,
|
||||
prefix: reg.prefix.map(RString::from).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -259,22 +238,15 @@ impl LuaRuntimeState {
|
||||
|
||||
extern "C" fn runtime_info() -> RuntimeInfo {
|
||||
RuntimeInfo {
|
||||
id: RString::from(RUNTIME_ID),
|
||||
name: RString::from(RUNTIME_NAME),
|
||||
version: RString::from(RUNTIME_VERSION),
|
||||
description: RString::from(RUNTIME_DESCRIPTION),
|
||||
api_version: LUA_RUNTIME_API_VERSION,
|
||||
name: RString::from("Lua"),
|
||||
version: RString::from(env!("CARGO_PKG_VERSION")),
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn runtime_init(plugins_dir: RStr<'_>) -> RuntimeHandle {
|
||||
extern "C" fn runtime_init(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> RuntimeHandle {
|
||||
let plugins_dir = PathBuf::from(plugins_dir.as_str());
|
||||
let mut state = Box::new(LuaRuntimeState::new(plugins_dir));
|
||||
|
||||
// TODO: Get owlry version from core somehow
|
||||
// For now, use a reasonable default
|
||||
state.discover_and_load("0.3.0");
|
||||
|
||||
state.discover_and_load(owlry_version.as_str());
|
||||
RuntimeHandle::from_box(state)
|
||||
}
|
||||
|
||||
@@ -346,8 +318,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_runtime_info() {
|
||||
let info = runtime_info();
|
||||
assert_eq!(info.id.as_str(), "lua");
|
||||
assert_eq!(info.api_version, LUA_RUNTIME_API_VERSION);
|
||||
assert_eq!(info.name.as_str(), "Lua");
|
||||
assert!(!info.version.as_str().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -200,7 +200,7 @@ version = "1.0.0"
|
||||
id, id
|
||||
);
|
||||
fs::write(plugin_dir.join("plugin.toml"), manifest).unwrap();
|
||||
fs::write(plugin_dir.join("init.lua"), "-- empty plugin").unwrap();
|
||||
fs::write(plugin_dir.join("main.lua"), "-- empty plugin").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -41,7 +41,7 @@ pub struct PluginInfo {
|
||||
#[serde(default = "default_owlry_version")]
|
||||
pub owlry_version: String,
|
||||
/// Entry point file (relative to plugin directory)
|
||||
#[serde(default = "default_entry")]
|
||||
#[serde(default = "default_entry", alias = "entry_point")]
|
||||
pub entry: String,
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fn default_owlry_version() -> String {
|
||||
}
|
||||
|
||||
fn default_entry() -> String {
|
||||
"init.lua".to_string()
|
||||
"main.lua".to_string()
|
||||
}
|
||||
|
||||
/// What the plugin provides
|
||||
@@ -160,7 +160,7 @@ version = "1.0.0"
|
||||
assert_eq!(manifest.plugin.id, "test-plugin");
|
||||
assert_eq!(manifest.plugin.name, "Test Plugin");
|
||||
assert_eq!(manifest.plugin.version, "1.0.0");
|
||||
assert_eq!(manifest.plugin.entry, "init.lua");
|
||||
assert_eq!(manifest.plugin.entry, "main.lua");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-api"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -297,26 +297,24 @@ pub struct HostAPI {
|
||||
pub log_error: extern "C" fn(message: RStr<'_>),
|
||||
}
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// Global host API pointer - set by the host when loading plugins
|
||||
static mut HOST_API: Option<&'static HostAPI> = None;
|
||||
static HOST_API: OnceLock<&'static HostAPI> = OnceLock::new();
|
||||
|
||||
/// Initialize the host API (called by the host)
|
||||
///
|
||||
/// # Safety
|
||||
/// Must only be called once by the host before any plugins use the API
|
||||
pub unsafe fn init_host_api(api: &'static HostAPI) {
|
||||
// SAFETY: Caller guarantees this is called once before any plugins use the API
|
||||
unsafe {
|
||||
HOST_API = Some(api);
|
||||
}
|
||||
let _ = HOST_API.set(api);
|
||||
}
|
||||
|
||||
/// Get the host API
|
||||
///
|
||||
/// Returns None if the host hasn't initialized the API yet
|
||||
pub fn host_api() -> Option<&'static HostAPI> {
|
||||
// SAFETY: We only read the pointer, and it's set once at startup
|
||||
unsafe { HOST_API }
|
||||
HOST_API.get().copied()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-rune"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "Rune scripting runtime for owlry plugins"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! This module provides the `owlry` module that Rune plugins can use.
|
||||
|
||||
use rune::{ContextError, Module};
|
||||
use rune::{Any, ContextError, Module};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use owlry_plugin_api::{PluginItem, RString};
|
||||
@@ -20,9 +20,9 @@ pub struct ProviderRegistration {
|
||||
|
||||
/// An item returned by a provider
|
||||
///
|
||||
/// Used for converting Rune plugin items to FFI format.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
/// Exposed to Rune scripts as `owlry::Item`.
|
||||
#[derive(Debug, Clone, Any)]
|
||||
#[rune(item = ::owlry)]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
@@ -34,8 +34,42 @@ pub struct Item {
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Constructor exposed to Rune via #[rune::function]
|
||||
#[rune::function(path = Self::new)]
|
||||
pub fn rune_new(id: String, name: String, command: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
command,
|
||||
description: None,
|
||||
icon: None,
|
||||
terminal: false,
|
||||
keywords: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set description (builder pattern for Rune)
|
||||
#[rune::function]
|
||||
fn description(mut self, desc: String) -> Self {
|
||||
self.description = Some(desc);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set icon (builder pattern for Rune)
|
||||
#[rune::function]
|
||||
fn icon(mut self, icon: String) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set keywords (builder pattern for Rune)
|
||||
#[rune::function]
|
||||
fn keywords(mut self, keywords: Vec<String>) -> Self {
|
||||
self.keywords = keywords;
|
||||
self
|
||||
}
|
||||
|
||||
/// Convert to PluginItem for FFI
|
||||
#[allow(dead_code)]
|
||||
pub fn to_plugin_item(&self) -> PluginItem {
|
||||
let mut item = PluginItem::new(
|
||||
RString::from(self.id.as_str()),
|
||||
@@ -62,7 +96,14 @@ pub static REGISTRATIONS: Mutex<Vec<ProviderRegistration>> = Mutex::new(Vec::new
|
||||
pub fn module() -> Result<Module, ContextError> {
|
||||
let mut module = Module::with_crate("owlry")?;
|
||||
|
||||
// Register logging functions using builder pattern
|
||||
// Register Item type with constructor and builder methods
|
||||
module.ty::<Item>()?;
|
||||
module.function_meta(Item::rune_new)?;
|
||||
module.function_meta(Item::description)?;
|
||||
module.function_meta(Item::icon)?;
|
||||
module.function_meta(Item::keywords)?;
|
||||
|
||||
// Register logging functions
|
||||
module.function("log_info", log_info).build()?;
|
||||
module.function("log_debug", log_debug).build()?;
|
||||
module.function("log_warn", log_warn).build()?;
|
||||
|
||||
@@ -72,7 +72,7 @@ struct RuntimeState {
|
||||
#[repr(C)]
|
||||
pub struct RuneRuntimeVTable {
|
||||
pub info: extern "C" fn() -> RuntimeInfo,
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> RuntimeHandle,
|
||||
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<RuneProviderInfo>,
|
||||
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(
|
||||
@@ -94,8 +94,9 @@ extern "C" fn runtime_info() -> RuntimeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn runtime_init(plugins_dir: RStr<'_>) -> RuntimeHandle {
|
||||
extern "C" fn runtime_init(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> RuntimeHandle {
|
||||
let _ = env_logger::try_init();
|
||||
let _version = owlry_version.as_str();
|
||||
|
||||
let plugins_dir = PathBuf::from(plugins_dir.as_str());
|
||||
log::info!(
|
||||
@@ -250,7 +251,7 @@ mod tests {
|
||||
let plugins_dir = temp.path().to_string_lossy();
|
||||
|
||||
// Initialize runtime
|
||||
let handle = runtime_init(RStr::from_str(&plugins_dir));
|
||||
let handle = runtime_init(RStr::from_str(&plugins_dir), RStr::from_str("1.0.0"));
|
||||
assert!(!handle.0.is_null());
|
||||
|
||||
// Get providers (should be empty with no plugins)
|
||||
|
||||
@@ -59,8 +59,20 @@ impl LoadedPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
// Collect registrations
|
||||
let registrations = api::get_registrations();
|
||||
// Collect registrations — from runtime API or from manifest [[providers]]
|
||||
let mut registrations = api::get_registrations();
|
||||
if registrations.is_empty() && !manifest.providers.is_empty() {
|
||||
for decl in &manifest.providers {
|
||||
registrations.push(ProviderRegistration {
|
||||
name: decl.id.clone(),
|
||||
display_name: decl.name.clone(),
|
||||
type_id: decl.type_id.clone().unwrap_or_else(|| decl.id.clone()),
|
||||
default_icon: decl.icon.clone().unwrap_or_else(|| "application-x-addon".to_string()),
|
||||
is_static: decl.provider_type != "dynamic",
|
||||
prefix: decl.prefix.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Loaded Rune plugin '{}' with {} provider(s)",
|
||||
@@ -92,16 +104,37 @@ impl LoadedPlugin {
|
||||
self.registrations.iter().any(|r| r.name == name)
|
||||
}
|
||||
|
||||
/// Refresh a static provider (stub for now)
|
||||
/// Refresh a static provider by calling the Rune `refresh()` function
|
||||
pub fn refresh_provider(&mut self, _name: &str) -> Result<Vec<PluginItem>, String> {
|
||||
// TODO: Implement provider refresh by calling Rune function
|
||||
Ok(Vec::new())
|
||||
let mut vm = create_vm(&self.context, self.unit.clone())
|
||||
.map_err(|e| format!("Failed to create VM: {}", e))?;
|
||||
|
||||
let output = vm
|
||||
.call(rune::Hash::type_hash(["refresh"]), ())
|
||||
.map_err(|e| format!("refresh() call failed: {}", e))?;
|
||||
|
||||
let items: Vec<crate::api::Item> = rune::from_value(output)
|
||||
.map_err(|e| format!("Failed to parse refresh() result: {}", e))?;
|
||||
|
||||
Ok(items.iter().map(|i| i.to_plugin_item()).collect())
|
||||
}
|
||||
|
||||
/// Query a dynamic provider (stub for now)
|
||||
pub fn query_provider(&mut self, _name: &str, _query: &str) -> Result<Vec<PluginItem>, String> {
|
||||
// TODO: Implement provider query by calling Rune function
|
||||
Ok(Vec::new())
|
||||
/// Query a dynamic provider by calling the Rune `query(q)` function
|
||||
pub fn query_provider(&mut self, _name: &str, query: &str) -> Result<Vec<PluginItem>, String> {
|
||||
let mut vm = create_vm(&self.context, self.unit.clone())
|
||||
.map_err(|e| format!("Failed to create VM: {}", e))?;
|
||||
|
||||
let output = vm
|
||||
.call(
|
||||
rune::Hash::type_hash(["query"]),
|
||||
(query.to_string(),),
|
||||
)
|
||||
.map_err(|e| format!("query() call failed: {}", e))?;
|
||||
|
||||
let items: Vec<crate::api::Item> = rune::from_value(output)
|
||||
.map_err(|e| format!("Failed to parse query() result: {}", e))?;
|
||||
|
||||
Ok(items.iter().map(|i| i.to_plugin_item()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,28 @@ pub struct PluginManifest {
|
||||
pub provides: PluginProvides,
|
||||
#[serde(default)]
|
||||
pub permissions: PluginPermissions,
|
||||
/// Provider declarations from [[providers]] sections
|
||||
#[serde(default)]
|
||||
pub providers: Vec<ProviderDecl>,
|
||||
}
|
||||
|
||||
/// A provider declared in [[providers]] section of plugin.toml
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ProviderDecl {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub prefix: Option<String>,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
#[serde(default = "default_provider_type", rename = "type")]
|
||||
pub provider_type: String,
|
||||
#[serde(default)]
|
||||
pub type_id: Option<String>,
|
||||
}
|
||||
|
||||
fn default_provider_type() -> String {
|
||||
"static".to_string()
|
||||
}
|
||||
|
||||
/// Core plugin information
|
||||
@@ -25,7 +47,7 @@ pub struct PluginInfo {
|
||||
pub author: String,
|
||||
#[serde(default = "default_owlry_version")]
|
||||
pub owlry_version: String,
|
||||
#[serde(default = "default_entry")]
|
||||
#[serde(default = "default_entry", alias = "entry_point")]
|
||||
pub entry: String,
|
||||
}
|
||||
|
||||
@@ -34,7 +56,7 @@ fn default_owlry_version() -> String {
|
||||
}
|
||||
|
||||
fn default_entry() -> String {
|
||||
"init.rn".to_string()
|
||||
"main.rn".to_string()
|
||||
}
|
||||
|
||||
/// What the plugin provides
|
||||
@@ -128,7 +150,7 @@ version = "1.0.0"
|
||||
"#;
|
||||
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(manifest.plugin.id, "test-plugin");
|
||||
assert_eq!(manifest.plugin.entry, "init.rn");
|
||||
assert_eq!(manifest.plugin.entry, "main.rn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||
|
||||
@@ -39,13 +39,18 @@ impl SearchBackend {
|
||||
) -> Vec<LaunchItem> {
|
||||
match self {
|
||||
SearchBackend::Daemon(client) => {
|
||||
let modes: Vec<String> = filter
|
||||
.enabled_providers()
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect();
|
||||
|
||||
let modes_param = if modes.is_empty() { None } else { Some(modes) };
|
||||
// When accept_all, send None so daemon doesn't restrict to a specific set
|
||||
// (otherwise dynamically loaded plugin types would be filtered out)
|
||||
let modes_param = if filter.is_accept_all() {
|
||||
None
|
||||
} else {
|
||||
let modes: Vec<String> = filter
|
||||
.enabled_providers()
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect();
|
||||
if modes.is_empty() { None } else { Some(modes) }
|
||||
};
|
||||
|
||||
match client.query(query, modes_param) {
|
||||
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
|
||||
@@ -105,13 +110,18 @@ impl SearchBackend {
|
||||
query.to_string()
|
||||
};
|
||||
|
||||
let modes: Vec<String> = filter
|
||||
.enabled_providers()
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect();
|
||||
|
||||
let modes_param = if modes.is_empty() { None } else { Some(modes) };
|
||||
// When accept_all, send None so daemon doesn't restrict to a specific set
|
||||
// (otherwise dynamically loaded plugin types would be filtered out)
|
||||
let modes_param = if filter.is_accept_all() {
|
||||
None
|
||||
} else {
|
||||
let modes: Vec<String> = filter
|
||||
.enabled_providers()
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect();
|
||||
if modes.is_empty() { None } else { Some(modes) }
|
||||
};
|
||||
|
||||
match client.query(&effective_query, modes_param) {
|
||||
Ok(items) => items.into_iter().map(result_to_launch_item).collect(),
|
||||
|
||||
@@ -1114,10 +1114,14 @@ fn execute_plugin_command(
|
||||
|
||||
// Load the appropriate runtime
|
||||
let loaded_runtime = match runtime {
|
||||
PluginRuntime::Lua => LoadedRuntime::load_lua(plugin_path.parent().unwrap_or(plugin_path))
|
||||
.map_err(|e| format!("Failed to load Lua runtime: {}", e))?,
|
||||
PluginRuntime::Lua => {
|
||||
let owlry_version = env!("CARGO_PKG_VERSION");
|
||||
LoadedRuntime::load_lua(plugin_path.parent().unwrap_or(plugin_path), owlry_version)
|
||||
.map_err(|e| format!("Failed to load Lua runtime: {}", e))?
|
||||
}
|
||||
PluginRuntime::Rune => {
|
||||
LoadedRuntime::load_rune(plugin_path.parent().unwrap_or(plugin_path))
|
||||
let owlry_version = env!("CARGO_PKG_VERSION");
|
||||
LoadedRuntime::load_rune(plugin_path.parent().unwrap_or(plugin_path), owlry_version)
|
||||
.map_err(|e| format!("Failed to load Rune runtime: {}", e))?
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user