From 97c6f655cae2cb38fa88bfa514b39f559eb78dfc Mon Sep 17 00:00:00 2001 From: vikingowl Date: Mon, 29 Dec 2025 19:36:26 +0100 Subject: [PATCH] feat: add widget providers (weather, media, pomodoro) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Weather widget with Open-Meteo/wttr.in/OpenWeatherMap API support - 15-minute weather caching with geocoding for city names - MPRIS media player widget with play/pause toggle via dbus-send - Pomodoro timer widget with configurable work/break cycles - Widgets display at top of results with emoji icons - Improved terminal detection for Hyprland/Sway environments - Updated gtk4 to 0.10, gtk4-layer-shell to 0.7 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 1608 +++++++++++++++++++++++++++++++++++-- Cargo.toml | 10 +- data/config.example.toml | 28 +- src/app.rs | 52 +- src/config/mod.rs | 135 +++- src/filter.rs | 19 +- src/providers/media.rs | 264 ++++++ src/providers/mod.rs | 89 +- src/providers/pomodoro.rs | 334 ++++++++ src/providers/weather.rs | 527 ++++++++++++ src/resources/base.css | 42 + src/ui/main_window.rs | 67 +- src/ui/result_row.rs | 83 +- 13 files changed, 3146 insertions(+), 112 deletions(-) create mode 100644 src/providers/media.rs create mode 100644 src/providers/pomodoro.rs create mode 100644 src/providers/weather.rs diff --git a/Cargo.lock b/Cargo.lock index 122e8ae..9ff8788 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,12 +70,141 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.10.0" @@ -88,6 +217,28 @@ 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 = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bstr" version = "1.12.1" @@ -113,9 +264,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cairo-rs" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" dependencies = [ "bitflags", "cairo-sys-rs", @@ -125,9 +276,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" dependencies = [ "glib-sys", "libc", @@ -160,6 +311,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -220,12 +377,56 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[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 = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "5.0.1" @@ -247,6 +448,44 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -286,6 +525,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "field-offset" version = "0.3.6" @@ -308,6 +574,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "freedesktop-desktop-entry" version = "0.7.19" @@ -330,6 +605,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -355,6 +631,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -366,6 +655,12 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -379,8 +674,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -397,9 +695,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c" dependencies = [ "gdk-pixbuf-sys", "gio", @@ -409,9 +707,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7" dependencies = [ "gio-sys", "glib-sys", @@ -422,9 +720,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -438,9 +736,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -453,6 +751,16 @@ dependencies = [ "system-deps", ] +[[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" @@ -460,8 +768,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "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", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -486,9 +810,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" dependencies = [ "futures-channel", "futures-core", @@ -503,15 +827,15 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -536,9 +860,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ "bitflags", "futures-channel", @@ -557,9 +881,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" dependencies = [ "heck", "proc-macro-crate", @@ -570,9 +894,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" dependencies = [ "libc", "system-deps", @@ -580,9 +904,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" dependencies = [ "glib-sys", "libc", @@ -591,9 +915,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9" dependencies = [ "glib", "graphene-sys", @@ -602,9 +926,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93" dependencies = [ "glib-sys", "libc", @@ -614,9 +938,9 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153" dependencies = [ "cairo-rs", "gdk4", @@ -629,9 +953,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -645,9 +969,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.9.7" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4" dependencies = [ "cairo-rs", "field-offset", @@ -666,9 +990,9 @@ dependencies = [ [[package]] name = "gtk4-layer-shell" -version = "0.4.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e1e1b1516be3d7ca089dfa6a1e688e268c74aef50c0c25fe8c46b1ba8ed1cc" +checksum = "c1d422cce9367945916b7a5083eedf67b0a5380d326af1943a0b5cef9afb6e48" dependencies = [ "bitflags", "gdk4", @@ -681,9 +1005,9 @@ dependencies = [ [[package]] name = "gtk4-layer-shell-sys" -version = "0.3.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3057dc117db2d664a9b45f1956568701914e80cf9f2c8cef0a755af4c1c8105" +checksum = "e386481f3d83ab32e4ee457d9706d4ebbafa29ea013f9ae5066070713d2efacc" dependencies = [ "gdk4-sys", "glib-sys", @@ -694,9 +1018,9 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -706,9 +1030,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -735,6 +1059,119 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "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]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -759,6 +1196,108 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -769,6 +1308,22 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -843,6 +1398,18 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "locale_config" version = "0.3.0" @@ -862,6 +1429,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -907,6 +1480,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "1.2.4" @@ -969,6 +1555,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "owlry" version = "0.3.9" @@ -984,18 +1580,20 @@ dependencies = [ "libc", "log", "meval", + "reqwest", "serde", "serde_json", "thiserror 2.0.17", "tokio", "toml 0.8.23", + "zbus", ] [[package]] name = "pango" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" dependencies = [ "gio", "glib", @@ -1005,9 +1603,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" dependencies = [ "glib-sys", "gobject-sys", @@ -1015,6 +1613,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1027,12 +1637,37 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" version = "1.13.0" @@ -1048,6 +1683,24 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1066,6 +1719,61 @@ dependencies = [ "unicode-ident", ] +[[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.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "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]] name = "quote" version = "1.0.42" @@ -1075,13 +1783,78 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +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 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]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "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]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -1115,6 +1888,66 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "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]] +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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1124,12 +1957,66 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + [[package]] name = "semver" version = "1.0.27" @@ -1179,6 +2066,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1197,6 +2095,29 @@ dependencies = [ "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]] +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 = "shlex" version = "1.3.0" @@ -1225,12 +2146,40 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.111" @@ -1242,6 +2191,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "system-deps" version = "7.0.7" @@ -1267,6 +2236,19 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964" +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1316,6 +2298,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -1327,9 +2334,21 @@ dependencies = [ "mio", "pin-project-lite", "signal-hook-registry", + "socket2", + "tracing", "windows-sys 0.61.2", ] +[[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 = "toml" version = "0.8.23" @@ -1422,6 +2441,105 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicase" version = "2.8.1" @@ -1434,6 +2552,30 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1446,12 +2588,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -1465,6 +2631,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -1497,6 +2676,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1587,6 +2795,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1596,6 +2813,15 @@ dependencies = [ "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]] name = "windows-sys" version = "0.61.2" @@ -1629,13 +2855,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 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", + "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]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1648,6 +2891,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1660,6 +2909,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1672,12 +2927,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1690,6 +2957,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1702,6 +2975,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1714,6 +2993,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1726,6 +3011,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "winnow" version = "0.7.14" @@ -1735,20 +3026,239 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "xdg" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "xml-rs" version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 3c1a45a..d062591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,10 @@ categories = ["gui"] [dependencies] # GTK4 for the UI -gtk4 = { version = "0.9", features = ["v4_12"] } +gtk4 = { version = "0.10", features = ["v4_12"] } # Layer shell support for Wayland overlay behavior -gtk4-layer-shell = "0.4" +gtk4-layer-shell = "0.7" # Async runtime for non-blocking operations tokio = { version = "1", features = ["rt", "sync", "process", "fs"] } @@ -55,6 +55,12 @@ serde_json = "1" # Date/time for frecency calculations chrono = { version = "0.4", features = ["serde"] } +# D-Bus for MPRIS media player integration +zbus = { version = "4", default-features = false, features = ["tokio"] } + +# HTTP client for weather API +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "blocking"] } + [features] default = [] # Enable verbose debug logging (for development/testing builds) diff --git a/data/config.example.toml b/data/config.example.toml index 7b70b08..268e65e 100644 --- a/data/config.example.toml +++ b/data/config.example.toml @@ -18,8 +18,10 @@ show_icons = true max_results = 10 -# Terminal emulator (auto-detected if not set) -terminal_command = "kitty" +# Terminal emulator for SSH, scripts, etc. +# Auto-detection order: $TERMINAL β†’ xdg-terminal-exec β†’ DE-native β†’ Wayland β†’ X11 β†’ xterm +# Uncomment to override: +# terminal_command = "kitty" # Launch wrapper for app execution (auto-detected for uwsm/Hyprland) # Examples: "uwsm app --", "hyprctl dispatch exec --", "" @@ -34,8 +36,8 @@ tabs = ["app", "cmd", "uuctl"] # ═══════════════════════════════════════════════════════════════════════ [appearance] -width = 700 -height = 500 +width = 850 +height = 650 font_size = 14 border_radius = 12 @@ -113,3 +115,21 @@ emoji = true # Scripts: :script - executables from ~/.local/share/owlry/scripts/ scripts = true + +# ─────────────────────────────────────────────────────────────────────── +# Widget Providers (shown at top of results) +# ─────────────────────────────────────────────────────────────────────── + +# MPRIS media player controls - shows now playing with play/pause/skip +media = true + +# Weather widget - shows current conditions +weather = false +weather_provider = "wttr.in" # wttr.in (default), openweathermap, open-meteo +# weather_api_key = "" # Required for OpenWeatherMap +weather_location = "Berlin" # City name, "lat,lon", or leave empty for auto + +# Pomodoro timer - work/break timer with controls +pomodoro = false +pomodoro_work_mins = 25 # Work session duration +pomodoro_break_mins = 5 # Break duration diff --git a/src/app.rs b/src/app.rs index f575a9f..f4a1b8f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::data::FrecencyStore; use crate::filter::ProviderFilter; use crate::paths; -use crate::providers::ProviderManager; +use crate::providers::{PomodoroConfig, ProviderManager, WeatherConfig, WeatherProviderType}; use crate::theme; use crate::ui::MainWindow; use gtk4::prelude::*; @@ -43,7 +43,38 @@ impl OwlryApp { let config = Rc::new(RefCell::new(Config::load_or_default())); let search_engine = config.borrow().providers.search_engine.clone(); let terminal = config.borrow().general.terminal_command.clone(); - let providers = Rc::new(RefCell::new(ProviderManager::with_config(&search_engine, &terminal))); + let media_enabled = config.borrow().providers.media; + + // Build weather config if enabled + let weather_config = if config.borrow().providers.weather { + let cfg = config.borrow(); + Some(WeatherConfig { + provider: cfg.providers.weather_provider.parse().unwrap_or(WeatherProviderType::WttrIn), + api_key: cfg.providers.weather_api_key.clone(), + location: cfg.providers.weather_location.clone().unwrap_or_default(), + }) + } else { + None + }; + + // Build pomodoro config if enabled + let pomodoro_config = if config.borrow().providers.pomodoro { + let cfg = config.borrow(); + Some(PomodoroConfig { + work_mins: cfg.providers.pomodoro_work_mins, + break_mins: cfg.providers.pomodoro_break_mins, + }) + } else { + None + }; + + let providers = Rc::new(RefCell::new(ProviderManager::with_config( + &search_engine, + &terminal, + media_enabled, + weather_config, + pomodoro_config, + ))); let frecency = Rc::new(RefCell::new(FrecencyStore::load_or_default())); // Create filter from CLI args and config @@ -71,12 +102,29 @@ impl OwlryApp { // Position from top window.set_margin(Edge::Top, 200); + // Set up icon theme fallbacks + Self::setup_icon_theme(); + // Load CSS styling with config for theming Self::load_css(&config.borrow()); window.present(); } + fn setup_icon_theme() { + // Ensure we have icon fallbacks for weather/media icons + // These may not exist in all icon themes + if let Some(display) = gtk4::gdk::Display::default() { + let icon_theme = gtk4::IconTheme::for_display(&display); + + // Add Adwaita as fallback search path (has weather and media icons) + icon_theme.add_search_path("/usr/share/icons/Adwaita"); + icon_theme.add_search_path("/usr/share/icons/breeze"); + + debug!("Icon theme search paths configured with Adwaita/breeze fallbacks"); + } + } + fn load_css(config: &Config) { let display = gtk4::gdk::Display::default().expect("Could not get default display"); diff --git a/src/config/mod.rs b/src/config/mod.rs index abbb3d7..cea2a2f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -120,6 +120,40 @@ pub struct ProvidersConfig { /// 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, + + /// Location for weather (city name or coordinates) + #[serde(default)] + pub weather_location: Option, + + /// 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, } fn default_search_engine() -> String { @@ -134,6 +168,18 @@ fn default_frecency_weight() -> f64 { 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 launch wrapper for the current session /// Checks for uwsm (Universal Wayland Session Manager) and hyprland fn detect_launch_wrapper() -> Option { @@ -163,13 +209,14 @@ fn detect_launch_wrapper() -> Option { /// Detect the best available terminal emulator /// Fallback chain: /// 1. $TERMINAL env var (user's explicit preference) -/// 2. xdg-terminal-exec (freedesktop standard) -/// 3. Common Wayland-native terminals (kitty, alacritty, wezterm, foot) -/// 4. Common X11/legacy terminals (gnome-terminal, konsole, xfce4-terminal) -/// 5. x-terminal-emulator (Debian alternatives) -/// 6. xterm (ultimate fallback) +/// 2. xdg-terminal-exec (freedesktop standard - if available) +/// 3. Desktop-environment native terminal (GNOMEβ†’gnome-terminal, KDEβ†’konsole, etc.) +/// 4. Common Wayland-native terminals (kitty, alacritty, wezterm, foot) +/// 5. Common X11/legacy terminals +/// 6. x-terminal-emulator (Debian alternatives) +/// 7. xterm (ultimate fallback - the cockroach of terminals) fn detect_terminal() -> String { - // 1. Check $TERMINAL env var first + // 1. Check $TERMINAL env var first (user's explicit preference) if let Ok(term) = std::env::var("TERMINAL") { if !term.is_empty() && command_exists(&term) { debug!("Using $TERMINAL: {}", term); @@ -183,7 +230,13 @@ fn detect_terminal() -> String { return "xdg-terminal-exec".to_string(); } - // 3. Common Wayland-native terminals (preferred) + // 3. Desktop-environment aware detection + if let Some(term) = detect_de_terminal() { + debug!("Using DE-native terminal: {}", term); + return term; + } + + // 4. Common Wayland-native terminals (preferred for modern setups) let wayland_terminals = ["kitty", "alacritty", "wezterm", "foot"]; for term in wayland_terminals { if command_exists(term) { @@ -192,8 +245,8 @@ fn detect_terminal() -> String { } } - // 4. Common X11/legacy terminals - let legacy_terminals = ["gnome-terminal", "konsole", "xfce4-terminal", "tilix", "terminator"]; + // 5. Common X11/legacy terminals + let legacy_terminals = ["gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal", "tilix", "terminator"]; for term in legacy_terminals { if command_exists(term) { debug!("Found legacy terminal: {}", term); @@ -201,17 +254,64 @@ fn detect_terminal() -> String { } } - // 5. Try x-terminal-emulator (Debian alternatives system) + // 6. Try x-terminal-emulator (Debian alternatives system) if command_exists("x-terminal-emulator") { debug!("Using x-terminal-emulator"); return "x-terminal-emulator".to_string(); } - // 6. Ultimate fallback + // 7. Ultimate fallback - xterm exists everywhere debug!("Falling back to xterm"); "xterm".to_string() } +/// Detect desktop environment and return its native terminal +fn detect_de_terminal() -> Option { + // Check XDG_CURRENT_DESKTOP first + let desktop = std::env::var("XDG_CURRENT_DESKTOP") + .ok() + .map(|s| s.to_lowercase()); + + // Also check for Wayland compositor-specific env vars + let is_hyprland = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok(); + let is_sway = std::env::var("SWAYSOCK").is_ok(); + + // Map desktop environments to their native/preferred terminals + let candidates: &[&str] = if is_hyprland { + // Hyprland: foot and kitty are most popular in the community + &["foot", "kitty", "alacritty", "wezterm"] + } else if is_sway { + // Sway: foot is the recommended terminal (lightweight, Wayland-native) + &["foot", "alacritty", "kitty", "wezterm"] + } else if let Some(ref de) = desktop { + match de.as_str() { + s if s.contains("gnome") => &["gnome-terminal", "gnome-console", "kgx"], + s if s.contains("kde") || s.contains("plasma") => &["konsole"], + s if s.contains("xfce") => &["xfce4-terminal"], + s if s.contains("mate") => &["mate-terminal"], + s if s.contains("lxqt") => &["qterminal"], + s if s.contains("lxde") => &["lxterminal"], + s if s.contains("cinnamon") => &["gnome-terminal"], + s if s.contains("budgie") => &["tilix", "gnome-terminal"], + s if s.contains("pantheon") => &["io.elementary.terminal", "pantheon-terminal"], + s if s.contains("deepin") => &["deepin-terminal"], + s if s.contains("hyprland") => &["foot", "kitty", "alacritty", "wezterm"], + s if s.contains("sway") => &["foot", "alacritty", "kitty", "wezterm"], + _ => return None, + } + } else { + return None; + }; + + for term in candidates { + if command_exists(term) { + return Some(term.to_string()); + } + } + + None +} + /// Check if a command exists in PATH fn command_exists(cmd: &str) -> bool { Command::new("which") @@ -235,8 +335,8 @@ impl Default for Config { tabs: default_tabs(), }, appearance: AppearanceConfig { - width: 700, - height: 500, + width: 850, + height: 650, font_size: 14, border_radius: 12, theme: None, @@ -258,6 +358,15 @@ impl Default for Config { 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, }, } } diff --git a/src/filter.rs b/src/filter.rs index a16096e..3da1aa1 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -292,11 +292,14 @@ impl ProviderFilter { ProviderType::Dmenu => 5, ProviderType::Emoji => 6, ProviderType::Files => 7, - ProviderType::Scripts => 8, - ProviderType::Ssh => 9, - ProviderType::System => 10, - ProviderType::Uuctl => 11, - ProviderType::WebSearch => 12, + ProviderType::MediaPlayer => 8, + ProviderType::Pomodoro => 9, + ProviderType::Scripts => 10, + ProviderType::Ssh => 11, + ProviderType::System => 12, + ProviderType::Uuctl => 13, + ProviderType::Weather => 14, + ProviderType::WebSearch => 15, }); providers } @@ -313,10 +316,13 @@ impl ProviderFilter { ProviderType::Dmenu => "dmenu", ProviderType::Emoji => "Emoji", ProviderType::Files => "Files", + ProviderType::MediaPlayer => "Media", + ProviderType::Pomodoro => "Pomodoro", ProviderType::Scripts => "Scripts", ProviderType::Ssh => "SSH", ProviderType::System => "System", ProviderType::Uuctl => "uuctl", + ProviderType::Weather => "Weather", ProviderType::WebSearch => "Web", }; } @@ -332,10 +338,13 @@ impl ProviderFilter { ProviderType::Dmenu => "dmenu", ProviderType::Emoji => "Emoji", ProviderType::Files => "Files", + ProviderType::MediaPlayer => "Media", + ProviderType::Pomodoro => "Pomodoro", ProviderType::Scripts => "Scripts", ProviderType::Ssh => "SSH", ProviderType::System => "System", ProviderType::Uuctl => "uuctl", + ProviderType::Weather => "Weather", ProviderType::WebSearch => "Web", } } else { diff --git a/src/providers/media.rs b/src/providers/media.rs new file mode 100644 index 0000000..3784639 --- /dev/null +++ b/src/providers/media.rs @@ -0,0 +1,264 @@ +//! MPRIS D-Bus media player widget provider +//! +//! Shows currently playing track as a single row with play/pause action. + +use super::{LaunchItem, Provider, ProviderType}; +use log::debug; +use std::process::Command; + +/// Media player provider using MPRIS D-Bus interface +pub struct MediaProvider { + items: Vec, +} + +#[derive(Debug, Default, Clone)] +struct MediaState { + player_name: String, + title: String, + artist: String, + is_playing: bool, +} + +impl MediaProvider { + pub fn new() -> Self { + let mut provider = Self { items: Vec::new() }; + provider.refresh(); + provider + } + + /// Find active MPRIS players via dbus-send + fn find_players() -> Vec { + let output = Command::new("dbus-send") + .args([ + "--session", + "--dest=org.freedesktop.DBus", + "--type=method_call", + "--print-reply", + "/org/freedesktop/DBus", + "org.freedesktop.DBus.ListNames", + ]) + .output(); + + match output { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout); + stdout + .lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with("string \"org.mpris.MediaPlayer2.") { + let start = "string \"org.mpris.MediaPlayer2.".len(); + let end = trimmed.len() - 1; + Some(trimmed[start..end].to_string()) + } else { + None + } + }) + .collect() + } + Err(e) => { + debug!("Failed to list D-Bus names: {}", e); + Vec::new() + } + } + } + + /// Get metadata from an MPRIS player + fn get_player_state(player: &str) -> Option { + let dest = format!("org.mpris.MediaPlayer2.{}", player); + + // Get playback status + let status_output = Command::new("dbus-send") + .args([ + "--session", + &format!("--dest={}", dest), + "--type=method_call", + "--print-reply", + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties.Get", + "string:org.mpris.MediaPlayer2.Player", + "string:PlaybackStatus", + ]) + .output() + .ok()?; + + let status_str = String::from_utf8_lossy(&status_output.stdout); + let is_playing = status_str.contains("\"Playing\""); + let is_paused = status_str.contains("\"Paused\""); + + // Only show if playing or paused (not stopped) + if !is_playing && !is_paused { + return None; + } + + // Get metadata + let metadata_output = Command::new("dbus-send") + .args([ + "--session", + &format!("--dest={}", dest), + "--type=method_call", + "--print-reply", + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties.Get", + "string:org.mpris.MediaPlayer2.Player", + "string:Metadata", + ]) + .output() + .ok()?; + + let metadata_str = String::from_utf8_lossy(&metadata_output.stdout); + + let title = Self::extract_string(&metadata_str, "xesam:title") + .unwrap_or_else(|| "Unknown".to_string()); + let artist = Self::extract_array(&metadata_str, "xesam:artist") + .unwrap_or_else(|| "Unknown".to_string()); + + Some(MediaState { + player_name: player.to_string(), + title, + artist, + is_playing, + }) + } + + /// Extract string value from D-Bus output + fn extract_string(output: &str, key: &str) -> Option { + let key_pattern = format!("\"{}\"", key); + let mut found = false; + + for line in output.lines() { + let trimmed = line.trim(); + if trimmed.contains(&key_pattern) { + found = true; + continue; + } + if found { + if let Some(pos) = trimmed.find("string \"") { + let start = pos + "string \"".len(); + if let Some(end) = trimmed[start..].find('"') { + let value = &trimmed[start..start + end]; + if !value.is_empty() { + return Some(value.to_string()); + } + } + } + if !trimmed.starts_with("variant") { + found = false; + } + } + } + None + } + + /// Extract array value from D-Bus output + fn extract_array(output: &str, key: &str) -> Option { + let key_pattern = format!("\"{}\"", key); + let mut found = false; + let mut in_array = false; + let mut values = Vec::new(); + + for line in output.lines() { + let trimmed = line.trim(); + if trimmed.contains(&key_pattern) { + found = true; + continue; + } + if found && trimmed.contains("array [") { + in_array = true; + continue; + } + if in_array { + if let Some(pos) = trimmed.find("string \"") { + let start = pos + "string \"".len(); + if let Some(end) = trimmed[start..].find('"') { + values.push(trimmed[start..start + end].to_string()); + } + } + if trimmed.contains(']') { + break; + } + } + } + + if values.is_empty() { + None + } else { + Some(values.join(", ")) + } + } + + /// Generate single LaunchItem for media state + fn generate_items(&mut self, state: &MediaState) { + self.items.clear(); + + let status_icon = if state.is_playing { "▢️" } else { "⏸️" }; + let action = if state.is_playing { "Pause" } else { "Play" }; + + // Single row: "🎡 ▢️ Title β€” Artist" + let name = format!("🎡 {} {} β€” {}", status_icon, state.title, state.artist); + + // Extract player display name (e.g., "firefox.instance_1_94" -> "Firefox") + let player_display = state.player_name + .split('.') + .next() + .unwrap_or(&state.player_name); + let player_display = player_display[0..1].to_uppercase() + &player_display[1..]; + + let command = format!( + "dbus-send --session --dest=org.mpris.MediaPlayer2.{} /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause", + state.player_name + ); + + self.items.push(LaunchItem { + id: "media-now-playing".to_string(), + name, + description: Some(format!("{} Β· Press Enter to {}", player_display, action)), + icon: None, // Using emoji in name instead (β–Ά/⏸) + provider: ProviderType::MediaPlayer, + command, + terminal: false, + tags: vec!["media".to_string(), "widget".to_string()], + }); + } +} + +impl Provider for MediaProvider { + fn name(&self) -> &str { + "Media Player" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::MediaPlayer + } + + fn refresh(&mut self) { + self.items.clear(); + + let players = Self::find_players(); + if players.is_empty() { + debug!("No MPRIS players found"); + return; + } + + // Find first active player + for player in &players { + if let Some(state) = Self::get_player_state(player) { + debug!("Found active player: {} - {}", player, state.title); + self.generate_items(&state); + return; + } + } + + debug!("No active media found"); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +impl Default for MediaProvider { + fn default() -> Self { + Self::new() + } +} diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 7a19925..db123da 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -6,10 +6,13 @@ mod command; mod dmenu; mod emoji; mod files; +mod media; +mod pomodoro; mod scripts; mod ssh; mod system; mod uuctl; +mod weather; mod websearch; pub use application::ApplicationProvider; @@ -20,10 +23,13 @@ pub use command::CommandProvider; pub use dmenu::DmenuProvider; pub use emoji::EmojiProvider; pub use files::FileSearchProvider; +pub use media::MediaProvider; +pub use pomodoro::{PomodoroConfig, PomodoroProvider}; pub use scripts::ScriptsProvider; pub use ssh::SshProvider; pub use system::SystemProvider; pub use uuctl::UuctlProvider; +pub use weather::{WeatherConfig, WeatherProvider, WeatherProviderType}; pub use websearch::WebSearchProvider; use fuzzy_matcher::FuzzyMatcher; @@ -60,10 +66,13 @@ pub enum ProviderType { Dmenu, Emoji, Files, + MediaPlayer, + Pomodoro, Scripts, Ssh, System, Uuctl, + Weather, WebSearch, } @@ -80,13 +89,16 @@ impl std::str::FromStr for ProviderType { "dmenu" => Ok(ProviderType::Dmenu), "emoji" | "emojis" => Ok(ProviderType::Emoji), "file" | "files" | "find" => 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), + "weather" => Ok(ProviderType::Weather), "web" | "websearch" | "search" => Ok(ProviderType::WebSearch), _ => Err(format!( - "Unknown provider: '{}'. Valid: app, bookmark, calc, clip, cmd, emoji, file, script, ssh, sys, web", + "Unknown provider: '{}'. Valid: app, bookmark, calc, clip, cmd, emoji, file, media, pomo, script, ssh, sys, weather, web", s )), } @@ -104,10 +116,13 @@ impl std::fmt::Display for ProviderType { ProviderType::Dmenu => write!(f, "dmenu"), ProviderType::Emoji => write!(f, "emoji"), ProviderType::Files => write!(f, "file"), + ProviderType::MediaPlayer => write!(f, "media"), + ProviderType::Pomodoro => write!(f, "pomo"), ProviderType::Scripts => write!(f, "script"), ProviderType::Ssh => write!(f, "ssh"), ProviderType::System => write!(f, "sys"), ProviderType::Uuctl => write!(f, "uuctl"), + ProviderType::Weather => write!(f, "weather"), ProviderType::WebSearch => write!(f, "web"), } } @@ -128,6 +143,10 @@ pub struct ProviderManager { calculator: CalculatorProvider, websearch: WebSearchProvider, filesearch: FileSearchProvider, + // Widget providers (optional, controlled by config) + media: Option, + weather: Option, + pomodoro: Option, matcher: SkimMatcherV2, } @@ -138,15 +157,26 @@ impl ProviderManager { } pub fn with_search_engine(search_engine: &str) -> Self { - Self::with_config(search_engine, "kitty") + // Use xterm as fallback - it's the universal cockroach of terminals + // In practice, app.rs passes the detected/configured terminal + Self::with_config(search_engine, "xterm", true, None, None) } - pub fn with_config(search_engine: &str, terminal: &str) -> Self { + pub fn with_config( + search_engine: &str, + terminal: &str, + media_enabled: bool, + weather_config: Option, + pomodoro_config: Option, + ) -> Self { let mut manager = Self { providers: Vec::new(), calculator: CalculatorProvider::new(), websearch: WebSearchProvider::with_engine(search_engine), filesearch: FileSearchProvider::new(), + media: if media_enabled { Some(MediaProvider::new()) } else { None }, + weather: weather_config.map(WeatherProvider::new), + pomodoro: pomodoro_config.map(PomodoroProvider::new), matcher: SkimMatcherV2::default(), }; @@ -195,6 +225,25 @@ impl ProviderManager { provider.items().len() ); } + + // Refresh widget providers + if let Some(ref mut media) = self.media { + media.refresh(); + info!("Media widget loaded {} items", media.items().len()); + } + if let Some(ref mut weather) = self.weather { + weather.refresh(); + info!("Weather widget loaded {} items", weather.items().len()); + } + if let Some(ref mut pomodoro) = self.pomodoro { + pomodoro.refresh(); + info!("Pomodoro widget loaded {} items", pomodoro.items().len()); + } + } + + /// Get mutable reference to pomodoro provider (for handling actions) + pub fn pomodoro_mut(&mut self) -> Option<&mut PomodoroProvider> { + self.pomodoro.as_mut() } #[allow(dead_code)] @@ -299,6 +348,30 @@ impl ProviderManager { let mut results: Vec<(LaunchItem, i64)> = Vec::new(); + // Add widget items first (highest priority) - only when no specific filter is active + if filter.active_prefix().is_none() { + // Weather widget (score 12000) - shown at very top + if let Some(ref weather) = self.weather { + for item in weather.items() { + results.push((item.clone(), 12000)); + } + } + + // Pomodoro widget (scores 11500-11502) + if let Some(ref pomodoro) = self.pomodoro { + for (idx, item) in pomodoro.items().iter().enumerate() { + results.push((item.clone(), 11502 - idx as i64)); + } + } + + // Media widget (scores 11000-11003) + if let Some(ref media) = self.media { + for (idx, item) in media.items().iter().enumerate() { + results.push((item.clone(), 11003 - idx as i64)); + } + } + } + // Check for calculator query (= or calc prefix) if CalculatorProvider::is_calculator_query(query) { if let Some(calc_result) = self.calculator.evaluate(query) { @@ -350,7 +423,7 @@ impl ProviderManager { // Empty query (after checking special providers) - return frecency-sorted items if query.is_empty() { - let mut items: Vec<(LaunchItem, i64)> = self + let items: Vec<(LaunchItem, i64)> = self .providers .iter() .filter(|p| filter.is_active(p.provider_type())) @@ -370,9 +443,11 @@ impl ProviderManager { }) .collect(); - items.sort_by(|a, b| b.1.cmp(&a.1)); - items.truncate(max_results); - return items; + // Combine widgets (already in results) with frecency items + results.extend(items); + results.sort_by(|a, b| b.1.cmp(&a.1)); + results.truncate(max_results); + return results; } // Regular search with frecency boost and tag matching diff --git a/src/providers/pomodoro.rs b/src/providers/pomodoro.rs new file mode 100644 index 0000000..ace67e5 --- /dev/null +++ b/src/providers/pomodoro.rs @@ -0,0 +1,334 @@ +//! Pomodoro timer widget provider +//! +//! Shows timer with work/break cycles and playback-style controls. +//! State persists across sessions via JSON file. + +use super::{LaunchItem, Provider, ProviderType}; +use crate::paths; +use log::{debug, warn}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Pomodoro timer provider with persistent state +pub struct PomodoroProvider { + items: Vec, + state: PomodoroState, + config: PomodoroConfig, +} + +#[derive(Debug, Clone)] +pub struct PomodoroConfig { + pub work_mins: u32, + pub break_mins: u32, +} + +impl Default for PomodoroConfig { + fn default() -> Self { + Self { + work_mins: 25, + break_mins: 5, + } + } +} + +/// Timer phase +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum PomodoroPhase { + Idle, + Working, + WorkPaused, + Break, + BreakPaused, +} + +impl Default for PomodoroPhase { + fn default() -> Self { + Self::Idle + } +} + +/// Persistent state (saved to disk) +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct PomodoroState { + phase: PomodoroPhase, + /// Remaining seconds in current phase + remaining_secs: u32, + /// Completed work sessions count + sessions: u32, + /// Unix timestamp when timer was last updated (for calculating elapsed time) + last_update: u64, +} + +impl PomodoroProvider { + pub fn new(config: PomodoroConfig) -> Self { + let state = Self::load_state().unwrap_or_else(|| PomodoroState { + phase: PomodoroPhase::Idle, + remaining_secs: config.work_mins * 60, + sessions: 0, + last_update: Self::now_secs(), + }); + + let mut provider = Self { + items: Vec::new(), + state, + config, + }; + + // Update timer based on elapsed time since last save + provider.update_elapsed_time(); + provider.generate_items(); + provider + } + + /// Current unix timestamp + fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + } + + /// Load state from disk + fn load_state() -> Option { + let path = paths::owlry_data_dir()?.join("pomodoro.json"); + let content = fs::read_to_string(&path).ok()?; + serde_json::from_str(&content).ok() + } + + /// Save state to disk + fn save_state(&self) { + if let Some(data_dir) = paths::owlry_data_dir() { + let path = data_dir.join("pomodoro.json"); + if let Err(e) = fs::create_dir_all(&data_dir) { + warn!("Failed to create data dir: {}", e); + return; + } + let mut state = self.state.clone(); + state.last_update = Self::now_secs(); + if let Ok(json) = serde_json::to_string_pretty(&state) { + if let Err(e) = fs::write(&path, json) { + warn!("Failed to save pomodoro state: {}", e); + } + } + } + } + + /// Update remaining time based on elapsed time since last update + fn update_elapsed_time(&mut self) { + let now = Self::now_secs(); + let elapsed = now.saturating_sub(self.state.last_update); + + match self.state.phase { + PomodoroPhase::Working | PomodoroPhase::Break => { + if elapsed >= self.state.remaining_secs as u64 { + // Timer completed while app was closed + self.complete_phase(); + } else { + self.state.remaining_secs -= elapsed as u32; + } + } + // Paused or idle - no time passes + _ => {} + } + self.state.last_update = now; + } + + /// Complete current phase and transition to next + fn complete_phase(&mut self) { + match self.state.phase { + PomodoroPhase::Working => { + self.state.sessions += 1; + self.state.phase = PomodoroPhase::Break; + self.state.remaining_secs = self.config.break_mins * 60; + debug!("Work session {} completed, starting break", self.state.sessions); + } + PomodoroPhase::Break => { + self.state.phase = PomodoroPhase::Idle; + self.state.remaining_secs = self.config.work_mins * 60; + debug!("Break completed, ready for next session"); + } + _ => {} + } + self.save_state(); + } + + /// Handle action commands (called from MainWindow) + pub fn handle_action(&mut self, action: &str) { + debug!("Pomodoro action: {}", action); + match action { + "start" | "resume" => self.start_or_resume(), + "pause" => self.pause(), + "reset" => self.reset(), + "skip" => self.skip_phase(), + _ => warn!("Unknown pomodoro action: {}", action), + } + self.save_state(); + self.generate_items(); + } + + fn start_or_resume(&mut self) { + match self.state.phase { + PomodoroPhase::Idle => { + self.state.phase = PomodoroPhase::Working; + self.state.remaining_secs = self.config.work_mins * 60; + } + PomodoroPhase::WorkPaused => { + self.state.phase = PomodoroPhase::Working; + } + PomodoroPhase::BreakPaused => { + self.state.phase = PomodoroPhase::Break; + } + _ => {} // Already running + } + self.state.last_update = Self::now_secs(); + } + + fn pause(&mut self) { + match self.state.phase { + PomodoroPhase::Working => { + self.state.phase = PomodoroPhase::WorkPaused; + } + PomodoroPhase::Break => { + self.state.phase = PomodoroPhase::BreakPaused; + } + _ => {} + } + } + + fn reset(&mut self) { + self.state.phase = PomodoroPhase::Idle; + self.state.remaining_secs = self.config.work_mins * 60; + self.state.sessions = 0; + } + + fn skip_phase(&mut self) { + self.complete_phase(); + } + + /// Format seconds as MM:SS + fn format_time(secs: u32) -> String { + let mins = secs / 60; + let secs = secs % 60; + format!("{:02}:{:02}", mins, secs) + } + + /// Generate LaunchItems from current state + fn generate_items(&mut self) { + self.items.clear(); + + let (phase_name, phase_icon, is_running) = match self.state.phase { + PomodoroPhase::Idle => ("Ready", "media-playback-start", false), + PomodoroPhase::Working => ("Work", "media-playback-start", true), + PomodoroPhase::WorkPaused => ("Work (Paused)", "media-playback-pause", false), + PomodoroPhase::Break => ("Break", "face-cool", true), + PomodoroPhase::BreakPaused => ("Break (Paused)", "media-playback-pause", false), + }; + + // Timer display row + let time_str = Self::format_time(self.state.remaining_secs); + let name = format!("{}: {}", phase_name, time_str); + let description = if self.state.sessions > 0 { + Some(format!("Sessions: {} | {}min work / {}min break", + self.state.sessions, self.config.work_mins, self.config.break_mins)) + } else { + Some(format!("{}min work / {}min break", + self.config.work_mins, self.config.break_mins)) + }; + + self.items.push(LaunchItem { + id: "pomo-timer".to_string(), + name, + description, + icon: Some(phase_icon.to_string()), + provider: ProviderType::Pomodoro, + command: String::new(), // Info only + terminal: false, + tags: vec!["pomodoro".to_string(), "widget".to_string(), "timer".to_string()], + }); + + // Primary control: Start/Pause + let (control_name, control_icon, control_action) = if is_running { + ("Pause", "media-playback-pause", "pause") + } else { + match self.state.phase { + PomodoroPhase::Idle => ("Start Work", "media-playback-start", "start"), + _ => ("Resume", "media-playback-start", "resume"), + } + }; + + self.items.push(LaunchItem { + id: "pomo-control".to_string(), + name: control_name.to_string(), + description: Some(format!("{} timer", control_name)), + icon: Some(control_icon.to_string()), + provider: ProviderType::Pomodoro, + command: format!("POMODORO:{}", control_action), + terminal: false, + tags: vec!["pomodoro".to_string(), "control".to_string()], + }); + + // Secondary controls: Reset and Skip + if self.state.phase != PomodoroPhase::Idle { + self.items.push(LaunchItem { + id: "pomo-skip".to_string(), + name: "Skip".to_string(), + description: Some("Skip to next phase".to_string()), + icon: Some("media-skip-forward".to_string()), + provider: ProviderType::Pomodoro, + command: "POMODORO:skip".to_string(), + terminal: false, + tags: vec!["pomodoro".to_string(), "control".to_string()], + }); + } + + self.items.push(LaunchItem { + id: "pomo-reset".to_string(), + name: "Reset".to_string(), + description: Some("Reset timer and sessions".to_string()), + icon: Some("view-refresh".to_string()), + provider: ProviderType::Pomodoro, + command: "POMODORO:reset".to_string(), + terminal: false, + tags: vec!["pomodoro".to_string(), "control".to_string()], + }); + } + + /// Check if timer has completed (for external polling) + #[allow(dead_code)] + pub fn check_completion(&mut self) -> bool { + if matches!(self.state.phase, PomodoroPhase::Working | PomodoroPhase::Break) { + self.update_elapsed_time(); + self.generate_items(); + self.save_state(); + true + } else { + false + } + } +} + +impl Provider for PomodoroProvider { + fn name(&self) -> &str { + "Pomodoro" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Pomodoro + } + + fn refresh(&mut self) { + self.update_elapsed_time(); + self.generate_items(); + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +impl Default for PomodoroProvider { + fn default() -> Self { + Self::new(PomodoroConfig::default()) + } +} diff --git a/src/providers/weather.rs b/src/providers/weather.rs new file mode 100644 index 0000000..d3841f8 --- /dev/null +++ b/src/providers/weather.rs @@ -0,0 +1,527 @@ +//! Weather widget provider with multiple API support +//! +//! Supports: +//! - wttr.in (default, no API key required) +//! - OpenWeatherMap (requires API key) +//! - Open-Meteo (no API key required) + +use super::{LaunchItem, Provider, ProviderType}; +use log::{debug, error, warn}; +use serde::Deserialize; +use std::time::{Duration, Instant}; + +const CACHE_DURATION: Duration = Duration::from_secs(900); // 15 minutes +const REQUEST_TIMEOUT: Duration = Duration::from_secs(15); +const USER_AGENT: &str = "owlry-launcher/0.3"; + +/// Weather provider with caching and multiple API support +pub struct WeatherProvider { + items: Vec, + config: WeatherConfig, + last_fetch: Option, + cached_data: Option, +} + +#[derive(Debug, Clone)] +pub struct WeatherConfig { + pub provider: WeatherProviderType, + pub api_key: Option, + pub location: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum WeatherProviderType { + WttrIn, + OpenWeatherMap, + OpenMeteo, +} + +impl std::str::FromStr for WeatherProviderType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "wttr.in" | "wttr" | "wttrin" => Ok(Self::WttrIn), + "openweathermap" | "owm" => Ok(Self::OpenWeatherMap), + "open-meteo" | "openmeteo" | "meteo" => Ok(Self::OpenMeteo), + _ => Err(format!("Unknown weather provider: {}", s)), + } + } +} + +#[derive(Debug, Clone)] +struct WeatherData { + temperature: f32, + feels_like: Option, + condition: String, + humidity: Option, + wind_speed: Option, + icon: String, + location: String, +} + +impl WeatherProvider { + pub fn new(config: WeatherConfig) -> Self { + let mut provider = Self { + items: Vec::new(), + config, + last_fetch: None, + cached_data: None, + }; + provider.refresh(); + provider + } + + /// Create with default config (wttr.in, auto-detect location) + pub fn with_defaults() -> Self { + Self::new(WeatherConfig { + provider: WeatherProviderType::WttrIn, + api_key: None, + location: String::new(), // Empty = auto-detect + }) + } + + /// Check if cache is still valid + fn is_cache_valid(&self) -> bool { + if let Some(last_fetch) = self.last_fetch { + last_fetch.elapsed() < CACHE_DURATION + } else { + false + } + } + + /// Fetch weather data from the configured provider + fn fetch_weather(&self) -> Option { + match self.config.provider { + WeatherProviderType::WttrIn => self.fetch_wttr_in(), + WeatherProviderType::OpenWeatherMap => self.fetch_openweathermap(), + WeatherProviderType::OpenMeteo => self.fetch_open_meteo(), + } + } + + /// Fetch from wttr.in + fn fetch_wttr_in(&self) -> Option { + let location = if self.config.location.is_empty() { + String::new() + } else { + self.config.location.clone() + }; + + let url = format!("https://wttr.in/{}?format=j1", location); + debug!("Fetching weather from: {}", url); + + let client = match reqwest::blocking::Client::builder() + .timeout(REQUEST_TIMEOUT) + .user_agent(USER_AGENT) + .build() + { + Ok(c) => c, + Err(e) => { + error!("Failed to build HTTP client: {}", e); + return None; + } + }; + + let response = match client.get(&url).send() { + Ok(r) => r, + Err(e) => { + error!("Weather request failed: {}", e); + return None; + } + }; + + let json: WttrInResponse = match response.json() { + Ok(j) => j, + Err(e) => { + error!("Failed to parse weather JSON: {}", e); + return None; + } + }; + + let current = json.current_condition.first()?; + let nearest = json.nearest_area.first()?; + + let location_name = nearest + .area_name + .first() + .map(|a| a.value.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + + Some(WeatherData { + temperature: current.temp_c.parse().unwrap_or(0.0), + feels_like: current.feels_like_c.parse().ok(), + condition: current + .weather_desc + .first() + .map(|d| d.value.clone()) + .unwrap_or_else(|| "Unknown".to_string()), + humidity: current.humidity.parse().ok(), + wind_speed: current.windspeed_kmph.parse().ok(), + icon: Self::condition_to_icon(¤t.weather_code), + location: location_name, + }) + } + + /// Fetch from OpenWeatherMap + fn fetch_openweathermap(&self) -> Option { + let api_key = self.config.api_key.as_ref()?; + let location = if self.config.location.is_empty() { + warn!("OpenWeatherMap requires a location to be configured"); + return None; + } else { + &self.config.location + }; + + let url = format!( + "https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric", + location, api_key + ); + debug!("Fetching weather from OpenWeatherMap"); + + let client = reqwest::blocking::Client::builder() + .timeout(REQUEST_TIMEOUT) + .build() + .ok()?; + + let response = client.get(&url).send().ok()?; + let json: OpenWeatherMapResponse = response.json().ok()?; + + let weather = json.weather.first()?; + + Some(WeatherData { + temperature: json.main.temp, + feels_like: Some(json.main.feels_like), + condition: weather.description.clone(), + humidity: Some(json.main.humidity), + wind_speed: Some(json.wind.speed * 3.6), // m/s to km/h + icon: Self::owm_icon_to_freedesktop(&weather.icon), + location: json.name, + }) + } + + /// Fetch from Open-Meteo + fn fetch_open_meteo(&self) -> Option { + // Open-Meteo requires coordinates, so we need to geocode first + // For simplicity, we'll use a geocoding step if location is a city name + let (lat, lon, location_name) = self.get_coordinates()?; + + let url = format!( + "https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&timezone=auto", + lat, lon + ); + debug!("Fetching weather from Open-Meteo for {}", location_name); + + let client = reqwest::blocking::Client::builder() + .timeout(REQUEST_TIMEOUT) + .build() + .ok()?; + + let response = client.get(&url).send().ok()?; + let json: OpenMeteoResponse = response.json().ok()?; + + let current = json.current; + + Some(WeatherData { + temperature: current.temperature_2m, + feels_like: None, + condition: Self::wmo_code_to_description(current.weather_code), + humidity: Some(current.relative_humidity_2m as u8), + wind_speed: Some(current.wind_speed_10m), + icon: Self::wmo_code_to_icon(current.weather_code), + location: location_name, + }) + } + + /// Get coordinates and location name for Open-Meteo (simple parsing or geocoding) + fn get_coordinates(&self) -> Option<(f64, f64, String)> { + let location = &self.config.location; + + // Check if location is already coordinates (lat,lon) + if location.contains(',') { + let parts: Vec<&str> = location.split(',').collect(); + if parts.len() == 2 { + if let (Ok(lat), Ok(lon)) = ( + parts[0].trim().parse::(), + parts[1].trim().parse::(), + ) { + // Use coordinates as location name (will be overwritten if we had a name) + return Some((lat, lon, location.clone())); + } + } + } + + // Use Open-Meteo geocoding API + let url = format!( + "https://geocoding-api.open-meteo.com/v1/search?name={}&count=1", + location + ); + + let client = reqwest::blocking::Client::builder() + .timeout(REQUEST_TIMEOUT) + .build() + .ok()?; + + let response = client.get(&url).send().ok()?; + let json: GeocodingResponse = response.json().ok()?; + + let result = json.results?.into_iter().next()?; + Some((result.latitude, result.longitude, result.name)) + } + + /// Convert wttr.in weather code to freedesktop icon name + fn condition_to_icon(code: &str) -> String { + let icon = match code { + "113" => "weather-clear", // Sunny + "116" => "weather-few-clouds", // Partly cloudy + "119" => "weather-overcast", // Cloudy + "122" => "weather-overcast", // Overcast + "143" | "248" | "260" => "weather-fog", + "176" | "263" | "266" | "293" | "296" | "299" | "302" | "305" | "308" => { + "weather-showers" + } + "179" | "182" | "185" | "227" | "230" | "323" | "326" | "329" | "332" | "335" + | "338" | "350" | "368" | "371" | "374" | "377" => "weather-snow", + "200" | "386" | "389" | "392" | "395" => "weather-storm", + _ => "weather-clear", + }; + // Try symbolic version first (more likely to exist) + format!("{}-symbolic", icon) + } + + /// Convert OpenWeatherMap icon code to freedesktop icon + fn owm_icon_to_freedesktop(icon: &str) -> String { + match icon { + "01d" | "01n" => "weather-clear", + "02d" | "02n" => "weather-few-clouds", + "03d" | "03n" | "04d" | "04n" => "weather-overcast", + "09d" | "09n" | "10d" | "10n" => "weather-showers", + "11d" | "11n" => "weather-storm", + "13d" | "13n" => "weather-snow", + "50d" | "50n" => "weather-fog", + _ => "weather-clear", + } + .to_string() + } + + /// Convert WMO weather code to description + fn wmo_code_to_description(code: i32) -> String { + match code { + 0 => "Clear sky", + 1 => "Mainly clear", + 2 => "Partly cloudy", + 3 => "Overcast", + 45 | 48 => "Foggy", + 51 | 53 | 55 => "Drizzle", + 61 | 63 | 65 => "Rain", + 66 | 67 => "Freezing rain", + 71 | 73 | 75 | 77 => "Snow", + 80 | 81 | 82 => "Rain showers", + 85 | 86 => "Snow showers", + 95 | 96 | 99 => "Thunderstorm", + _ => "Unknown", + } + .to_string() + } + + /// Convert WMO weather code to icon name (used for emoji conversion) + fn wmo_code_to_icon(code: i32) -> String { + match code { + 0 | 1 => "weather-clear", + 2 => "weather-few-clouds", + 3 => "weather-overcast", + 45 | 48 => "weather-fog", + 51 | 53 | 55 | 61 | 63 | 65 | 80 | 81 | 82 => "weather-showers", + 66 | 67 | 71 | 73 | 75 | 77 | 85 | 86 => "weather-snow", + 95 | 96 | 99 => "weather-storm", + _ => "weather-clear", + } + .to_string() + } + + /// Generate LaunchItems from weather data + fn generate_items(&mut self, data: &WeatherData) { + self.items.clear(); + + // Use emoji in name since icon themes are unreliable + let emoji = Self::weather_icon_to_emoji(&data.icon); + let temp_str = format!("{}Β°C", data.temperature.round() as i32); + let name = format!("{} {} {}", emoji, temp_str, data.condition); + + let mut details = vec![data.location.clone()]; + if let Some(humidity) = data.humidity { + details.push(format!("Humidity {}%", humidity)); + } + if let Some(wind) = data.wind_speed { + details.push(format!("Wind {} km/h", wind.round() as i32)); + } + if let Some(feels) = data.feels_like { + if (feels - data.temperature).abs() > 2.0 { + details.push(format!("Feels like {}Β°C", feels.round() as i32)); + } + } + + // Simple URL encoding for location + let encoded_location = data.location.replace(' ', "+"); + + self.items.push(LaunchItem { + id: "weather-current".to_string(), + name, + description: Some(details.join(" | ")), + icon: None, // Use emoji in name instead + provider: ProviderType::Weather, + command: format!("xdg-open 'https://wttr.in/{}'", encoded_location), + terminal: false, + tags: vec!["weather".to_string(), "widget".to_string()], + }); + } + + /// Convert icon name to emoji for display + fn weather_icon_to_emoji(icon: &str) -> &'static str { + if icon.contains("clear") { + "β˜€οΈ" + } else if icon.contains("few-clouds") { + "β›…" + } else if icon.contains("overcast") || icon.contains("clouds") { + "☁️" + } else if icon.contains("fog") { + "🌫️" + } else if icon.contains("showers") || icon.contains("rain") { + "🌧️" + } else if icon.contains("snow") { + "❄️" + } else if icon.contains("storm") { + "β›ˆοΈ" + } else { + "🌑️" + } + } +} + +impl Provider for WeatherProvider { + fn name(&self) -> &str { + "Weather" + } + + fn provider_type(&self) -> ProviderType { + ProviderType::Weather + } + + fn refresh(&mut self) { + // Use cache if still valid + if self.is_cache_valid() { + if let Some(data) = self.cached_data.clone() { + self.generate_items(&data); + return; + } + } + + // Fetch new data + match self.fetch_weather() { + Some(data) => { + debug!("Weather fetched: {}Β°C, {}", data.temperature, data.condition); + self.cached_data = Some(data.clone()); + self.last_fetch = Some(Instant::now()); + self.generate_items(&data); + } + None => { + warn!("Failed to fetch weather data"); + self.items.clear(); + } + } + } + + fn items(&self) -> &[LaunchItem] { + &self.items + } +} + +impl Default for WeatherProvider { + fn default() -> Self { + Self::with_defaults() + } +} + +// ─── API Response Types ───────────────────────────────────────────────────── + +#[derive(Debug, Deserialize)] +struct WttrInResponse { + current_condition: Vec, + nearest_area: Vec, +} + +#[derive(Debug, Deserialize)] +struct WttrInCurrent { + #[serde(rename = "temp_C")] + temp_c: String, + #[serde(rename = "FeelsLikeC")] + feels_like_c: String, + humidity: String, + #[serde(rename = "weatherCode")] + weather_code: String, + #[serde(rename = "weatherDesc")] + weather_desc: Vec, + #[serde(rename = "windspeedKmph")] + windspeed_kmph: String, +} + +#[derive(Debug, Deserialize)] +struct WttrInValue { + value: String, +} + +#[derive(Debug, Deserialize)] +struct WttrInArea { + #[serde(rename = "areaName")] + area_name: Vec, +} + +#[derive(Debug, Deserialize)] +struct OpenWeatherMapResponse { + main: OwmMain, + weather: Vec, + wind: OwmWind, + name: String, +} + +#[derive(Debug, Deserialize)] +struct OwmMain { + temp: f32, + feels_like: f32, + humidity: u8, +} + +#[derive(Debug, Deserialize)] +struct OwmWeather { + description: String, + icon: String, +} + +#[derive(Debug, Deserialize)] +struct OwmWind { + speed: f32, +} + +#[derive(Debug, Deserialize)] +struct OpenMeteoResponse { + current: OpenMeteoCurrent, +} + +#[derive(Debug, Deserialize)] +struct OpenMeteoCurrent { + temperature_2m: f32, + relative_humidity_2m: f32, + weather_code: i32, + wind_speed_10m: f32, +} + +#[derive(Debug, Deserialize)] +struct GeocodingResponse { + results: Option>, +} + +#[derive(Debug, Deserialize)] +struct GeocodingResult { + name: String, + latitude: f64, + longitude: f64, +} diff --git a/src/resources/base.css b/src/resources/base.css index 247a83c..4197be1 100644 --- a/src/resources/base.css +++ b/src/resources/base.css @@ -67,6 +67,13 @@ opacity: 1; } +/* Emoji icon for widgets (weather, media, pomodoro) */ +.owlry-emoji-icon { + font-size: 24px; + min-width: 32px; + min-height: 32px; +} + /* Result name */ .owlry-result-name { font-size: var(--owlry-font-size, 14px); @@ -166,6 +173,22 @@ color: var(--owlry-badge-web, @teal_3); } +/* Widget provider badges */ +.owlry-badge-media { + background-color: alpha(var(--owlry-badge-media, #ec4899), 0.2); + color: var(--owlry-badge-media, #ec4899); +} + +.owlry-badge-weather { + background-color: alpha(var(--owlry-badge-weather, #06b6d4), 0.2); + color: var(--owlry-badge-weather, #06b6d4); +} + +.owlry-badge-pomo { + background-color: alpha(var(--owlry-badge-pomo, #f97316), 0.2); + color: var(--owlry-badge-pomo, #f97316); +} + /* Header bar */ .owlry-header { margin-bottom: 4px; @@ -283,6 +306,25 @@ border-color: alpha(var(--owlry-badge-web, @teal_3), 0.4); } +/* Widget filter buttons */ +.owlry-filter-media:checked { + background-color: alpha(var(--owlry-badge-media, #ec4899), 0.2); + color: var(--owlry-badge-media, #ec4899); + border-color: alpha(var(--owlry-badge-media, #ec4899), 0.4); +} + +.owlry-filter-weather:checked { + background-color: alpha(var(--owlry-badge-weather, #06b6d4), 0.2); + color: var(--owlry-badge-weather, #06b6d4); + border-color: alpha(var(--owlry-badge-weather, #06b6d4), 0.4); +} + +.owlry-filter-pomodoro:checked { + background-color: alpha(var(--owlry-badge-pomo, #f97316), 0.2); + color: var(--owlry-badge-pomo, #f97316); + border-color: alpha(var(--owlry-badge-pomo, #f97316), 0.4); +} + /* Hints bar at bottom */ .owlry-hints { padding-top: 8px; diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 37742ea..079230c 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -248,10 +248,13 @@ impl MainWindow { ProviderType::Dmenu => "Dmenu", ProviderType::Emoji => "Emoji", ProviderType::Files => "Files", + ProviderType::MediaPlayer => "Media", + ProviderType::Pomodoro => "Pomo", ProviderType::Scripts => "Scripts", ProviderType::Ssh => "SSH", ProviderType::System => "System", ProviderType::Uuctl => "uuctl", + ProviderType::Weather => "Weather", ProviderType::WebSearch => "Web", } } @@ -267,10 +270,13 @@ impl MainWindow { ProviderType::Dmenu => "owlry-filter-dmenu", ProviderType::Emoji => "owlry-filter-emoji", ProviderType::Files => "owlry-filter-file", + ProviderType::MediaPlayer => "owlry-filter-media", + ProviderType::Pomodoro => "owlry-filter-pomodoro", ProviderType::Scripts => "owlry-filter-script", ProviderType::Ssh => "owlry-filter-ssh", ProviderType::System => "owlry-filter-sys", ProviderType::Uuctl => "owlry-filter-uuctl", + ProviderType::Weather => "owlry-filter-weather", ProviderType::WebSearch => "owlry-filter-web", } } @@ -288,10 +294,13 @@ impl MainWindow { ProviderType::Dmenu => "options", ProviderType::Emoji => "emoji", ProviderType::Files => "files", + ProviderType::MediaPlayer => "media", + ProviderType::Pomodoro => "pomodoro", ProviderType::Scripts => "scripts", ProviderType::Ssh => "SSH hosts", ProviderType::System => "system", ProviderType::Uuctl => "uuctl units", + ProviderType::Weather => "weather", ProviderType::WebSearch => "web", }) .collect(); @@ -531,10 +540,13 @@ impl MainWindow { ProviderType::Dmenu => "options", ProviderType::Emoji => "emoji", ProviderType::Files => "files", + ProviderType::MediaPlayer => "media", + ProviderType::Pomodoro => "pomodoro", ProviderType::Scripts => "scripts", ProviderType::Ssh => "SSH hosts", ProviderType::System => "system", ProviderType::Uuctl => "uuctl units", + ProviderType::Weather => "weather", ProviderType::WebSearch => "web", }; search_entry_for_change @@ -584,13 +596,14 @@ impl MainWindow { let current_results_for_activate = self.current_results.clone(); let config_for_activate = self.config.clone(); let frecency_for_activate = self.frecency.clone(); + let providers_for_activate = self.providers.clone(); let window_for_activate = self.window.clone(); let submenu_state_for_activate = self.submenu_state.clone(); let mode_label_for_activate = self.mode_label.clone(); let hints_label_for_activate = self.hints_label.clone(); let search_entry_for_activate = self.search_entry.clone(); - self.search_entry.connect_activate(move |_| { + self.search_entry.connect_activate(move |entry| { let selected = results_list_for_activate .selected_row() .or_else(|| results_list_for_activate.row_at_index(0)); @@ -616,9 +629,21 @@ impl MainWindow { is_active, ); } else { - // Execute the command - Self::launch_item(item, &config_for_activate.borrow(), &frecency_for_activate); - window_for_activate.close(); + // Execute the command (or handle internal commands) + let item = item.clone(); + drop(results); + let should_close = Self::handle_item_action( + &item, + &config_for_activate.borrow(), + &frecency_for_activate, + &providers_for_activate, + ); + if should_close { + window_for_activate.close(); + } else { + // Trigger search refresh for updated widget state + entry.emit_by_name::<()>("changed", &[]); + } } } } @@ -794,6 +819,7 @@ impl MainWindow { let current_results = self.current_results.clone(); let config = self.config.clone(); let frecency = self.frecency.clone(); + let providers = self.providers.clone(); let window = self.window.clone(); let submenu_state = self.submenu_state.clone(); let results_list_for_click = self.results_list.clone(); @@ -822,8 +848,15 @@ impl MainWindow { is_active, ); } else { - Self::launch_item(item, &config.borrow(), &frecency); - window.close(); + let item = item.clone(); + drop(results); + let should_close = Self::handle_item_action(&item, &config.borrow(), &frecency, &providers); + if should_close { + window.close(); + } else { + // Trigger search refresh for updated widget state + search_entry.emit_by_name::<()>("changed", &[]); + } } } }); @@ -928,6 +961,28 @@ impl MainWindow { *self.current_results.borrow_mut() = results; } + /// Handle item activation - returns true if window should close + fn handle_item_action( + item: &LaunchItem, + config: &Config, + frecency: &Rc>, + providers: &Rc>, + ) -> bool { + // Check for POMODORO: internal command + if item.command.starts_with("POMODORO:") { + let action = item.command.strip_prefix("POMODORO:").unwrap_or(""); + if let Some(pomodoro) = providers.borrow_mut().pomodoro_mut() { + pomodoro.handle_action(action); + } + // Don't close window - user might want to see updated state + return false; + } + + // Regular item launch + Self::launch_item(item, config, frecency); + true + } + fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc>) { // Record this launch for frecency tracking if config.providers.frecency { diff --git a/src/ui/result_row.rs b/src/ui/result_row.rs index 3ebb98e..2684644 100644 --- a/src/ui/result_row.rs +++ b/src/ui/result_row.rs @@ -1,6 +1,6 @@ use crate::providers::LaunchItem; use gtk4::prelude::*; -use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation}; +use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget}; #[allow(dead_code)] pub struct ResultRow { @@ -25,32 +25,55 @@ impl ResultRow { .margin_end(12) .build(); - // Icon - let icon = if let Some(icon_name) = &item.icon { - Image::from_icon_name(icon_name) + // Icon - handle file paths, icon names, emoji widgets, and fallbacks + let icon_widget: Widget = if let Some(icon_name) = &item.icon { + let img = if icon_name.starts_with('/') { + // Absolute file path + Image::from_file(icon_name) + } else { + Image::from_icon_name(icon_name) + }; + img.set_pixel_size(32); + img.add_css_class("owlry-result-icon"); + img.upcast() } else { // Default icon based on provider type - 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::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", - }; - Image::from_icon_name(default_icon) + // For widgets, use emoji labels (more reliable than icon themes) + match item.provider { + crate::providers::ProviderType::Weather => { + Self::create_emoji_icon("🌀️") + } + crate::providers::ProviderType::MediaPlayer => { + Self::create_emoji_icon("🎡") + } + crate::providers::ProviderType::Pomodoro => { + Self::create_emoji_icon("πŸ…") + } + _ => { + 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::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", + _ => "application-x-executable", + }; + let img = Image::from_icon_name(default_icon); + img.set_pixel_size(32); + img.add_css_class("owlry-result-icon"); + img.upcast() + } + } }; - icon.set_pixel_size(32); - icon.add_css_class("owlry-result-icon"); - // Text container let text_box = GtkBox::builder() .orientation(Orientation::Vertical) @@ -111,7 +134,7 @@ impl ResultRow { badge.add_css_class("owlry-result-badge"); badge.add_css_class(&format!("owlry-badge-{}", item.provider)); - hbox.append(&icon); + hbox.append(&icon_widget); hbox.append(&text_box); hbox.append(&badge); @@ -119,4 +142,16 @@ impl ResultRow { row } + + /// Create an emoji-based icon widget for widgets (weather, media, pomodoro) + /// More reliable than icon themes which may not have all icons + fn create_emoji_icon(emoji: &str) -> Widget { + let label = Label::builder() + .label(emoji) + .width_request(32) + .height_request(32) + .build(); + label.add_css_class("owlry-emoji-icon"); + label.upcast() + } }