Compare commits

...

20 Commits

Author SHA1 Message Date
7275fcab35 fix: implement all 24 FIX_PLAN issues across 6 phases
Phase 1 — Critical Safety:
- #11: bounded IPC reads via read_bounded_line (server + client)
- #13: sound Send+Sync via Arc<Mutex<RuntimeHandle>>; remove unsafe impl Sync
- #10: ItemSource enum (Core/NativePlugin/ScriptPlugin) on LaunchItem;
  script plugin allowlist guard in launch_item()

Phase 2 — Config System Overhaul:
- #6: remove dead enabled_plugins field
- #1: replace #[serde(flatten)] with explicit Config::plugin_config
- #4: Server.config Arc<RwLock<Config>>; ConfigProvider shares same Arc
- #2/#3: atomic config save (temp+rename); TOCTOU fixed — write lock held
  across mutation and save() in config_editor
- #23: fs2 lock_exclusive() on .lock sidecar file in Config::save()
- #16: SIGHUP handler reloads config; ExecReload in systemd service

Phase 3 — Plugin Architecture:
- #7: HostAPI v4 with get_config_string/int/bool; PLUGIN_CONFIG OnceLock
  in native_loader, set_shared_config() called from Server::bind()
- #5: PluginEntry + Request::PluginList + Response::PluginList; plugin_registry
  in ProviderManager tracks active and suppressed native plugins;
  cmd_list_installed shows both script and native plugins
- #9: suppressed native plugin log level info! → warn!
- #8: ProviderType doc glossary; plugins/mod.rs terminology table

Phase 4 — Data Integrity:
- #12: all into_inner() in server.rs + providers/mod.rs → explicit Response::Error;
  watcher exits on poisoned lock
- #14: FrecencyStore::prune() (180-day age + 5000-entry cap) called on load
- #17: empty command guard in launch_item(); warn in lua_provider
- #24: 5-min periodic frecency save thread; SIGTERM/SIGINT saves frecency
  before exit (replaces ctrlc handler)

Phase 5 — UI & UX:
- #19: provider_meta.rs ProviderMeta + meta_for(); three match blocks collapsed
- #18: desktop file dedup via seen_basenames HashSet in ApplicationProvider
- #20: search_filtered gains tag_filter param; non-frecency path now filters
- #15: widget refresh 5s→10s; skip when user is typing

Phase 6 — Hardening:
- #22: catch_unwind removed from reload_runtimes(); direct drop()
- #21: AtomicUsize + RAII ConnectionGuard; MAX_CONNECTIONS = 16

Deps: add fs2 = "0.4"; remove ctrlc and toml_edit from owlry-core
2026-04-08 16:43:52 +02:00
4d7e913657 chore(aur): update all packages to latest versions 2026-04-06 02:42:09 +02:00
f8d011447e chore(owlry): bump version to 1.0.8 2026-04-06 02:39:19 +02:00
9163b1ea6c chore(owlry-rune): bump version to 1.1.4 2026-04-06 02:38:47 +02:00
6586f5d6c2 fix(plugins): close remaining gaps in new plugin format support
- Fix cmd_runtimes() help text: init.lua/init.rn → main.lua/main.rn
- Add .lua extension validation to owlry-lua manifest (mirrors Rune)
- Replace eprintln! with log::warn!/log::debug! in owlry-lua loader
- Add log = "0.4" dependency to owlry-lua
- Add tests: [[providers]] deserialization in owlry-core manifest,
  manifest provider fallback in owlry-lua and owlry-rune loaders,
  non-runtime plugin filtering in both runtimes
2026-04-06 02:38:42 +02:00
a6e94deb3c fix(runtime): prevent dlclose() to avoid SIGSEGV on runtime teardown
Wrap LoadedRuntime._library in ManuallyDrop so dlclose() is never called.
dlclose() unmaps the library code; thread-local destructors inside liblua.so
then SIGSEGV when they try to run against the unmapped addresses.

Also filter out non-.lua plugins in the Lua runtime's discover_plugins()
so liblua.so does not attempt to load Rune plugins.
2026-04-06 02:26:12 +02:00
de74cac67d chore(owlry-lua): bump version to 1.1.3 2026-04-06 02:22:08 +02:00
2f396306fd chore(owlry-core): bump version to 1.3.4 2026-04-06 02:22:07 +02:00
133d5264ea feat(plugins): update plugin format to new entry_point + [[providers]] style
- owlry-core/manifest: add entry_point alias for entry field, add ProviderSpec
  struct for [[providers]] array, change default entry to main.lua
- owlry-lua/manifest: add ProviderDecl struct and providers: Vec<ProviderDecl>
  for [[providers]] support
- owlry-lua/loader: fall back to manifest [[providers]] when script has no API
  registrations; fall back to global refresh() for manifest-declared providers
- owlry-lua/api: expose call_global_refresh() that calls the top-level Lua
  refresh() function directly
- owlry/plugin_commands: update create templates to emit new format:
  entry_point instead of entry, [[providers]] instead of [provides],
  main.rn/main.lua instead of init.rn/init.lua, Rune uses Item::new() builder
  pattern, Lua uses standalone refresh() function
- cmd_validate: accept [[providers]] declarations as a valid provides source
2026-04-06 02:22:03 +02:00
a16c3a0523 fix(just): skip meta packages without PKGBUILD in aur-publish-all 2026-04-06 02:11:32 +02:00
33b4f410e5 fix(scripts): fix duplicate inject deduplication in aur-local-test
dedup_inject_files() was called before its definition in the script's
execution order. Moved the call to the Main section where all functions
are already defined.
2026-04-06 02:04:24 +02:00
a7683f16bf docs: update config example and README to reflect provider field removal 2026-04-06 02:02:06 +02:00
178f81082a fix(owlry-core,owlry): preserve config on save, fix tab filtering, clean provider fields
- Config::save() now merges into existing file via toml_edit to preserve
  user comments and unknown keys instead of overwriting
- Remove 16 dead ProvidersConfig plugin-toggle fields (uuctl, ssh, clipboard,
  bookmarks, emoji, scripts, files, media, weather_*, pomodoro_*) that became
  unreachable after accept_all=true was introduced
- Wire general.tabs to ProviderFilter::new() to drive UI tab display
- Fix set_single_mode() and toggle() not clearing accept_all, making tab
  filtering a no-op in default launch mode
- Add restore_all_mode() for cycle-back-to-All path in tab cycling
- Reduce PROVIDER_TOGGLES to built-in providers only
2026-04-06 01:59:48 +02:00
7863de9971 chore(owlry): bump version to 1.0.7 2026-04-06 01:57:42 +02:00
dacc194d02 chore(owlry-core): bump version to 1.3.3 2026-04-06 01:57:39 +02:00
5871609c73 chore(aur): update owlry-rune to 1.1.3 2026-04-05 18:18:11 +02:00
e3c4988e01 chore(owlry-rune): bump version to 1.1.3 2026-04-05 18:18:05 +02:00
46b5d8518f chore(aur): update owlry-rune to 1.1.2 2026-04-05 18:05:59 +02:00
95a698225c chore(owlry-rune): bump version to 1.1.2 2026-04-05 18:05:54 +02:00
709e1b04cb chore(aur): update owlry-lua to 1.1.2 2026-04-05 18:05:31 +02:00
63 changed files with 1868 additions and 1318 deletions

545
Cargo.lock generated
View File

@@ -50,12 +50,6 @@ dependencies = [
"core_extensions", "core_extensions",
] ]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.12" version = "0.8.12"
@@ -178,18 +172,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-compression"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.14.0" version = "1.14.0"
@@ -427,12 +409,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.44" version = "0.4.44"
@@ -503,23 +479,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "compression-codecs"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
"compression-core",
"flate2",
"memchr",
]
[[package]]
name = "compression-core"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -569,15 +528,6 @@ version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533d38ecd2709b7608fb8e18e4504deb99e9a72879e6aa66373a76d8dc4259ea" checksum = "533d38ecd2709b7608fb8e18e4504deb99e9a72879e6aa66373a76d8dc4259ea"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "critical-section" name = "critical-section"
version = "1.2.0" version = "1.2.0"
@@ -599,17 +549,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "ctrlc"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
dependencies = [
"dispatch2",
"nix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.8" version = "0.5.8"
@@ -647,8 +586,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"block2",
"libc",
"objc2", "objc2",
] ]
@@ -800,16 +737,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -861,6 +788,16 @@ dependencies = [
"xdg", "xdg",
] ]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "4.1.0" version = "4.1.0"
@@ -1053,24 +990,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1081,7 +1002,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi 6.0.0", "r-efi",
"wasip2", "wasip2",
"wasip3", "wasip3",
] ]
@@ -1532,23 +1453,6 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.6.0" version = "0.6.0"
@@ -1952,12 +1856,6 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lua-src" name = "lua-src"
version = "550.0.0" version = "550.0.0"
@@ -2032,16 +1930,6 @@ dependencies = [
"nom", "nom",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.1" version = "1.1.1"
@@ -2119,15 +2007,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom 0.2.17",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.18" version = "0.2.18"
@@ -2145,18 +2024,6 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "nix"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "1.2.4" version = "1.2.4"
@@ -2460,7 +2327,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry" name = "owlry"
version = "1.0.6" version = "1.0.8"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@@ -2481,13 +2348,13 @@ dependencies = [
[[package]] [[package]]
name = "owlry-core" name = "owlry-core"
version = "1.3.2" version = "1.3.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"ctrlc",
"dirs", "dirs",
"env_logger", "env_logger",
"freedesktop-desktop-entry", "freedesktop-desktop-entry",
"fs2",
"fuzzy-matcher", "fuzzy-matcher",
"libloading 0.8.9", "libloading 0.8.9",
"log", "log",
@@ -2497,10 +2364,11 @@ dependencies = [
"notify-debouncer-mini", "notify-debouncer-mini",
"notify-rust", "notify-rust",
"owlry-plugin-api", "owlry-plugin-api",
"reqwest 0.13.2", "reqwest",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"signal-hook",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"toml 0.8.23", "toml 0.8.23",
@@ -2508,15 +2376,16 @@ dependencies = [
[[package]] [[package]]
name = "owlry-lua" name = "owlry-lua"
version = "1.1.2" version = "1.1.3"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"chrono", "chrono",
"dirs", "dirs",
"log",
"meval", "meval",
"mlua", "mlua",
"owlry-plugin-api", "owlry-plugin-api",
"reqwest 0.13.2", "reqwest",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@@ -2534,16 +2403,15 @@ dependencies = [
[[package]] [[package]]
name = "owlry-rune" name = "owlry-rune"
version = "1.1.1" version = "1.1.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",
"env_logger", "env_logger",
"log", "log",
"owlry-plugin-api", "owlry-plugin-api",
"reqwest 0.13.2", "reqwest",
"rune", "rune",
"rune-modules",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@@ -2715,15 +2583,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@@ -2761,61 +2620,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -2825,47 +2629,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "6.0.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -2933,44 +2702,6 @@ dependencies = [
"tstr", "tstr",
] ]
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.2" version = "0.13.2"
@@ -3008,20 +2739,6 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rune" name = "rune"
version = "0.14.1" version = "0.14.1"
@@ -3095,21 +2812,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "rune-modules"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ef5dc3546042989f4abc70d6b9f707a539d5cbb5cb2fb167f8fbe891e1b64"
dependencies = [
"base64",
"nanorand",
"reqwest 0.12.28",
"rune",
"serde_json",
"tokio",
"toml 0.8.23",
]
[[package]] [[package]]
name = "rune-tracing" name = "rune-tracing"
version = "0.14.1" version = "0.14.1"
@@ -3158,41 +2860,15 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "rustls"
version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.0" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-webpki"
version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
@@ -3346,18 +3022,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@@ -3373,6 +3037,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.8" version = "1.4.8"
@@ -3383,12 +3057,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]] [[package]]
name = "simdutf8" name = "simdutf8"
version = "0.1.5" version = "0.1.5"
@@ -3429,12 +3097,6 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@@ -3620,21 +3282,6 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.50.0" version = "1.50.0"
@@ -3645,7 +3292,6 @@ dependencies = [
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -3660,29 +3306,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.23" version = "0.8.23"
@@ -3805,18 +3428,13 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [ dependencies = [
"async-compression",
"bitflags 2.11.0", "bitflags 2.11.0",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util",
"iri-string", "iri-string",
"pin-project-lite", "pin-project-lite",
"tokio",
"tokio-util",
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
@@ -3975,12 +3593,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"
@@ -4186,25 +3798,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "which" name = "which"
version = "8.0.2" version = "8.0.2"
@@ -4411,15 +4004,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -4453,30 +4037,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6", "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6", "windows_i686_gnullvm",
"windows_i686_msvc 0.52.6", "windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6", "windows_x86_64_msvc 0.52.6",
] ]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]] [[package]]
name = "windows-threading" name = "windows-threading"
version = "0.1.0" version = "0.1.0"
@@ -4507,12 +4074,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
@@ -4525,12 +4086,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
@@ -4543,24 +4098,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]] [[package]]
name = "windows_i686_gnullvm" name = "windows_i686_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
@@ -4573,12 +4116,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
@@ -4591,12 +4128,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.5" version = "0.48.5"
@@ -4609,12 +4140,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
@@ -4627,12 +4152,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" version = "0.7.15"

View File

@@ -254,7 +254,7 @@ Type `:config` to browse and modify settings without editing files:
| Command | What it does | | Command | What it does |
|---------|-------------| |---------|-------------|
| `:config` | Show all setting categories | | `:config` | Show all setting categories |
| `:config providers` | Toggle providers on/off | | `:config providers` | Toggle built-in providers on/off (calculator, converter, system, frecency) |
| `:config theme` | Select color theme | | `:config theme` | Select color theme |
| `:config engine` | Select web search engine | | `:config engine` | Select web search engine |
| `:config frecency` | Toggle frecency, set weight | | `:config frecency` | Toggle frecency, set weight |
@@ -265,6 +265,8 @@ Type `:config` to browse and modify settings without editing files:
Changes are saved to `config.toml` immediately. Some settings (theme, frecency) take effect on the next search. Others (font size, dimensions) require a restart. Changes are saved to `config.toml` immediately. Some settings (theme, frecency) take effect on the next search. Others (font size, dimensions) require a restart.
> **Note:** `:config providers` only covers built-in providers. To enable or disable plugins, use `owlry plugin enable/disable <name>` or set `disabled_plugins` in `[plugins]`.
### Search Prefixes ### Search Prefixes
| Prefix | Provider | Example | | Prefix | Provider | Example |
@@ -332,33 +334,54 @@ Or configure from within the launcher: type `:config` to interactively change se
```toml ```toml
[general] [general]
show_icons = true show_icons = true
max_results = 10 max_results = 100
tabs = ["app", "cmd", "uuctl"] tabs = ["app", "cmd", "uuctl"] # Provider tabs shown in the header bar
# terminal_command = "kitty" # Auto-detected # terminal_command = "kitty" # Auto-detected; overrides $TERMINAL and xdg-terminal-exec
# use_uwsm = false # Enable for systemd session integration # use_uwsm = false # Enable for systemd session integration (uwsm app --)
[appearance] [appearance]
width = 850 width = 850
height = 650 height = 650
font_size = 14 font_size = 14
border_radius = 12 border_radius = 12
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc. # theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc. (see Theming section)
[plugins] # Optional per-element color overrides — all fields are optional, unset inherits from theme
disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"] # [appearance.colors]
# background = "#1e1e2e"
# background_secondary = "#313244"
# border = "#45475a"
# text = "#cdd6f4"
# accent = "#cba6f7"
# badge_app = "#a6e3a1" # All badge_* keys: app, cmd, clip, ssh, emoji, file,
# badge_web = "#89dceb" # script, sys, uuctl, web, calc, bm, dmenu,
# badge_media = "#f38ba8" # media, weather, pomo
[providers] [providers]
applications = true # .desktop files applications = true # .desktop files
commands = true # PATH executables commands = true # PATH executables
calculator = true # Built-in math expressions calculator = true # Built-in math expressions (= or calc trigger)
converter = true # Built-in unit/currency conversion converter = true # Built-in unit/currency converter (> trigger)
system = true # Built-in shutdown/reboot/lock actions system = true # Built-in shutdown/reboot/lock actions
frecency = true # Boost frequently used items frecency = true # Boost frequently used items
frecency_weight = 0.3 # 0.0-1.0 frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
# Web search engine: google, duckduckgo, bing, startpage, brave, ecosia # Web search engine: google, duckduckgo, bing, startpage, searxng, brave, ecosia
# Or a custom URL with a {query} placeholder: "https://example.com/search?q={query}"
search_engine = "duckduckgo" search_engine = "duckduckgo"
[plugins]
# disabled_plugins = ["emoji", "pomodoro"] # Plugin IDs to disable
# enabled_plugins = [] # Empty = all discovered plugins are loaded
# registry_url = "https://..." # Custom plugin registry URL
# Sandboxing for Lua/Rune user plugins (~/.config/owlry/plugins/)
# [plugins.sandbox]
# allow_filesystem = false # Allow access outside plugin directory
# allow_network = false # Allow outbound network requests
# allow_commands = false # Allow shell command execution
# memory_limit = 67108864 # Lua memory cap in bytes (default: 64 MB)
# Profiles: named sets of modes # Profiles: named sets of modes
[profiles.dev] [profiles.dev]
modes = ["app", "cmd", "ssh"] modes = ["app", "cmd", "ssh"]
@@ -382,10 +405,17 @@ Add plugin IDs to the disabled list in your config:
```toml ```toml
[plugins] [plugins]
disabled = ["emoji", "pomodoro"] disabled_plugins = ["emoji", "pomodoro"]
``` ```
Or toggle providers interactively: type `:config providers` in the launcher. Or use the CLI:
```bash
owlry plugin disable emoji
owlry plugin enable emoji
```
> **Note:** `:config providers` in the launcher only manages built-in providers (calculator, converter, system). Use `disabled_plugins` or `owlry plugin disable` for plugins.
### Plugin Management CLI ### Plugin Management CLI

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-core pkgbase = owlry-core
pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search
pkgver = 1.3.2 pkgver = 1.3.4
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
@@ -8,7 +8,7 @@ pkgbase = owlry-core
makedepends = cargo makedepends = cargo
depends = gcc-libs depends = gcc-libs
depends = openssl depends = openssl
source = owlry-core-1.3.2.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.3.2.tar.gz source = owlry-core-1.3.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.3.4.tar.gz
b2sums = 36a1e31cadcfdbe70c0a10c13eddbcea7ae21b7dcfb0aa10a75f44a82a377d6598c4237228457c13260ca4b4b88f12d416541ad7698cf28076124b1a4d3dbbc6 b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry-core pkgname = owlry-core

