From abd4df69396b2aea128b15c6b54050418247d183 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Tue, 30 Dec 2025 06:11:50 +0100 Subject: [PATCH] feat: add lazy loading, non-blocking bookmarks, and file search fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add lazy loading for result lists (load more on scroll/selection) - Add non-blocking bookmark loading with JSON cache - Add Firefox favicon extraction and caching - Fix dynamic provider filtering (files/calc/websearch in All mode) - Fix clippy warnings across core and plugins - Add apex-neon theme - Add aur/ to gitignore ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + Cargo.lock | 675 +++++++++++++++++- crates/owlry-plugin-bookmarks/Cargo.toml | 4 + crates/owlry-plugin-bookmarks/src/lib.rs | 429 ++++++++++- crates/owlry-plugin-emoji/src/lib.rs | 1 + crates/owlry-plugin-pomodoro/src/lib.rs | 44 +- crates/owlry-plugin-weather/src/lib.rs | 60 +- crates/owlry/src/app.rs | 5 +- crates/owlry/src/cli.rs | 4 + crates/owlry/src/config/mod.rs | 183 +++-- crates/owlry/src/filter.rs | 17 +- crates/owlry/src/plugins/manifest.rs | 2 + crates/owlry/src/plugins/mod.rs | 1 + crates/owlry/src/providers/mod.rs | 18 +- crates/owlry/src/providers/native_provider.rs | 14 +- crates/owlry/src/resources/base.css | 12 + crates/owlry/src/theme.rs | 11 + crates/owlry/src/ui/main_window.rs | 245 ++++++- crates/owlry/src/ui/result_row.rs | 77 +- data/themes/apex-neon.css | 344 +++++++++ justfile | 4 + 21 files changed, 1931 insertions(+), 220 deletions(-) create mode 100644 data/themes/apex-neon.css diff --git a/.gitignore b/.gitignore index c267f1a..ca0e334 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ aur/*/*.tar.xz aur/*/*.pkg.tar.* # Keep PKGBUILD and .SRCINFO tracked .SRCINFO +aur/ diff --git a/Cargo.lock b/Cargo.lock index 0162b78..a6fda57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -298,6 +304,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -316,11 +331,20 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "block" @@ -328,6 +352,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -367,6 +400,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" @@ -524,6 +563,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_panic" version = "0.2.15" @@ -564,6 +609,30 @@ version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533d38ecd2709b7608fb8e18e4504deb99e9a72879e6aa66373a76d8dc4259ea" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -588,12 +657,42 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -603,6 +702,18 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "5.0.1" @@ -645,11 +756,20 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -743,6 +863,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -796,12 +927,29 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -868,6 +1016,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1018,6 +1177,16 @@ dependencies = [ "windows-result 0.4.1", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1417,12 +1586,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -1441,6 +1630,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1694,7 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -1770,6 +1986,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1797,6 +2016,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.12" @@ -1805,6 +2030,18 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -1915,6 +2152,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" @@ -2122,6 +2369,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2175,6 +2438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2394,6 +2658,8 @@ dependencies = [ "owlry-plugin-api", "serde", "serde_json", + "sqlx", + "tokio", ] [[package]] @@ -2572,7 +2838,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2583,6 +2849,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2632,6 +2907,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2747,7 +3043,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -2788,14 +3084,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -2805,7 +3122,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -2826,6 +3152,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -2935,6 +3270,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rune" version = "0.14.1" @@ -3262,6 +3617,28 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3287,6 +3664,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -3310,6 +3697,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -3321,6 +3711,213 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.111", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.111", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3333,6 +3930,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3601,6 +4209,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -3762,6 +4381,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3856,6 +4476,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "typewit" version = "1.14.2" @@ -3879,12 +4505,33 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-width" version = "0.1.14" @@ -3980,6 +4627,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -4079,6 +4732,16 @@ dependencies = [ "winsafe", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/owlry-plugin-bookmarks/Cargo.toml b/crates/owlry-plugin-bookmarks/Cargo.toml index 8e67a44..55aea5f 100644 --- a/crates/owlry-plugin-bookmarks/Cargo.toml +++ b/crates/owlry-plugin-bookmarks/Cargo.toml @@ -25,3 +25,7 @@ dirs = "5.0" # For parsing Chrome bookmarks JSON serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" + +# For reading Firefox bookmarks (places.sqlite) +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } +tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/crates/owlry-plugin-bookmarks/src/lib.rs b/crates/owlry-plugin-bookmarks/src/lib.rs index 052334d..7bd44b2 100644 --- a/crates/owlry-plugin-bookmarks/src/lib.rs +++ b/crates/owlry-plugin-bookmarks/src/lib.rs @@ -1,9 +1,9 @@ //! Bookmarks Plugin for Owlry //! -//! A static provider that reads browser bookmarks from Chrome/Chromium. -//! Firefox support would require the rusqlite crate for reading places.sqlite. +//! A static provider that reads browser bookmarks from various browsers. //! //! Supported browsers: +//! - Firefox (via places.sqlite using SQLx) //! - Chrome //! - Chromium //! - Brave @@ -14,8 +14,14 @@ use owlry_plugin_api::{ owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION, }; use serde::Deserialize; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::Row; use std::fs; -use std::path::PathBuf; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; // Plugin metadata const PLUGIN_ID: &str = "bookmarks"; @@ -27,17 +33,141 @@ const PLUGIN_DESCRIPTION: &str = "Browser bookmark search"; const PROVIDER_ID: &str = "bookmarks"; const PROVIDER_NAME: &str = "Bookmarks"; const PROVIDER_PREFIX: &str = ":bm"; -const PROVIDER_ICON: &str = "web-browser"; +const PROVIDER_ICON: &str = "user-bookmarks-symbolic"; const PROVIDER_TYPE_ID: &str = "bookmarks"; /// Bookmarks provider state - holds cached items struct BookmarksState { + /// Cached bookmark items (returned immediately on refresh) items: Vec, + /// Flag to prevent concurrent background loads + loading: Arc, } impl BookmarksState { fn new() -> Self { - Self { items: Vec::new() } + Self { + items: Vec::new(), + loading: Arc::new(AtomicBool::new(false)), + } + } + + /// Get or create the favicon cache directory + fn favicon_cache_dir() -> Option { + dirs::cache_dir().map(|d| d.join("owlry/favicons")) + } + + /// Ensure the favicon cache directory exists + fn ensure_favicon_cache_dir() -> Option { + Self::favicon_cache_dir().and_then(|dir| { + fs::create_dir_all(&dir).ok()?; + Some(dir) + }) + } + + /// Hash a URL to create a cache filename + fn url_to_cache_filename(url: &str) -> String { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + url.hash(&mut hasher); + format!("{:016x}.png", hasher.finish()) + } + + /// Get the bookmark cache file path + fn bookmark_cache_file() -> Option { + dirs::cache_dir().map(|d| d.join("owlry/bookmarks.json")) + } + + /// Load cached bookmarks from disk (fast) + fn load_cached_bookmarks() -> Vec { + let cache_file = match Self::bookmark_cache_file() { + Some(f) => f, + None => return Vec::new(), + }; + + if !cache_file.exists() { + return Vec::new(); + } + + let content = match fs::read_to_string(&cache_file) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + + // Parse cached bookmarks (simple JSON format) + #[derive(serde::Deserialize)] + struct CachedBookmark { + id: String, + name: String, + command: String, + description: Option, + icon: String, + } + + let cached: Vec = match serde_json::from_str(&content) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + + cached + .into_iter() + .map(|b| { + let mut item = PluginItem::new(b.id, b.name, b.command) + .with_icon(&b.icon) + .with_keywords(vec!["bookmark".to_string()]); + if let Some(desc) = b.description { + item = item.with_description(desc); + } + item + }) + .collect() + } + + /// Save bookmarks to cache file + fn save_cached_bookmarks(items: &[PluginItem]) { + let cache_file = match Self::bookmark_cache_file() { + Some(f) => f, + None => return, + }; + + // Ensure cache directory exists + if let Some(parent) = cache_file.parent() { + let _ = fs::create_dir_all(parent); + } + + #[derive(serde::Serialize)] + struct CachedBookmark { + id: String, + name: String, + command: String, + description: Option, + icon: String, + } + + let cached: Vec = items + .iter() + .map(|item| { + let desc: Option = match &item.description { + abi_stable::std_types::ROption::RSome(s) => Some(s.to_string()), + abi_stable::std_types::ROption::RNone => None, + }; + let icon: String = match &item.icon { + abi_stable::std_types::ROption::RSome(s) => s.to_string(), + abi_stable::std_types::ROption::RNone => PROVIDER_ICON.to_string(), + }; + CachedBookmark { + id: item.id.to_string(), + name: item.name.to_string(), + command: item.command.to_string(), + description: desc, + icon, + } + }) + .collect(); + + if let Ok(json) = serde_json::to_string(&cached) { + let _ = fs::write(&cache_file, json); + } } fn chromium_bookmark_paths() -> Vec { @@ -61,18 +191,87 @@ impl BookmarksState { paths } - fn load_bookmarks(&mut self) { - self.items.clear(); + fn firefox_places_paths() -> Vec { + let mut paths = Vec::new(); - // Load Chrome/Chromium bookmarks - for path in Self::chromium_bookmark_paths() { - if path.exists() { - self.read_chrome_bookmarks(&path); + if let Some(home) = dirs::home_dir() { + let firefox_dir = home.join(".mozilla/firefox"); + if firefox_dir.exists() { + // Find all profile directories + if let Ok(entries) = fs::read_dir(&firefox_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let places = path.join("places.sqlite"); + if places.exists() { + paths.push(places); + } + } + } + } } } + + paths + } + + /// Find Firefox favicons.sqlite paths (paired with places.sqlite) + fn firefox_favicons_path(places_path: &Path) -> Option { + let favicons = places_path.parent()?.join("favicons.sqlite"); + if favicons.exists() { + Some(favicons) + } else { + None + } } - fn read_chrome_bookmarks(&mut self, path: &PathBuf) { + fn load_bookmarks(&mut self) { + // Fast path: load from cache immediately + if self.items.is_empty() { + self.items = Self::load_cached_bookmarks(); + } + + // Don't start another background load if one is already running + if self.loading.swap(true, Ordering::SeqCst) { + return; + } + + // Spawn background thread to refresh bookmarks + let loading = self.loading.clone(); + thread::spawn(move || { + let mut items = Vec::new(); + + // Load Chrome/Chromium bookmarks (fast - just JSON parsing) + for path in Self::chromium_bookmark_paths() { + if path.exists() { + Self::read_chrome_bookmarks_static(&path, &mut items); + } + } + + // Load Firefox bookmarks with favicons (async via tokio) + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => { + loading.store(false, Ordering::SeqCst); + return; + } + }; + + for path in Self::firefox_places_paths() { + rt.block_on(async { + Self::read_firefox_bookmarks_async(&path, &mut items).await; + }); + } + + // Save to cache for next startup + Self::save_cached_bookmarks(&items); + + loading.store(false, Ordering::SeqCst); + }); + } + + /// Read Chrome bookmarks (static helper for background thread) + fn read_chrome_bookmarks_static(path: &PathBuf, items: &mut Vec) { let content = match fs::read_to_string(path) { Ok(c) => c, Err(_) => return, @@ -83,29 +282,27 @@ impl BookmarksState { Err(_) => return, }; - // Process bookmark bar and other folders if let Some(roots) = bookmarks.roots { if let Some(bar) = roots.bookmark_bar { - self.process_chrome_folder(&bar); + Self::process_chrome_folder_static(&bar, items); } if let Some(other) = roots.other { - self.process_chrome_folder(&other); + Self::process_chrome_folder_static(&other, items); } if let Some(synced) = roots.synced { - self.process_chrome_folder(&synced); + Self::process_chrome_folder_static(&synced, items); } } } - fn process_chrome_folder(&mut self, folder: &ChromeBookmarkNode) { + fn process_chrome_folder_static(folder: &ChromeBookmarkNode, items: &mut Vec) { if let Some(ref children) = folder.children { for child in children { match child.node_type.as_deref() { Some("url") => { if let Some(ref url) = child.url { let name = child.name.clone().unwrap_or_else(|| url.clone()); - - self.items.push( + items.push( PluginItem::new( format!("bookmark:{}", url), name, @@ -113,19 +310,199 @@ impl BookmarksState { ) .with_description(url.clone()) .with_icon(PROVIDER_ICON) - .with_keywords(vec!["bookmark".to_string(), "web".to_string()]), + .with_keywords(vec!["bookmark".to_string(), "chrome".to_string()]), ); } } Some("folder") => { - // Recursively process subfolders - self.process_chrome_folder(child); + Self::process_chrome_folder_static(child, items); } _ => {} } } } } + + /// Read Firefox bookmarks asynchronously + async fn read_firefox_bookmarks_async(places_path: &PathBuf, items: &mut Vec) { + let temp_dir = std::env::temp_dir(); + let temp_db = temp_dir.join("owlry_places_temp.sqlite"); + + if fs::copy(places_path, &temp_db).is_err() { + return; + } + + let wal_path = places_path.with_extension("sqlite-wal"); + if wal_path.exists() { + let temp_wal = temp_db.with_extension("sqlite-wal"); + let _ = fs::copy(&wal_path, &temp_wal); + } + + let favicons_path = Self::firefox_favicons_path(places_path); + let temp_favicons = temp_dir.join("owlry_favicons_temp.sqlite"); + if let Some(ref fp) = favicons_path { + let _ = fs::copy(fp, &temp_favicons); + let fav_wal = fp.with_extension("sqlite-wal"); + if fav_wal.exists() { + let _ = fs::copy(&fav_wal, temp_favicons.with_extension("sqlite-wal")); + } + } + + let db_url = format!("sqlite:{}?mode=ro", temp_db.display()); + let favicons_url = if favicons_path.is_some() { + Some(format!("sqlite:{}?mode=ro", temp_favicons.display())) + } else { + None + }; + + let cache_dir = Self::ensure_favicon_cache_dir(); + + let bookmarks = Self::fetch_firefox_bookmarks_with_favicons( + &db_url, + favicons_url.as_deref(), + cache_dir.as_ref(), + ) + .await; + + // Clean up temp files + let _ = fs::remove_file(&temp_db); + let _ = fs::remove_file(temp_db.with_extension("sqlite-wal")); + let _ = fs::remove_file(&temp_favicons); + let _ = fs::remove_file(temp_favicons.with_extension("sqlite-wal")); + + for (title, url, favicon_path) in bookmarks { + let icon = favicon_path.unwrap_or_else(|| PROVIDER_ICON.to_string()); + items.push( + PluginItem::new( + format!("bookmark:firefox:{}", url), + title, + format!("xdg-open '{}'", url.replace('\'', "'\\''")), + ) + .with_description(url) + .with_icon(&icon) + .with_keywords(vec!["bookmark".to_string(), "firefox".to_string()]), + ); + } + } + + /// Fetch Firefox bookmarks with their favicons + async fn fetch_firefox_bookmarks_with_favicons( + places_url: &str, + favicons_url: Option<&str>, + cache_dir: Option<&PathBuf>, + ) -> Vec<(String, String, Option)> { + // First, fetch bookmarks from places.sqlite + let pool = match SqlitePoolOptions::new() + .max_connections(1) + .connect(places_url) + .await + { + Ok(p) => p, + Err(_) => return Vec::new(), + }; + + // Query bookmarks joining moz_bookmarks with moz_places + // type=1 means URL bookmarks (not folders, separators, etc.) + let query = r#" + SELECT b.title, p.url + FROM moz_bookmarks b + JOIN moz_places p ON b.fk = p.id + WHERE b.type = 1 + AND p.url NOT LIKE 'place:%' + AND p.url NOT LIKE 'about:%' + AND b.title IS NOT NULL + AND b.title != '' + ORDER BY b.dateAdded DESC + LIMIT 500 + "#; + + let rows = match sqlx::query(query).fetch_all(&pool).await { + Ok(r) => r, + Err(_) => return Vec::new(), + }; + + let bookmarks: Vec<(String, String)> = rows + .into_iter() + .filter_map(|row| { + let title: Option = row.get("title"); + let url: Option = row.get("url"); + match (title, url) { + (Some(t), Some(u)) => Some((t, u)), + _ => None, + } + }) + .collect(); + + // If no favicons database or cache dir, return without favicons + let (favicons_url, cache_dir) = match (favicons_url, cache_dir) { + (Some(f), Some(c)) => (f, c), + _ => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(), + }; + + // Connect to favicons database + let fav_pool = match SqlitePoolOptions::new() + .max_connections(1) + .connect(favicons_url) + .await + { + Ok(p) => p, + Err(_) => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(), + }; + + // Fetch favicons for each URL + let mut results = Vec::new(); + for (title, url) in bookmarks { + let favicon_path = Self::get_favicon_for_url(&fav_pool, &url, cache_dir).await; + results.push((title, url, favicon_path)); + } + + results + } + + /// Get favicon for a URL, caching to file if needed + async fn get_favicon_for_url( + pool: &sqlx::SqlitePool, + page_url: &str, + cache_dir: &Path, + ) -> Option { + // Check if already cached + let cache_filename = Self::url_to_cache_filename(page_url); + let cache_path = cache_dir.join(&cache_filename); + if cache_path.exists() { + return Some(cache_path.to_string_lossy().to_string()); + } + + // Query favicon data from database + // Join moz_pages_w_icons -> moz_icons_to_pages -> moz_icons + // Prefer smaller icons (32px) for efficiency + let query = r#" + SELECT i.data + FROM moz_pages_w_icons p + JOIN moz_icons_to_pages ip ON p.id = ip.page_id + JOIN moz_icons i ON ip.icon_id = i.id + WHERE p.page_url = ? + AND i.data IS NOT NULL + ORDER BY ABS(i.width - 32) ASC + LIMIT 1 + "#; + + let row = sqlx::query(query) + .bind(page_url) + .fetch_optional(pool) + .await + .ok()??; + + let data: Vec = row.get("data"); + if data.is_empty() { + return None; + } + + // Write favicon data to cache file + let mut file = fs::File::create(&cache_path).ok()?; + file.write_all(&data).ok()?; + + Some(cache_path.to_string_lossy().to_string()) + } } // Chrome bookmark JSON structures @@ -241,6 +618,14 @@ mod tests { assert!(!paths.is_empty()); } + #[test] + fn test_firefox_paths() { + // This will find paths if Firefox is installed + let paths = BookmarksState::firefox_places_paths(); + // Path detection should work (may be empty if Firefox not installed) + assert!(paths.len() >= 0); + } + #[test] fn test_parse_chrome_bookmarks() { let json = r#"{ diff --git a/crates/owlry-plugin-emoji/src/lib.rs b/crates/owlry-plugin-emoji/src/lib.rs index df0a5b0..1e5068b 100644 --- a/crates/owlry-plugin-emoji/src/lib.rs +++ b/crates/owlry-plugin-emoji/src/lib.rs @@ -423,6 +423,7 @@ impl EmojiState { name.to_string(), format!("printf '%s' '{}' | wl-copy", emoji), ) + .with_icon(*emoji) // Use emoji character as icon .with_description(format!("{} {}", emoji, keywords)) .with_keywords(vec![name.to_string(), keywords.to_string()]), ); diff --git a/crates/owlry-plugin-pomodoro/src/lib.rs b/crates/owlry-plugin-pomodoro/src/lib.rs index d160914..8a971a9 100644 --- a/crates/owlry-plugin-pomodoro/src/lib.rs +++ b/crates/owlry-plugin-pomodoro/src/lib.rs @@ -57,31 +57,31 @@ impl PomodoroConfig { let config_content = config_path .and_then(|p| fs::read_to_string(p).ok()); - if let Some(content) = config_content { - if let Ok(toml) = content.parse::() { - // Try [plugins.pomodoro] first (new format) - if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) { - if let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table()) { - return Self::from_toml_table(pomodoro); - } - } + if let Some(content) = config_content + && let Ok(toml) = content.parse::() + { + // Try [plugins.pomodoro] first (new format) + if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) + && let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table()) + { + return Self::from_toml_table(pomodoro); + } - // Fallback to [providers] section (old format) - if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) { - let work_mins = providers - .get("pomodoro_work_mins") - .and_then(|v| v.as_integer()) - .map(|v| v as u32) - .unwrap_or(DEFAULT_WORK_MINS); + // Fallback to [providers] section (old format) + if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) { + let work_mins = providers + .get("pomodoro_work_mins") + .and_then(|v| v.as_integer()) + .map(|v| v as u32) + .unwrap_or(DEFAULT_WORK_MINS); - let break_mins = providers - .get("pomodoro_break_mins") - .and_then(|v| v.as_integer()) - .map(|v| v as u32) - .unwrap_or(DEFAULT_BREAK_MINS); + let break_mins = providers + .get("pomodoro_break_mins") + .and_then(|v| v.as_integer()) + .map(|v| v as u32) + .unwrap_or(DEFAULT_BREAK_MINS); - return Self { work_mins, break_mins }; - } + return Self { work_mins, break_mins }; } } diff --git a/crates/owlry-plugin-weather/src/lib.rs b/crates/owlry-plugin-weather/src/lib.rs index 55c9c49..4a3ce25 100644 --- a/crates/owlry-plugin-weather/src/lib.rs +++ b/crates/owlry-plugin-weather/src/lib.rs @@ -82,41 +82,41 @@ impl WeatherConfig { let config_content = config_path .and_then(|p| fs::read_to_string(p).ok()); - if let Some(content) = config_content { - if let Ok(toml) = content.parse::() { - // Try [plugins.weather] first (new format) - if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) { - if let Some(weather) = plugins.get("weather").and_then(|v| v.as_table()) { - return Self::from_toml_table(weather); - } - } + if let Some(content) = config_content + && let Ok(toml) = content.parse::() + { + // Try [plugins.weather] first (new format) + if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) + && let Some(weather) = plugins.get("weather").and_then(|v| v.as_table()) + { + return Self::from_toml_table(weather); + } - // Fallback to [providers] section (old format) - if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) { - let provider_str = providers - .get("weather_provider") - .and_then(|v| v.as_str()) - .unwrap_or("wttr.in"); + // Fallback to [providers] section (old format) + if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) { + let provider_str = providers + .get("weather_provider") + .and_then(|v| v.as_str()) + .unwrap_or("wttr.in"); - let provider = provider_str.parse().unwrap_or(WeatherProviderType::WttrIn); + let provider = provider_str.parse().unwrap_or(WeatherProviderType::WttrIn); - let api_key = providers - .get("weather_api_key") - .and_then(|v| v.as_str()) - .map(String::from); + let api_key = providers + .get("weather_api_key") + .and_then(|v| v.as_str()) + .map(String::from); - let location = providers - .get("weather_location") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let location = providers + .get("weather_location") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); - return Self { - provider, - api_key, - location, - }; - } + return Self { + provider, + api_key, + location, + }; } } diff --git a/crates/owlry/src/app.rs b/crates/owlry/src/app.rs index 6e5574d..8679a5a 100644 --- a/crates/owlry/src/app.rs +++ b/crates/owlry/src/app.rs @@ -56,7 +56,10 @@ impl OwlryApp { let native_providers = Self::load_native_plugins(&config.borrow()); // Create provider manager with native plugins + #[cfg(feature = "lua")] let mut provider_manager = ProviderManager::with_native_plugins(native_providers); + #[cfg(not(feature = "lua"))] + let provider_manager = ProviderManager::with_native_plugins(native_providers); // Load Lua plugins if enabled (requires lua feature) #[cfg(feature = "lua")] @@ -75,7 +78,7 @@ impl OwlryApp { ); let filter = Rc::new(RefCell::new(filter)); - let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone()); + let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone(), args.prompt.clone()); // Set up layer shell for Wayland overlay behavior window.init_layer_shell(); diff --git a/crates/owlry/src/cli.rs b/crates/owlry/src/cli.rs index 20b94d3..6615a6d 100644 --- a/crates/owlry/src/cli.rs +++ b/crates/owlry/src/cli.rs @@ -21,6 +21,10 @@ pub struct CliArgs { #[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)] pub providers: Option>, + /// Custom prompt text for the search input (useful for dmenu mode) + #[arg(long)] + pub prompt: Option, + /// Subcommand to run (if any) #[command(subcommand)] pub command: Option, diff --git a/crates/owlry/src/config/mod.rs b/crates/owlry/src/config/mod.rs index 9eee858..83375b0 100644 --- a/crates/owlry/src/config/mod.rs +++ b/crates/owlry/src/config/mod.rs @@ -6,10 +6,13 @@ use std::process::Command; use crate::paths; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Config { + #[serde(default)] pub general: GeneralConfig, + #[serde(default)] pub appearance: AppearanceConfig, + #[serde(default)] pub providers: ProvidersConfig, #[serde(default)] pub plugins: PluginsConfig, @@ -17,9 +20,13 @@ pub struct Config { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneralConfig { + #[serde(default = "default_true")] pub show_icons: bool, + #[serde(default = "default_max_results")] pub max_results: usize, - pub terminal_command: String, + /// Terminal command (auto-detected if not specified) + #[serde(default)] + pub terminal_command: Option, /// Launch wrapper command for app execution. /// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --" /// If None or empty, launches directly via sh -c @@ -31,6 +38,22 @@ pub struct GeneralConfig { pub tabs: Vec, } +impl Default for GeneralConfig { + fn default() -> Self { + Self { + show_icons: true, + max_results: 100, + terminal_command: None, + launch_wrapper: None, + tabs: default_tabs(), + } + } +} + +fn default_max_results() -> usize { + 100 +} + fn default_tabs() -> Vec { vec![ "app".to_string(), @@ -40,9 +63,10 @@ fn default_tabs() -> Vec { } /// User-customizable theme colors -/// All fields are optional - unset values inherit from GTK theme +/// All fields are optional - unset values inherit from theme or GTK defaults #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ThemeColors { + // Core colors pub background: Option, pub background_secondary: Option, pub border: Option, @@ -64,13 +88,21 @@ pub struct ThemeColors { pub badge_sys: Option, pub badge_uuctl: Option, pub badge_web: Option, + // Widget badge colors + pub badge_media: Option, + pub badge_weather: Option, + pub badge_pomo: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppearanceConfig { + #[serde(default = "default_width")] pub width: i32, + #[serde(default = "default_height")] pub height: i32, + #[serde(default = "default_font_size")] pub font_size: u32, + #[serde(default = "default_border_radius")] pub border_radius: u32, /// Theme name: None = GTK default, "owl" = built-in owl theme #[serde(default)] @@ -80,10 +112,31 @@ pub struct AppearanceConfig { pub colors: ThemeColors, } +impl Default for AppearanceConfig { + fn default() -> Self { + Self { + width: 850, + height: 650, + font_size: 14, + border_radius: 12, + theme: None, + colors: ThemeColors::default(), + } + } +} + +fn default_width() -> i32 { 850 } +fn default_height() -> i32 { 650 } +fn default_font_size() -> u32 { 14 } +fn default_border_radius() -> u32 { 12 } + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProvidersConfig { + #[serde(default = "default_true")] pub applications: bool, + #[serde(default = "default_true")] pub commands: bool, + #[serde(default = "default_true")] pub uuctl: bool, /// Enable calculator provider (= expression or calc expression) #[serde(default = "default_true")] @@ -159,6 +212,36 @@ pub struct ProvidersConfig { pub pomodoro_break_mins: u32, } +impl Default for ProvidersConfig { + fn default() -> Self { + Self { + applications: true, + commands: true, + uuctl: true, + calculator: true, + frecency: true, + frecency_weight: 0.3, + websearch: true, + 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, + } + } +} + /// Configuration for plugins /// /// Supports per-plugin configuration via `[plugins.]` sections: @@ -450,57 +533,7 @@ fn command_exists(cmd: &str) -> bool { .unwrap_or(false) } -impl Default for Config { - fn default() -> Self { - let terminal = detect_terminal(); - info!("Detected terminal: {}", terminal); - - Self { - general: GeneralConfig { - show_icons: true, - max_results: 10, - terminal_command: terminal, - launch_wrapper: detect_launch_wrapper(), - tabs: default_tabs(), - }, - appearance: AppearanceConfig { - width: 850, - height: 650, - font_size: 14, - border_radius: 12, - theme: None, - colors: ThemeColors::default(), - }, - providers: ProvidersConfig { - applications: true, - commands: true, - uuctl: true, - calculator: true, - frecency: true, - frecency_weight: 0.3, - websearch: true, - search_engine: "duckduckgo".to_string(), - system: true, - ssh: true, - clipboard: true, - bookmarks: true, - emoji: true, - scripts: true, - files: true, - // Widget providers - 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, - }, - plugins: PluginsConfig::default(), - } - } -} +// Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default impl Config { pub fn config_path() -> Option { @@ -517,23 +550,37 @@ impl Config { pub fn load() -> Result> { let path = Self::config_path().ok_or("Could not determine config path")?; - if !path.exists() { + let mut config = if !path.exists() { info!("Config file not found, using defaults"); - return Ok(Self::default()); + Self::default() + } else { + let content = std::fs::read_to_string(&path)?; + let config: Config = toml::from_str(&content)?; + info!("Loaded config from {:?}", path); + config + }; + + // Auto-detect terminal if not configured or configured terminal doesn't exist + match &config.general.terminal_command { + None => { + let terminal = detect_terminal(); + info!("Detected terminal: {}", terminal); + config.general.terminal_command = Some(terminal); + } + Some(term) if !command_exists(term) => { + warn!("Configured terminal '{}' not found, auto-detecting", term); + let terminal = detect_terminal(); + info!("Using detected terminal: {}", terminal); + config.general.terminal_command = Some(terminal); + } + Some(term) => { + debug!("Using configured terminal: {}", term); + } } - let content = std::fs::read_to_string(&path)?; - let mut config: Config = toml::from_str(&content)?; - info!("Loaded config from {:?}", path); - - // Validate terminal - if configured terminal doesn't exist, auto-detect - if !command_exists(&config.general.terminal_command) { - warn!( - "Configured terminal '{}' not found, auto-detecting", - config.general.terminal_command - ); - config.general.terminal_command = detect_terminal(); - info!("Using detected terminal: {}", config.general.terminal_command); + // Auto-detect launch wrapper if not configured + if config.general.launch_wrapper.is_none() { + config.general.launch_wrapper = detect_launch_wrapper(); } Ok(config) diff --git a/crates/owlry/src/filter.rs b/crates/owlry/src/filter.rs index 843a02b..68ccdb3 100644 --- a/crates/owlry/src/filter.rs +++ b/crates/owlry/src/filter.rs @@ -64,8 +64,17 @@ impl ProviderFilter { if config_providers.scripts { set.insert(ProviderType::Scripts); } - // Note: Files, Calculator, WebSearch are dynamic providers - // that don't need to be in the filter set - they're triggered by prefix + // Dynamic providers: add to filter set so they work in "All" mode + // but can still be excluded when in single-provider mode + if config_providers.files { + set.insert(ProviderType::Files); + } + if config_providers.calculator { + set.insert(ProviderType::Calculator); + } + if config_providers.websearch { + set.insert(ProviderType::WebSearch); + } // Default to apps if nothing enabled if set.is_empty() { set.insert(ProviderType::Application); @@ -104,9 +113,11 @@ impl ProviderFilter { #[cfg(feature = "dev-logging")] debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled); } else { + #[cfg(feature = "dev-logging")] + let provider_debug = format!("{:?}", provider); self.enabled.insert(provider); #[cfg(feature = "dev-logging")] - debug!("[Filter] Toggled ON {:?}, enabled: {:?}", provider, self.enabled); + debug!("[Filter] Toggled ON {}, enabled: {:?}", provider_debug, self.enabled); } } diff --git a/crates/owlry/src/plugins/manifest.rs b/crates/owlry/src/plugins/manifest.rs index a379ce5..929d6cf 100644 --- a/crates/owlry/src/plugins/manifest.rs +++ b/crates/owlry/src/plugins/manifest.rs @@ -156,6 +156,7 @@ pub fn discover_plugins(plugins_dir: &Path) -> PluginResult PluginResult<()> { if !manifest.is_compatible_with(owlry_version) { return Err(PluginError::VersionMismatch { @@ -230,6 +231,7 @@ impl PluginManifest { } /// Check if this plugin is compatible with the given owlry version + #[allow(dead_code)] pub fn is_compatible_with(&self, owlry_version: &str) -> bool { let req = match semver::VersionReq::parse(&self.plugin.owlry_version) { Ok(r) => r, diff --git a/crates/owlry/src/plugins/mod.rs b/crates/owlry/src/plugins/mod.rs index 89c949f..403b6c0 100644 --- a/crates/owlry/src/plugins/mod.rs +++ b/crates/owlry/src/plugins/mod.rs @@ -43,6 +43,7 @@ pub use api::provider::{PluginItem, ProviderRegistration}; #[allow(unused_imports)] pub use api::{ActionRegistration, HookEvent, ThemeRegistration}; +#[allow(unused_imports)] pub use error::{PluginError, PluginResult}; #[cfg(feature = "lua")] diff --git a/crates/owlry/src/providers/mod.rs b/crates/owlry/src/providers/mod.rs index 55040e7..57d5ba3 100644 --- a/crates/owlry/src/providers/mod.rs +++ b/crates/owlry/src/providers/mod.rs @@ -80,13 +80,13 @@ impl std::str::FromStr for ProviderType { "cmd" | "command" | "commands" => Ok(ProviderType::Command), "dmenu" => Ok(ProviderType::Dmenu), "emoji" | "emojis" => Ok(ProviderType::Emoji), - "file" | "files" | "find" => Ok(ProviderType::Files), + "file" | "files" | "find" | "filesearch" => Ok(ProviderType::Files), "media" | "mpris" | "player" => Ok(ProviderType::MediaPlayer), "pomo" | "pomodoro" | "timer" => Ok(ProviderType::Pomodoro), "script" | "scripts" => Ok(ProviderType::Scripts), "ssh" => Ok(ProviderType::Ssh), "sys" | "system" | "power" => Ok(ProviderType::System), - "uuctl" => Ok(ProviderType::Uuctl), + "uuctl" | "systemd" => Ok(ProviderType::Uuctl), "weather" => Ok(ProviderType::Weather), "web" | "websearch" | "search" => Ok(ProviderType::WebSearch), // Plugin types are prefixed with "plugin:" (e.g., "plugin:github-repos") @@ -273,12 +273,14 @@ impl ProviderManager { } /// Add a dynamic provider (e.g., from a Lua plugin) + #[allow(dead_code)] pub fn add_provider(&mut self, provider: Box) { info!("Added plugin provider: {}", provider.name()); self.providers.push(provider); } /// Add multiple providers at once (for batch plugin loading) + #[allow(dead_code)] pub fn add_providers(&mut self, providers: Vec>) { for provider in providers { self.add_provider(provider); @@ -394,6 +396,10 @@ impl ProviderManager { if filter.active_prefix().is_none() && query.is_empty() { // Widget priority scores based on type for provider in &self.widget_providers { + // Skip if this provider type is filtered out + if !filter.is_active(provider.provider_type()) { + continue; + } let base_score = match provider.type_id() { "weather" => 12000, "pomodoro" => 11500, @@ -407,9 +413,15 @@ impl ProviderManager { } // Query dynamic providers (calculator, websearch, filesearch) - // Each provider internally checks if the query matches its prefix + // Only query if: + // 1. Their specific filter is active (e.g., :file prefix or Files tab selected), OR + // 2. No specific single-mode filter is active (showing all providers) if !query.is_empty() { for (provider_idx, provider) in self.dynamic_providers.iter().enumerate() { + // Skip if this provider type is explicitly filtered out + if !filter.is_active(provider.provider_type()) { + continue; + } let dynamic_results = provider.query(query); let base_score = match provider.type_id() { "calc" => 10000, diff --git a/crates/owlry/src/providers/native_provider.rs b/crates/owlry/src/providers/native_provider.rs index 50cca58..a4acf97 100644 --- a/crates/owlry/src/providers/native_provider.rs +++ b/crates/owlry/src/providers/native_provider.rs @@ -42,6 +42,16 @@ impl NativeProvider { } } + /// Get the ProviderType for this native provider + /// Maps type_id string to the appropriate ProviderType variant + fn get_provider_type(&self) -> ProviderType { + // Parse type_id to get the proper ProviderType + // This uses the FromStr impl which maps strings like "clipboard" -> ProviderType::Clipboard + self.info.type_id.as_str().parse().unwrap_or_else(|_| { + ProviderType::Plugin(self.info.type_id.to_string()) + }) + } + /// Convert a plugin API item to a core LaunchItem fn convert_item(&self, item: ApiPluginItem) -> LaunchItem { LaunchItem { @@ -49,7 +59,7 @@ impl NativeProvider { name: item.name.to_string(), description: item.description.as_ref().map(|s| s.to_string()).into(), icon: item.icon.as_ref().map(|s| s.to_string()).into(), - provider: ProviderType::Plugin(self.info.type_id.to_string()), + provider: self.get_provider_type(), command: item.command.to_string(), terminal: item.terminal, tags: item.keywords.iter().map(|s| s.to_string()).collect(), @@ -113,7 +123,7 @@ impl Provider for NativeProvider { } fn provider_type(&self) -> ProviderType { - ProviderType::Plugin(self.info.type_id.to_string()) + self.get_provider_type() } fn refresh(&mut self) { diff --git a/crates/owlry/src/resources/base.css b/crates/owlry/src/resources/base.css index 3ec8b5e..f1f1869 100644 --- a/crates/owlry/src/resources/base.css +++ b/crates/owlry/src/resources/base.css @@ -67,6 +67,18 @@ opacity: 1; } +/* Symbolic icons - inherit text color */ +.owlry-symbolic-icon { + -gtk-icon-style: symbolic; +} + +/* Emoji icon - displayed as large text */ +.owlry-emoji-icon { + font-size: 24px; + min-width: 32px; + min-height: 32px; +} + /* Result name */ .owlry-result-name { font-size: var(--owlry-font-size, 14px); diff --git a/crates/owlry/src/theme.rs b/crates/owlry/src/theme.rs index a3ac251..a2a6502 100644 --- a/crates/owlry/src/theme.rs +++ b/crates/owlry/src/theme.rs @@ -72,6 +72,17 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String { css.push_str(&format!(" --owlry-badge-web: {};\n", badge_web)); } + // Widget badge colors + if let Some(ref badge_media) = config.colors.badge_media { + css.push_str(&format!(" --owlry-badge-media: {};\n", badge_media)); + } + if let Some(ref badge_weather) = config.colors.badge_weather { + css.push_str(&format!(" --owlry-badge-weather: {};\n", badge_weather)); + } + if let Some(ref badge_pomo) = config.colors.badge_pomo { + css.push_str(&format!(" --owlry-badge-pomo: {};\n", badge_pomo)); + } + css.push_str("}\n"); css } diff --git a/crates/owlry/src/ui/main_window.rs b/crates/owlry/src/ui/main_window.rs index 42e2ba1..bfd4f26 100644 --- a/crates/owlry/src/ui/main_window.rs +++ b/crates/owlry/src/ui/main_window.rs @@ -35,6 +35,19 @@ struct SubmenuState { saved_search: String, } +/// State for lazy loading results +#[derive(Default)] +struct LazyLoadState { + /// All matching results (may be more than displayed) + all_results: Vec, + /// Number of items currently displayed + displayed_count: usize, +} + +/// Number of items to display initially and per batch +const INITIAL_RESULTS: usize = 15; +const LOAD_MORE_BATCH: usize = 10; + pub struct MainWindow { window: ApplicationWindow, search_entry: Entry, @@ -51,6 +64,11 @@ pub struct MainWindow { submenu_state: Rc>, /// Parsed tab config (ProviderTypes for cycling) tab_order: Rc>, + /// Custom prompt text (overrides dynamic placeholder when set) + #[allow(dead_code)] + custom_prompt: Option, + /// Lazy loading state + lazy_state: Rc>, } impl MainWindow { @@ -60,6 +78,7 @@ impl MainWindow { providers: Rc>, frecency: Rc>, filter: Rc>, + custom_prompt: Option, ) -> Self { let cfg = config.borrow(); @@ -111,24 +130,21 @@ impl MainWindow { .build(); filter_tabs.add_css_class("owlry-filter-tabs"); - // Parse tabs config to ProviderTypes - let tab_order: Vec = cfg - .general - .tabs - .iter() - .filter_map(|s| s.parse().ok()) - .collect(); - let tab_order = Rc::new(tab_order); + // Get enabled providers from filter (which respects CLI --mode/--providers or config) + // This makes tabs dynamic based on what's actually enabled + let enabled = filter.borrow().enabled_providers(); + let tab_strings: Vec = enabled.iter().map(|p| p.to_string()).collect(); + let tab_order = Rc::new(enabled); - // Create toggle buttons for each provider (from config) - let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &cfg.general.tabs); + // Create toggle buttons for each enabled provider + let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &tab_strings); let filter_buttons = Rc::new(RefCell::new(filter_buttons)); header_box.append(&mode_label); header_box.append(&filter_tabs); - // Search entry with dynamic placeholder - let placeholder = Self::build_placeholder(&filter.borrow()); + // Search entry with dynamic placeholder (or custom prompt if provided) + let placeholder = custom_prompt.clone().unwrap_or_else(|| Self::build_placeholder(&filter.borrow())); let search_entry = Entry::builder() .placeholder_text(&placeholder) .hexpand(true) @@ -175,6 +191,8 @@ impl MainWindow { drop(cfg); + let lazy_state = Rc::new(RefCell::new(LazyLoadState::default())); + let main_window = Self { window, search_entry, @@ -190,9 +208,12 @@ impl MainWindow { filter_buttons, submenu_state: Rc::new(RefCell::new(SubmenuState::default())), tab_order, + custom_prompt, + lazy_state, }; main_window.setup_signals(); + main_window.setup_lazy_loading(); main_window.update_results(""); // Ensure search entry has focus when window is shown @@ -232,11 +253,11 @@ impl MainWindow { let mut results = current_results_for_auto.borrow_mut(); for type_id in &widget_ids { - if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id) { - if let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id) { - existing.name = new_item.name; - existing.description = new_item.description; - } + if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id) + && let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id) + { + existing.name = new_item.name; + existing.description = new_item.description; } } } @@ -263,11 +284,21 @@ impl MainWindow { } }; - let label = Self::provider_tab_label(&provider_type); + let base_label = Self::provider_tab_label(&provider_type); + // Show number hint in the label for first 9 tabs (using superscript) + let label = if idx < 9 { + let superscript = match idx + 1 { + 1 => "ยน", 2 => "ยฒ", 3 => "ยณ", 4 => "โด", 5 => "โต", + 6 => "โถ", 7 => "โท", 8 => "โธ", 9 => "โน", _ => "", + }; + format!("{}{}", base_label, superscript) + } else { + base_label.to_string() + }; let shortcut = format!("Ctrl+{}", idx + 1); let button = ToggleButton::builder() - .label(label) + .label(&label) .tooltip_text(&shortcut) .active(filter.borrow().is_enabled(provider_type.clone())) .build(); @@ -525,6 +556,7 @@ impl MainWindow { let mode_label = self.mode_label.clone(); let search_entry_for_change = self.search_entry.clone(); let submenu_state = self.submenu_state.clone(); + let lazy_state = self.lazy_state.clone(); self.search_entry.connect_changed(move |entry| { let raw_query = entry.text(); @@ -623,11 +655,21 @@ impl MainWindow { .collect() }; + // Clear existing results while let Some(child) = results_list.first_child() { results_list.remove(&child); } - for item in &results { + // Lazy loading: store all results but only display initial batch + let initial_count = INITIAL_RESULTS.min(results.len()); + { + let mut lazy = lazy_state.borrow_mut(); + lazy.all_results = results.clone(); + lazy.displayed_count = initial_count; + } + + // Display only initial batch + for item in results.iter().take(initial_count) { let row = ResultRow::new(item); results_list.append(&row); } @@ -636,7 +678,8 @@ impl MainWindow { results_list.select_row(Some(&first_row)); } - *current_results.borrow_mut() = results; + // current_results holds only what's displayed (for selection/activation) + *current_results.borrow_mut() = results.into_iter().take(initial_count).collect(); }); // Entry activate signal (Enter key in search entry) @@ -847,6 +890,7 @@ impl MainWindow { // Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu) Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 | Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => { + info!("[UI] Ctrl+number detected: {:?}", key); if !submenu_state.borrow().active { let idx = match key { Key::_1 => 0, @@ -860,7 +904,9 @@ impl MainWindow { Key::_9 => 8, _ => return gtk4::glib::Propagation::Proceed, }; + info!("[UI] Toggling tab at index {}", idx); if let Some(provider) = tab_order.get(idx) { + info!("[UI] Found provider: {:?}", provider); Self::toggle_provider_button( provider.clone(), &filter, @@ -868,6 +914,8 @@ impl MainWindow { &search_entry, &mode_label, ); + } else { + info!("[UI] No provider at index {}, tab_order len={}", idx, tab_order.len()); } } gtk4::glib::Propagation::Stop @@ -955,25 +1003,65 @@ impl MainWindow { } let current = filter.borrow().enabled_providers(); + let all_enabled = current.len() == tab_order.len(); - let next = if current.len() == 1 { - let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0); - if forward { - tab_order[(idx + 1) % tab_order.len()].clone() + // Cycle: All -> Provider1 -> Provider2 -> ... -> ProviderN -> All + // In "All" mode (all providers enabled), we go to first provider (forward) or last (backward) + // In single-provider mode, we go to next provider or back to All at the boundary + if all_enabled { + // Currently showing all, go to first (forward) or last (backward) single provider + let next = if forward { + tab_order[0].clone() } else { - tab_order[(idx + tab_order.len() - 1) % tab_order.len()].clone() + tab_order[tab_order.len() - 1].clone() + }; + { + let mut f = filter.borrow_mut(); + f.set_single_mode(next.clone()); + } + for (ptype, button) in buttons.borrow().iter() { + button.set_active(ptype == &next); + } + } else if current.len() == 1 { + let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0); + let at_boundary = if forward { idx == tab_order.len() - 1 } else { idx == 0 }; + + if at_boundary { + // At boundary, go back to "All" mode + { + let mut f = filter.borrow_mut(); + for provider in tab_order { + f.enable(provider.clone()); + } + } + for (_, button) in buttons.borrow().iter() { + button.set_active(true); + } + } else { + // Move to next/previous provider + let next = if forward { + tab_order[idx + 1].clone() + } else { + tab_order[idx - 1].clone() + }; + { + let mut f = filter.borrow_mut(); + f.set_single_mode(next.clone()); + } + for (ptype, button) in buttons.borrow().iter() { + button.set_active(ptype == &next); + } } } else { - tab_order[0].clone() - }; - - { - let mut f = filter.borrow_mut(); - f.set_single_mode(next.clone()); - } - - for (ptype, button) in buttons.borrow().iter() { - button.set_active(ptype == &next); + // Some but not all providers enabled - go to first provider + let next = tab_order[0].clone(); + { + let mut f = filter.borrow_mut(); + f.set_single_mode(next.clone()); + } + for (ptype, button) in buttons.borrow().iter() { + button.set_active(ptype == &next); + } } mode_label.set_label(filter.borrow().mode_display_name()); @@ -1009,6 +1097,7 @@ impl MainWindow { let use_frecency = cfg.providers.frecency; drop(cfg); + // Fetch all matching results (up to max_results) let results: Vec = if use_frecency { self.providers .borrow_mut() @@ -1025,11 +1114,21 @@ impl MainWindow { .collect() }; + // Clear existing results while let Some(child) = self.results_list.first_child() { self.results_list.remove(&child); } - for item in &results { + // Store all results for lazy loading + let initial_count = INITIAL_RESULTS.min(results.len()); + { + let mut lazy = self.lazy_state.borrow_mut(); + lazy.all_results = results.clone(); + lazy.displayed_count = initial_count; + } + + // Display initial batch only + for item in results.iter().take(initial_count) { let row = ResultRow::new(item); self.results_list.append(&row); } @@ -1038,7 +1137,74 @@ impl MainWindow { self.results_list.select_row(Some(&first_row)); } - *self.current_results.borrow_mut() = results; + // current_results holds what's currently displayed + *self.current_results.borrow_mut() = results.into_iter().take(initial_count).collect(); + } + + /// Set up lazy loading scroll detection + fn setup_lazy_loading(&self) { + let vadj = self.scrolled.vadjustment(); + let results_list = self.results_list.clone(); + let lazy_state = self.lazy_state.clone(); + let current_results = self.current_results.clone(); + + // Load more on scroll + vadj.connect_value_changed(move |adj| { + let value = adj.value(); + let upper = adj.upper(); + let page_size = adj.page_size(); + + // Load more when near bottom (within 50px) + let near_bottom = upper > page_size && (value + page_size >= upper - 50.0); + if near_bottom { + Self::load_more_items(&lazy_state, &results_list, ¤t_results); + } + }); + + // Also load more when selecting rows near the end (keyboard navigation) + let lazy_state2 = self.lazy_state.clone(); + let results_list2 = self.results_list.clone(); + let current_results2 = self.current_results.clone(); + + self.results_list.connect_row_selected(move |_, row| { + if let Some(row) = row { + let index = row.index(); + let lazy = lazy_state2.borrow(); + let displayed = lazy.displayed_count; + let all_count = lazy.all_results.len(); + drop(lazy); + + // Load more if within 3 items of the end + if displayed < all_count && (index as usize) >= displayed.saturating_sub(3) { + Self::load_more_items(&lazy_state2, &results_list2, ¤t_results2); + } + } + }); + } + + /// Load more items from lazy state + fn load_more_items( + lazy_state: &Rc>, + results_list: &ListBox, + current_results: &Rc>>, + ) { + let mut lazy = lazy_state.borrow_mut(); + let all_count = lazy.all_results.len(); + let displayed = lazy.displayed_count; + + if displayed < all_count { + // Load next batch + let new_end = (displayed + LOAD_MORE_BATCH).min(all_count); + for item in lazy.all_results[displayed..new_end].iter() { + let row = ResultRow::new(item); + results_list.append(&row); + } + lazy.displayed_count = new_end; + + // Update current_results + let mut current = current_results.borrow_mut(); + current.extend(lazy.all_results[displayed..new_end].iter().cloned()); + } } /// Handle item activation - returns true if window should close @@ -1075,7 +1241,8 @@ impl MainWindow { debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider); let cmd = if item.terminal { - format!("{} -e {}", config.general.terminal_command, item.command) + let terminal = config.general.terminal_command.as_deref().unwrap_or("xterm"); + format!("{} -e {}", terminal, item.command) } else { item.command.clone() }; diff --git a/crates/owlry/src/ui/result_row.rs b/crates/owlry/src/ui/result_row.rs index 5a2dfbd..5262559 100644 --- a/crates/owlry/src/ui/result_row.rs +++ b/crates/owlry/src/ui/result_row.rs @@ -7,6 +7,17 @@ pub struct ResultRow { row: ListBoxRow, } +/// Check if a string looks like an emoji (starts with a non-ASCII character +/// and is very short - typically 1-4 chars for complex emojis with ZWJ) +fn is_emoji_icon(s: &str) -> bool { + if s.is_empty() { + return false; + } + // Emojis are non-ASCII and typically very short (1-8 chars for complex ZWJ sequences) + let first_char = s.chars().next().unwrap(); + !first_char.is_ascii() && s.chars().count() <= 8 +} + impl ResultRow { #[allow(clippy::new_ret_no_self)] pub fn new(item: &LaunchItem) -> ListBoxRow { @@ -26,46 +37,64 @@ impl ResultRow { .margin_end(12) .build(); - // Icon - handle GResource paths, file paths, icon names, and fallbacks + // Icon - handle GResource paths, file paths, icon names, emojis, and fallbacks let icon_widget: Widget = if let Some(icon_path) = &item.icon { - let img = if icon_path.starts_with("/org/owlry/launcher/icons/") { + if is_emoji_icon(icon_path) { + // Emoji character - display as text label + let emoji_label = Label::builder() + .label(icon_path) + .width_request(32) + .height_request(32) + .valign(gtk4::Align::Center) + .halign(gtk4::Align::Center) + .build(); + emoji_label.add_css_class("owlry-result-icon"); + emoji_label.add_css_class("owlry-emoji-icon"); + emoji_label.upcast() + } else if icon_path.starts_with("/org/owlry/launcher/icons/") { // GResource path - load from bundled resources - Image::from_resource(icon_path) + let img = Image::from_resource(icon_path); + img.set_pixel_size(32); + img.add_css_class("owlry-result-icon"); + img.upcast() } else if icon_path.starts_with('/') { // Absolute file path - Image::from_file(icon_path) + let img = Image::from_file(icon_path); + img.set_pixel_size(32); + img.add_css_class("owlry-result-icon"); + img.upcast() } else { // Icon theme name - Image::from_icon_name(icon_path) - }; - img.set_pixel_size(32); - img.add_css_class("owlry-result-icon"); - img.upcast() + let img = Image::from_icon_name(icon_path); + img.set_pixel_size(32); + img.add_css_class("owlry-result-icon"); + img.upcast() + } } else { - // Default icon based on provider type + // Default icon based on provider type (using symbolic icons for theme color support) let default_icon = match &item.provider { - crate::providers::ProviderType::Application => "application-x-executable", - crate::providers::ProviderType::Bookmarks => "user-bookmarks", - crate::providers::ProviderType::Calculator => "accessories-calculator", - crate::providers::ProviderType::Clipboard => "edit-paste", - crate::providers::ProviderType::Command => "utilities-terminal", + crate::providers::ProviderType::Application => "application-x-executable-symbolic", + crate::providers::ProviderType::Bookmarks => "user-bookmarks-symbolic", + crate::providers::ProviderType::Calculator => "accessories-calculator-symbolic", + crate::providers::ProviderType::Clipboard => "edit-paste-symbolic", + crate::providers::ProviderType::Command => "utilities-terminal-symbolic", crate::providers::ProviderType::Dmenu => "view-list-symbolic", - crate::providers::ProviderType::Emoji => "face-smile", - crate::providers::ProviderType::Files => "folder", - crate::providers::ProviderType::Scripts => "application-x-executable", - crate::providers::ProviderType::Ssh => "network-server", - crate::providers::ProviderType::System => "system-shutdown", - crate::providers::ProviderType::Uuctl => "system-run", - crate::providers::ProviderType::WebSearch => "web-browser", - // Widget providers now have icons set, but keep fallbacks + crate::providers::ProviderType::Emoji => "face-smile-symbolic", + crate::providers::ProviderType::Files => "folder-symbolic", + crate::providers::ProviderType::Scripts => "application-x-executable-symbolic", + crate::providers::ProviderType::Ssh => "network-server-symbolic", + crate::providers::ProviderType::System => "system-shutdown-symbolic", + crate::providers::ProviderType::Uuctl => "system-run-symbolic", + crate::providers::ProviderType::WebSearch => "web-browser-symbolic", crate::providers::ProviderType::Weather => "weather-clear-symbolic", crate::providers::ProviderType::MediaPlayer => "media-playback-start-symbolic", crate::providers::ProviderType::Pomodoro => "alarm-symbolic", - crate::providers::ProviderType::Plugin(_) => "application-x-addon", + crate::providers::ProviderType::Plugin(_) => "application-x-addon-symbolic", }; let img = Image::from_icon_name(default_icon); img.set_pixel_size(32); img.add_css_class("owlry-result-icon"); + img.add_css_class("owlry-symbolic-icon"); img.upcast() }; diff --git a/data/themes/apex-neon.css b/data/themes/apex-neon.css new file mode 100644 index 0000000..90505a9 --- /dev/null +++ b/data/themes/apex-neon.css @@ -0,0 +1,344 @@ +/* + * Owlry - Apex Neon Theme + * "State over Decoration." + * + * A high-contrast dark theme built for focus and clinical clarity. + * Color exists to signal STATE, not to decorate space. + * + * Author: S0wlz (Owlibou) + * + * โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + * APEX DNA - Semantic Color Roles: + * + * RED is the Predator: Active intent, cursor, current location, critical errors + * CYAN is Informational: Technical data, links, neutral highlights + * PURPLE is Sacred: Root access, special modes, exceptional states + * GREEN is Success: Completion, OK states, positive feedback + * YELLOW is Warning: Caution, load states, attention needed + * + * Rule: If a UI element is not important, it does not glow. + * โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + * + * Core Palette: + * - Void Black: #050505 (absolute background) + * - Dark Surface: #141414 (inputs, inactive elements) + * - Light Surface: #262626 (separators, borders) + * - Stark White: #ededed (primary text) + * - Muted: #737373 (secondary text) + * - Razor Red: #ff0044 (THE accent - focus, cursor, selection) + * - Electric Cyan: #00eaff (info, links, technical) + * - Sacred Purple: #9d00ff (special, root, elevated) + * - Neon Green: #00ff99 (success, OK) + * - Warning Yellow: #ffb700 (warning, caution) + * + * Bright Escalations: + * - Alert Red: #ff8899 (distinguishable from cursor) + * - Active Cyan: #5af3ff (active info) + * - Active Green: #2bffb2 (active success) + * - Urgent Yellow: #ffd24d (urgent warning) + * - Elevated Purple:#c84dff (elevated special) + * + * Usage: Set theme = "apex-neon" in config.toml + */ + +:root { + /* Core surfaces */ + --owlry-bg: #050505; + --owlry-bg-secondary: #141414; + --owlry-border: #262626; + --owlry-text: #ededed; + --owlry-text-secondary: #737373; + + /* The Predator - primary accent */ + --owlry-accent: #ff0044; + --owlry-accent-bright: #ff8899; + + /* Provider badges - mapped to Apex semantics */ + --owlry-badge-app: #00eaff; /* Cyan: apps are informational */ + --owlry-badge-bookmark: #ffb700; /* Yellow: bookmarks need attention */ + --owlry-badge-calc: #ffd24d; /* Bright Yellow: calculator results */ + --owlry-badge-clip: #9d00ff; /* Purple: clipboard is special */ + --owlry-badge-cmd: #9d00ff; /* Purple: commands are elevated */ + --owlry-badge-dmenu: #00ff99; /* Green: dmenu is success/pipe */ + --owlry-badge-emoji: #c84dff; /* Bright Purple: emoji is special */ + --owlry-badge-file: #5af3ff; /* Bright Cyan: file search is active info */ + --owlry-badge-script: #2bffb2; /* Bright Green: scripts execute successfully */ + --owlry-badge-ssh: #00eaff; /* Cyan: SSH is technical/info */ + --owlry-badge-sys: #ff0044; /* Red: system actions are critical */ + --owlry-badge-uuctl: #ffb700; /* Yellow: uuctl requires attention */ + --owlry-badge-web: #00eaff; /* Cyan: web is informational */ + + /* Widget badges */ + --owlry-badge-media: #c84dff; /* Bright Purple: media is special */ + --owlry-badge-weather: #5af3ff; /* Bright Cyan: weather is active info */ + --owlry-badge-pomo: #ff8899; /* Alert Red: pomodoro demands attention */ +} + +.owlry-main { + background-color: rgba(5, 5, 5, 0.98); + border: 1px solid rgba(38, 38, 38, 0.8); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), + 0 0 0 1px rgba(255, 0, 68, 0.1); +} + +.owlry-search { + background-color: rgba(20, 20, 20, 0.9); + border: 2px solid rgba(38, 38, 38, 0.8); + color: var(--owlry-text); + caret-color: var(--owlry-accent); +} + +.owlry-search:focus { + border-color: var(--owlry-accent); + box-shadow: 0 0 0 2px rgba(255, 0, 68, 0.3); +} + +.owlry-result-row:hover { + background-color: rgba(20, 20, 20, 0.8); +} + +.owlry-result-row:selected { + background-color: rgba(255, 0, 68, 0.15); + border-left: 3px solid var(--owlry-accent); +} + +.owlry-result-row:selected .owlry-result-name { + color: var(--owlry-accent-bright); +} + +.owlry-result-row:selected .owlry-result-icon { + color: var(--owlry-accent); +} + +/* Provider badges - styled per Apex semantics */ +.owlry-badge-app { + background-color: rgba(0, 234, 255, 0.15); + color: var(--owlry-badge-app); +} + +.owlry-badge-bookmark { + background-color: rgba(255, 183, 0, 0.15); + color: var(--owlry-badge-bookmark); +} + +.owlry-badge-calc { + background-color: rgba(255, 210, 77, 0.15); + color: var(--owlry-badge-calc); +} + +.owlry-badge-clip { + background-color: rgba(157, 0, 255, 0.15); + color: var(--owlry-badge-clip); +} + +.owlry-badge-cmd { + background-color: rgba(157, 0, 255, 0.15); + color: var(--owlry-badge-cmd); +} + +.owlry-badge-dmenu { + background-color: rgba(0, 255, 153, 0.15); + color: var(--owlry-badge-dmenu); +} + +.owlry-badge-emoji { + background-color: rgba(200, 77, 255, 0.15); + color: var(--owlry-badge-emoji); +} + +.owlry-badge-file { + background-color: rgba(90, 243, 255, 0.15); + color: var(--owlry-badge-file); +} + +.owlry-badge-script { + background-color: rgba(43, 255, 178, 0.15); + color: var(--owlry-badge-script); +} + +.owlry-badge-ssh { + background-color: rgba(0, 234, 255, 0.15); + color: var(--owlry-badge-ssh); +} + +.owlry-badge-sys { + background-color: rgba(255, 0, 68, 0.15); + color: var(--owlry-badge-sys); +} + +.owlry-badge-uuctl { + background-color: rgba(255, 183, 0, 0.15); + color: var(--owlry-badge-uuctl); +} + +.owlry-badge-web { + background-color: rgba(0, 234, 255, 0.15); + color: var(--owlry-badge-web); +} + +/* Widget badges */ +.owlry-badge-media { + background-color: rgba(200, 77, 255, 0.15); + color: var(--owlry-badge-media); +} + +.owlry-badge-weather { + background-color: rgba(90, 243, 255, 0.15); + color: var(--owlry-badge-weather); +} + +.owlry-badge-pomo { + background-color: rgba(255, 136, 153, 0.15); + color: var(--owlry-badge-pomo); +} + +/* Filter button - default uses The Predator */ +.owlry-filter-button:checked { + background-color: rgba(255, 0, 68, 0.2); + color: var(--owlry-accent); + border-color: rgba(255, 0, 68, 0.5); +} + +/* Provider-specific filter buttons - follow Apex semantics */ +.owlry-filter-app:checked { + background-color: rgba(0, 234, 255, 0.15); + color: var(--owlry-badge-app); + border-color: rgba(0, 234, 255, 0.5); +} + +.owlry-filter-bookmark:checked { + background-color: rgba(255, 183, 0, 0.15); + color: var(--owlry-badge-bookmark); + border-color: rgba(255, 183, 0, 0.5); +} + +.owlry-filter-calc:checked { + background-color: rgba(255, 210, 77, 0.15); + color: var(--owlry-badge-calc); + border-color: rgba(255, 210, 77, 0.5); +} + +.owlry-filter-clip:checked { + background-color: rgba(157, 0, 255, 0.15); + color: var(--owlry-badge-clip); + border-color: rgba(157, 0, 255, 0.5); +} + +.owlry-filter-cmd:checked { + background-color: rgba(157, 0, 255, 0.15); + color: var(--owlry-badge-cmd); + border-color: rgba(157, 0, 255, 0.5); +} + +.owlry-filter-dmenu:checked { + background-color: rgba(0, 255, 153, 0.15); + color: var(--owlry-badge-dmenu); + border-color: rgba(0, 255, 153, 0.5); +} + +.owlry-filter-emoji:checked { + background-color: rgba(200, 77, 255, 0.15); + color: var(--owlry-badge-emoji); + border-color: rgba(200, 77, 255, 0.5); +} + +.owlry-filter-file:checked { + background-color: rgba(90, 243, 255, 0.15); + color: var(--owlry-badge-file); + border-color: rgba(90, 243, 255, 0.5); +} + +.owlry-filter-script:checked { + background-color: rgba(43, 255, 178, 0.15); + color: var(--owlry-badge-script); + border-color: rgba(43, 255, 178, 0.5); +} + +.owlry-filter-ssh:checked { + background-color: rgba(0, 234, 255, 0.15); + color: var(--owlry-badge-ssh); + border-color: rgba(0, 234, 255, 0.5); +} + +.owlry-filter-sys:checked { + background-color: rgba(255, 0, 68, 0.15); + color: var(--owlry-badge-sys); + border-color: rgba(255, 0, 68, 0.5); +} + +.owlry-filter-uuctl:checked { + background-color: rgba(255, 183, 0, 0.15); + color: var(--owlry-badge-uuctl); + border-color: rgba(255, 183, 0, 0.5); +} + +.owlry-filter-web:checked { + background-color: rgba(0, 234, 255, 0.15); + color: var(--owlry-badge-web); + border-color: rgba(0, 234, 255, 0.5); +} + +/* Widget filter buttons */ +.owlry-filter-media:checked { + background-color: rgba(200, 77, 255, 0.15); + color: var(--owlry-badge-media); + border-color: rgba(200, 77, 255, 0.5); +} + +.owlry-filter-weather:checked { + background-color: rgba(90, 243, 255, 0.15); + color: var(--owlry-badge-weather); + border-color: rgba(90, 243, 255, 0.5); +} + +.owlry-filter-pomodoro:checked { + background-color: rgba(255, 136, 153, 0.15); + color: var(--owlry-badge-pomo); + border-color: rgba(255, 136, 153, 0.5); +} + +/* Scrollbar - subtle in Void, The Predator on active */ +scrollbar slider { + background-color: rgba(38, 38, 38, 0.8); +} + +scrollbar slider:hover { + background-color: rgba(64, 64, 64, 0.9); +} + +scrollbar slider:active { + background-color: var(--owlry-accent); +} + +/* Text selection - Apex Hard Rule: black text on red (target locked) */ +selection { + background-color: var(--owlry-accent); + color: #050505; +} + +/* Mode indicator - The Predator marks current mode */ +.owlry-mode-indicator { + background-color: rgba(255, 0, 68, 0.2); + color: var(--owlry-accent); + border: 1px solid rgba(255, 0, 68, 0.3); +} + +/* Hints bar */ +.owlry-hints { + border-top: 1px solid rgba(38, 38, 38, 0.8); +} + +.owlry-hints-label { + color: var(--owlry-text-secondary); +} + +/* Tag badges in results */ +.owlry-tag-badge { + background-color: rgba(38, 38, 38, 0.6); + color: var(--owlry-text-secondary); +} + +.owlry-result-row:selected .owlry-tag-badge { + background-color: rgba(255, 136, 153, 0.25); + color: var(--owlry-accent-bright); +} diff --git a/justfile b/justfile index 1bd3eb5..11029f3 100644 --- a/justfile +++ b/justfile @@ -60,6 +60,10 @@ install-local: sudo mkdir -p /usr/lib/owlry/plugins sudo mkdir -p /usr/lib/owlry/runtimes + echo "Cleaning up stale files..." + # Remove runtime files that may have ended up in plugins dir (from old installs) + sudo rm -f /usr/lib/owlry/plugins/libowlry_lua.so /usr/lib/owlry/plugins/libowlry_rune.so + echo "Installing core binary..." sudo install -Dm755 target/release/owlry /usr/bin/owlry