Compare commits
20 Commits
owlry-lua-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7275fcab35 | |||
| 4d7e913657 | |||
| f8d011447e | |||
| 9163b1ea6c | |||
| 6586f5d6c2 | |||
| a6e94deb3c | |||
| de74cac67d | |||
| 2f396306fd | |||
| 133d5264ea | |||
| a16c3a0523 | |||
| 33b4f410e5 | |||
| a7683f16bf | |||
| 178f81082a | |||
| 7863de9971 | |||
| dacc194d02 | |||
| 5871609c73 | |||
| e3c4988e01 | |||
| 46b5d8518f | |||
| 95a698225c | |||
| 709e1b04cb |
545
Cargo.lock
generated
545
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
10
aur/owlry-core/.gitignore
vendored
Normal 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/
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
10
aur/owlry-lua/.gitignore
vendored
Normal 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/
|
||||||
@@ -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
@@ -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
10
aur/owlry-rune/.gitignore
vendored
Normal 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/
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
10
aur/owlry/.gitignore
vendored
Normal 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/
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,51 +47,24 @@ 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.
|
||||||
|
let mut set: HashSet<ProviderType> = tabs
|
||||||
|
.iter()
|
||||||
|
.map(|s| Self::mode_string_to_provider_type(s))
|
||||||
|
.collect();
|
||||||
|
if set.is_empty() {
|
||||||
if config_providers.applications {
|
if config_providers.applications {
|
||||||
set.insert(ProviderType::Application);
|
set.insert(ProviderType::Application);
|
||||||
}
|
}
|
||||||
if config_providers.commands {
|
if config_providers.commands {
|
||||||
set.insert(ProviderType::Command);
|
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);
|
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.)
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
|||||||
@@ -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#"
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,40 +46,42 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +130,107 @@ 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) => {
|
||||||
|
match ConnectionGuard::try_acquire() {
|
||||||
|
Some(guard) => {
|
||||||
let pm = Arc::clone(&self.provider_manager);
|
let pm = Arc::clone(&self.provider_manager);
|
||||||
let frecency = Arc::clone(&self.frecency);
|
let frecency = Arc::clone(&self.frecency);
|
||||||
let config = Arc::clone(&self.config);
|
let config = Arc::clone(&self.config);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
let _guard = guard; // released on thread exit
|
||||||
if let Err(e) = Self::handle_client(stream, pm, frecency, config) {
|
if let Err(e) = Self::handle_client(stream, pm, frecency, config) {
|
||||||
warn!("Client handler error: {}", e);
|
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 => {
|
||||||
}
|
|
||||||
|
|
||||||
if line.len() > MAX_REQUEST_SIZE {
|
|
||||||
let resp = Response::Error {
|
let resp = Response::Error {
|
||||||
message: format!(
|
message: format!("request too large (max {} bytes)", MAX_REQUEST_SIZE),
|
||||||
"request too large ({} bytes, max {})",
|
|
||||||
line.len(),
|
|
||||||
MAX_REQUEST_SIZE
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
write_response(&mut writer, &resp)?;
|
write_response(&mut writer, &resp)?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
let discovered = discover_plugins(&plugins_dir).map_err(|e| e.to_string())?;
|
||||||
|
discovered
|
||||||
|
.into_iter()
|
||||||
.map(|(id, (manifest, _path))| {
|
.map(|(id, (manifest, _path))| {
|
||||||
let is_disabled = disabled_list.contains(id);
|
let is_disabled = disabled_list.contains(&id);
|
||||||
let runtime = detect_runtime(manifest);
|
let runtime = detect_runtime(&manifest);
|
||||||
(id.clone(), manifest.clone(), is_disabled, runtime)
|
(id, manifest, is_disabled, runtime)
|
||||||
})
|
})
|
||||||
.collect();
|
.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,15 +197,33 @@ 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();
|
||||||
|
for entry in &native_entries {
|
||||||
|
json_list.push(serde_json::json!({
|
||||||
|
"id": entry.id,
|
||||||
|
"name": entry.name,
|
||||||
|
"version": entry.version,
|
||||||
|
"status": entry.status,
|
||||||
|
"status_detail": entry.status_detail,
|
||||||
|
"runtime": entry.runtime,
|
||||||
|
"providers": entry.providers,
|
||||||
|
"source": "native",
|
||||||
|
}));
|
||||||
|
}
|
||||||
println!("{}", serde_json::to_string_pretty(&json_list).unwrap());
|
println!("{}", serde_json::to_string_pretty(&json_list).unwrap());
|
||||||
} else if plugins.is_empty() {
|
|
||||||
println!("No plugins found.");
|
|
||||||
} else {
|
} else {
|
||||||
println!("Installed plugins:\n");
|
let total = script_plugins.len() + native_entries.len();
|
||||||
for (id, manifest, is_disabled, runtime) in &plugins {
|
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 status = if *is_disabled { " (disabled)" } else { "" };
|
||||||
let runtime_available = match runtime {
|
let runtime_available = match runtime {
|
||||||
PluginRuntime::Lua => lua_available,
|
PluginRuntime::Lua => lua_available,
|
||||||
@@ -223,7 +247,32 @@ fn cmd_list_installed(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
println!("\n{} plugin(s) installed.", plugins.len());
|
}
|
||||||
|
|
||||||
|
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,19 +855,11 @@ 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({{
|
|
||||||
name = "{name}",
|
|
||||||
display_name = "{display}",
|
|
||||||
type_id = "{name}",
|
|
||||||
default_icon = "application-x-executable",
|
|
||||||
|
|
||||||
refresh = function()
|
|
||||||
-- Return a list of items
|
|
||||||
return {{
|
return {{
|
||||||
{{
|
{{
|
||||||
id = "{name}:example",
|
id = "{name}:example",
|
||||||
@@ -812,71 +867,39 @@ owlry.provider.register({{
|
|||||||
description = "This is an example item from {display}",
|
description = "This is an example item from {display}",
|
||||||
icon = "dialog-information",
|
icon = "dialog-information",
|
||||||
command = "echo 'Hello from {name}!'",
|
command = "echo 'Hello from {name}!'",
|
||||||
terminal = false,
|
tags = {{}},
|
||||||
tags = {{}}
|
}},
|
||||||
}}
|
|
||||||
}}
|
}}
|
||||||
end
|
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!();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
101
crates/owlry/src/ui/provider_meta.rs
Normal file
101
crates/owlry/src/ui/provider_meta.rs
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
1
justfile
1
justfile
@@ -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 ""
|
||||||
|
|||||||
@@ -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" ]] \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user