10
aur/owlry-core/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-core pkgname=owlry-core
pkgver=1.3.2 pkgver=1.3.4
pkgrel=1 pkgrel=1
pkgdesc='Core daemon for the Owlry application launcher — manages plugins, providers, and search' pkgdesc='Core daemon for the Owlry application launcher — manages plugins, providers, and search'
arch=('x86_64') arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('gcc-libs' 'openssl') depends=('gcc-libs' 'openssl')
makedepends=('cargo') makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v$pkgver.tar.gz")
b2sums=('36a1e31cadcfdbe70c0a10c13eddbcea7ae21b7dcfb0aa10a75f44a82a377d6598c4237228457c13260ca4b4b88f12d416541ad7698cf28076124b1a4d3dbbc6') b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
prepare() { prepare() {
cd "owlry" cd "owlry"

View File

@@ -1,13 +1,14 @@
pkgbase = owlry-lua pkgbase = owlry-lua
pkgdesc = Lua scripting runtime for Owlry — enables user-created Lua plugins pkgdesc = Lua scripting runtime for Owlry — enables user-created Lua plugins
pkgver = 1.1.1 pkgver = 1.1.3
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
license = GPL-3.0-or-later license = GPL-3.0-or-later
makedepends = cargo makedepends = cargo
depends = owlry-core depends = owlry-core
source = owlry-lua-1.1.1.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.1.tar.gz depends = openssl
b2sums = a0e1fa032db8dda8e6bc24457f3c04948129d3f14c1d3e61b8e080340b24f560d43294beb133ad4b1c6eb7942d401108ea91c367b074eaeeefa284e9b2a9dbc8 source = owlry-lua-1.1.3.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.3.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry-lua pkgname = owlry-lua

10
aur/owlry-lua/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-lua pkgname=owlry-lua
pkgver=1.1.1 pkgver=1.1.3
pkgrel=1 pkgrel=1
pkgdesc="Lua scripting runtime for Owlry — enables user-created Lua plugins" pkgdesc="Lua scripting runtime for Owlry — enables user-created Lua plugins"
arch=('x86_64') arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('owlry-core' 'openssl') depends=('owlry-core' 'openssl')
makedepends=('cargo') makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v$pkgver.tar.gz")
b2sums=('a0e1fa032db8dda8e6bc24457f3c04948129d3f14c1d3e61b8e080340b24f560d43294beb133ad4b1c6eb7942d401108ea91c367b074eaeeefa284e9b2a9dbc8') b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
_cratename=owlry-lua _cratename=owlry-lua

Submodule aur/owlry-meta-essentials updated: 4a09cfb73c...ed91b61709

Submodule aur/owlry-meta-full updated: 8f85087731...2115aa08f8

Submodule aur/owlry-meta-tools updated: 28c78b7953...bc821ff47f

Submodule aur/owlry-meta-widgets updated: aa4c2cd217...8ba6dd318c

View File

@@ -1,13 +1,14 @@
pkgbase = owlry-rune pkgbase = owlry-rune
pkgdesc = Rune scripting runtime for Owlry — enables user-created Rune plugins pkgdesc = Rune scripting runtime for Owlry — enables user-created Rune plugins
pkgver = 1.1.1 pkgver = 1.1.4
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
license = GPL-3.0-or-later license = GPL-3.0-or-later
makedepends = cargo makedepends = cargo
depends = owlry-core depends = owlry-core
source = owlry-rune-1.1.1.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.1.tar.gz depends = openssl
b2sums = a0e1fa032db8dda8e6bc24457f3c04948129d3f14c1d3e61b8e080340b24f560d43294beb133ad4b1c6eb7942d401108ea91c367b074eaeeefa284e9b2a9dbc8 source = owlry-rune-1.1.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.4.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry-rune pkgname = owlry-rune

10
aur/owlry-rune/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-rune pkgname=owlry-rune
pkgver=1.1.1 pkgver=1.1.4
pkgrel=1 pkgrel=1
pkgdesc="Rune scripting runtime for Owlry — enables user-created Rune plugins" pkgdesc="Rune scripting runtime for Owlry — enables user-created Rune plugins"
arch=('x86_64') arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('owlry-core' 'openssl') depends=('owlry-core' 'openssl')
makedepends=('cargo') makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v$pkgver.tar.gz")
b2sums=('a0e1fa032db8dda8e6bc24457f3c04948129d3f14c1d3e61b8e080340b24f560d43294beb133ad4b1c6eb7942d401108ea91c367b074eaeeefa284e9b2a9dbc8') b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
_cratename=owlry-rune _cratename=owlry-rune

View File

@@ -1,6 +1,6 @@
pkgbase = owlry pkgbase = owlry
pkgdesc = Lightweight Wayland application launcher with plugin support pkgdesc = Lightweight Wayland application launcher with plugin support
pkgver = 1.0.6 pkgver = 1.0.8
pkgrel = 1 pkgrel = 1
url = https://somegit.dev/Owlibou/owlry url = https://somegit.dev/Owlibou/owlry
arch = x86_64 arch = x86_64
@@ -28,7 +28,7 @@ pkgbase = owlry
optdepends = owlry-plugin-pomodoro: pomodoro timer widget optdepends = owlry-plugin-pomodoro: pomodoro timer widget
optdepends = owlry-lua: Lua runtime for user plugins optdepends = owlry-lua: Lua runtime for user plugins
optdepends = owlry-rune: Rune runtime for user plugins optdepends = owlry-rune: Rune runtime for user plugins
source = owlry-1.0.6.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.6.tar.gz source = owlry-1.0.8.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.8.tar.gz
b2sums = 8967562bda33820b282350eaad17e8194699926b721eabe978fb0b70af2a75e399866c6bfa7abb449141701bad618df56079c7e81358708b1852b1070b0b7c05 b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry pkgname = owlry

10
aur/owlry/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev> # Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry pkgname=owlry
pkgver=1.0.6 pkgver=1.0.8
pkgrel=1 pkgrel=1
pkgdesc="Lightweight Wayland application launcher with plugin support" pkgdesc="Lightweight Wayland application launcher with plugin support"
arch=('x86_64') arch=('x86_64')
@@ -29,7 +29,7 @@ optdepends=(
'owlry-rune: Rune runtime for user plugins' 'owlry-rune: Rune runtime for user plugins'
) )
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v$pkgver.tar.gz") source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v$pkgver.tar.gz")
b2sums=('8967562bda33820b282350eaad17e8194699926b721eabe978fb0b70af2a75e399866c6bfa7abb449141701bad618df56079c7e81358708b1852b1070b0b7c05') b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
prepare() { prepare() {
cd "owlry" cd "owlry"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-core" name = "owlry-core"
version = "1.3.2" version = "1.3.4"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true
@@ -30,6 +30,7 @@ semver = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
toml = "0.8" toml = "0.8"
fs2 = "0.4"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
dirs = "5" dirs = "5"
@@ -41,7 +42,7 @@ notify = "7"
notify-debouncer-mini = "0.5" notify-debouncer-mini = "0.5"
# Signal handling # Signal handling
ctrlc = { version = "3", features = ["termination"] } signal-hook = "0.3"
# Logging & notifications # Logging & notifications
log = "0.4" log = "0.4"

View File

@@ -1,3 +1,4 @@
use fs2::FileExt;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -32,6 +33,10 @@ pub struct Config {
pub plugins: PluginsConfig, pub plugins: PluginsConfig,
#[serde(default)] #[serde(default)]
pub profiles: HashMap<String, ProfileConfig>, pub profiles: HashMap<String, ProfileConfig>,
/// Per-plugin configuration tables.
/// Defined as `[plugin_config.<plugin_name>]` in config.toml.
#[serde(default)]
pub plugin_config: HashMap<String, toml::Value>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -157,82 +162,26 @@ pub struct ProvidersConfig {
pub applications: bool, pub applications: bool,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub commands: bool, pub commands: bool,
#[serde(default = "default_true")] /// Enable built-in calculator provider (= or calc trigger)
pub uuctl: bool,
/// Enable calculator provider (= expression or calc expression)
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub calculator: bool, pub calculator: bool,
/// Enable converter provider (> expression or auto-detect) /// Enable built-in unit/currency converter (> trigger)
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub converter: bool, pub converter: bool,
/// Enable built-in system actions (shutdown, reboot, lock, etc.)
#[serde(default = "default_true")]
pub system: bool,
/// Enable frecency-based result ranking /// Enable frecency-based result ranking
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub frecency: bool, pub frecency: bool,
/// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost) /// Weight for frecency boost (0.0 = disabled, 1.0 = strong boost)
#[serde(default = "default_frecency_weight")] #[serde(default = "default_frecency_weight")]
pub frecency_weight: f64, pub frecency_weight: f64,
/// Enable web search provider (? query or web query) /// Search engine for web search (used by owlry-plugin-websearch)
#[serde(default = "default_true")]
pub websearch: bool,
/// Search engine for web search
/// Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia /// Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
/// Or custom URL with {query} placeholder /// Or a custom URL with a {query} placeholder
#[serde(default = "default_search_engine")] #[serde(default = "default_search_engine")]
pub search_engine: String, pub search_engine: String,
/// Enable system commands (shutdown, reboot, etc.)
#[serde(default = "default_true")]
pub system: bool,
/// Enable SSH connections from ~/.ssh/config
#[serde(default = "default_true")]
pub ssh: bool,
/// Enable clipboard history (requires cliphist)
#[serde(default = "default_true")]
pub clipboard: bool,
/// Enable browser bookmarks
#[serde(default = "default_true")]
pub bookmarks: bool,
/// Enable emoji picker
#[serde(default = "default_true")]
pub emoji: bool,
/// Enable custom scripts from ~/.config/owlry/scripts/
#[serde(default = "default_true")]
pub scripts: bool,
/// Enable file search (requires fd or locate)
#[serde(default = "default_true")]
pub files: bool,
// ─── Widget Providers ───────────────────────────────────────────────
/// Enable MPRIS media player widget
#[serde(default = "default_true")]
pub media: bool,
/// Enable weather widget
#[serde(default)]
pub weather: bool,
/// Weather provider: wttr.in (default), openweathermap, open-meteo
#[serde(default = "default_weather_provider")]
pub weather_provider: String,
/// API key for weather services that require it (e.g., OpenWeatherMap)
#[serde(default)]
pub weather_api_key: Option<String>,
/// Location for weather (city name or coordinates)
#[serde(default)]
pub weather_location: Option<String>,
/// Enable pomodoro timer widget
#[serde(default)]
pub pomodoro: bool,
/// Pomodoro work duration in minutes
#[serde(default = "default_pomodoro_work")]
pub pomodoro_work_mins: u32,
/// Pomodoro break duration in minutes
#[serde(default = "default_pomodoro_break")]
pub pomodoro_break_mins: u32,
} }
impl Default for ProvidersConfig { impl Default for ProvidersConfig {
@@ -240,28 +189,12 @@ impl Default for ProvidersConfig {
Self { Self {
applications: true, applications: true,
commands: true, commands: true,
uuctl: true,
calculator: true, calculator: true,
converter: true, converter: true,
system: true,
frecency: true, frecency: true,
frecency_weight: 0.3, frecency_weight: 0.3,
websearch: true,
search_engine: "duckduckgo".to_string(), search_engine: "duckduckgo".to_string(),
system: true,
ssh: true,
clipboard: true,
bookmarks: true,
emoji: true,
scripts: true,
files: true,
media: true,
weather: false,
weather_provider: "wttr.in".to_string(),
weather_api_key: None,
weather_location: Some("Berlin".to_string()),
pomodoro: false,
pomodoro_work_mins: 25,
pomodoro_break_mins: 5,
} }
} }
} }
@@ -287,10 +220,6 @@ pub struct PluginsConfig {
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub enabled: bool, pub enabled: bool,
/// List of plugin IDs to enable (empty = all discovered plugins)
#[serde(default)]
pub enabled_plugins: Vec<String>,
/// List of plugin IDs to explicitly disable /// List of plugin IDs to explicitly disable
#[serde(default)] #[serde(default)]
pub disabled_plugins: Vec<String>, pub disabled_plugins: Vec<String>,
@@ -304,11 +233,6 @@ pub struct PluginsConfig {
#[serde(default)] #[serde(default)]
pub registry_url: Option<String>, pub registry_url: Option<String>,
/// Per-plugin configuration tables
/// Accessed via `[plugins.<plugin_name>]` sections in config.toml
/// Each plugin can define its own config schema
#[serde(flatten)]
pub plugin_configs: HashMap<String, toml::Value>,
} }
/// Sandbox settings for plugin security /// Sandbox settings for plugin security
@@ -335,43 +259,13 @@ impl Default for PluginsConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: true, enabled: true,
enabled_plugins: Vec::new(),
disabled_plugins: Vec::new(), disabled_plugins: Vec::new(),
sandbox: SandboxConfig::default(), sandbox: SandboxConfig::default(),
registry_url: None, registry_url: None,
plugin_configs: HashMap::new(),
} }
} }
} }
impl PluginsConfig {
/// Get configuration for a specific plugin by name
///
/// Returns the plugin's config table if it exists in `[plugins.<name>]`
#[allow(dead_code)]
pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&toml::Value> {
self.plugin_configs.get(plugin_name)
}
/// Get a string value from a plugin's config
#[allow(dead_code)]
pub fn get_plugin_string(&self, plugin_name: &str, key: &str) -> Option<&str> {
self.plugin_configs.get(plugin_name)?.get(key)?.as_str()
}
/// Get an integer value from a plugin's config
#[allow(dead_code)]
pub fn get_plugin_int(&self, plugin_name: &str, key: &str) -> Option<i64> {
self.plugin_configs.get(plugin_name)?.get(key)?.as_integer()
}
/// Get a boolean value from a plugin's config
#[allow(dead_code)]
pub fn get_plugin_bool(&self, plugin_name: &str, key: &str) -> Option<bool> {
self.plugin_configs.get(plugin_name)?.get(key)?.as_bool()
}
}
impl Default for SandboxConfig { impl Default for SandboxConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -399,18 +293,6 @@ fn default_frecency_weight() -> f64 {
0.3 0.3
} }
fn default_weather_provider() -> String {
"wttr.in".to_string()
}
fn default_pomodoro_work() -> u32 {
25
}
fn default_pomodoro_break() -> u32 {
5
}
/// Detect the best available terminal emulator /// Detect the best available terminal emulator
/// Fallback chain: /// Fallback chain:
/// 1. $TERMINAL env var (user's explicit preference) /// 1. $TERMINAL env var (user's explicit preference)
@@ -539,11 +421,51 @@ fn command_exists(cmd: &str) -> bool {
// Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default // Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default
/// Extract leading comment lines (lines beginning with `#`) from a TOML file's content.
/// Stops at the first non-comment, non-empty line.
fn extract_header_comments(content: &str) -> String {
let mut header = String::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') || trimmed.is_empty() {
header.push_str(line);
header.push('\n');
} else {
break;
}
}
header
}
impl Config { impl Config {
pub fn config_path() -> Option<PathBuf> { pub fn config_path() -> Option<PathBuf> {
paths::config_file() paths::config_file()
} }
/// Get configuration table for a plugin by name.
#[allow(dead_code)]
pub fn get_plugin_config(&self, plugin_name: &str) -> Option<&toml::Value> {
self.plugin_config.get(plugin_name)
}
/// Get a string value from a plugin's config.
#[allow(dead_code)]
pub fn get_plugin_string(&self, plugin_name: &str, key: &str) -> Option<&str> {
self.plugin_config.get(plugin_name)?.get(key)?.as_str()
}
/// Get an integer value from a plugin's config.
#[allow(dead_code)]
pub fn get_plugin_int(&self, plugin_name: &str, key: &str) -> Option<i64> {
self.plugin_config.get(plugin_name)?.get(key)?.as_integer()
}
/// Get a boolean value from a plugin's config.
#[allow(dead_code)]
pub fn get_plugin_bool(&self, plugin_name: &str, key: &str) -> Option<bool> {
self.plugin_config.get(plugin_name)?.get(key)?.as_bool()
}
pub fn load_or_default() -> Self { pub fn load_or_default() -> Self {
Self::load().unwrap_or_else(|e| { Self::load().unwrap_or_else(|e| {
warn!("Failed to load config: {}, using defaults", e); warn!("Failed to load config: {}, using defaults", e);
@@ -559,8 +481,27 @@ impl Config {
Self::default() Self::default()
} else { } else {
let content = std::fs::read_to_string(&path)?; let content = std::fs::read_to_string(&path)?;
let config: Config = toml::from_str(&content)?; let mut config: Config = toml::from_str(&content)?;
info!("Loaded config from {:?}", path); info!("Loaded config from {:?}", path);
// Migrate legacy [plugins.<name>] entries to [plugin_config.<name>].
// Known PluginsConfig fields are excluded from migration.
const KNOWN_PLUGINS_KEYS: &[&str] =
&["enabled", "disabled_plugins", "sandbox", "registry_url"];
if let Ok(raw) = toml::from_str::<toml::Value>(&content)
&& let Some(plugins_table) = raw.get("plugins").and_then(|v| v.as_table())
{
for (key, value) in plugins_table {
if !KNOWN_PLUGINS_KEYS.contains(&key.as_str())
&& !config.plugin_config.contains_key(key)
{
warn!(
"Config: [plugins.{}] is deprecated; move to [plugin_config.{}]",
key, key
);
config.plugin_config.insert(key.clone(), value.clone());
}
}
}
config config
}; };
@@ -585,14 +526,46 @@ impl Config {
Ok(config) Ok(config)
} }
#[allow(dead_code)]
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let path = Self::config_path().ok_or("Could not determine config path")?; let path = Self::config_path().ok_or("Could not determine config path")?;
paths::ensure_parent_dir(&path)?; paths::ensure_parent_dir(&path)?;
let content = toml::to_string_pretty(self)?; // Acquire an exclusive advisory lock via a sibling lock file.
std::fs::write(&path, content)?; // Concurrent writers (e.g. two `owlry plugin enable` invocations) will
// block here until the first one finishes, preventing interleaved writes.
let lock_path = path.with_extension("toml.lock");
let lock_file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false) // lock files are never written to; don't clobber existing
.open(&lock_path)?;
lock_file.lock_exclusive()?;
// Preserve any leading comment block (e.g. user docs / generated header).
let header = if path.exists() {
let existing = std::fs::read_to_string(&path)?;
extract_header_comments(&existing)
} else {
String::new()
};
let body = toml::to_string_pretty(self)?;
let content = if header.is_empty() {
body
} else {
format!("{}\n{}", header.trim_end(), body)
};
// Atomic write: write to a sibling temp file, then rename over the target.
// rename(2) is atomic on POSIX — readers always see either the old or new file.
let tmp_path = path.with_extension("toml.tmp");
std::fs::write(&tmp_path, &content)?;
std::fs::rename(&tmp_path, &path)?;
// Lock is released when lock_file is dropped here.
drop(lock_file);
info!("Saved config to {:?}", path); info!("Saved config to {:?}", path);
Ok(()) Ok(())
} }

View File

@@ -29,6 +29,10 @@ impl Default for FrecencyData {
} }
} }
const MAX_ENTRIES: usize = 5000;
const PRUNE_AGE_DAYS: i64 = 180;
const MIN_LAUNCHES_TO_KEEP: u32 = 3;
/// Frecency store for tracking and boosting recently/frequently used items /// Frecency store for tracking and boosting recently/frequently used items
pub struct FrecencyStore { pub struct FrecencyStore {
data: FrecencyData, data: FrecencyData,
@@ -44,10 +48,49 @@ impl FrecencyStore {
info!("Frecency store loaded with {} entries", data.entries.len()); info!("Frecency store loaded with {} entries", data.entries.len());
Self { let mut store = Self {
data, data,
path, path,
dirty: false, dirty: false,
};
store.prune();
store
}
/// Remove stale low-usage entries and enforce the hard cap.
///
/// Entries older than `PRUNE_AGE_DAYS` with fewer than `MIN_LAUNCHES_TO_KEEP`
/// launches are removed. After age-based pruning, entries are sorted by score
/// (descending) and the list is truncated to `MAX_ENTRIES`.
fn prune(&mut self) {
let now = Utc::now();
let cutoff = now - chrono::Duration::days(PRUNE_AGE_DAYS);
let before = self.data.entries.len();
self.data.entries.retain(|_, e| {
e.last_launch > cutoff || e.launch_count >= MIN_LAUNCHES_TO_KEEP
});
if self.data.entries.len() > MAX_ENTRIES {
// Sort by score descending and keep the top MAX_ENTRIES
let mut scored: Vec<(String, f64)> = self
.data
.entries
.iter()
.map(|(k, e)| {
(k.clone(), Self::calculate_frecency_at(e.launch_count, e.last_launch, now))
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let keep: std::collections::HashSet<String> =
scored.into_iter().take(MAX_ENTRIES).map(|(k, _)| k).collect();
self.data.entries.retain(|k, _| keep.contains(k));
}
let removed = before - self.data.entries.len();
if removed > 0 {
info!("Frecency: pruned {} stale entries ({} remaining)", removed, self.data.entries.len());
self.dirty = true;
} }
} }

View File

@@ -26,11 +26,17 @@ pub struct ParsedQuery {
} }
impl ProviderFilter { impl ProviderFilter {
/// Create filter from CLI args and config /// Create filter from CLI args and config.
///
/// `tabs` is `general.tabs` from config and drives which provider tabs are
/// shown in the UI when no explicit CLI mode is active. It has no effect on
/// query routing: when no CLI mode is set, `accept_all=true` causes
/// `is_active()` to return `true` for every provider regardless.
pub fn new( pub fn new(
cli_mode: Option<ProviderType>, cli_mode: Option<ProviderType>,
cli_providers: Option<Vec<ProviderType>>, cli_providers: Option<Vec<ProviderType>>,
config_providers: &ProvidersConfig, config_providers: &ProvidersConfig,
tabs: &[String],
) -> Self { ) -> Self {
let accept_all = cli_mode.is_none() && cli_providers.is_none(); let accept_all = cli_mode.is_none() && cli_providers.is_none();
@@ -41,50 +47,23 @@ impl ProviderFilter {
// --providers overrides config // --providers overrides config
providers.into_iter().collect() providers.into_iter().collect()
} else { } else {
// Use config file settings, default to apps only // No CLI restriction: accept_all=true, so is_active() returns true for
let mut set = HashSet::new(); // everything. Build the enabled set only for UI tab display, driven by
// Core providers // general.tabs. Falls back to Application + Command if tabs is empty.
if config_providers.applications { let mut set: HashSet<ProviderType> = tabs
set.insert(ProviderType::Application); .iter()
} .map(|s| Self::mode_string_to_provider_type(s))
if config_providers.commands { .collect();
set.insert(ProviderType::Command);
}
// Plugin providers - use Plugin(type_id) for all
if config_providers.uuctl {
set.insert(ProviderType::Plugin("uuctl".to_string()));
}
if config_providers.system {
set.insert(ProviderType::Plugin("system".to_string()));
}
if config_providers.ssh {
set.insert(ProviderType::Plugin("ssh".to_string()));
}
if config_providers.clipboard {
set.insert(ProviderType::Plugin("clipboard".to_string()));
}
if config_providers.bookmarks {
set.insert(ProviderType::Plugin("bookmarks".to_string()));
}
if config_providers.emoji {
set.insert(ProviderType::Plugin("emoji".to_string()));
}
if config_providers.scripts {
set.insert(ProviderType::Plugin("scripts".to_string()));
}
// Dynamic providers
if config_providers.files {
set.insert(ProviderType::Plugin("filesearch".to_string()));
}
if config_providers.calculator {
set.insert(ProviderType::Plugin("calc".to_string()));
}
if config_providers.websearch {
set.insert(ProviderType::Plugin("websearch".to_string()));
}
// Default to apps if nothing enabled
if set.is_empty() { if set.is_empty() {
set.insert(ProviderType::Application); if config_providers.applications {
set.insert(ProviderType::Application);
}
if config_providers.commands {
set.insert(ProviderType::Command);
}
if set.is_empty() {
set.insert(ProviderType::Application);
}
} }
set set
}; };
@@ -114,7 +93,8 @@ impl ProviderFilter {
} }
} }
/// Toggle a provider on/off /// Toggle a provider on/off. Clears accept_all so the enabled set is
/// actually used for routing — use restore_all_mode() to go back to All.
pub fn toggle(&mut self, provider: ProviderType) { pub fn toggle(&mut self, provider: ProviderType) {
if self.enabled.contains(&provider) { if self.enabled.contains(&provider) {
self.enabled.remove(&provider); self.enabled.remove(&provider);
@@ -137,6 +117,7 @@ impl ProviderFilter {
provider_debug, self.enabled provider_debug, self.enabled
); );
} }
self.accept_all = false;
} }
/// Enable a specific provider /// Enable a specific provider
@@ -156,6 +137,12 @@ impl ProviderFilter {
pub fn set_single_mode(&mut self, provider: ProviderType) { pub fn set_single_mode(&mut self, provider: ProviderType) {
self.enabled.clear(); self.enabled.clear();
self.enabled.insert(provider); self.enabled.insert(provider);
self.accept_all = false;
}
/// Restore accept-all mode (used when cycling back to the "All" tab).
pub fn restore_all_mode(&mut self) {
self.accept_all = true;
} }
/// Set prefix mode (from :app, :cmd, etc.) /// Set prefix mode (from :app, :cmd, etc.)

View File

@@ -24,6 +24,8 @@ pub enum Request {
PluginAction { PluginAction {
command: String, command: String,
}, },
/// Query the daemon's plugin registry (native plugins + suppressed entries).
PluginList,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -32,10 +34,30 @@ pub enum Response {
Results { items: Vec<ResultItem> }, Results { items: Vec<ResultItem> },
Providers { list: Vec<ProviderDesc> }, Providers { list: Vec<ProviderDesc> },
SubmenuItems { items: Vec<ResultItem> }, SubmenuItems { items: Vec<ResultItem> },
PluginList { entries: Vec<PluginEntry> },
Ack, Ack,
Error { message: String }, Error { message: String },
} }
/// Registry entry for a loaded or suppressed plugin (native plugins only).
/// Script plugins are tracked separately via filesystem discovery.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PluginEntry {
pub id: String,
pub name: String,
pub version: String,
/// Plugin runtime type: "native", "builtin"
pub runtime: String,
/// Load status: "active" or "suppressed"
pub status: String,
/// Human-readable detail for non-active status (e.g. suppression reason)
#[serde(default, skip_serializing_if = "String::is_empty")]
pub status_detail: String,
/// Provider type IDs registered by this plugin
#[serde(default)]
pub providers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResultItem { pub struct ResultItem {
pub id: String, pub id: String,
@@ -50,6 +72,14 @@ pub struct ResultItem {
pub terminal: bool, pub terminal: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>, pub tags: Vec<String>,
/// Item trust level: "core", "native_plugin", or "script_plugin".
/// Defaults to "core" when absent (backwards-compatible with old daemons).
#[serde(default = "default_source")]
pub source: String,
}
fn default_source() -> String {
"core".to_string()
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@@ -1,4 +1,4 @@
use log::{info, warn}; use log::info;
use owlry_core::paths; use owlry_core::paths;
use owlry_core::server::Server; use owlry_core::server::Server;
@@ -23,14 +23,8 @@ fn main() {
} }
}; };
// Graceful shutdown on SIGTERM/SIGINT // SIGTERM/SIGINT are handled inside Server::run() via signal-hook,
let sock_cleanup = sock.clone(); // which saves frecency before exiting.
if let Err(e) = ctrlc::set_handler(move || {
let _ = std::fs::remove_file(&sock_cleanup);
std::process::exit(0);
}) {
warn!("Failed to set signal handler: {}", e);
}
if let Err(e) = server.run() { if let Err(e) = server.run() {
eprintln!("Server error: {e}"); eprintln!("Server error: {e}");

View File

@@ -10,6 +10,10 @@ use super::error::{PluginError, PluginResult};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest { pub struct PluginManifest {
pub plugin: PluginInfo, pub plugin: PluginInfo,
/// Provider declarations from [[providers]] sections (new-style)
#[serde(default)]
pub providers: Vec<ProviderSpec>,
/// Legacy provides block (old-style)
#[serde(default)] #[serde(default)]
pub provides: PluginProvides, pub provides: PluginProvides,
#[serde(default)] #[serde(default)]
@@ -43,7 +47,7 @@ pub struct PluginInfo {
#[serde(default = "default_owlry_version")] #[serde(default = "default_owlry_version")]
pub owlry_version: String, pub owlry_version: String,
/// Entry point file (relative to plugin directory) /// Entry point file (relative to plugin directory)
#[serde(default = "default_entry")] #[serde(default = "default_entry", alias = "entry_point")]
pub entry: String, pub entry: String,
} }
@@ -52,7 +56,27 @@ fn default_owlry_version() -> String {
} }
fn default_entry() -> String { fn default_entry() -> String {
"init.lua".to_string() "main.lua".to_string()
}
/// A provider declared in a [[providers]] section
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSpec {
pub id: String,
pub name: String,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub icon: Option<String>,
/// "static" (default) or "dynamic"
#[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()
} }
/// What the plugin provides /// What the plugin provides
@@ -278,7 +302,7 @@ version = "1.0.0"
assert_eq!(manifest.plugin.id, "test-plugin"); assert_eq!(manifest.plugin.id, "test-plugin");
assert_eq!(manifest.plugin.name, "Test Plugin"); assert_eq!(manifest.plugin.name, "Test Plugin");
assert_eq!(manifest.plugin.version, "1.0.0"); assert_eq!(manifest.plugin.version, "1.0.0");
assert_eq!(manifest.plugin.entry, "init.lua"); assert_eq!(manifest.plugin.entry, "main.lua");
} }
#[test] #[test]
@@ -317,6 +341,70 @@ api_url = "https://api.example.com"
assert_eq!(manifest.permissions.run_commands, vec!["myapp"]); assert_eq!(manifest.permissions.run_commands, vec!["myapp"]);
} }
#[test]
fn test_parse_new_format_with_providers_array() {
let toml_str = r#"
[plugin]
id = "my-plugin"
name = "My Plugin"
version = "0.1.0"
description = "Test"
entry_point = "main.rn"
[[providers]]
id = "my-plugin"
name = "My Plugin"
type = "static"
type_id = "myplugin"
icon = "system-run"
prefix = ":mp"
"#;
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.plugin.entry, "main.rn");
assert_eq!(manifest.providers.len(), 1);
let p = &manifest.providers[0];
assert_eq!(p.id, "my-plugin");
assert_eq!(p.name, "My Plugin");
assert_eq!(p.provider_type, "static");
assert_eq!(p.type_id.as_deref(), Some("myplugin"));
assert_eq!(p.icon.as_deref(), Some("system-run"));
assert_eq!(p.prefix.as_deref(), Some(":mp"));
}
#[test]
fn test_parse_new_format_entry_point_alias() {
let toml_str = r#"
[plugin]
id = "test"
name = "Test"
version = "1.0.0"
entry_point = "main.lua"
"#;
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.plugin.entry, "main.lua");
}
#[test]
fn test_provider_spec_defaults() {
let toml_str = r#"
[plugin]
id = "test"
name = "Test"
version = "1.0.0"
[[providers]]
id = "test"
name = "Test"
"#;
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.providers.len(), 1);
let p = &manifest.providers[0];
assert_eq!(p.provider_type, "static"); // default
assert!(p.prefix.is_none());
assert!(p.icon.is_none());
assert!(p.type_id.is_none());
}
#[test] #[test]
fn test_version_compatibility() { fn test_version_compatibility() {
let toml_str = r#" let toml_str = r#"

View File

@@ -1,24 +1,38 @@
//! Owlry Plugin System //! Owlry Plugin System
//! //!
//! This module provides plugin support for extending owlry's functionality. //! This module loads and manages *plugins* — external code that extends owlry
//! Plugins can register providers, actions, themes, and hooks. //! with additional *plugin providers* beyond the built-in ones.
//!
//! # Terminology
//!
//! | Term | Meaning |
//! |------|---------|
//! | **Provider** | Abstract source of [`LaunchItem`]s (the core interface) |
//! | **Built-in provider** | Provider compiled into owlry-core (Application, Command) |
//! | **Plugin** | External code (native `.so` or script) loaded at runtime |
//! | **Plugin provider** | A provider registered by a plugin via its `type_id` |
//! | **Native plugin** | Pre-compiled Rust `.so` from `/usr/lib/owlry/plugins/` |
//! | **Script plugin** | Lua or Rune plugin from `~/.config/owlry/plugins/` |
//! //!
//! # Plugin Types //! # Plugin Types
//! //!
//! - **Native plugins** (.so): Pre-compiled Rust plugins loaded from `/usr/lib/owlry/plugins/` //! - **Native plugins** (`.so`): Pre-compiled Rust plugins loaded from `/usr/lib/owlry/plugins/`
//! - **Lua plugins**: Script-based plugins from `~/.config/owlry/plugins/` (requires `lua` feature) //! - **Script plugins**: Lua or Rune scripts from `~/.config/owlry/plugins/`
//! (requires the corresponding runtime: `owlry-lua` or `owlry-rune`)
//! //!
//! # Plugin Structure (Lua) //! # Script Plugin Structure
//! //!
//! Each Lua plugin lives in its own directory under `~/.config/owlry/plugins/`: //! Each script plugin lives in its own directory under `~/.config/owlry/plugins/`:
//! //!
//! ```text //! ```text
//! ~/.config/owlry/plugins/ //! ~/.config/owlry/plugins/
//! my-plugin/ //! my-plugin/
//! plugin.toml # Plugin manifest //! plugin.toml # Plugin manifest
//! init.lua # Entry point //! init.lua # Entry point (Lua) or init.rn (Rune)
//! lib/ # Optional modules //! lib/ # Optional modules
//! ``` //! ```
//!
//! [`LaunchItem`]: crate::providers::LaunchItem
// Always available // Always available
pub mod error; pub mod error;
@@ -60,10 +74,9 @@ pub use manifest::{PluginManifest, check_compatibility, discover_plugins};
#[cfg(feature = "lua")] #[cfg(feature = "lua")]
mod lua_manager { mod lua_manager {
use super::*; use super::*;
use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::sync::{Arc, Mutex};
use manifest::{check_compatibility, discover_plugins}; use manifest::{check_compatibility, discover_plugins};
@@ -73,8 +86,8 @@ mod lua_manager {
plugins_dir: PathBuf, plugins_dir: PathBuf,
/// Current owlry version for compatibility checks /// Current owlry version for compatibility checks
owlry_version: String, owlry_version: String,
/// Loaded plugins by ID (Rc<RefCell<>> allows sharing with LuaProviders) /// Loaded plugins by ID. Arc<Mutex<>> allows sharing with LuaProviders across threads.
plugins: HashMap<String, Rc<RefCell<LoadedPlugin>>>, plugins: HashMap<String, Arc<Mutex<LoadedPlugin>>>,
/// Plugin IDs that are explicitly disabled /// Plugin IDs that are explicitly disabled
disabled: Vec<String>, disabled: Vec<String>,
} }
@@ -116,7 +129,7 @@ mod lua_manager {
} }
let plugin = LoadedPlugin::new(manifest, path); let plugin = LoadedPlugin::new(manifest, path);
self.plugins.insert(id, Rc::new(RefCell::new(plugin))); self.plugins.insert(id, Arc::new(Mutex::new(plugin)));
loaded_count += 1; loaded_count += 1;
} }
@@ -129,7 +142,7 @@ mod lua_manager {
let mut errors = Vec::new(); let mut errors = Vec::new();
for (id, plugin_rc) in &self.plugins { for (id, plugin_rc) in &self.plugins {
let mut plugin = plugin_rc.borrow_mut(); let mut plugin = plugin_rc.lock().unwrap();
if !plugin.enabled { if !plugin.enabled {
continue; continue;
} }
@@ -145,23 +158,23 @@ mod lua_manager {
errors errors
} }
/// Get a loaded plugin by ID (returns Rc for shared ownership) /// Get a loaded plugin by ID
#[allow(dead_code)] #[allow(dead_code)]
pub fn get(&self, id: &str) -> Option<Rc<RefCell<LoadedPlugin>>> { pub fn get(&self, id: &str) -> Option<Arc<Mutex<LoadedPlugin>>> {
self.plugins.get(id).cloned() self.plugins.get(id).cloned()
} }
/// Get all loaded plugins /// Get all loaded plugins
#[allow(dead_code)] #[allow(dead_code)]
pub fn plugins(&self) -> impl Iterator<Item = Rc<RefCell<LoadedPlugin>>> + '_ { pub fn plugins(&self) -> impl Iterator<Item = Arc<Mutex<LoadedPlugin>>> + '_ {
self.plugins.values().cloned() self.plugins.values().cloned()
} }
/// Get all enabled plugins /// Get all enabled plugins
pub fn enabled_plugins(&self) -> impl Iterator<Item = Rc<RefCell<LoadedPlugin>>> + '_ { pub fn enabled_plugins(&self) -> impl Iterator<Item = Arc<Mutex<LoadedPlugin>>> + '_ {
self.plugins self.plugins
.values() .values()
.filter(|p| p.borrow().enabled) .filter(|p| p.lock().unwrap().enabled)
.cloned() .cloned()
} }
@@ -174,7 +187,7 @@ mod lua_manager {
/// Get the number of enabled plugins /// Get the number of enabled plugins
#[allow(dead_code)] #[allow(dead_code)]
pub fn enabled_count(&self) -> usize { pub fn enabled_count(&self) -> usize {
self.plugins.values().filter(|p| p.borrow().enabled).count() self.plugins.values().filter(|p| p.lock().unwrap().enabled).count()
} }
/// Enable a plugin by ID /// Enable a plugin by ID
@@ -184,7 +197,7 @@ mod lua_manager {
.plugins .plugins
.get(id) .get(id)
.ok_or_else(|| PluginError::NotFound(id.to_string()))?; .ok_or_else(|| PluginError::NotFound(id.to_string()))?;
let mut plugin = plugin_rc.borrow_mut(); let mut plugin = plugin_rc.lock().unwrap();
if !plugin.enabled { if !plugin.enabled {
plugin.enabled = true; plugin.enabled = true;
@@ -202,7 +215,7 @@ mod lua_manager {
.plugins .plugins
.get(id) .get(id)
.ok_or_else(|| PluginError::NotFound(id.to_string()))?; .ok_or_else(|| PluginError::NotFound(id.to_string()))?;
plugin_rc.borrow_mut().enabled = false; plugin_rc.lock().unwrap().enabled = false;
Ok(()) Ok(())
} }
@@ -211,13 +224,14 @@ mod lua_manager {
pub fn providers_for(&self, provider_name: &str) -> Vec<String> { pub fn providers_for(&self, provider_name: &str) -> Vec<String> {
self.enabled_plugins() self.enabled_plugins()
.filter(|p| { .filter(|p| {
p.borrow() p.lock()
.unwrap()
.manifest .manifest
.provides .provides
.providers .providers
.contains(&provider_name.to_string()) .contains(&provider_name.to_string())
}) })
.map(|p| p.borrow().id().to_string()) .map(|p| p.lock().unwrap().id().to_string())
.collect() .collect()
} }
@@ -225,21 +239,21 @@ mod lua_manager {
#[allow(dead_code)] #[allow(dead_code)]
pub fn has_action_plugins(&self) -> bool { pub fn has_action_plugins(&self) -> bool {
self.enabled_plugins() self.enabled_plugins()
.any(|p| p.borrow().manifest.provides.actions) .any(|p| p.lock().unwrap().manifest.provides.actions)
} }
/// Check if any plugin provides hooks /// Check if any plugin provides hooks
#[allow(dead_code)] #[allow(dead_code)]
pub fn has_hook_plugins(&self) -> bool { pub fn has_hook_plugins(&self) -> bool {
self.enabled_plugins() self.enabled_plugins()
.any(|p| p.borrow().manifest.provides.hooks) .any(|p| p.lock().unwrap().manifest.provides.hooks)
} }
/// Get all theme names provided by plugins /// Get all theme names provided by plugins
#[allow(dead_code)] #[allow(dead_code)]
pub fn theme_names(&self) -> Vec<String> { pub fn theme_names(&self) -> Vec<String> {
self.enabled_plugins() self.enabled_plugins()
.flat_map(|p| p.borrow().manifest.provides.themes.clone()) .flat_map(|p| p.lock().unwrap().manifest.provides.themes.clone())
.collect() .collect()
} }

View File

@@ -12,17 +12,70 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Once}; use std::sync::{Arc, Once, OnceLock, RwLock};
use libloading::Library; use libloading::Library;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use owlry_plugin_api::{ use owlry_plugin_api::{
API_VERSION, HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo, API_VERSION, HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo,
ProviderKind, RStr, ProviderKind, ROption, RStr, RString,
}; };
use crate::config::Config;
use crate::notify; use crate::notify;
// ============================================================================
// Plugin config access
// ============================================================================
/// Shared config reference, set by the host before any plugins are loaded.
static PLUGIN_CONFIG: OnceLock<Arc<RwLock<Config>>> = OnceLock::new();
/// Share the config with the native plugin loader so plugins can read their
/// own config sections. Must be called before `NativePluginLoader::discover()`.
pub fn set_shared_config(config: Arc<RwLock<Config>>) {
let _ = PLUGIN_CONFIG.set(config);
}
extern "C" fn host_get_config_string(plugin_id: RStr<'_>, key: RStr<'_>) -> ROption<RString> {
let Some(cfg_arc) = PLUGIN_CONFIG.get() else {
return ROption::RNone;
};
let Ok(cfg) = cfg_arc.read() else {
return ROption::RNone;
};
match cfg.get_plugin_string(plugin_id.as_str(), key.as_str()) {
Some(v) => ROption::RSome(RString::from(v)),
None => ROption::RNone,
}
}
extern "C" fn host_get_config_int(plugin_id: RStr<'_>, key: RStr<'_>) -> ROption<i64> {
let Some(cfg_arc) = PLUGIN_CONFIG.get() else {
return ROption::RNone;
};
let Ok(cfg) = cfg_arc.read() else {
return ROption::RNone;
};
match cfg.get_plugin_int(plugin_id.as_str(), key.as_str()) {
Some(v) => ROption::RSome(v),
None => ROption::RNone,
}
}
extern "C" fn host_get_config_bool(plugin_id: RStr<'_>, key: RStr<'_>) -> ROption<bool> {
let Some(cfg_arc) = PLUGIN_CONFIG.get() else {
return ROption::RNone;
};
let Ok(cfg) = cfg_arc.read() else {
return ROption::RNone;
};
match cfg.get_plugin_bool(plugin_id.as_str(), key.as_str()) {
Some(v) => ROption::RSome(v),
None => ROption::RNone,
}
}
// ============================================================================ // ============================================================================
// Host API Implementation // Host API Implementation
// ============================================================================ // ============================================================================
@@ -71,6 +124,9 @@ static HOST_API: HostAPI = HostAPI {
log_info: host_log_info, log_info: host_log_info,
log_warn: host_log_warn, log_warn: host_log_warn,
log_error: host_log_error, log_error: host_log_error,
get_config_string: host_get_config_string,
get_config_int: host_get_config_int,
get_config_bool: host_get_config_bool,
}; };
/// Initialize the host API (called once before loading plugins) /// Initialize the host API (called once before loading plugins)

View File

@@ -10,14 +10,15 @@
//! Note: This module is infrastructure for the runtime architecture. Full integration //! Note: This module is infrastructure for the runtime architecture. Full integration
//! is pending Phase 5 (AUR Packaging) when runtime packages will be available. //! is pending Phase 5 (AUR Packaging) when runtime packages will be available.
use std::mem::ManuallyDrop;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::{Arc, Mutex};
use libloading::{Library, Symbol}; use libloading::{Library, Symbol};
use owlry_plugin_api::{PluginItem, RStr, RString, RVec}; use owlry_plugin_api::{PluginItem, RStr, RString, RVec};
use super::error::{PluginError, PluginResult}; use super::error::{PluginError, PluginResult};
use crate::providers::{LaunchItem, Provider, ProviderType}; use crate::providers::{ItemSource, LaunchItem, Provider, ProviderType};
/// System directory for runtime libraries /// System directory for runtime libraries
pub const SYSTEM_RUNTIMES_DIR: &str = "/usr/lib/owlry/runtimes"; pub const SYSTEM_RUNTIMES_DIR: &str = "/usr/lib/owlry/runtimes";
@@ -50,6 +51,11 @@ pub type LuaProviderInfo = ScriptProviderInfo;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct RuntimeHandle(pub *mut ()); pub struct RuntimeHandle(pub *mut ());
// SAFETY: The underlying runtime state (Lua VM, Rune VM) is Send — mlua enables
// the "send" feature and Rune wraps its state in Mutex internally. Access is always
// serialized through Arc<Mutex<RuntimeHandle>>, so there are no data races.
unsafe impl Send for RuntimeHandle {}
/// VTable for script runtime functions (used by both Lua and Rune) /// VTable for script runtime functions (used by both Lua and Rune)
#[repr(C)] #[repr(C)]
pub struct ScriptRuntimeVTable { pub struct ScriptRuntimeVTable {
@@ -69,12 +75,17 @@ pub struct ScriptRuntimeVTable {
pub struct LoadedRuntime { pub struct LoadedRuntime {
/// Runtime name (for logging) /// Runtime name (for logging)
name: &'static str, name: &'static str,
/// Keep library alive /// Keep library alive — wrapped in ManuallyDrop so we never call dlclose().
_library: Arc<Library>, /// dlclose() unmaps the library code; any thread-local destructors inside the
/// library then SIGSEGV when they try to run against the unmapped addresses.
/// Runtime libraries live for the process lifetime, so leaking the handle is safe.
_library: ManuallyDrop<Arc<Library>>,
/// Runtime vtable /// Runtime vtable
vtable: &'static ScriptRuntimeVTable, vtable: &'static ScriptRuntimeVTable,
/// Runtime handle (state) /// Runtime handle shared with all RuntimeProvider instances for this runtime.
handle: RuntimeHandle, /// Mutex serializes concurrent vtable calls. Arc shares ownership so all
/// RuntimeProviders can call into the runtime through the same handle.
handle: Arc<Mutex<RuntimeHandle>>,
/// Provider information /// Provider information
providers: Vec<ScriptProviderInfo>, providers: Vec<ScriptProviderInfo>,
} }
@@ -124,10 +135,14 @@ impl LoadedRuntime {
// Initialize the runtime // Initialize the runtime
let plugins_dir_str = plugins_dir.to_string_lossy(); let plugins_dir_str = plugins_dir.to_string_lossy();
let handle = (vtable.init)(RStr::from_str(&plugins_dir_str), RStr::from_str(owlry_version)); let raw_handle = (vtable.init)(RStr::from_str(&plugins_dir_str), RStr::from_str(owlry_version));
let handle = Arc::new(Mutex::new(raw_handle));
// Get provider information // Get provider information — lock to serialize the vtable call
let providers_rvec = (vtable.providers)(handle); let providers_rvec = {
let h = handle.lock().unwrap();
(vtable.providers)(*h)
};
let providers: Vec<ScriptProviderInfo> = providers_rvec.into_iter().collect(); let providers: Vec<ScriptProviderInfo> = providers_rvec.into_iter().collect();
log::info!( log::info!(
@@ -138,7 +153,7 @@ impl LoadedRuntime {
Ok(Self { Ok(Self {
name, name,
_library: library, _library: ManuallyDrop::new(library),
vtable, vtable,
handle, handle,
providers, providers,
@@ -155,8 +170,12 @@ impl LoadedRuntime {
self.providers self.providers
.iter() .iter()
.map(|info| { .map(|info| {
let provider = let provider = RuntimeProvider::new(
RuntimeProvider::new(self.name, self.vtable, self.handle, info.clone()); self.name,
self.vtable,
Arc::clone(&self.handle),
info.clone(),
);
Box::new(provider) as Box<dyn Provider> Box::new(provider) as Box<dyn Provider>
}) })
.collect() .collect()
@@ -165,17 +184,16 @@ impl LoadedRuntime {
impl Drop for LoadedRuntime { impl Drop for LoadedRuntime {
fn drop(&mut self) { fn drop(&mut self) {
(self.vtable.drop)(self.handle); let h = self.handle.lock().unwrap();
(self.vtable.drop)(*h);
// Do NOT drop _library: ManuallyDrop ensures dlclose() is never called.
// See field comment for rationale.
} }
} }
// LoadedRuntime is Send + Sync because:
// LoadedRuntime needs to be Send + Sync because ProviderManager is shared across // - Arc<Mutex<RuntimeHandle>> is Send + Sync (RuntimeHandle: Send via unsafe impl above)
// threads via Arc<RwLock<ProviderManager>>. // - All other fields are 'static references or Send types
// Safety: RuntimeHandle is an opaque FFI handle accessed only through extern "C" // No unsafe impl needed — this is derived automatically.
// 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 /// A provider backed by a dynamically loaded runtime
pub struct RuntimeProvider { pub struct RuntimeProvider {
@@ -183,7 +201,9 @@ pub struct RuntimeProvider {
#[allow(dead_code)] #[allow(dead_code)]
runtime_name: &'static str, runtime_name: &'static str,
vtable: &'static ScriptRuntimeVTable, vtable: &'static ScriptRuntimeVTable,
handle: RuntimeHandle, /// Shared with the owning LoadedRuntime and sibling RuntimeProviders.
/// Mutex serializes concurrent vtable calls on the same runtime handle.
handle: Arc<Mutex<RuntimeHandle>>,
info: ScriptProviderInfo, info: ScriptProviderInfo,
items: Vec<LaunchItem>, items: Vec<LaunchItem>,
} }
@@ -192,7 +212,7 @@ impl RuntimeProvider {
fn new( fn new(
runtime_name: &'static str, runtime_name: &'static str,
vtable: &'static ScriptRuntimeVTable, vtable: &'static ScriptRuntimeVTable,
handle: RuntimeHandle, handle: Arc<Mutex<RuntimeHandle>>,
info: ScriptProviderInfo, info: ScriptProviderInfo,
) -> Self { ) -> Self {
Self { Self {
@@ -214,6 +234,7 @@ impl RuntimeProvider {
command: item.command.to_string(), command: item.command.to_string(),
terminal: item.terminal, terminal: item.terminal,
tags: item.keywords.iter().map(|s| s.to_string()).collect(), tags: item.keywords.iter().map(|s| s.to_string()).collect(),
source: ItemSource::ScriptPlugin,
} }
} }
} }
@@ -233,7 +254,10 @@ impl Provider for RuntimeProvider {
} }
let name_rstr = RStr::from_str(self.info.name.as_str()); let name_rstr = RStr::from_str(self.info.name.as_str());
let items_rvec = (self.vtable.refresh)(self.handle, name_rstr); let items_rvec = {
let h = self.handle.lock().unwrap();
(self.vtable.refresh)(*h, name_rstr)
};
self.items = items_rvec self.items = items_rvec
.into_iter() .into_iter()
.map(|i| self.convert_item(i)) .map(|i| self.convert_item(i))
@@ -251,12 +275,10 @@ impl Provider for RuntimeProvider {
} }
} }
// RuntimeProvider needs to be Send + Sync for the Provider trait. // RuntimeProvider is Send + Sync because:
// Safety: RuntimeHandle is an opaque FFI handle accessed only through // - Arc<Mutex<RuntimeHandle>> is Send + Sync (RuntimeHandle: Send via unsafe impl above)
// extern "C" vtable functions. The same safety argument that justifies // - vtable is &'static (Send + Sync), info and items are Send
// Send applies to Sync — all access is mediated by the vtable. // No unsafe impl needed — this is derived automatically.
unsafe impl Send for RuntimeProvider {}
unsafe impl Sync for RuntimeProvider {}
/// Check if the Lua runtime is available /// Check if the Lua runtime is available
pub fn lua_runtime_available() -> bool { pub fn lua_runtime_available() -> bool {

View File

@@ -89,8 +89,13 @@ fn watch_loop(
if has_relevant_change { if has_relevant_change {
info!("Plugin file change detected, reloading runtimes..."); info!("Plugin file change detected, reloading runtimes...");
let mut pm_guard = pm.write().unwrap_or_else(|e| e.into_inner()); match pm.write() {
pm_guard.reload_runtimes(); Ok(mut pm_guard) => pm_guard.reload_runtimes(),
Err(_) => {
log::error!("Plugin watcher: provider lock poisoned; stopping watcher");
return Err(Box::from("provider lock poisoned"));
}
}
} }
} }
Ok(Err(error)) => { Ok(Err(error)) => {

View File

@@ -1,4 +1,6 @@
use super::{LaunchItem, Provider, ProviderType}; use std::collections::HashSet;
use super::{ItemSource, LaunchItem, Provider, ProviderType};
use crate::paths; use crate::paths;
use freedesktop_desktop_entry::{DesktopEntry, Iter}; use freedesktop_desktop_entry::{DesktopEntry, Iter};
use log::{debug, warn}; use log::{debug, warn};
@@ -118,7 +120,21 @@ impl Provider for ApplicationProvider {
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect(); .collect();
// Track seen .desktop file basenames to skip duplicates.
// XDG dirs are iterated user-first per spec, so the first occurrence wins.
let mut seen_basenames: HashSet<String> = HashSet::new();
for path in Iter::new(dirs.into_iter()) { for path in Iter::new(dirs.into_iter()) {
// Skip if we've already loaded a .desktop with this basename from a higher-priority dir.
if path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|basename| !seen_basenames.insert(basename.to_string()))
{
debug!("Skipping duplicate desktop entry: {:?}", path);
continue;
}
let content = match std::fs::read_to_string(&path) { let content = match std::fs::read_to_string(&path) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
@@ -196,6 +212,7 @@ impl Provider for ApplicationProvider {
command: run_cmd, command: run_cmd,
terminal: desktop_entry.terminal(), terminal: desktop_entry.terminal(),
tags, tags,
source: ItemSource::Core,
}; };
self.items.push(item); self.items.push(item);

View File

@@ -1,4 +1,4 @@
use super::{DynamicProvider, LaunchItem, ProviderType}; use super::{DynamicProvider, ItemSource, LaunchItem, ProviderType};
/// Built-in calculator provider. Evaluates mathematical expressions via `meval`. /// Built-in calculator provider. Evaluates mathematical expressions via `meval`.
/// ///
@@ -42,6 +42,7 @@ impl DynamicProvider for CalculatorProvider {
command: copy_cmd, command: copy_cmd,
terminal: false, terminal: false,
tags: vec!["math".into(), "calculator".into()], tags: vec!["math".into(), "calculator".into()],
source: ItemSource::Core,
}] }]
} }
Err(_) => Vec::new(), Err(_) => Vec::new(),

View File

@@ -1,4 +1,4 @@
use super::{LaunchItem, Provider, ProviderType}; use super::{ItemSource, LaunchItem, Provider, ProviderType};
use log::debug; use log::debug;
use std::collections::HashSet; use std::collections::HashSet;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
@@ -89,6 +89,7 @@ impl Provider for CommandProvider {
command: name, command: name,
terminal: false, terminal: false,
tags: Vec::new(), tags: Vec::new(),
source: ItemSource::Core,
}; };
self.items.push(item); self.items.push(item);

View File

@@ -2,7 +2,7 @@ use std::sync::{Arc, RwLock};
use log::warn; use log::warn;
use super::{DynamicProvider, LaunchItem, ProviderType}; use super::{DynamicProvider, ItemSource, LaunchItem, ProviderType};
use crate::config::Config; use crate::config::Config;
const ICON: &str = "preferences-system-symbolic"; const ICON: &str = "preferences-system-symbolic";
@@ -23,24 +23,15 @@ const SEARCH_ENGINES: &[&str] = &[
const BUILTIN_THEMES: &[&str] = &["owl"]; const BUILTIN_THEMES: &[&str] = &["owl"];
/// Boolean provider fields that can be toggled via CONFIG:toggle:providers.*. /// Boolean provider fields that can be toggled via CONFIG:toggle:providers.*.
/// Only built-in providers are listed here; plugins are enabled/disabled via
/// [plugins] disabled_plugins in config.toml or `owlry plugin enable/disable`.
const PROVIDER_TOGGLES: &[(&str, &str)] = &[ const PROVIDER_TOGGLES: &[(&str, &str)] = &[
("applications", "Applications"), ("applications", "Applications"),
("commands", "Commands"), ("commands", "Commands"),
("uuctl", "Systemd Units"),
("calculator", "Calculator"), ("calculator", "Calculator"),
("converter", "Unit Converter"), ("converter", "Unit Converter"),
("frecency", "Frecency Ranking"),
("websearch", "Web Search"),
("system", "System Actions"), ("system", "System Actions"),
("ssh", "SSH Connections"), ("frecency", "Frecency Ranking"),
("clipboard", "Clipboard History"),
("bookmarks", "Bookmarks"),
("emoji", "Emoji Picker"),
("scripts", "Scripts"),
("files", "File Search"),
("media", "Media Widget"),
("weather", "Weather Widget"),
("pomodoro", "Pomodoro Widget"),
]; ];
/// Built-in config editor provider. Interprets query text as a navigation path /// Built-in config editor provider. Interprets query text as a navigation path
@@ -55,27 +46,34 @@ impl ConfigProvider {
} }
/// Execute a `CONFIG:*` action command. Returns `true` if handled. /// Execute a `CONFIG:*` action command. Returns `true` if handled.
///
/// Acquires the write lock once and holds it across both the mutation and
/// the subsequent save, eliminating the TOCTOU window that would exist if
/// the sub-handlers each acquired the lock independently.
fn handle_config_action(&self, command: &str) -> bool { fn handle_config_action(&self, command: &str) -> bool {
let Some(rest) = command.strip_prefix("CONFIG:") else { let Some(rest) = command.strip_prefix("CONFIG:") else {
return false; return false;
}; };
let mut cfg = match self.config.write() {
Ok(c) => c,
Err(_) => return false,
};
let result = if let Some(path) = rest.strip_prefix("toggle:") { let result = if let Some(path) = rest.strip_prefix("toggle:") {
self.handle_toggle(path) Self::toggle_config(&mut cfg, path)
} else if let Some(kv) = rest.strip_prefix("set:") { } else if let Some(kv) = rest.strip_prefix("set:") {
self.handle_set(kv) Self::set_config(&mut cfg, kv)
} else if let Some(profile_cmd) = rest.strip_prefix("profile:") { } else if let Some(profile_cmd) = rest.strip_prefix("profile:") {
self.handle_profile(profile_cmd) Self::profile_config(&mut cfg, profile_cmd)
} else { } else {
false false
}; };
if result { if result
if let Ok(cfg) = self.config.read() { && let Err(e) = cfg.save()
if let Err(e) = cfg.save() { {
warn!("Failed to save config: {}", e); warn!("Failed to save config: {}", e);
}
}
} }
result result
@@ -83,12 +81,7 @@ impl ConfigProvider {
// ── Toggle handler ────────────────────────────────────────────────── // ── Toggle handler ──────────────────────────────────────────────────
fn handle_toggle(&self, path: &str) -> bool { fn toggle_config(cfg: &mut Config, path: &str) -> bool {
let mut cfg = match self.config.write() {
Ok(c) => c,
Err(_) => return false,
};
match path { match path {
"providers.applications" => { "providers.applications" => {
cfg.providers.applications = !cfg.providers.applications; cfg.providers.applications = !cfg.providers.applications;
@@ -98,10 +91,6 @@ impl ConfigProvider {
cfg.providers.commands = !cfg.providers.commands; cfg.providers.commands = !cfg.providers.commands;
true true
} }
"providers.uuctl" => {
cfg.providers.uuctl = !cfg.providers.uuctl;
true
}
"providers.calculator" => { "providers.calculator" => {
cfg.providers.calculator = !cfg.providers.calculator; cfg.providers.calculator = !cfg.providers.calculator;
true true
@@ -110,52 +99,12 @@ impl ConfigProvider {
cfg.providers.converter = !cfg.providers.converter; cfg.providers.converter = !cfg.providers.converter;
true true
} }
"providers.frecency" => {
cfg.providers.frecency = !cfg.providers.frecency;
true
}
"providers.websearch" => {
cfg.providers.websearch = !cfg.providers.websearch;
true
}
"providers.system" => { "providers.system" => {
cfg.providers.system = !cfg.providers.system; cfg.providers.system = !cfg.providers.system;
true true
} }
"providers.ssh" => { "providers.frecency" => {
cfg.providers.ssh = !cfg.providers.ssh; cfg.providers.frecency = !cfg.providers.frecency;
true
}
"providers.clipboard" => {
cfg.providers.clipboard = !cfg.providers.clipboard;
true
}
"providers.bookmarks" => {
cfg.providers.bookmarks = !cfg.providers.bookmarks;
true
}
"providers.emoji" => {
cfg.providers.emoji = !cfg.providers.emoji;
true
}
"providers.scripts" => {
cfg.providers.scripts = !cfg.providers.scripts;
true
}
"providers.files" => {
cfg.providers.files = !cfg.providers.files;
true
}
"providers.media" => {
cfg.providers.media = !cfg.providers.media;
true
}
"providers.weather" => {
cfg.providers.weather = !cfg.providers.weather;
true
}
"providers.pomodoro" => {
cfg.providers.pomodoro = !cfg.providers.pomodoro;
true true
} }
"general.show_icons" => { "general.show_icons" => {
@@ -172,14 +121,10 @@ impl ConfigProvider {
// ── Set handler ───────────────────────────────────────────────────── // ── Set handler ─────────────────────────────────────────────────────
fn handle_set(&self, kv: &str) -> bool { fn set_config(cfg: &mut Config, kv: &str) -> bool {
let Some((path, value)) = kv.split_once(':') else { let Some((path, value)) = kv.split_once(':') else {
return false; return false;
}; };
let mut cfg = match self.config.write() {
Ok(c) => c,
Err(_) => return false,
};
match path { match path {
"appearance.theme" => { "appearance.theme" => {
@@ -240,12 +185,8 @@ impl ConfigProvider {
// ── Profile handler ───────────────────────────────────────────────── // ── Profile handler ─────────────────────────────────────────────────
fn handle_profile(&self, cmd: &str) -> bool { fn profile_config(cfg: &mut Config, cmd: &str) -> bool {
if let Some(name) = cmd.strip_prefix("create:") { if let Some(name) = cmd.strip_prefix("create:") {
let mut cfg = match self.config.write() {
Ok(c) => c,
Err(_) => return false,
};
if !name.is_empty() && !cfg.profiles.contains_key(name) { if !name.is_empty() && !cfg.profiles.contains_key(name) {
cfg.profiles.insert( cfg.profiles.insert(
name.to_string(), name.to_string(),
@@ -256,10 +197,6 @@ impl ConfigProvider {
false false
} }
} else if let Some(name) = cmd.strip_prefix("delete:") { } else if let Some(name) = cmd.strip_prefix("delete:") {
let mut cfg = match self.config.write() {
Ok(c) => c,
Err(_) => return false,
};
cfg.profiles.remove(name).is_some() cfg.profiles.remove(name).is_some()
} else if let Some(rest) = cmd.strip_prefix("mode:") { } else if let Some(rest) = cmd.strip_prefix("mode:") {
// format: profile_name:toggle:mode_name // format: profile_name:toggle:mode_name
@@ -267,10 +204,6 @@ impl ConfigProvider {
if parts.len() == 3 && parts[1] == "toggle" { if parts.len() == 3 && parts[1] == "toggle" {
let profile_name = parts[0]; let profile_name = parts[0];
let mode_name = parts[2]; let mode_name = parts[2];
let mut cfg = match self.config.write() {
Ok(c) => c,
Err(_) => return false,
};
if let Some(profile) = cfg.profiles.get_mut(profile_name) { if let Some(profile) = cfg.profiles.get_mut(profile_name) {
if let Some(pos) = profile.modes.iter().position(|m| m == mode_name) { if let Some(pos) = profile.modes.iter().position(|m| m == mode_name) {
profile.modes.remove(pos); profile.modes.remove(pos);
@@ -329,6 +262,7 @@ impl ConfigProvider {
command: format!("CONFIG:toggle:providers.{}", field), command: format!("CONFIG:toggle:providers.{}", field),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
} }
}) })
.collect() .collect()
@@ -383,6 +317,7 @@ impl ConfigProvider {
command: format!("CONFIG:set:appearance.theme:{}", theme_name), command: format!("CONFIG:set:appearance.theme:{}", theme_name),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
} }
}) })
.collect() .collect()
@@ -413,6 +348,7 @@ impl ConfigProvider {
command: format!("CONFIG:set:providers.search_engine:{}", engine), command: format!("CONFIG:set:providers.search_engine:{}", engine),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
} }
}) })
.collect() .collect()
@@ -439,6 +375,7 @@ impl ConfigProvider {
command: "CONFIG:toggle:providers.frecency".into(), command: "CONFIG:toggle:providers.frecency".into(),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}); });
// If numeric input, offer a set-weight action // If numeric input, offer a set-weight action
@@ -453,6 +390,7 @@ impl ConfigProvider {
command: format!("CONFIG:set:providers.frecency_weight:{}", clamped), command: format!("CONFIG:set:providers.frecency_weight:{}", clamped),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}); });
} }
@@ -479,6 +417,7 @@ impl ConfigProvider {
command: String::new(), command: String::new(),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}); });
// If numeric input, offer a set action // If numeric input, offer a set action
@@ -495,6 +434,7 @@ impl ConfigProvider {
command: format!("CONFIG:set:{}:{}", config_path, input), command: format!("CONFIG:set:{}:{}", config_path, input),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}); });
} }
} }
@@ -538,6 +478,7 @@ impl ConfigProvider {
command: String::new(), command: String::new(),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}); });
} }
@@ -563,6 +504,7 @@ impl ConfigProvider {
command: format!("CONFIG:profile:create:{}", name), command: format!("CONFIG:profile:create:{}", name),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}] }]
} }
@@ -591,6 +533,7 @@ impl ConfigProvider {
command: format!("CONFIG:profile:delete:{}", profile_name), command: format!("CONFIG:profile:delete:{}", profile_name),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
}, },
] ]
} }
@@ -627,6 +570,7 @@ impl ConfigProvider {
), ),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
} }
}) })
.collect() .collect()
@@ -754,6 +698,7 @@ fn nav_item(id: &str, name: &str, description: &str) -> LaunchItem {
command: String::new(), command: String::new(),
terminal: false, terminal: false,
tags: vec!["config".into(), "settings".into()], tags: vec!["config".into(), "settings".into()],
source: ItemSource::Core,
} }
} }
@@ -762,21 +707,10 @@ fn get_provider_bool(cfg: &Config, field: &str) -> bool {
match field { match field {
"applications" => cfg.providers.applications, "applications" => cfg.providers.applications,
"commands" => cfg.providers.commands, "commands" => cfg.providers.commands,
"uuctl" => cfg.providers.uuctl,
"calculator" => cfg.providers.calculator, "calculator" => cfg.providers.calculator,
"converter" => cfg.providers.converter, "converter" => cfg.providers.converter,
"frecency" => cfg.providers.frecency,
"websearch" => cfg.providers.websearch,
"system" => cfg.providers.system, "system" => cfg.providers.system,
"ssh" => cfg.providers.ssh, "frecency" => cfg.providers.frecency,
"clipboard" => cfg.providers.clipboard,
"bookmarks" => cfg.providers.bookmarks,
"emoji" => cfg.providers.emoji,
"scripts" => cfg.providers.scripts,
"files" => cfg.providers.files,
"media" => cfg.providers.media,
"weather" => cfg.providers.weather,
"pomodoro" => cfg.providers.pomodoro,
_ => false, _ => false,
} }
} }

View File

@@ -2,7 +2,7 @@ mod currency;
mod parser; mod parser;
mod units; mod units;
use super::{DynamicProvider, LaunchItem, ProviderType}; use super::{DynamicProvider, ItemSource, LaunchItem, ProviderType};
const PROVIDER_TYPE_ID: &str = "conv"; const PROVIDER_TYPE_ID: &str = "conv";
const PROVIDER_ICON: &str = "edit-find-replace-symbolic"; const PROVIDER_ICON: &str = "edit-find-replace-symbolic";
@@ -69,6 +69,7 @@ impl DynamicProvider for ConverterProvider {
), ),
terminal: false, terminal: false,
tags: vec!["converter".into(), "units".into()], tags: vec!["converter".into(), "units".into()],
source: ItemSource::Core,
}) })
.collect() .collect()
} }

View File

@@ -3,12 +3,11 @@
//! This module provides a `LuaProvider` struct that implements the `Provider` trait //! This module provides a `LuaProvider` struct that implements the `Provider` trait
//! by delegating to a Lua plugin's registered provider functions. //! by delegating to a Lua plugin's registered provider functions.
use std::cell::RefCell; use std::sync::{Arc, Mutex};
use std::rc::Rc;
use crate::plugins::{LoadedPlugin, PluginItem, ProviderRegistration}; use crate::plugins::{LoadedPlugin, PluginItem, ProviderRegistration};
use super::{LaunchItem, Provider, ProviderType}; use super::{ItemSource, LaunchItem, Provider, ProviderType};
/// A provider backed by a Lua plugin /// A provider backed by a Lua plugin
/// ///
@@ -17,15 +16,16 @@ use super::{LaunchItem, Provider, ProviderType};
pub struct LuaProvider { pub struct LuaProvider {
/// Provider registration info /// Provider registration info
registration: ProviderRegistration, registration: ProviderRegistration,
/// Reference to the loaded plugin (shared with other providers from same plugin) /// Reference to the loaded plugin (shared with other providers from same plugin).
plugin: Rc<RefCell<LoadedPlugin>>, /// Mutex serializes concurrent refresh calls; Arc allows sharing across threads.
plugin: Arc<Mutex<LoadedPlugin>>,
/// Cached items from last refresh /// Cached items from last refresh
items: Vec<LaunchItem>, items: Vec<LaunchItem>,
} }
impl LuaProvider { impl LuaProvider {
/// Create a new LuaProvider /// Create a new LuaProvider
pub fn new(registration: ProviderRegistration, plugin: Rc<RefCell<LoadedPlugin>>) -> Self { pub fn new(registration: ProviderRegistration, plugin: Arc<Mutex<LoadedPlugin>>) -> Self {
Self { Self {
registration, registration,
plugin, plugin,
@@ -35,6 +35,9 @@ impl LuaProvider {
/// Convert a PluginItem to a LaunchItem /// Convert a PluginItem to a LaunchItem
fn convert_item(&self, item: PluginItem) -> LaunchItem { fn convert_item(&self, item: PluginItem) -> LaunchItem {
if item.command.is_none() {
log::warn!("Plugin item '{}' has no command", item.name);
}
LaunchItem { LaunchItem {
id: item.id, id: item.id,
name: item.name, name: item.name,
@@ -44,6 +47,7 @@ impl LuaProvider {
command: item.command.unwrap_or_default(), command: item.command.unwrap_or_default(),
terminal: item.terminal, terminal: item.terminal,
tags: item.tags, tags: item.tags,
source: ItemSource::ScriptPlugin,
} }
} }
} }
@@ -63,7 +67,7 @@ impl Provider for LuaProvider {
return; return;
} }
let plugin = self.plugin.borrow(); let plugin = self.plugin.lock().unwrap();
match plugin.call_provider_refresh(&self.registration.name) { match plugin.call_provider_refresh(&self.registration.name) {
Ok(items) => { Ok(items) => {
self.items = items.into_iter().map(|i| self.convert_item(i)).collect(); self.items = items.into_iter().map(|i| self.convert_item(i)).collect();
@@ -89,17 +93,15 @@ impl Provider for LuaProvider {
} }
} }
// LuaProvider needs to be Send + Sync for the Provider trait. // LuaProvider is Send + Sync because:
// Rc<RefCell<>> is !Send and !Sync, but the ProviderManager RwLock ensures // - Arc<Mutex<LoadedPlugin>> is Send + Sync (LoadedPlugin: Send with mlua "send" feature)
// Rc<RefCell<>> is only accessed during refresh() (write lock = exclusive access). // - All other fields are Send + Sync
// Read-only operations (items(), search) only touch self.items (Vec<LaunchItem>). // No unsafe impl needed.
unsafe impl Send for LuaProvider {}
unsafe impl Sync for LuaProvider {}
/// Create LuaProviders from all registered providers in a plugin /// Create LuaProviders from all registered providers in a plugin
pub fn create_providers_from_plugin(plugin: Rc<RefCell<LoadedPlugin>>) -> Vec<Box<dyn Provider>> { pub fn create_providers_from_plugin(plugin: Arc<Mutex<LoadedPlugin>>) -> Vec<Box<dyn Provider>> {
let registrations = { let registrations = {
let p = plugin.borrow(); let p = plugin.lock().unwrap();
match p.get_provider_registrations() { match p.get_provider_registrations() {
Ok(regs) => regs, Ok(regs) => regs,
Err(e) => { Err(e) => {

View File

@@ -23,11 +23,13 @@ pub use native_provider::NativeProvider;
use chrono::Utc; use chrono::Utc;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use log::info; use log::{info, warn};
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
use log::debug; use log::debug;
use std::sync::{Arc, RwLock};
use crate::config::Config; use crate::config::Config;
use crate::data::FrecencyStore; use crate::data::FrecencyStore;
use crate::plugins::runtime_loader::LoadedRuntime; use crate::plugins::runtime_loader::LoadedRuntime;
@@ -42,6 +44,38 @@ pub struct ProviderDescriptor {
pub position: String, pub position: String,
} }
/// Trust level of a [`LaunchItem`]'s command, used to gate `sh -c` execution.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ItemSource {
/// Built-in provider compiled into owlry-core (trusted).
Core,
/// Native plugin (.so from /usr/lib/owlry/plugins/) — trusted at install time.
NativePlugin,
/// Script plugin (Lua/Rune from ~/.config/owlry/plugins/) — user-installed, untrusted.
ScriptPlugin,
}
impl ItemSource {
pub fn as_str(&self) -> &'static str {
match self {
ItemSource::Core => "core",
ItemSource::NativePlugin => "native_plugin",
ItemSource::ScriptPlugin => "script_plugin",
}
}
}
impl std::str::FromStr for ItemSource {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"native_plugin" => Ok(ItemSource::NativePlugin),
"script_plugin" => Ok(ItemSource::ScriptPlugin),
_ => Ok(ItemSource::Core),
}
}
}
/// Represents a single searchable/launchable item /// Represents a single searchable/launchable item
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LaunchItem { pub struct LaunchItem {
@@ -55,21 +89,29 @@ pub struct LaunchItem {
pub terminal: bool, pub terminal: bool,
/// Tags/categories for filtering (e.g., from .desktop Categories) /// Tags/categories for filtering (e.g., from .desktop Categories)
pub tags: Vec<String>, pub tags: Vec<String>,
/// Trust level — gates `sh -c` execution for script plugin items.
pub source: ItemSource,
} }
/// Provider type identifier for filtering and badge display /// Provider type identifier for filtering and badge display.
/// ///
/// Core types are built-in providers. All native plugins use Plugin(type_id). /// **Glossary:**
/// This keeps the core app free of plugin-specific knowledge. /// - *Provider*: An abstract source of [`LaunchItem`]s (interface).
/// - *Built-in provider*: A provider compiled into owlry-core (Application, Command).
/// - *Plugin*: External code (native `.so` or Lua/Rune script) loaded at runtime.
/// - *Plugin provider*: A provider registered by a plugin, identified by its `type_id`.
///
/// All plugin-provided types use `Plugin(type_id)`. The core has no hardcoded
/// knowledge of individual plugin types — this keeps the core app extensible.
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProviderType { pub enum ProviderType {
/// Built-in: Desktop applications from XDG directories /// Built-in provider: desktop applications from XDG data directories.
Application, Application,
/// Built-in: Shell commands from PATH /// Built-in provider: shell commands from `$PATH`.
Command, Command,
/// Built-in: Pipe-based input (dmenu compatibility) /// Built-in provider: pipe-based input for dmenu compatibility (client-local only).
Dmenu, Dmenu,
/// Plugin-defined provider type with its type_id (e.g., "calc", "weather", "emoji") /// Plugin provider with its declared `type_id` (e.g. `"calc"`, `"weather"`, `"emoji"`).
Plugin(String), Plugin(String),
} }
@@ -145,6 +187,9 @@ pub struct ProviderManager {
runtimes: Vec<LoadedRuntime>, runtimes: Vec<LoadedRuntime>,
/// Type IDs of providers from script runtimes (for hot-reload removal) /// Type IDs of providers from script runtimes (for hot-reload removal)
runtime_type_ids: std::collections::HashSet<String>, runtime_type_ids: std::collections::HashSet<String>,
/// Registry of native plugins that were loaded or suppressed at startup.
/// Used by `Request::PluginList` to report plugin status to the CLI.
pub plugin_registry: Vec<crate::ipc::PluginEntry>,
} }
impl ProviderManager { impl ProviderManager {
@@ -166,6 +211,7 @@ impl ProviderManager {
matcher: SkimMatcherV2::default(), matcher: SkimMatcherV2::default(),
runtimes: Vec::new(), runtimes: Vec::new(),
runtime_type_ids: std::collections::HashSet::new(), runtime_type_ids: std::collections::HashSet::new(),
plugin_registry: Vec::new(),
}; };
// Categorize native plugins based on their declared ProviderKind and ProviderPosition // Categorize native plugins based on their declared ProviderKind and ProviderPosition
@@ -207,9 +253,8 @@ impl ProviderManager {
/// Loads native plugins, creates core providers (Application + Command), /// Loads native plugins, creates core providers (Application + Command),
/// categorizes everything, and performs initial refresh. Used by the daemon /// categorizes everything, and performs initial refresh. Used by the daemon
/// which doesn't have the UI-driven setup path from `app.rs`. /// which doesn't have the UI-driven setup path from `app.rs`.
pub fn new_with_config(config: &Config) -> Self { pub fn new_with_config(config: Arc<RwLock<Config>>) -> Self {
use crate::plugins::native_loader::NativePluginLoader; use crate::plugins::native_loader::NativePluginLoader;
use std::sync::Arc;
// Create core providers // Create core providers
let mut core_providers: Vec<Box<dyn Provider>> = vec![ let mut core_providers: Vec<Box<dyn Provider>> = vec![
@@ -217,9 +262,23 @@ impl ProviderManager {
Box::new(CommandProvider::new()), Box::new(CommandProvider::new()),
]; ];
// Take a read lock once for configuration reads during setup.
let (disabled_plugins, calc_enabled, conv_enabled, sys_enabled) = match config.read() {
Ok(cfg) => (
cfg.plugins.disabled_plugins.clone(),
cfg.providers.calculator,
cfg.providers.converter,
cfg.providers.system,
),
Err(_) => {
warn!("Config lock poisoned during provider init; using defaults");
(Vec::new(), true, true, true)
}
};
// Load native plugins // Load native plugins
let mut loader = NativePluginLoader::new(); let mut loader = NativePluginLoader::new();
loader.set_disabled(config.plugins.disabled_plugins.clone()); loader.set_disabled(disabled_plugins);
let native_providers = match loader.discover() { let native_providers = match loader.discover() {
Ok(count) => { Ok(count) => {
@@ -304,23 +363,22 @@ impl ProviderManager {
// Built-in dynamic providers // Built-in dynamic providers
let mut builtin_dynamic: Vec<Box<dyn DynamicProvider>> = Vec::new(); let mut builtin_dynamic: Vec<Box<dyn DynamicProvider>> = Vec::new();
if config.providers.calculator { if calc_enabled {
builtin_dynamic.push(Box::new(calculator::CalculatorProvider)); builtin_dynamic.push(Box::new(calculator::CalculatorProvider));
info!("Registered built-in calculator provider"); info!("Registered built-in calculator provider");
} }
if config.providers.converter { if conv_enabled {
builtin_dynamic.push(Box::new(converter::ConverterProvider::new())); builtin_dynamic.push(Box::new(converter::ConverterProvider::new()));
info!("Registered built-in converter provider"); info!("Registered built-in converter provider");
} }
// Config editor — always enabled // Config editor — always enabled; shares the same Arc<RwLock<Config>>
let config_arc = std::sync::Arc::new(std::sync::RwLock::new(config.clone())); builtin_dynamic.push(Box::new(config_editor::ConfigProvider::new(Arc::clone(&config))));
builtin_dynamic.push(Box::new(config_editor::ConfigProvider::new(config_arc)));
info!("Registered built-in config editor provider"); info!("Registered built-in config editor provider");
// Built-in static providers // Built-in static providers
if config.providers.system { if sys_enabled {
core_providers.push(Box::new(system::SystemProvider::new())); core_providers.push(Box::new(system::SystemProvider::new()));
info!("Registered built-in system provider"); info!("Registered built-in system provider");
} }
@@ -345,15 +403,28 @@ impl ProviderManager {
ids ids
}; };
let mut suppressed_registry: Vec<crate::ipc::PluginEntry> = Vec::new();
let native_providers: Vec<NativeProvider> = native_providers let native_providers: Vec<NativeProvider> = native_providers
.into_iter() .into_iter()
.filter(|provider| { .filter(|provider| {
let type_id = provider.type_id(); let type_id = provider.type_id();
if builtin_ids.contains(type_id) { if builtin_ids.contains(type_id) {
info!( log::warn!(
"Skipping native plugin '{}' built-in provider exists", "Native plugin '{}' suppressed — a built-in provider with the same type ID exists",
type_id type_id
); );
suppressed_registry.push(crate::ipc::PluginEntry {
id: provider.plugin_id().to_string(),
name: provider.plugin_name().to_string(),
version: provider.plugin_version().to_string(),
runtime: "native".to_string(),
status: "suppressed".to_string(),
status_detail: format!(
"built-in provider '{}' takes precedence",
type_id
),
providers: vec![type_id.to_string()],
});
false false
} else { } else {
true true
@@ -361,10 +432,28 @@ impl ProviderManager {
}) })
.collect(); .collect();
// Capture active native plugin entries before ownership moves into Self::new().
let active_registry: Vec<crate::ipc::PluginEntry> = native_providers
.iter()
.map(|p| crate::ipc::PluginEntry {
id: p.plugin_id().to_string(),
name: p.plugin_name().to_string(),
version: p.plugin_version().to_string(),
runtime: "native".to_string(),
status: "active".to_string(),
status_detail: String::new(),
providers: vec![p.type_id().to_string()],
})
.collect();
let mut manager = Self::new(core_providers, native_providers); let mut manager = Self::new(core_providers, native_providers);
manager.builtin_dynamic = builtin_dynamic; manager.builtin_dynamic = builtin_dynamic;
manager.runtimes = runtimes; manager.runtimes = runtimes;
manager.runtime_type_ids = runtime_type_ids; manager.runtime_type_ids = runtime_type_ids;
manager.plugin_registry = active_registry;
manager.plugin_registry.extend(suppressed_registry);
manager manager
} }
@@ -378,11 +467,11 @@ impl ProviderManager {
!self.runtime_type_ids.contains(&type_str) !self.runtime_type_ids.contains(&type_str)
}); });
// Drop old runtimes (catch panics from runtime cleanup) // Drop old runtimes. Panics here will poison the ProviderManager RwLock,
// which is caught and reported by the watcher thread (see plugins/watcher.rs).
info!("Dropping old runtimes before reload");
let old_runtimes = std::mem::take(&mut self.runtimes); let old_runtimes = std::mem::take(&mut self.runtimes);
drop(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { drop(old_runtimes);
drop(old_runtimes);
})));
self.runtime_type_ids.clear(); self.runtime_type_ids.clear();
let owlry_version = env!("CARGO_PKG_VERSION"); let owlry_version = env!("CARGO_PKG_VERSION");
@@ -600,6 +689,7 @@ impl ProviderManager {
query: &str, query: &str,
max_results: usize, max_results: usize,
filter: &crate::filter::ProviderFilter, filter: &crate::filter::ProviderFilter,
tag_filter: Option<&str>,
) -> Vec<(LaunchItem, i64)> { ) -> Vec<(LaunchItem, i64)> {
// Collect items from core providers // Collect items from core providers
let core_items = self let core_items = self
@@ -615,16 +705,15 @@ impl ProviderManager {
.filter(|p| filter.is_active(p.provider_type())) .filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned()); .flat_map(|p| p.items().iter().cloned());
let all_items = core_items.chain(native_items).filter(|item| {
tag_filter.is_none_or(|t| item.tags.iter().any(|it| it == t))
});
if query.is_empty() { if query.is_empty() {
return core_items return all_items.take(max_results).map(|item| (item, 0)).collect();
.chain(native_items)
.take(max_results)
.map(|item| (item, 0))
.collect();
} }
let mut results: Vec<(LaunchItem, i64)> = core_items let mut results: Vec<(LaunchItem, i64)> = all_items
.chain(native_items)
.filter_map(|item| { .filter_map(|item| {
let name_score = self.matcher.fuzzy_match(&item.name, query); let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item let desc_score = item
@@ -1146,6 +1235,7 @@ mod tests {
command: format!("run-{}", id), command: format!("run-{}", id),
terminal: false, terminal: false,
tags: Vec::new(), tags: Vec::new(),
source: ItemSource::Core,
} }
} }

View File

@@ -13,7 +13,7 @@ use owlry_plugin_api::{
PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition, PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition,
}; };
use super::{LaunchItem, Provider, ProviderType}; use super::{ItemSource, LaunchItem, Provider, ProviderType};
use crate::plugins::native_loader::NativePlugin; use crate::plugins::native_loader::NativePlugin;
/// A provider backed by a native plugin /// A provider backed by a native plugin
@@ -50,6 +50,21 @@ impl NativeProvider {
ProviderType::Plugin(self.info.type_id.to_string()) ProviderType::Plugin(self.info.type_id.to_string())
} }
/// The ID of the plugin that owns this provider.
pub fn plugin_id(&self) -> &str {
self.plugin.id()
}
/// The human-readable name of the plugin that owns this provider.
pub fn plugin_name(&self) -> &str {
self.plugin.name()
}
/// The version string of the plugin that owns this provider.
pub fn plugin_version(&self) -> &str {
self.plugin.info.version.as_str()
}
/// Convert a plugin API item to a core LaunchItem /// Convert a plugin API item to a core LaunchItem
fn convert_item(&self, item: ApiPluginItem) -> LaunchItem { fn convert_item(&self, item: ApiPluginItem) -> LaunchItem {
LaunchItem { LaunchItem {
@@ -61,6 +76,7 @@ impl NativeProvider {
command: item.command.to_string(), command: item.command.to_string(),
terminal: item.terminal, terminal: item.terminal,
tags: item.keywords.iter().map(|s| s.to_string()).collect(), tags: item.keywords.iter().map(|s| s.to_string()).collect(),
source: ItemSource::NativePlugin,
} }
} }

View File

@@ -1,4 +1,4 @@
use super::{LaunchItem, Provider, ProviderType}; use super::{ItemSource, LaunchItem, Provider, ProviderType};
/// Built-in system provider. Returns a fixed set of power and session management actions. /// Built-in system provider. Returns a fixed set of power and session management actions.
/// ///
@@ -72,6 +72,7 @@ impl SystemProvider {
command: command.to_string(), command: command.to_string(),
terminal: false, terminal: false,
tags: vec!["system".into()], tags: vec!["system".into()],
source: ItemSource::Core,
}) })
.collect(); .collect();

View File

@@ -2,6 +2,7 @@ use std::io::{self, BufRead, BufReader, Write};
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream}; use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::time::Duration; use std::time::Duration;
use std::thread; use std::thread;
@@ -9,6 +10,73 @@ use std::thread;
/// Maximum allowed size for a single IPC request line (1 MiB). /// Maximum allowed size for a single IPC request line (1 MiB).
const MAX_REQUEST_SIZE: usize = 1_048_576; const MAX_REQUEST_SIZE: usize = 1_048_576;
/// Maximum number of concurrently active client connections.
const MAX_CONNECTIONS: usize = 16;
/// Tracks active connection count across all handler threads.
static ACTIVE_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
/// RAII guard that increments the connection counter on creation and decrements on drop.
struct ConnectionGuard;
impl ConnectionGuard {
/// Try to acquire a connection slot. Returns `None` if at capacity.
fn try_acquire() -> Option<Self> {
let prev = ACTIVE_CONNECTIONS.fetch_add(1, Ordering::SeqCst);
if prev >= MAX_CONNECTIONS {
ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::SeqCst);
None
} else {
Some(ConnectionGuard)
}
}
}
impl Drop for ConnectionGuard {
fn drop(&mut self) {
ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::SeqCst);
}
}
/// Read a newline-terminated line from `reader` without allocating beyond `max` bytes.
///
/// Unlike `BufRead::read_line`, this checks the size limit incrementally against
/// the internal buffer rather than after the full allocation. Returns `Ok(None)`
/// on clean EOF, `Err(InvalidData)` when `max` is exceeded before finding `\n`.
fn read_bounded_line(reader: &mut BufReader<UnixStream>, max: usize) -> io::Result<Option<String>> {
let mut buf: Vec<u8> = Vec::with_capacity(4096);
loop {
let available = reader.fill_buf()?;
if available.is_empty() {
return if buf.is_empty() {
Ok(None)
} else {
Ok(Some(String::from_utf8_lossy(&buf).into_owned()))
};
}
if let Some(pos) = available.iter().position(|&b| b == b'\n') {
if buf.len() + pos > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("request too large (exceeded {} bytes)", max),
));
}
buf.extend_from_slice(&available[..pos]);
reader.consume(pos + 1);
return Ok(Some(String::from_utf8_lossy(&buf).into_owned()));
}
let len = available.len();
if buf.len() + len > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("request too large (exceeded {} bytes)", max),
));
}
buf.extend_from_slice(available);
reader.consume(len);
}
}
use log::{error, info, warn}; use log::{error, info, warn};
use crate::config::Config; use crate::config::Config;
@@ -24,7 +92,7 @@ pub struct Server {
socket_path: PathBuf, socket_path: PathBuf,
provider_manager: Arc<RwLock<ProviderManager>>, provider_manager: Arc<RwLock<ProviderManager>>,
frecency: Arc<RwLock<FrecencyStore>>, frecency: Arc<RwLock<FrecencyStore>>,
config: Arc<Config>, config: Arc<RwLock<Config>>,
} }
impl Server { impl Server {
@@ -42,8 +110,10 @@ impl Server {
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))?; std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))?;
info!("IPC server listening on {:?}", socket_path); info!("IPC server listening on {:?}", socket_path);
let config = Config::load_or_default(); let config = Arc::new(RwLock::new(Config::load_or_default()));
let provider_manager = ProviderManager::new_with_config(&config); // Share config with native plugin loader so plugins can read their own config sections.
crate::plugins::native_loader::set_shared_config(Arc::clone(&config));
let provider_manager = ProviderManager::new_with_config(Arc::clone(&config));
let frecency = FrecencyStore::new(); let frecency = FrecencyStore::new();
Ok(Self { Ok(Self {
@@ -51,7 +121,7 @@ impl Server {
socket_path: socket_path.to_path_buf(), socket_path: socket_path.to_path_buf(),
provider_manager: Arc::new(RwLock::new(provider_manager)), provider_manager: Arc::new(RwLock::new(provider_manager)),
frecency: Arc::new(RwLock::new(frecency)), frecency: Arc::new(RwLock::new(frecency)),
config: Arc::new(config), config,
}) })
} }
@@ -60,18 +130,106 @@ impl Server {
// Start filesystem watcher for user plugin hot-reload // Start filesystem watcher for user plugin hot-reload
crate::plugins::watcher::start_watching(Arc::clone(&self.provider_manager)); crate::plugins::watcher::start_watching(Arc::clone(&self.provider_manager));
// SIGHUP handler: reload config from disk into the shared Arc<RwLock<Config>>.
{
use signal_hook::consts::SIGHUP;
use signal_hook::iterator::Signals;
let config = Arc::clone(&self.config);
let mut signals = Signals::new([SIGHUP])
.map_err(io::Error::other)?;
thread::spawn(move || {
for _sig in signals.forever() {
match Config::load() {
Ok(new_cfg) => {
match config.write() {
Ok(mut cfg) => {
*cfg = new_cfg;
info!("Config reloaded via SIGHUP");
}
Err(_) => {
warn!("SIGHUP: config lock poisoned; reload skipped");
}
}
}
Err(e) => {
warn!("SIGHUP: failed to reload config: {}", e);
}
}
}
});
}
// SIGTERM/SIGINT handler: save frecency before exiting.
// Replaces the ctrlc handler in main.rs so all signal management lives here.
{
use signal_hook::consts::{SIGINT, SIGTERM};
use signal_hook::iterator::Signals;
let frecency = Arc::clone(&self.frecency);
let socket_path = self.socket_path.clone();
let mut signals = Signals::new([SIGTERM, SIGINT])
.map_err(io::Error::other)?;
thread::spawn(move || {
// Block until we receive SIGTERM or SIGINT, then save and exit.
let _ = signals.forever().next();
match frecency.write() {
Ok(mut f) => {
if let Err(e) = f.save() {
warn!("Shutdown: frecency save failed: {}", e);
} else {
info!("Shutdown: frecency saved");
}
}
Err(_) => {
warn!("Shutdown: frecency lock poisoned; skipping save");
}
}
let _ = std::fs::remove_file(&socket_path);
std::process::exit(0);
});
}
// Periodic frecency auto-save every 5 minutes.
{
let frecency = Arc::clone(&self.frecency);
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(300));
match frecency.write() {
Ok(mut f) => {
if let Err(e) = f.save() {
warn!("Periodic frecency save failed: {}", e);
}
}
Err(_) => {
warn!("Periodic frecency save: lock poisoned; skipping");
}
}
});
}
info!("Server entering accept loop"); info!("Server entering accept loop");
for stream in self.listener.incoming() { for stream in self.listener.incoming() {
match stream { match stream {
Ok(stream) => { Ok(mut stream) => {
let pm = Arc::clone(&self.provider_manager); match ConnectionGuard::try_acquire() {
let frecency = Arc::clone(&self.frecency); Some(guard) => {
let config = Arc::clone(&self.config); let pm = Arc::clone(&self.provider_manager);
thread::spawn(move || { let frecency = Arc::clone(&self.frecency);
if let Err(e) = Self::handle_client(stream, pm, frecency, config) { let config = Arc::clone(&self.config);
warn!("Client handler error: {}", e); thread::spawn(move || {
let _guard = guard; // released on thread exit
if let Err(e) = Self::handle_client(stream, pm, frecency, config) {
warn!("Client handler error: {}", e);
}
});
} }
}); None => {
warn!("Connection limit reached ({} max); rejecting client", MAX_CONNECTIONS);
let resp = Response::Error {
message: format!("server busy: max {} concurrent connections", MAX_CONNECTIONS),
};
let _ = write_response(&mut stream, &resp);
}
}
} }
Err(e) => { Err(e) => {
error!("Failed to accept connection: {}", e); error!("Failed to accept connection: {}", e);
@@ -101,30 +259,25 @@ impl Server {
stream: UnixStream, stream: UnixStream,
pm: Arc<RwLock<ProviderManager>>, pm: Arc<RwLock<ProviderManager>>,
frecency: Arc<RwLock<FrecencyStore>>, frecency: Arc<RwLock<FrecencyStore>>,
config: Arc<Config>, config: Arc<RwLock<Config>>,
) -> io::Result<()> { ) -> io::Result<()> {
stream.set_read_timeout(Some(Duration::from_secs(30)))?; stream.set_read_timeout(Some(Duration::from_secs(30)))?;
let mut reader = BufReader::new(stream.try_clone()?); let mut reader = BufReader::new(stream.try_clone()?);
let mut writer = stream; let mut writer = stream;
loop { loop {
let mut line = String::new(); let line = match read_bounded_line(&mut reader, MAX_REQUEST_SIZE) {
let bytes_read = reader.read_line(&mut line)?; Ok(Some(l)) => l,
if bytes_read == 0 { Ok(None) => break,
break; Err(e) if e.kind() == io::ErrorKind::InvalidData => {
} let resp = Response::Error {
message: format!("request too large (max {} bytes)", MAX_REQUEST_SIZE),
if line.len() > MAX_REQUEST_SIZE { };
let resp = Response::Error { write_response(&mut writer, &resp)?;
message: format!( break;
"request too large ({} bytes, max {})", }
line.len(), Err(e) => return Err(e),
MAX_REQUEST_SIZE };
),
};
write_response(&mut writer, &resp)?;
break;
}
let trimmed = line.trim(); let trimmed = line.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
@@ -156,7 +309,7 @@ impl Server {
request: &Request, request: &Request,
pm: &Arc<RwLock<ProviderManager>>, pm: &Arc<RwLock<ProviderManager>>,
frecency: &Arc<RwLock<FrecencyStore>>, frecency: &Arc<RwLock<FrecencyStore>>,
config: &Arc<Config>, config: &Arc<RwLock<Config>>,
) -> Response { ) -> Response {
match request { match request {
Request::Query { text, modes } => { Request::Query { text, modes } => {
@@ -164,11 +317,22 @@ impl Server {
Some(m) => ProviderFilter::from_mode_strings(m), Some(m) => ProviderFilter::from_mode_strings(m),
None => ProviderFilter::all(), None => ProviderFilter::all(),
}; };
let max = config.general.max_results; let (max, weight) = {
let weight = config.providers.frecency_weight; let cfg = match config.read() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: config lock poisoned".into() },
};
(cfg.general.max_results, cfg.providers.frecency_weight)
};
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner()); let pm_guard = match pm.read() {
let frecency_guard = frecency.read().unwrap_or_else(|e| e.into_inner()); Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: provider lock poisoned".into() },
};
let frecency_guard = match frecency.read() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: frecency lock poisoned".into() },
};
let results = pm_guard.search_with_frecency( let results = pm_guard.search_with_frecency(
text, text,
max, max,
@@ -190,13 +354,19 @@ impl Server {
item_id, item_id,
provider: _, provider: _,
} => { } => {
let mut frecency_guard = frecency.write().unwrap_or_else(|e| e.into_inner()); let mut frecency_guard = match frecency.write() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: frecency lock poisoned".into() },
};
frecency_guard.record_launch(item_id); frecency_guard.record_launch(item_id);
Response::Ack Response::Ack
} }
Request::Providers => { Request::Providers => {
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner()); let pm_guard = match pm.read() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: provider lock poisoned".into() },
};
let descs = pm_guard.available_providers(); let descs = pm_guard.available_providers();
Response::Providers { Response::Providers {
list: descs.into_iter().map(descriptor_to_desc).collect(), list: descs.into_iter().map(descriptor_to_desc).collect(),
@@ -204,7 +374,10 @@ impl Server {
} }
Request::Refresh { provider } => { Request::Refresh { provider } => {
let mut pm_guard = pm.write().unwrap_or_else(|e| e.into_inner()); let mut pm_guard = match pm.write() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: provider lock poisoned".into() },
};
pm_guard.refresh_provider(provider); pm_guard.refresh_provider(provider);
Response::Ack Response::Ack
} }
@@ -215,7 +388,10 @@ impl Server {
} }
Request::Submenu { plugin_id, data } => { Request::Submenu { plugin_id, data } => {
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner()); let pm_guard = match pm.read() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: provider lock poisoned".into() },
};
match pm_guard.query_submenu_actions(plugin_id, data, plugin_id) { match pm_guard.query_submenu_actions(plugin_id, data, plugin_id) {
Some((_name, actions)) => Response::SubmenuItems { Some((_name, actions)) => Response::SubmenuItems {
items: actions items: actions
@@ -230,7 +406,10 @@ impl Server {
} }
Request::PluginAction { command } => { Request::PluginAction { command } => {
let pm_guard = pm.read().unwrap_or_else(|e| e.into_inner()); let pm_guard = match pm.read() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: provider lock poisoned".into() },
};
if pm_guard.execute_plugin_action(command) { if pm_guard.execute_plugin_action(command) {
Response::Ack Response::Ack
} else { } else {
@@ -239,6 +418,16 @@ impl Server {
} }
} }
} }
Request::PluginList => {
let pm_guard = match pm.read() {
Ok(g) => g,
Err(_) => return Response::Error { message: "internal error: provider lock poisoned".into() },
};
Response::PluginList {
entries: pm_guard.plugin_registry.clone(),
}
}
} }
} }
} }
@@ -272,6 +461,7 @@ fn launch_item_to_result(item: LaunchItem, score: i64) -> ResultItem {
command: Some(item.command), command: Some(item.command),
terminal: item.terminal, terminal: item.terminal,
tags: item.tags, tags: item.tags,
source: item.source.as_str().to_string(),
} }
} }
@@ -284,3 +474,57 @@ fn descriptor_to_desc(desc: crate::providers::ProviderDescriptor) -> ProviderDes
position: desc.position, position: desc.position,
} }
} }
#[cfg(test)]
mod tests {
use super::*;
// Wrap a Cursor in a BufReader backed by a UnixStream-like interface.
// Since read_bounded_line takes BufReader<UnixStream>, we test it indirectly
// through an in-memory byte slice via a helper.
fn bounded_line_from_bytes(data: &[u8], max: usize) -> io::Result<Option<String>> {
// Use a pipe to simulate UnixStream I/O.
use std::os::unix::net::UnixStream;
let (mut write_end, read_end) = UnixStream::pair()?;
write_end.write_all(data)?;
drop(write_end); // Signal EOF to reader
let mut reader = BufReader::new(read_end);
read_bounded_line(&mut reader, max)
}
#[test]
fn normal_line_within_limit() {
let result = bounded_line_from_bytes(b"hello world\n", 100).unwrap();
assert_eq!(result, Some("hello world".to_string()));
}
#[test]
fn line_at_exactly_max_succeeds() {
// "aaa...a\n" where content is exactly max bytes
let mut data = vec![b'a'; 100];
data.push(b'\n');
let result = bounded_line_from_bytes(&data, 100).unwrap();
assert_eq!(result, Some("a".repeat(100)));
}
#[test]
fn line_exceeding_max_errors() {
let mut data = vec![b'a'; 101];
data.push(b'\n');
let result = bounded_line_from_bytes(&data, 100);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
}
#[test]
fn empty_input_returns_none() {
let result = bounded_line_from_bytes(b"", 100).unwrap();
assert_eq!(result, None);
}
#[test]
fn no_trailing_newline_returns_content() {
let result = bounded_line_from_bytes(b"hello", 100).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
}

View File

@@ -47,6 +47,7 @@ fn test_results_response_roundtrip() {
command: Some("firefox".into()), command: Some("firefox".into()),
terminal: false, terminal: false,
tags: vec![], tags: vec![],
source: "core".into(),
}], }],
}; };
let json = serde_json::to_string(&resp).unwrap(); let json = serde_json::to_string(&resp).unwrap();
@@ -140,6 +141,7 @@ fn test_terminal_field_roundtrip() {
command: Some("htop".into()), command: Some("htop".into()),
terminal: true, terminal: true,
tags: vec![], tags: vec![],
source: "cmd".into(),
}; };
let json = serde_json::to_string(&item).unwrap(); let json = serde_json::to_string(&item).unwrap();
assert!(json.contains("\"terminal\":true")); assert!(json.contains("\"terminal\":true"));

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-lua" name = "owlry-lua"
version = "1.1.2" version = "1.1.3"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true
@@ -30,6 +30,9 @@ serde_json = "1.0"
# Version compatibility # Version compatibility
semver = "1" semver = "1"
# Logging
log = "0.4"
# HTTP client for plugins # HTTP client for plugins
reqwest = { version = "0.13", default-features = false, features = ["native-tls", "blocking", "json"] } reqwest = { version = "0.13", default-features = false, features = ["native-tls", "blocking", "json"] }

View File

@@ -50,3 +50,8 @@ pub fn call_refresh(lua: &Lua, provider_name: &str) -> LuaResult<Vec<PluginItem>
pub fn call_query(lua: &Lua, provider_name: &str, query: &str) -> LuaResult<Vec<PluginItem>> { pub fn call_query(lua: &Lua, provider_name: &str, query: &str) -> LuaResult<Vec<PluginItem>> {
provider::call_query(lua, provider_name, query) provider::call_query(lua, provider_name, query)
} }
/// Call the global `refresh()` function (for manifest-declared providers)
pub fn call_global_refresh(lua: &Lua) -> LuaResult<Vec<PluginItem>> {
provider::call_global_refresh(lua)
}

View File

@@ -76,6 +76,15 @@ fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> {
Ok(()) Ok(())
} }
/// Call the top-level `refresh()` global function (for manifest-declared providers)
pub fn call_global_refresh(lua: &Lua) -> LuaResult<Vec<PluginItem>> {
let globals = lua.globals();
match globals.get::<Function>("refresh") {
Ok(refresh_fn) => parse_items_result(refresh_fn.call(())?),
Err(_) => Ok(Vec::new()),
}
}
/// Get all registered providers /// Get all registered providers
pub fn get_registrations(lua: &Lua) -> LuaResult<Vec<ProviderRegistration>> { pub fn get_registrations(lua: &Lua) -> LuaResult<Vec<ProviderRegistration>> {
// Suppress unused warning // Suppress unused warning

View File

@@ -68,8 +68,11 @@ pub struct RuntimeHandle {
pub ptr: *mut (), pub ptr: *mut (),
} }
// SAFETY: LuaRuntimeState (pointed to by RuntimeHandle) contains mlua::Lua, which is
// Send when the "send" feature is enabled (enabled in Cargo.toml). RuntimeHandle itself
// is Copy and has no interior mutability — Sync is NOT implemented because concurrent
// access is serialized by Arc<Mutex<RuntimeHandle>> in the runtime loader.
unsafe impl Send for RuntimeHandle {} unsafe impl Send for RuntimeHandle {}
unsafe impl Sync for RuntimeHandle {}
impl RuntimeHandle { impl RuntimeHandle {
/// Create a null handle (reserved for error cases) /// Create a null handle (reserved for error cases)

View File

@@ -96,8 +96,28 @@ impl LoadedPlugin {
.as_ref() .as_ref()
.ok_or_else(|| "Plugin not initialized".to_string())?; .ok_or_else(|| "Plugin not initialized".to_string())?;
api::get_provider_registrations(lua) let mut regs = api::get_provider_registrations(lua)
.map_err(|e| format!("Failed to get registrations: {}", e)) .map_err(|e| format!("Failed to get registrations: {}", e))?;
// Fall back to manifest [[providers]] declarations when the script
// doesn't call owlry.provider.register() (new-style plugins)
if regs.is_empty() {
for decl in &self.manifest.providers {
regs.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()),
prefix: decl.prefix.clone(),
is_dynamic: decl.provider_type == "dynamic",
});
}
}
Ok(regs)
} }
/// Call a provider's refresh function /// Call a provider's refresh function
@@ -107,7 +127,17 @@ impl LoadedPlugin {
.as_ref() .as_ref()
.ok_or_else(|| "Plugin not initialized".to_string())?; .ok_or_else(|| "Plugin not initialized".to_string())?;
api::call_refresh(lua, provider_name).map_err(|e| format!("Refresh failed: {}", e)) let items = api::call_refresh(lua, provider_name)
.map_err(|e| format!("Refresh failed: {}", e))?;
// If the API path returned nothing, try calling the global refresh()
// function directly (new-style plugins with manifest [[providers]])
if items.is_empty() {
return api::call_global_refresh(lua)
.map_err(|e| format!("Refresh failed: {}", e));
}
Ok(items)
} }
/// Call a provider's query function /// Call a provider's query function
@@ -156,9 +186,18 @@ pub fn discover_plugins(
match PluginManifest::load(&manifest_path) { match PluginManifest::load(&manifest_path) {
Ok(manifest) => { Ok(manifest) => {
// Skip plugins whose entry point is not a Lua file
if !manifest.plugin.entry.ends_with(".lua") {
log::debug!(
"owlry-lua: Skipping non-Lua plugin at {} (entry: {})",
path.display(),
manifest.plugin.entry
);
continue;
}
let id = manifest.plugin.id.clone(); let id = manifest.plugin.id.clone();
if plugins.contains_key(&id) { if plugins.contains_key(&id) {
eprintln!( log::warn!(
"owlry-lua: Duplicate plugin ID '{}', skipping {}", "owlry-lua: Duplicate plugin ID '{}', skipping {}",
id, id,
path.display() path.display()
@@ -168,7 +207,7 @@ pub fn discover_plugins(
plugins.insert(id, (manifest, path)); plugins.insert(id, (manifest, path));
} }
Err(e) => { Err(e) => {
eprintln!( log::warn!(
"owlry-lua: Failed to load plugin at {}: {}", "owlry-lua: Failed to load plugin at {}: {}",
path.display(), path.display(),
e e
@@ -229,4 +268,79 @@ version = "1.0.0"
let plugins = discover_plugins(Path::new("/nonexistent/path")).unwrap(); let plugins = discover_plugins(Path::new("/nonexistent/path")).unwrap();
assert!(plugins.is_empty()); assert!(plugins.is_empty());
} }
#[test]
fn test_discover_skips_non_lua_plugins() {
let temp = TempDir::new().unwrap();
let plugins_dir = temp.path();
// Rune plugin — should be skipped by the Lua runtime
let rune_dir = plugins_dir.join("rune-plugin");
fs::create_dir_all(&rune_dir).unwrap();
fs::write(
rune_dir.join("plugin.toml"),
r#"
[plugin]
id = "rune-plugin"
name = "Rune Plugin"
version = "1.0.0"
entry_point = "main.rn"
[[providers]]
id = "rune-plugin"
name = "Rune Plugin"
"#,
)
.unwrap();
fs::write(rune_dir.join("main.rn"), "pub fn refresh() { [] }").unwrap();
// Lua plugin — should be discovered
create_test_plugin(plugins_dir, "lua-plugin");
let plugins = discover_plugins(plugins_dir).unwrap();
assert_eq!(plugins.len(), 1);
assert!(plugins.contains_key("lua-plugin"));
assert!(!plugins.contains_key("rune-plugin"));
}
#[test]
fn test_manifest_provider_fallback() {
let temp = TempDir::new().unwrap();
let plugin_dir = temp.path().join("test-plugin");
fs::create_dir_all(&plugin_dir).unwrap();
fs::write(
plugin_dir.join("plugin.toml"),
r#"
[plugin]
id = "test-plugin"
name = "Test Plugin"
version = "1.0.0"
entry_point = "main.lua"
[[providers]]
id = "test-plugin"
name = "Test Plugin"
type = "static"
type_id = "testplugin"
icon = "system-run"
prefix = ":tp"
"#,
)
.unwrap();
// Script that does NOT call owlry.provider.register()
fs::write(plugin_dir.join("main.lua"), "function refresh() return {} end").unwrap();
let manifest =
crate::manifest::PluginManifest::load(&plugin_dir.join("plugin.toml")).unwrap();
let mut plugin = LoadedPlugin::new(manifest, plugin_dir);
plugin.initialize().unwrap();
let regs = plugin.get_provider_registrations().unwrap();
assert_eq!(regs.len(), 1, "should fall back to [[providers]] declaration");
assert_eq!(regs[0].name, "test-plugin");
assert_eq!(regs[0].type_id, "testplugin");
assert_eq!(regs[0].prefix.as_deref(), Some(":tp"));
assert!(!regs[0].is_dynamic);
}
} }

View File

@@ -8,6 +8,10 @@ use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest { pub struct PluginManifest {
pub plugin: PluginInfo, pub plugin: PluginInfo,
/// Provider declarations from [[providers]] sections (new-style)
#[serde(default)]
pub providers: Vec<ProviderDecl>,
/// Legacy provides block (old-style)
#[serde(default)] #[serde(default)]
pub provides: PluginProvides, pub provides: PluginProvides,
#[serde(default)] #[serde(default)]
@@ -16,6 +20,26 @@ pub struct PluginManifest {
pub settings: HashMap<String, toml::Value>, pub settings: HashMap<String, toml::Value>,
} }
/// A provider declared in a [[providers]] section
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderDecl {
pub id: String,
pub name: String,
#[serde(default)]
pub prefix: Option<String>,
#[serde(default)]
pub icon: Option<String>,
/// "static" (default) or "dynamic"
#[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 /// Core plugin information
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo { pub struct PluginInfo {
@@ -127,6 +151,11 @@ impl PluginManifest {
)); ));
} }
// Lua plugins must have a .lua entry point
if !self.plugin.entry.ends_with(".lua") {
return Err("Entry point must be a .lua file for Lua plugins".to_string());
}
Ok(()) Ok(())
} }

View File

@@ -33,7 +33,8 @@ pub use abi_stable::std_types::{ROption, RStr, RString, RVec};
/// Current plugin API version - plugins must match this /// Current plugin API version - plugins must match this
/// v2: Added ProviderPosition for widget support /// v2: Added ProviderPosition for widget support
/// v3: Added priority field for plugin-declared result ordering /// v3: Added priority field for plugin-declared result ordering
pub const API_VERSION: u32 = 3; /// v4: Added get_config_string/int/bool to HostAPI for plugin config access
pub const API_VERSION: u32 = 4;
/// Plugin metadata returned by the info function /// Plugin metadata returned by the info function
#[repr(C)] #[repr(C)]
@@ -295,6 +296,18 @@ pub struct HostAPI {
/// Log a message at error level /// Log a message at error level
pub log_error: extern "C" fn(message: RStr<'_>), pub log_error: extern "C" fn(message: RStr<'_>),
/// Read a string value from this plugin's config section.
/// Parameters: plugin_id (the calling plugin's ID), key
/// Returns RSome(value) if set, RNone otherwise.
pub get_config_string:
extern "C" fn(plugin_id: RStr<'_>, key: RStr<'_>) -> ROption<RString>,
/// Read an integer value from this plugin's config section.
pub get_config_int: extern "C" fn(plugin_id: RStr<'_>, key: RStr<'_>) -> ROption<i64>,
/// Read a boolean value from this plugin's config section.
pub get_config_bool: extern "C" fn(plugin_id: RStr<'_>, key: RStr<'_>) -> ROption<bool>,
} }
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -378,6 +391,30 @@ pub fn log_error(message: &str) {
} }
} }
/// Read a string value from this plugin's config section (convenience wrapper).
/// `plugin_id` must match the ID the plugin declares in its `PluginInfo`.
pub fn get_config_string(plugin_id: &str, key: &str) -> Option<String> {
host_api().and_then(|api| {
(api.get_config_string)(RStr::from_str(plugin_id), RStr::from_str(key))
.into_option()
.map(|s| s.into_string())
})
}
/// Read an integer value from this plugin's config section (convenience wrapper).
pub fn get_config_int(plugin_id: &str, key: &str) -> Option<i64> {
host_api().and_then(|api| {
(api.get_config_int)(RStr::from_str(plugin_id), RStr::from_str(key)).into_option()
})
}
/// Read a boolean value from this plugin's config section (convenience wrapper).
pub fn get_config_bool(plugin_id: &str, key: &str) -> Option<bool> {
host_api().and_then(|api| {
(api.get_config_bool)(RStr::from_str(plugin_id), RStr::from_str(key)).into_option()
})
}
/// Helper macro for defining plugin vtables /// Helper macro for defining plugin vtables
/// ///
/// Usage: /// Usage:

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-rune" name = "owlry-rune"
version = "1.1.1" version = "1.1.4"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "Rune scripting runtime for owlry plugins" description = "Rune scripting runtime for owlry plugins"
@@ -15,7 +15,6 @@ owlry-plugin-api = { path = "../owlry-plugin-api" }
# Rune scripting language # Rune scripting language
rune = "0.14" rune = "0.14"
rune-modules = { version = "0.14", features = ["full"] }
# Logging # Logging
log = "0.4" log = "0.4"

View File

@@ -203,6 +203,7 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result<HashMap<String, Loade
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs;
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[test]
@@ -211,4 +212,81 @@ mod tests {
let plugins = discover_rune_plugins(temp.path()).unwrap(); let plugins = discover_rune_plugins(temp.path()).unwrap();
assert!(plugins.is_empty()); assert!(plugins.is_empty());
} }
#[test]
fn test_discover_skips_non_rune_plugins() {
let temp = TempDir::new().unwrap();
let plugins_dir = temp.path();
// Lua plugin — should be skipped by the Rune runtime
let lua_dir = plugins_dir.join("lua-plugin");
fs::create_dir_all(&lua_dir).unwrap();
fs::write(
lua_dir.join("plugin.toml"),
r#"
[plugin]
id = "lua-plugin"
name = "Lua Plugin"
version = "1.0.0"
entry_point = "main.lua"
[[providers]]
id = "lua-plugin"
name = "Lua Plugin"
"#,
)
.unwrap();
fs::write(lua_dir.join("main.lua"), "function refresh() return {} end").unwrap();
let plugins = discover_rune_plugins(plugins_dir).unwrap();
assert!(plugins.is_empty(), "Lua plugin should be skipped by Rune runtime");
}
#[test]
fn test_manifest_provider_fallback() {
let temp = TempDir::new().unwrap();
let plugin_dir = temp.path().join("test-plugin");
fs::create_dir_all(&plugin_dir).unwrap();
fs::write(
plugin_dir.join("plugin.toml"),
r#"
[plugin]
id = "test-plugin"
name = "Test Plugin"
version = "1.0.0"
entry_point = "main.rn"
[[providers]]
id = "test-plugin"
name = "Test Plugin"
type = "static"
type_id = "testplugin"
icon = "system-run"
prefix = ":tp"
"#,
)
.unwrap();
// Script that exports refresh() but doesn't call register_provider()
fs::write(
plugin_dir.join("main.rn"),
r#"use owlry::Item;
pub fn refresh() {
[]
}
"#,
)
.unwrap();
let manifest =
crate::manifest::PluginManifest::load(&plugin_dir.join("plugin.toml")).unwrap();
let plugin = LoadedPlugin::new(manifest, plugin_dir).unwrap();
let regs = plugin.provider_registrations();
assert_eq!(regs.len(), 1, "should fall back to [[providers]] declaration");
assert_eq!(regs[0].name, "test-plugin");
assert_eq!(regs[0].type_id, "testplugin");
assert_eq!(regs[0].prefix.as_deref(), Some(":tp"));
assert!(regs[0].is_static);
}
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry" name = "owlry"
version = "1.0.6" version = "1.0.8"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland" description = "A lightweight, owl-themed application launcher for Wayland"

View File

@@ -91,17 +91,20 @@ impl OwlryApp {
.iter() .iter()
.map(|s| ProviderFilter::mode_string_to_provider_type(s)) .map(|s| ProviderFilter::mode_string_to_provider_type(s))
.collect(); .collect();
let tabs = &config.borrow().general.tabs.clone();
if provider_types.len() == 1 { if provider_types.len() == 1 {
ProviderFilter::new( ProviderFilter::new(
Some(provider_types[0].clone()), Some(provider_types[0].clone()),
None, None,
&config.borrow().providers, &config.borrow().providers,
tabs,
) )
} else { } else {
ProviderFilter::new(None, Some(provider_types), &config.borrow().providers) ProviderFilter::new(None, Some(provider_types), &config.borrow().providers, tabs)
} }
} else { } else {
ProviderFilter::new(None, None, &config.borrow().providers) let tabs = config.borrow().general.tabs.clone();
ProviderFilter::new(None, None, &config.borrow().providers, &tabs)
}; };
let filter = Rc::new(RefCell::new(filter)); let filter = Rc::new(RefCell::new(filter));

View File

@@ -9,7 +9,7 @@ use owlry_core::config::Config;
use owlry_core::data::FrecencyStore; use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter; use owlry_core::filter::ProviderFilter;
use owlry_core::ipc::ResultItem; use owlry_core::ipc::ResultItem;
use owlry_core::providers::{LaunchItem, ProviderManager, ProviderType}; use owlry_core::providers::{ItemSource, LaunchItem, ProviderManager, ProviderType};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
/// Parameters needed to run a search query on a background thread. /// Parameters needed to run a search query on a background thread.
@@ -167,7 +167,7 @@ impl SearchBackend {
.collect() .collect()
} else { } else {
providers providers
.search_filtered(query, max_results, filter) .search_filtered(query, max_results, filter, None)
.into_iter() .into_iter()
.map(|(item, _)| item) .map(|(item, _)| item)
.collect() .collect()
@@ -230,7 +230,7 @@ impl SearchBackend {
.collect() .collect()
} else { } else {
providers providers
.search_filtered(query, max_results, filter) .search_filtered(query, max_results, filter, tag_filter)
.into_iter() .into_iter()
.map(|(item, _)| item) .map(|(item, _)| item)
.collect() .collect()
@@ -378,6 +378,7 @@ impl SearchBackend {
/// Convert an IPC ResultItem to the internal LaunchItem type. /// Convert an IPC ResultItem to the internal LaunchItem type.
fn result_to_launch_item(item: ResultItem) -> LaunchItem { fn result_to_launch_item(item: ResultItem) -> LaunchItem {
let provider: ProviderType = item.provider.parse().unwrap_or(ProviderType::Application); let provider: ProviderType = item.provider.parse().unwrap_or(ProviderType::Application);
let source: ItemSource = item.source.parse().unwrap_or(ItemSource::Core);
LaunchItem { LaunchItem {
id: item.id, id: item.id,
name: item.title, name: item.title,
@@ -395,5 +396,6 @@ fn result_to_launch_item(item: ResultItem) -> LaunchItem {
command: item.command.unwrap_or_default(), command: item.command.unwrap_or_default(),
terminal: item.terminal, terminal: item.terminal,
tags: item.tags, tags: item.tags,
source,
} }
} }

View File

@@ -3,7 +3,46 @@ use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use owlry_core::ipc::{ProviderDesc, Request, Response, ResultItem}; use owlry_core::ipc::{PluginEntry, ProviderDesc, Request, Response, ResultItem};
/// Maximum allowed size for a single IPC response line (4 MiB).
/// Larger than the request limit because responses carry result sets.
const MAX_RESPONSE_SIZE: usize = 4_194_304;
/// Read a newline-terminated line from `reader` without allocating beyond `max` bytes.
fn read_bounded_line(reader: &mut BufReader<UnixStream>, max: usize) -> io::Result<Option<String>> {
let mut buf: Vec<u8> = Vec::with_capacity(4096);
loop {
let available = reader.fill_buf()?;
if available.is_empty() {
return if buf.is_empty() {
Ok(None)
} else {
Ok(Some(String::from_utf8_lossy(&buf).into_owned()))
};
}
if let Some(pos) = available.iter().position(|&b| b == b'\n') {
if buf.len() + pos > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("response too large (exceeded {} bytes)", max),
));
}
buf.extend_from_slice(&available[..pos]);
reader.consume(pos + 1);
return Ok(Some(String::from_utf8_lossy(&buf).into_owned()));
}
let len = available.len();
if buf.len() + len > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("response too large (exceeded {} bytes)", max),
));
}
buf.extend_from_slice(available);
reader.consume(len);
}
}
/// IPC client that connects to the owlryd daemon Unix socket /// IPC client that connects to the owlryd daemon Unix socket
/// and provides typed methods for all IPC operations. /// and provides typed methods for all IPC operations.
@@ -157,6 +196,19 @@ impl CoreClient {
} }
} }
/// Query the daemon's native plugin registry (loaded + suppressed entries).
pub fn plugin_list(&mut self) -> io::Result<Vec<PluginEntry>> {
self.send(&Request::PluginList)?;
match self.receive()? {
Response::PluginList { entries } => Ok(entries),
Response::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected response to PluginList: {other:?}"),
)),
}
}
/// Query a plugin's submenu actions. /// Query a plugin's submenu actions.
pub fn submenu(&mut self, plugin_id: &str, data: &str) -> io::Result<Vec<ResultItem>> { pub fn submenu(&mut self, plugin_id: &str, data: &str) -> io::Result<Vec<ResultItem>> {
self.send(&Request::Submenu { self.send(&Request::Submenu {
@@ -186,14 +238,15 @@ impl CoreClient {
} }
fn receive(&mut self) -> io::Result<Response> { fn receive(&mut self) -> io::Result<Response> {
let mut line = String::new(); let line = match read_bounded_line(&mut self.reader, MAX_RESPONSE_SIZE)? {
self.reader.read_line(&mut line)?; Some(l) => l,
if line.is_empty() { None => {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::UnexpectedEof, io::ErrorKind::UnexpectedEof,
"daemon closed the connection", "daemon closed the connection",
)); ))
} }
};
serde_json::from_str(line.trim()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) serde_json::from_str(line.trim()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
} }
} }
@@ -256,6 +309,7 @@ mod tests {
command: Some("firefox".into()), command: Some("firefox".into()),
terminal: false, terminal: false,
tags: vec![], tags: vec![],
source: "app".into(),
}], }],
}; };
@@ -327,6 +381,7 @@ mod tests {
command: Some("systemctl --user start foo".into()), command: Some("systemctl --user start foo".into()),
terminal: false, terminal: false,
tags: vec![], tags: vec![],
source: "native_plugin".into(),
}], }],
}; };

View File

@@ -7,6 +7,7 @@ use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::cli::{PluginCommand as CliPluginCommand, PluginRuntime}; use crate::cli::{PluginCommand as CliPluginCommand, PluginRuntime};
use crate::client::CoreClient;
use owlry_core::config::Config; use owlry_core::config::Config;
use owlry_core::paths; use owlry_core::paths;
use owlry_core::plugins::manifest::{PluginManifest, discover_plugins}; use owlry_core::plugins::manifest::{PluginManifest, discover_plugins};
@@ -135,48 +136,53 @@ fn cmd_list_installed(
json_output: bool, json_output: bool,
) -> CommandResult { ) -> CommandResult {
let plugins_dir = paths::plugins_dir().ok_or("Could not determine plugins directory")?; let plugins_dir = paths::plugins_dir().ok_or("Could not determine plugins directory")?;
if !plugins_dir.exists() {
if json_output {
println!("[]");
} else {
println!("No plugins installed.");
}
return Ok(());
}
let discovered = discover_plugins(&plugins_dir).map_err(|e| e.to_string())?;
let config = Config::load().unwrap_or_default(); let config = Config::load().unwrap_or_default();
let disabled_list = &config.plugins.disabled_plugins; let disabled_list = &config.plugins.disabled_plugins;
let lua_available = lua_runtime_available(); let lua_available = lua_runtime_available();
let rune_available = rune_runtime_available(); let rune_available = rune_runtime_available();
let mut plugins: Vec<_> = discovered // ── Script plugins (from filesystem) ────────────────────────────────
.iter() let mut script_plugins: Vec<_> = if plugins_dir.exists() {
.map(|(id, (manifest, _path))| { let discovered = discover_plugins(&plugins_dir).map_err(|e| e.to_string())?;
let is_disabled = disabled_list.contains(id); discovered
let runtime = detect_runtime(manifest); .into_iter()
(id.clone(), manifest.clone(), is_disabled, runtime) .map(|(id, (manifest, _path))| {
}) let is_disabled = disabled_list.contains(&id);
.collect(); let runtime = detect_runtime(&manifest);
(id, manifest, is_disabled, runtime)
})
.collect()
} else {
Vec::new()
};
// Apply filters // Apply filters to script plugins
if only_enabled { if only_enabled {
plugins.retain(|(_, _, is_disabled, _)| !*is_disabled); script_plugins.retain(|(_, _, is_disabled, _)| !*is_disabled);
} }
if only_disabled { if only_disabled {
plugins.retain(|(_, _, is_disabled, _)| *is_disabled); script_plugins.retain(|(_, _, is_disabled, _)| *is_disabled);
} }
if let Some(rt) = runtime_filter { if let Some(ref rt) = runtime_filter {
plugins.retain(|(_, _, _, runtime)| *runtime == rt); let rt_clone = *rt;
script_plugins.retain(|(_, _, _, runtime)| *runtime == rt_clone);
} }
script_plugins.sort_by(|a, b| a.0.cmp(&b.0));
// Sort by ID // ── Native plugins (from daemon, if running) ─────────────────────────
plugins.sort_by(|a, b| a.0.cmp(&b.0)); // Skip native plugins if a runtime filter is active (they have no script runtime).
let native_entries = if runtime_filter.is_none() {
CoreClient::connect(&CoreClient::socket_path())
.ok()
.and_then(|mut client| client.plugin_list().ok())
.unwrap_or_default()
} else {
Vec::new()
};
// ── Output ───────────────────────────────────────────────────────────
if json_output { if json_output {
let json_list: Vec<_> = plugins let mut json_list: Vec<_> = script_plugins
.iter() .iter()
.map(|(id, manifest, is_disabled, runtime)| { .map(|(id, manifest, is_disabled, runtime)| {
let runtime_available = match runtime { let runtime_available = match runtime {
@@ -191,39 +197,82 @@ fn cmd_list_installed(
"enabled": !is_disabled, "enabled": !is_disabled,
"runtime": runtime.to_string(), "runtime": runtime.to_string(),
"runtime_available": runtime_available, "runtime_available": runtime_available,
"source": "script",
}) })
}) })
.collect(); .collect();
println!("{}", serde_json::to_string_pretty(&json_list).unwrap()); for entry in &native_entries {
} else if plugins.is_empty() { json_list.push(serde_json::json!({
println!("No plugins found."); "id": entry.id,
} else { "name": entry.name,
println!("Installed plugins:\n"); "version": entry.version,
for (id, manifest, is_disabled, runtime) in &plugins { "status": entry.status,
let status = if *is_disabled { " (disabled)" } else { "" }; "status_detail": entry.status_detail,
let runtime_available = match runtime { "runtime": entry.runtime,
PluginRuntime::Lua => lua_available, "providers": entry.providers,
PluginRuntime::Rune => rune_available, "source": "native",
}; }));
let runtime_status = if !runtime_available {
format!(" [{} - NOT INSTALLED]", runtime)
} else {
format!(" [{}]", runtime)
};
println!(
" {} v{}{}{}\n {}",
id,
manifest.plugin.version,
status,
runtime_status,
if manifest.plugin.description.is_empty() {
"No description"
} else {
&manifest.plugin.description
}
);
} }
println!("\n{} plugin(s) installed.", plugins.len()); println!("{}", serde_json::to_string_pretty(&json_list).unwrap());
} else {
let total = script_plugins.len() + native_entries.len();
if total == 0 {
println!("No plugins found.");
return Ok(());
}
if !script_plugins.is_empty() {
println!("Script plugins:\n");
for (id, manifest, is_disabled, runtime) in &script_plugins {
let status = if *is_disabled { " (disabled)" } else { "" };
let runtime_available = match runtime {
PluginRuntime::Lua => lua_available,
PluginRuntime::Rune => rune_available,
};
let runtime_status = if !runtime_available {
format!(" [{} - NOT INSTALLED]", runtime)
} else {
format!(" [{}]", runtime)
};
println!(
" {} v{}{}{}\n {}",
id,
manifest.plugin.version,
status,
runtime_status,
if manifest.plugin.description.is_empty() {
"No description"
} else {
&manifest.plugin.description
}
);
}
}
if !native_entries.is_empty() {
if !script_plugins.is_empty() {
println!();
}
println!("Native plugins:\n");
for entry in &native_entries {
let status_label = if entry.status == "suppressed" {
format!(" (suppressed: {})", entry.status_detail)
} else {
String::new()
};
let providers_label = if entry.providers.is_empty() {
String::new()
} else {
format!(" [{}]", entry.providers.join(", "))
};
println!(" {} v{}{}{}", entry.id, entry.version, status_label, providers_label);
if !entry.name.is_empty() && entry.name != entry.id {
println!(" {}", entry.name);
}
}
}
println!("\n{} plugin(s) total.", total);
} }
Ok(()) Ok(())
@@ -366,6 +415,14 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
"runtime": runtime.to_string(), "runtime": runtime.to_string(),
"runtime_available": runtime_available, "runtime_available": runtime_available,
"path": plugin_path.display().to_string(), "path": plugin_path.display().to_string(),
"providers": manifest.providers.iter().map(|p| serde_json::json!({
"id": p.id,
"name": p.name,
"type": p.provider_type,
"type_id": p.type_id,
"prefix": p.prefix,
"icon": p.icon,
})).collect::<Vec<_>>(),
"provides": { "provides": {
"providers": manifest.provides.providers, "providers": manifest.provides.providers,
"actions": manifest.provides.actions, "actions": manifest.provides.actions,
@@ -406,9 +463,13 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
); );
println!("Path: {}", plugin_path.display()); println!("Path: {}", plugin_path.display());
println!(); println!();
println!("Provides:"); println!("Providers:");
for p in &manifest.providers {
let prefix = p.prefix.as_deref().map(|s| format!(" ({})", s)).unwrap_or_default();
println!(" {} [{}]{}", p.name, p.provider_type, prefix);
}
if !manifest.provides.providers.is_empty() { if !manifest.provides.providers.is_empty() {
println!(" Providers: {}", manifest.provides.providers.join(", ")); println!(" {}", manifest.provides.providers.join(", "));
} }
if manifest.provides.actions { if manifest.provides.actions {
println!(" Actions: yes"); println!(" Actions: yes");
@@ -754,10 +815,16 @@ fn cmd_create(
let desc = description.unwrap_or("A custom owlry plugin"); let desc = description.unwrap_or("A custom owlry plugin");
let (entry_file, entry_ext) = match runtime { let (entry_file, entry_ext) = match runtime {
PluginRuntime::Lua => ("init.lua", "lua"), PluginRuntime::Lua => ("main.lua", "lua"),
PluginRuntime::Rune => ("init.rn", "rn"), PluginRuntime::Rune => ("main.rn", "rn"),
}; };
// Derive a short type_id from the plugin name (strip common prefixes)
let type_id = name
.strip_prefix("owlry-")
.unwrap_or(name)
.replace('-', "_");
// Create plugin.toml // Create plugin.toml
let manifest = format!( let manifest = format!(
r#"[plugin] r#"[plugin]
@@ -765,25 +832,21 @@ id = "{name}"
name = "{display}" name = "{display}"
version = "0.1.0" version = "0.1.0"
description = "{desc}" description = "{desc}"
author = "" entry_point = "{entry_file}"
owlry_version = ">=0.3.0"
entry = "{entry_file}"
[provides] [[providers]]
providers = ["{name}"] id = "{name}"
actions = false name = "{display}"
themes = [] type = "static"
hooks = false type_id = "{type_id}"
icon = "application-x-addon"
[permissions] # prefix = ":{type_id}"
network = false
filesystem = []
run_commands = []
"#, "#,
name = name, name = name,
display = display, display = display,
desc = desc, desc = desc,
entry_file = entry_file, entry_file = entry_file,
type_id = type_id,
); );
fs::write(plugin_dir.join("plugin.toml"), manifest) fs::write(plugin_dir.join("plugin.toml"), manifest)
@@ -792,91 +855,51 @@ run_commands = []
// Create entry point template based on runtime // Create entry point template based on runtime
match runtime { match runtime {
PluginRuntime::Lua => { PluginRuntime::Lua => {
let init_lua = format!( let main_lua = format!(
r#"-- {display} Plugin for Owlry r#"-- {display} Plugin for Owlry
-- {desc} -- {desc}
-- Register the provider function refresh()
owlry.provider.register({{ return {{
name = "{name}", {{
display_name = "{display}", id = "{name}:example",
type_id = "{name}", name = "Example Item",
default_icon = "application-x-executable", description = "This is an example item from {display}",
icon = "dialog-information",
refresh = function() command = "echo 'Hello from {name}!'",
-- Return a list of items tags = {{}},
return {{ }},
{{ }}
id = "{name}:example", end
name = "Example Item",
description = "This is an example item from {display}",
icon = "dialog-information",
command = "echo 'Hello from {name}!'",
terminal = false,
tags = {{}}
}}
}}
end
}})
owlry.log.info("{display} plugin loaded")
"#, "#,
name = name, name = name,
display = display, display = display,
desc = desc, desc = desc,
); );
fs::write(plugin_dir.join(entry_file), init_lua) fs::write(plugin_dir.join(entry_file), main_lua)
.map_err(|e| format!("Failed to write {}: {}", entry_file, e))?; .map_err(|e| format!("Failed to write {}: {}", entry_file, e))?;
} }
PluginRuntime::Rune => { PluginRuntime::Rune => {
// Note: Rune uses #{{ for object literals, so we build manually let main_rn = format!(
let init_rn = format!( r#"use owlry::Item;
r#"//! {display} Plugin for Owlry
//! {desc}
/// Plugin item structure pub fn refresh() {{
struct Item {{{{ let items = [];
id: String,
name: String,
description: String,
icon: String,
command: String,
terminal: bool,
tags: Vec<String>,
}}}}
/// Provider registration items.push(
pub fn register(owlry) {{{{ Item::new("{name}:example", "Example Item", "echo 'Hello from {name}!'")
owlry.provider.register(#{{{{ .description("This is an example item from {display}")
name: "{name}", .icon("dialog-information")
display_name: "{display}", .keywords(["example"]),
type_id: "{name}", );
default_icon: "application-x-executable",
refresh: || {{{{ items
// Return a list of items }}
[
Item {{{{
id: "{name}:example",
name: "Example Item",
description: "This is an example item from {display}",
icon: "dialog-information",
command: "echo 'Hello from {name}!'",
terminal: false,
tags: [],
}}}},
]
}}}},
}}}});
owlry.log.info("{display} plugin loaded");
}}}}
"#, "#,
name = name, name = name,
display = display, display = display,
desc = desc,
); );
fs::write(plugin_dir.join(entry_file), init_rn) fs::write(plugin_dir.join(entry_file), main_rn)
.map_err(|e| format!("Failed to write {}: {}", entry_file, e))?; .map_err(|e| format!("Failed to write {}: {}", entry_file, e))?;
} }
} }
@@ -955,13 +978,14 @@ fn cmd_validate(path: Option<&str>) -> CommandResult {
)); ));
} }
// Check for empty provides // Check for empty provides (accept either [[providers]] or [provides])
if manifest.provides.providers.is_empty() let has_providers = !manifest.providers.is_empty()
&& !manifest.provides.actions || !manifest.provides.providers.is_empty()
&& manifest.provides.themes.is_empty() || manifest.provides.actions
&& !manifest.provides.hooks || !manifest.provides.themes.is_empty()
{ || manifest.provides.hooks;
warnings.push("Plugin does not provide any features".to_string()); if !has_providers {
warnings.push("Plugin does not declare any providers".to_string());
} }
println!(" Plugin ID: {}", manifest.plugin.id); println!(" Plugin ID: {}", manifest.plugin.id);
@@ -1013,11 +1037,11 @@ fn cmd_runtimes() -> CommandResult {
if lua_available { if lua_available {
println!(" ✓ Lua - Installed"); println!(" ✓ Lua - Installed");
println!(" Package: owlry-lua"); println!(" Package: owlry-lua");
println!(" Entry point: init.lua"); println!(" Entry point: main.lua");
} else { } else {
println!(" ✗ Lua - Not installed"); println!(" ✗ Lua - Not installed");
println!(" Install: yay -S owlry-lua"); println!(" Install: yay -S owlry-lua");
println!(" Entry point: init.lua"); println!(" Entry point: main.lua");
} }
println!(); println!();
@@ -1026,11 +1050,11 @@ fn cmd_runtimes() -> CommandResult {
if rune_available { if rune_available {
println!(" ✓ Rune - Installed"); println!(" ✓ Rune - Installed");
println!(" Package: owlry-rune"); println!(" Package: owlry-rune");
println!(" Entry point: init.rn"); println!(" Entry point: main.rn");
} else { } else {
println!(" ✗ Rune - Not installed"); println!(" ✗ Rune - Not installed");
println!(" Install: yay -S owlry-rune"); println!(" Install: yay -S owlry-rune");
println!(" Entry point: init.rn"); println!(" Entry point: main.rn");
} }
println!(); println!();

View File

@@ -1,5 +1,5 @@
use log::debug; use log::debug;
use owlry_core::providers::{LaunchItem, Provider, ProviderType}; use owlry_core::providers::{ItemSource, LaunchItem, Provider, ProviderType};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
/// Provider for dmenu-style input from stdin /// Provider for dmenu-style input from stdin
@@ -102,6 +102,7 @@ impl Provider for DmenuProvider {
command: line.to_string(), command: line.to_string(),
terminal: false, terminal: false,
tags: Vec::new(), tags: Vec::new(),
source: ItemSource::Core,
}; };
self.items.push(item); self.items.push(item);

View File

@@ -1,5 +1,6 @@
use crate::backend::SearchBackend; use crate::backend::SearchBackend;
use crate::ui::ResultRow; use crate::ui::ResultRow;
use crate::ui::provider_meta;
use crate::ui::submenu; use crate::ui::submenu;
use gtk4::gdk::Key; use gtk4::gdk::Key;
use gtk4::prelude::*; use gtk4::prelude::*;
@@ -10,7 +11,7 @@ use gtk4::{
use log::info; use log::info;
use owlry_core::config::Config; use owlry_core::config::Config;
use owlry_core::filter::ProviderFilter; use owlry_core::filter::ProviderFilter;
use owlry_core::providers::{LaunchItem, ProviderType}; use owlry_core::providers::{ItemSource, LaunchItem, ProviderType};
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
use log::debug; use log::debug;
@@ -248,7 +249,12 @@ impl MainWindow {
// scroll position and selection. // scroll position and selection.
if !matches!(&*main_window.backend.borrow(), SearchBackend::Daemon(_)) { if !matches!(&*main_window.backend.borrow(), SearchBackend::Daemon(_)) {
let backend_for_auto = main_window.backend.clone(); let backend_for_auto = main_window.backend.clone();
gtk4::glib::timeout_add_local(std::time::Duration::from_secs(5), move || { let debounce_for_auto = main_window.debounce_source.clone();
gtk4::glib::timeout_add_local(std::time::Duration::from_secs(10), move || {
// Skip widget refresh while the user is actively typing.
if debounce_for_auto.borrow().is_some() {
return gtk4::glib::ControlFlow::Continue;
}
backend_for_auto.borrow_mut().refresh_widgets(); backend_for_auto.borrow_mut().refresh_widgets();
gtk4::glib::ControlFlow::Continue gtk4::glib::ControlFlow::Continue
}); });
@@ -315,86 +321,26 @@ impl MainWindow {
/// Get display label for a provider tab /// Get display label for a provider tab
/// Core types have fixed labels; plugins derive labels from type_id /// Core types have fixed labels; plugins derive labels from type_id
fn provider_tab_label(provider: &ProviderType) -> &'static str { fn provider_tab_label(provider: &ProviderType) -> &'static str {
match provider { provider_meta::meta_for(provider).tab_label
ProviderType::Application => "Apps",
ProviderType::Command => "Cmds",
ProviderType::Dmenu => "Dmenu",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "Bookmarks",
"calc" => "Calc",
"clipboard" => "Clip",
"emoji" => "Emoji",
"filesearch" => "Files",
"media" => "Media",
"pomodoro" => "Pomo",
"scripts" => "Scripts",
"ssh" => "SSH",
"system" => "System",
"uuctl" => "uuctl",
"weather" => "Weather",
"websearch" => "Web",
_ => "Plugin",
},
}
} }
/// Get CSS class for a provider
/// Core types have fixed CSS classes; plugins derive from type_id
fn provider_css_class(provider: &ProviderType) -> &'static str { fn provider_css_class(provider: &ProviderType) -> &'static str {
match provider { provider_meta::meta_for(provider).css_class
ProviderType::Application => "owlry-filter-app",
ProviderType::Command => "owlry-filter-cmd",
ProviderType::Dmenu => "owlry-filter-dmenu",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "owlry-filter-bookmark",
"calc" => "owlry-filter-calc",
"clipboard" => "owlry-filter-clip",
"emoji" => "owlry-filter-emoji",
"filesearch" => "owlry-filter-file",
"media" => "owlry-filter-media",
"pomodoro" => "owlry-filter-pomodoro",
"scripts" => "owlry-filter-script",
"ssh" => "owlry-filter-ssh",
"system" => "owlry-filter-sys",
"uuctl" => "owlry-filter-uuctl",
"weather" => "owlry-filter-weather",
"websearch" => "owlry-filter-web",
_ => "owlry-filter-plugin",
},
}
} }
fn build_placeholder(filter: &ProviderFilter) -> String { fn build_placeholder(filter: &ProviderFilter) -> String {
let active: Vec<&str> = filter let active: Vec<&str> = filter
.enabled_providers() .enabled_providers()
.iter() .iter()
.map(|p| match p { .map(|p| provider_meta::meta_for(p).search_noun)
ProviderType::Application => "applications",
ProviderType::Command => "commands",
ProviderType::Dmenu => "options",
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => "bookmarks",
"calc" => "calculator",
"clipboard" => "clipboard",
"emoji" => "emoji",
"filesearch" => "files",
"media" => "media",
"pomodoro" => "pomodoro",
"scripts" => "scripts",
"ssh" => "SSH hosts",
"system" => "system",
"uuctl" => "uuctl units",
"weather" => "weather",
"websearch" => "web",
_ => "plugins",
},
})
.collect(); .collect();
format!("Search {}...", active.join(", ")) format!("Search {}...", active.join(", "))
} }
/// Build dynamic hints based on enabled providers /// Build hints string for the status bar based on enabled built-in providers.
/// Plugin trigger hints (? web, / files, etc.) are not included here since
/// plugin availability is not tracked in ProvidersConfig.
fn build_hints(config: &owlry_core::config::ProvidersConfig) -> String { fn build_hints(config: &owlry_core::config::ProvidersConfig) -> String {
let mut parts: Vec<String> = vec![ let mut parts: Vec<String> = vec![
"Tab: cycle".to_string(), "Tab: cycle".to_string(),
@@ -403,38 +349,14 @@ impl MainWindow {
"Esc: close".to_string(), "Esc: close".to_string(),
]; ];
// Add trigger hints for enabled dynamic providers
if config.calculator { if config.calculator {
parts.push("= calc".to_string()); parts.push("= calc".to_string());
} }
if config.websearch { if config.converter {
parts.push("? web".to_string()); parts.push("> conv".to_string());
} }
if config.files {
parts.push("/ files".to_string());
}
// Add prefix hints for static providers
let mut prefixes = Vec::new();
if config.system { if config.system {
prefixes.push(":sys"); parts.push(":sys".to_string());
}
if config.emoji {
prefixes.push(":emoji");
}
if config.ssh {
prefixes.push(":ssh");
}
if config.clipboard {
prefixes.push(":clip");
}
if config.bookmarks {
prefixes.push(":bm");
}
// Only show first few prefixes to avoid overflow
if !prefixes.is_empty() {
parts.push(prefixes[..prefixes.len().min(4)].join(" "));
} }
parts.join(" ") parts.join(" ")
@@ -1159,6 +1081,7 @@ impl MainWindow {
for provider in tab_order { for provider in tab_order {
f.enable(provider.clone()); f.enable(provider.clone());
} }
f.restore_all_mode();
} }
for (_, button) in buttons.borrow().iter() { for (_, button) in buttons.borrow().iter() {
button.set_active(true); button.set_active(true);
@@ -1368,6 +1291,36 @@ impl MainWindow {
item.terminal, item.provider, item.id item.terminal, item.provider, item.id
); );
// Reject script plugin commands that don't match the known-safe allowlist.
// Script plugins (Lua/Rune user plugins) are untrusted code; only allow
// patterns that can't escalate privileges or exfiltrate data.
if item.source == ItemSource::ScriptPlugin {
let cmd = &item.command;
let allowed = cmd.is_empty()
|| cmd.starts_with("xdg-open ")
|| cmd.starts_with("wl-copy")
|| cmd.starts_with("wl-paste")
|| cmd.starts_with("SUBMENU:")
|| cmd.starts_with('!');
if !allowed {
let msg = format!(
"Blocked untrusted script plugin command from '{}': {}",
item.name, cmd
);
log::warn!("{}", msg);
owlry_core::notify::notify("Command blocked", &msg);
return;
}
}
// Reject items with no command — nothing to execute.
if item.command.is_empty() && !matches!(item.provider, ProviderType::Application) {
let msg = format!("Item '{}' has no command; cannot launch", item.name);
log::warn!("{}", msg);
owlry_core::notify::notify("Launch failed", &msg);
return;
}
// Check if this is a desktop application (has .desktop file as ID) // Check if this is a desktop application (has .desktop file as ID)
let is_desktop_app = let is_desktop_app =
matches!(item.provider, ProviderType::Application) && item.id.ends_with(".desktop"); matches!(item.provider, ProviderType::Application) && item.id.ends_with(".desktop");

View File

@@ -1,4 +1,5 @@
mod main_window; mod main_window;
pub mod provider_meta;
mod result_row; mod result_row;
pub mod submenu; pub mod submenu;

View File

@@ -0,0 +1,101 @@
use owlry_core::providers::ProviderType;
/// Display metadata for a provider.
pub struct ProviderMeta {
pub tab_label: &'static str,
pub css_class: &'static str,
pub search_noun: &'static str,
}
/// Return display metadata for a provider type.
pub fn meta_for(provider: &ProviderType) -> ProviderMeta {
match provider {
ProviderType::Application => ProviderMeta {
tab_label: "Apps",
css_class: "owlry-filter-app",
search_noun: "applications",
},
ProviderType::Command => ProviderMeta {
tab_label: "Cmds",
css_class: "owlry-filter-cmd",
search_noun: "commands",
},
ProviderType::Dmenu => ProviderMeta {
tab_label: "Dmenu",
css_class: "owlry-filter-dmenu",
search_noun: "options",
},
ProviderType::Plugin(type_id) => match type_id.as_str() {
"bookmarks" => ProviderMeta {
tab_label: "Bookmarks",
css_class: "owlry-filter-bookmark",
search_noun: "bookmarks",
},
"calc" => ProviderMeta {
tab_label: "Calc",
css_class: "owlry-filter-calc",
search_noun: "calculator",
},
"clipboard" => ProviderMeta {
tab_label: "Clip",
css_class: "owlry-filter-clip",
search_noun: "clipboard",
},
"emoji" => ProviderMeta {
tab_label: "Emoji",
css_class: "owlry-filter-emoji",
search_noun: "emoji",
},
"filesearch" => ProviderMeta {
tab_label: "Files",
css_class: "owlry-filter-file",
search_noun: "files",
},
"media" => ProviderMeta {
tab_label: "Media",
css_class: "owlry-filter-media",
search_noun: "media",
},
"pomodoro" => ProviderMeta {
tab_label: "Pomo",
css_class: "owlry-filter-pomodoro",
search_noun: "pomodoro",
},
"scripts" => ProviderMeta {
tab_label: "Scripts",
css_class: "owlry-filter-script",
search_noun: "scripts",
},
"ssh" => ProviderMeta {
tab_label: "SSH",
css_class: "owlry-filter-ssh",
search_noun: "SSH hosts",
},
"system" => ProviderMeta {
tab_label: "System",
css_class: "owlry-filter-sys",
search_noun: "system",
},
"uuctl" => ProviderMeta {
tab_label: "uuctl",
css_class: "owlry-filter-uuctl",
search_noun: "uuctl units",
},
"weather" => ProviderMeta {
tab_label: "Weather",
css_class: "owlry-filter-weather",
search_noun: "weather",
},
"websearch" => ProviderMeta {
tab_label: "Web",
css_class: "owlry-filter-web",
search_noun: "web",
},
_ => ProviderMeta {
tab_label: "Plugin",
css_class: "owlry-filter-plugin",
search_noun: "plugins",
},
},
}
}

View File

@@ -66,7 +66,7 @@ pub fn is_submenu_item(item: &LaunchItem) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use owlry_core::providers::ProviderType; use owlry_core::providers::{ItemSource, ProviderType};
#[test] #[test]
fn test_parse_submenu_command() { fn test_parse_submenu_command() {
@@ -94,6 +94,7 @@ mod tests {
command: "SUBMENU:plugin:data".to_string(), command: "SUBMENU:plugin:data".to_string(),
terminal: false, terminal: false,
tags: vec![], tags: vec![],
source: ItemSource::NativePlugin,
}; };
assert!(is_submenu_item(&submenu_item)); assert!(is_submenu_item(&submenu_item));
@@ -106,6 +107,7 @@ mod tests {
command: "some-command".to_string(), command: "some-command".to_string(),
terminal: false, terminal: false,
tags: vec![], tags: vec![],
source: ItemSource::NativePlugin,
}; };
assert!(!is_submenu_item(&normal_item)); assert!(!is_submenu_item(&normal_item));
} }

View File

@@ -59,8 +59,10 @@ max_results = 100
# Requires: uwsm to be installed # Requires: uwsm to be installed
# use_uwsm = true # use_uwsm = true
# Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.) # Header tabs provider tabs shown in the UI bar (Ctrl+1..9 to toggle)
# Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web # Core values: app, cmd, dmenu
# Plugin values: uuctl, calc, clip, emoji, file, script, ssh, sys, web, bm
# Any installed plugin's type_id is valid (e.g. "weather", "media")
tabs = ["app", "cmd", "uuctl"] tabs = ["app", "cmd", "uuctl"]
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
@@ -140,55 +142,25 @@ disabled_plugins = []
# PROVIDERS # PROVIDERS
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# #
# Enable/disable providers and configure their settings. # Controls built-in providers only. Plugins are enabled/disabled via
# Core providers (applications, commands) are built into the binary. # [plugins] disabled_plugins or `owlry plugin enable/disable <name>`.
# Plugin providers require their .so to be installed.
[providers] [providers]
# Core providers (always available) # Core providers
applications = true # .desktop applications from XDG dirs applications = true # .desktop applications from XDG dirs
commands = true # Executables from $PATH commands = true # Executables from $PATH
# Frecency - boost frequently/recently used items # Built-in providers (compiled into owlry-core)
calculator = true # Math expressions (= or calc trigger)
converter = true # Unit/currency conversion (> trigger)
system = true # System actions: shutdown, reboot, lock, etc.
# Frecency — boost frequently/recently used items
# Data stored in: ~/.local/share/owlry/frecency.json # Data stored in: ~/.local/share/owlry/frecency.json
frecency = true frecency = true
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
# ───────────────────────────────────────────────────────────────────────── # Web search engine (used by owlry-plugin-websearch)
# Plugin provider toggles (require corresponding plugin installed)
# ─────────────────────────────────────────────────────────────────────────
uuctl = true # systemd user units
system = true # System commands (shutdown, reboot, etc.)
ssh = true # SSH hosts from ~/.ssh/config
clipboard = true # Clipboard history (requires cliphist)
bookmarks = true # Browser bookmarks
emoji = true # Emoji picker
scripts = true # Custom scripts from ~/.local/share/owlry/scripts/
files = true # File search (requires fd or mlocate)
calculator = true # Calculator (= expression)
websearch = true # Web search (? query)
# ─────────────────────────────────────────────────────────────────────────
# Widget providers (displayed at top of results)
# ─────────────────────────────────────────────────────────────────────────
media = true # MPRIS media player controls
weather = false # Weather widget (disabled by default)
pomodoro = false # Pomodoro timer (disabled by default)
# ─────────────────────────────────────────────────────────────────────────
# Provider settings
# ─────────────────────────────────────────────────────────────────────────
# Web search engine
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia # Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
# Or custom URL: "https://search.example.com/?q={query}" # Or a custom URL: "https://search.example.com/?q={query}"
search_engine = "duckduckgo" search_engine = "duckduckgo"
# Weather settings (when weather = true)
# weather_provider = "wttr.in" # Options: wttr.in, openweathermap, open-meteo
# weather_location = "Berlin" # City name or coordinates
# weather_api_key = "" # Required for openweathermap
# Pomodoro settings (when pomodoro = true)
# pomodoro_work_mins = 25 # Work session duration
# pomodoro_break_mins = 5 # Break duration

View File

@@ -252,6 +252,7 @@ aur-publish-all:
for dir in aur/*/; do for dir in aur/*/; do
pkg=$(basename "$dir") pkg=$(basename "$dir")
[ -d "$dir/.git" ] || continue [ -d "$dir/.git" ] || continue
[ -f "$dir/PKGBUILD" ] || continue
echo "=== $pkg ===" echo "=== $pkg ==="
just aur-publish-pkg "$pkg" just aur-publish-pkg "$pkg"
echo "" echo ""

View File

@@ -135,6 +135,45 @@ done
[[ ${#INPUT_PKGS[@]} -eq 0 ]] && usage [[ ${#INPUT_PKGS[@]} -eq 0 ]] && usage
# ─── Inject deduplication ────────────────────────────────────────────────────
# Extract the package name from a .pkg.tar.zst filename.
# Arch package filenames follow: {pkgname}-{pkgver}-{pkgrel}-{arch}.pkg.tar.zst
# pkgver is guaranteed to have no dashes, so stripping the last three
# dash-separated segments leaves pkgname.
pkg_name_from_file() {
local base
base=$(basename "$1" .pkg.tar.zst)
base="${base%-*}" # strip arch
base="${base%-*}" # strip pkgrel
base="${base%-*}" # strip pkgver (no dashes in pkgver by Arch policy)
echo "$base"
}
# Deduplicate a list of .pkg.tar.zst paths by package name.
# When the same package name appears more than once, keep the highest version
# (determined by sort -V on the filenames) and warn about the dropped ones.
dedup_inject_files() {
[[ $# -eq 0 ]] && return 0
local -A best=()
local f name winner
for f in "$@"; do
name=$(pkg_name_from_file "$f")
if [[ -v "best[$name]" ]]; then
winner=$(printf '%s\n%s\n' "${best[$name]}" "$f" | sort -V | tail -1)
if [[ "$winner" == "$f" ]]; then
warn "Dropping duplicate inject (older): $(basename "${best[$name]}")"
best[$name]="$f"
else
warn "Dropping duplicate inject (older): $(basename "$f")"
fi
else
best[$name]="$f"
fi
done
printf '%s\n' "${best[@]}"
}
# ─── Dependency resolution ─────────────────────────────────────────────────── # ─── Dependency resolution ───────────────────────────────────────────────────
# Return the names of local AUR packages that PKG depends on. # Return the names of local AUR packages that PKG depends on.
@@ -296,6 +335,11 @@ build_one() {
# ─── Main ──────────────────────────────────────────────────────────────────── # ─── Main ────────────────────────────────────────────────────────────────────
# Deduplicate external inject files by package name (keep highest version)
if [[ ${#EXTRA_INJECT[@]} -gt 1 ]]; then
mapfile -t EXTRA_INJECT < <(dedup_inject_files "${EXTRA_INJECT[@]}")
fi
# Validate all requested packages exist # Validate all requested packages exist
for pkg in "${INPUT_PKGS[@]}"; do for pkg in "${INPUT_PKGS[@]}"; do
[[ -d "$AUR_DIR/$pkg" && -f "$AUR_DIR/$pkg/PKGBUILD" ]] \ [[ -d "$AUR_DIR/$pkg" && -f "$AUR_DIR/$pkg/PKGBUILD" ]] \

View File

@@ -6,6 +6,7 @@ After=graphical-session.target
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/owlryd ExecStart=/usr/bin/owlryd
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure Restart=on-failure
RestartSec=3 RestartSec=3
Environment=RUST_LOG=warn Environment=RUST_LOG=warn