diff --git a/Cargo.lock b/Cargo.lock index e1fdf102..4a48e8dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "approx" @@ -340,7 +340,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -472,7 +472,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -507,7 +507,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -800,9 +800,9 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -821,7 +821,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1001,9 +1001,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" dependencies = [ "clap_builder", "clap_derive", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" dependencies = [ "anstream", "anstyle", @@ -1031,7 +1031,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1366,7 +1366,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1390,7 +1390,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1401,7 +1401,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1520,7 +1520,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1791,7 +1791,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1812,7 +1812,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1862,7 +1862,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -1966,7 +1966,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -2106,7 +2106,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -2228,7 +2228,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -2920,6 +2920,7 @@ dependencies = [ "ravif", "rayon", "rgb", + "serde", "tiff 0.10.3", "zune-core 0.5.1", "zune-jpeg 0.5.12", @@ -2998,7 +2999,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -3112,9 +3113,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.87" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -3201,6 +3202,7 @@ dependencies = [ "dunce", "eframe", "egui-phosphor", + "egui_extras", "font-loader", "hotwatch", "image 0.25.9", @@ -3213,12 +3215,15 @@ dependencies = [ "num-traits", "parking_lot", "random_word", + "regex", "reqwest", "schemars 1.2.1", "serde", "serde_json_lenient", "starship-battery", "sysinfo 0.38.2", + "systray-util", + "tokio", "tracing", "tracing-subscriber", "which", @@ -3500,7 +3505,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -3630,7 +3635,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -3765,9 +3770,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.18" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" dependencies = [ "libc", "log", @@ -4021,7 +4026,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -4095,7 +4100,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -4564,7 +4569,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -4808,7 +4813,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "unicase", ] @@ -4857,7 +4862,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5009,7 +5014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5046,7 +5051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5391,7 +5396,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5650,7 +5655,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5686,9 +5691,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.7.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ "bitflags 2.11.0", "core-foundation 0.10.1", @@ -5699,9 +5704,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.17.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" dependencies = [ "core-foundation-sys", "libc", @@ -5740,7 +5745,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5751,7 +5756,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5788,7 +5793,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -5840,7 +5845,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -6184,7 +6189,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -6227,9 +6232,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -6253,7 +6258,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -6305,6 +6310,23 @@ dependencies = [ "libc", ] +[[package]] +name = "systray-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "988063a25f37b77024a34a00047c4377a32d861626192c77c6fff0ae377b31a4" +dependencies = [ + "image 0.25.9", + "serde", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "windows 0.58.0", + "windows-core 0.58.0", +] + [[package]] name = "tempfile" version = "3.25.0" @@ -6373,7 +6395,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -6384,7 +6406,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -6498,11 +6520,25 @@ dependencies = [ "bytes", "libc", "mio 1.1.1", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -6652,7 +6688,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -6948,9 +6984,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.110" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -6961,9 +6997,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.60" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if 1.0.4", "futures-util", @@ -6975,9 +7011,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.110" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6985,22 +7021,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.110" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.110" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -7176,9 +7212,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.87" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -7660,7 +7696,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -7671,7 +7707,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -7682,7 +7718,7 @@ checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -7693,7 +7729,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -7704,7 +7740,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -7715,7 +7751,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -7726,7 +7762,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -8286,7 +8322,7 @@ dependencies = [ "heck", "indexmap 2.13.0", "prettyplease", - "syn 2.0.117", + "syn 2.0.116", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -8302,7 +8338,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -8462,7 +8498,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "synstructure", ] @@ -8519,7 +8555,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "zbus-lockstep", "zbus_xml", "zvariant", @@ -8534,7 +8570,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "zbus_names", "zvariant", "zvariant_utils", @@ -8580,7 +8616,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -8600,7 +8636,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "synstructure", ] @@ -8640,7 +8676,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", ] [[package]] @@ -8711,7 +8747,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.116", "zvariant_utils", ] @@ -8724,6 +8760,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", + "syn 2.0.116", "winnow", ] diff --git a/dependencies.json b/dependencies.json index d43856fe..81744061 100644 --- a/dependencies.json +++ b/dependencies.json @@ -51,8 +51,8 @@ "cfg-if 1.0.4 registry+https://github.com/rust-lang/crates.io-index", "chrono 0.4.43 registry+https://github.com/rust-lang/crates.io-index", "chrono-tz 0.10.4 registry+https://github.com/rust-lang/crates.io-index", - "clap 4.5.58 registry+https://github.com/rust-lang/crates.io-index", - "clap_builder 4.5.58 registry+https://github.com/rust-lang/crates.io-index", + "clap 4.5.59 registry+https://github.com/rust-lang/crates.io-index", + "clap_builder 4.5.59 registry+https://github.com/rust-lang/crates.io-index", "clap_derive 4.5.55 registry+https://github.com/rust-lang/crates.io-index", "clap_lex 1.0.0 registry+https://github.com/rust-lang/crates.io-index", "color-eyre 0.6.5 registry+https://github.com/rust-lang/crates.io-index", @@ -107,15 +107,15 @@ "flate2 1.1.9 registry+https://github.com/rust-lang/crates.io-index", "fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index", "form_urlencoded 1.2.2 registry+https://github.com/rust-lang/crates.io-index", - "futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index", + "futures 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-channel 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-core 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-executor 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-io 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-macro 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-sink 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-task 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-util 0.3.32 registry+https://github.com/rust-lang/crates.io-index", "getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index", "getrandom 0.2.17 registry+https://github.com/rust-lang/crates.io-index", "getrandom 0.3.4 registry+https://github.com/rust-lang/crates.io-index", @@ -275,7 +275,7 @@ "supports-hyperlinks 3.2.0 registry+https://github.com/rust-lang/crates.io-index", "supports-unicode 3.0.0 registry+https://github.com/rust-lang/crates.io-index", "syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index", - "syn 2.0.115 registry+https://github.com/rust-lang/crates.io-index", + "syn 2.0.116 registry+https://github.com/rust-lang/crates.io-index", "sync_wrapper 1.0.2 registry+https://github.com/rust-lang/crates.io-index", "tempfile 3.25.0 registry+https://github.com/rust-lang/crates.io-index", "terminal_size 0.4.3 registry+https://github.com/rust-lang/crates.io-index", @@ -291,7 +291,7 @@ "tzdb 0.7.3 registry+https://github.com/rust-lang/crates.io-index", "tzdb_data 0.2.3 registry+https://github.com/rust-lang/crates.io-index", "unicase 2.9.0 registry+https://github.com/rust-lang/crates.io-index", - "unicode-ident 1.0.23 registry+https://github.com/rust-lang/crates.io-index", + "unicode-ident 1.0.24 registry+https://github.com/rust-lang/crates.io-index", "unicode-linebreak 0.1.5 registry+https://github.com/rust-lang/crates.io-index", "unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index", "unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index", @@ -301,6 +301,7 @@ "url 2.5.8 registry+https://github.com/rust-lang/crates.io-index", "utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index", "utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index", + "uuid 1.21.0 registry+https://github.com/rust-lang/crates.io-index", "vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index", "version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index", "web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index", @@ -499,8 +500,8 @@ "chrono 0.4.43 registry+https://github.com/rust-lang/crates.io-index", "chrono-tz 0.10.4 registry+https://github.com/rust-lang/crates.io-index", "chumsky 0.9.3 registry+https://github.com/rust-lang/crates.io-index", - "clap 4.5.58 registry+https://github.com/rust-lang/crates.io-index", - "clap_builder 4.5.58 registry+https://github.com/rust-lang/crates.io-index", + "clap 4.5.59 registry+https://github.com/rust-lang/crates.io-index", + "clap_builder 4.5.59 registry+https://github.com/rust-lang/crates.io-index", "clap_derive 4.5.55 registry+https://github.com/rust-lang/crates.io-index", "clap_lex 1.0.0 registry+https://github.com/rust-lang/crates.io-index", "color-eyre 0.6.5 registry+https://github.com/rust-lang/crates.io-index", @@ -566,15 +567,15 @@ "font-loader 0.11.0 registry+https://github.com/rust-lang/crates.io-index", "form_urlencoded 1.2.2 registry+https://github.com/rust-lang/crates.io-index", "fs-tail 0.1.4 registry+https://github.com/rust-lang/crates.io-index", - "futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index", - "futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index", + "futures 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-channel 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-core 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-executor 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-io 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-macro 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-sink 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-task 0.3.32 registry+https://github.com/rust-lang/crates.io-index", + "futures-util 0.3.32 registry+https://github.com/rust-lang/crates.io-index", "generic-array 0.14.7 registry+https://github.com/rust-lang/crates.io-index", "getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index", "getrandom 0.2.17 registry+https://github.com/rust-lang/crates.io-index", @@ -769,10 +770,11 @@ "strum 0.27.2 registry+https://github.com/rust-lang/crates.io-index", "strum_macros 0.27.2 registry+https://github.com/rust-lang/crates.io-index", "syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index", - "syn 2.0.115 registry+https://github.com/rust-lang/crates.io-index", + "syn 2.0.116 registry+https://github.com/rust-lang/crates.io-index", "synstructure 0.13.2 registry+https://github.com/rust-lang/crates.io-index", "sysinfo 0.33.1 registry+https://github.com/rust-lang/crates.io-index", - "sysinfo 0.38.1 registry+https://github.com/rust-lang/crates.io-index", + "sysinfo 0.38.2 registry+https://github.com/rust-lang/crates.io-index", + "systray-util 0.2.0 registry+https://github.com/rust-lang/crates.io-index", "tempfile 3.25.0 registry+https://github.com/rust-lang/crates.io-index", "terminal_size 0.4.3 registry+https://github.com/rust-lang/crates.io-index", "textwrap 0.16.2 registry+https://github.com/rust-lang/crates.io-index", @@ -784,6 +786,7 @@ "time 0.3.47 registry+https://github.com/rust-lang/crates.io-index", "time-core 0.1.8 registry+https://github.com/rust-lang/crates.io-index", "tokio 1.49.0 registry+https://github.com/rust-lang/crates.io-index", + "tokio-macros 2.6.0 registry+https://github.com/rust-lang/crates.io-index", "tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index", "tokio-util 0.7.18 registry+https://github.com/rust-lang/crates.io-index", "toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index", @@ -805,7 +808,7 @@ "tzdb_data 0.2.3 registry+https://github.com/rust-lang/crates.io-index", "uds_windows 1.1.0 registry+https://github.com/rust-lang/crates.io-index", "unicase 2.9.0 registry+https://github.com/rust-lang/crates.io-index", - "unicode-ident 1.0.23 registry+https://github.com/rust-lang/crates.io-index", + "unicode-ident 1.0.24 registry+https://github.com/rust-lang/crates.io-index", "unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index", "unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index", "unicode-width 0.2.2 registry+https://github.com/rust-lang/crates.io-index", @@ -815,6 +818,7 @@ "url 2.5.8 registry+https://github.com/rust-lang/crates.io-index", "utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index", "utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index", + "uuid 1.21.0 registry+https://github.com/rust-lang/crates.io-index", "vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index", "version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index", "walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index", @@ -941,7 +945,7 @@ "litemap 0.8.1 registry+https://github.com/rust-lang/crates.io-index", "potential_utf 0.1.4 registry+https://github.com/rust-lang/crates.io-index", "tinystr 0.8.2 registry+https://github.com/rust-lang/crates.io-index", - "unicode-ident 1.0.23 registry+https://github.com/rust-lang/crates.io-index", + "unicode-ident 1.0.24 registry+https://github.com/rust-lang/crates.io-index", "writeable 0.6.2 registry+https://github.com/rust-lang/crates.io-index", "yoke 0.8.1 registry+https://github.com/rust-lang/crates.io-index", "yoke-derive 0.8.1 registry+https://github.com/rust-lang/crates.io-index", diff --git a/docs/common-workflows/bar-widgets/systray.md b/docs/common-workflows/bar-widgets/systray.md new file mode 100644 index 00000000..b610fc3f --- /dev/null +++ b/docs/common-workflows/bar-widgets/systray.md @@ -0,0 +1,218 @@ +# System Tray + +The System Tray widget brings native Windows system tray icons into +`komorebi-bar`. It intercepts tray icon data by creating a hidden window that +mimics the Windows taskbar, receiving the same broadcast messages that +applications send via `Shell_NotifyIcon`. + +## Basic configuration + +```json +{ + "right_widgets": [ + { + "Systray": { + "enable": true + } + } + ] +} +``` + +## Hiding icons + +The `hidden_icons` config field accepts a list of rules. Each rule can be either +a plain string or a structured object. + +A **plain string** matches the exe name (case-insensitive). This is the original +format, so existing configs continue to work without changes: + +```json +"hidden_icons": [ + "SecurityHealthSystray.exe", + "PhoneExperienceHost.exe" +] +``` + +A **structured object** matches one or more icon properties. All specified fields +must match (AND logic). By default matching is exact and case-insensitive. + +```json +"hidden_icons": [ + { "exe": "svchost.exe", "tooltip": "Some Specific App" }, + { "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" }, + { "tooltip": "App I want hidden" } +] +``` + +The two forms can be mixed freely: + +```json +"hidden_icons": [ + "PhoneExperienceHost.exe", + { "exe": "svchost.exe", "tooltip": "Specific Notification" }, + { "guid": "{7820AE73-23E3-4229-82C1-E41CB67D5B9C}" } +] +``` + +Available fields for structured rules: + +| Field | Description | +|-----------|----------------------------------------------------------| +| `exe` | Executable name (e.g. `"SecurityHealthSystray.exe"`) | +| `tooltip` | Tooltip text shown on hover | +| `guid` | Icon GUID — most stable identifier across app restarts | + +### Matching strategies + +Each field can be a plain string (exact case-insensitive match) or an object +with `value` and `matching_strategy` for advanced matching. This uses the same +`MatchingStrategy` as komorebi's window rules. + +```json +"hidden_icons": [ + { + "exe": "explorer.exe", + "tooltip": { "value": "Network", "matching_strategy": "StartsWith" } + } +] +``` + +The above hides explorer.exe icons whose tooltip starts with "Network", while +leaving other explorer.exe icons visible. + +Available strategies: + +| Strategy | Description | +|---------------------|---------------------------------------------------| +| `Equals` | Exact match (default when using a plain string) | +| `StartsWith` | Value starts with the given text | +| `EndsWith` | Value ends with the given text | +| `Contains` | Value contains the given text | +| `Regex` | Value matches a regular expression | +| `DoesNotEqual` | Value does not exactly equal the given text | +| `DoesNotStartWith` | Value does not start with the given text | +| `DoesNotEndWith` | Value does not end with the given text | +| `DoesNotContain` | Value does not contain the given text | + +All strategies except `Regex` are case-insensitive. For case-insensitive regex, +include `(?i)` in the pattern. + +Plain strings and strategy objects can be mixed across fields: + +```json +{ + "exe": "explorer.exe", + "tooltip": { "value": "notification", "matching_strategy": "Contains" } +} +``` + +Run komorebi-bar with `RUST_LOG=info` to see the exe, tooltip, and GUID of every +systray icon in the log output. + +## Stale icon cleanup + +Some applications (e.g. Docker Desktop) may exit without properly removing their +tray icon. The widget detects these stale icons by checking whether the owning +window still exists via the Win32 `IsWindow` API. + +### Automatic cleanup + +By default, the widget checks for stale icons every 60 seconds. The interval +can be configured with `stale_icons_check_interval` (in seconds). The value is +clamped between 30 and 600. Set to 0 to disable automatic cleanup. + +```json +"stale_icons_check_interval": 120 +``` + +### Refresh button + +A manual refresh button can be shown by setting `refresh_button`. Clicking it +immediately removes any stale icons. + +- `"Visible"` — shows the button in the main icon area +- `"Overflow"` — shows the button in the hidden/overflow section (appears when + the overflow toggle is expanded) + +```json +"refresh_button": "Overflow" +``` + +When set to `"Overflow"`, the overflow toggle arrow will appear even if there are +no hidden icons, so the refresh button remains accessible. + +## Info button + +An info button can be shown to open a floating panel that lists all systray icons +with their exe name, tooltip, GUID, and visibility status. This is useful for +identifying which icons to filter with `hidden_icons` rules. + +- `"Visible"` — shows the button in the main icon area +- `"Overflow"` — shows the button in the hidden/overflow section + +```json +"info_button": "Visible" +``` + +The info panel shows **all** icons, including those hidden by rules or the OS. +Each row shows the icon image, exe name, tooltip, GUID, and whether it is visible. +Copy buttons are provided on the exe, tooltip, and GUID cells for easy copying +(e.g. to paste a GUID into a filter rule). + +Like the refresh button, setting `info_button` to `"Overflow"` will make the +overflow toggle arrow appear even if there are no hidden icons. + +## Shortcuts button + +A button that toggles komorebi-shortcuts. If the shortcuts process is running +it will be killed; otherwise it will be started. + +- `"Visible"` — shows the button in the main icon area +- `"Overflow"` — shows the button in the hidden/overflow section + +```json +"shortcuts_button": "Visible" +``` + +Like the other buttons, setting `shortcuts_button` to `"Overflow"` will make the +overflow toggle arrow appear even if there are no hidden icons. + +## Mouse interactions + +The widget supports left-click, right-click, middle-click, and double-click on +tray icons. Double-click sends the `LeftDoubleClick` action (via systray-util +0.2.0), which delivers `WM_LBUTTONDBLCLK` and `NIN_SELECT` messages to the icon. + +## Click fallbacks + +Some systray icons register a click callback but never actually respond to click +messages, effectively becoming "zombie" icons from an interaction standpoint. For +known problematic icons, the widget overrides the native click action with a +direct shell command. Fallback commands take priority — if a fallback is defined +for an icon, it always runs regardless of whether the icon reports itself as +clickable. + +| Exe | Tooltip condition | Fallback command | +|--------------------------------|-------------------|---------------------------------| +| `SecurityHealthSystray.exe` | any | `start windowsdefender://` | +| `explorer.exe` | ends with `%` | `start ms-settings:apps-volume` | +| `explorer.exe` | empty | `start ms-settings:batterysaver`| + +## Full example + +```json +{ + "Systray": { + "enable": true, + "hidden_icons": [ + "SecurityHealthSystray.exe", + { "exe": "explorer.exe", "tooltip": { "value": "Network", "matching_strategy": "StartsWith" } } + ], + "stale_icons_check_interval": 60, + "refresh_button": "Overflow", + "info_button": "Visible", + "shortcuts_button": "Overflow" + } +} +``` diff --git a/docs/common-workflows/bar.md b/docs/common-workflows/bar.md new file mode 100644 index 00000000..14e8abab --- /dev/null +++ b/docs/common-workflows/bar.md @@ -0,0 +1,40 @@ +# Komorebi Bar + +`komorebi-bar` is a status bar for komorebi that renders on top of the tiling +window manager. It is configured through a `komorebi.bar.json` file, either +alongside your `komorebi.json` or at the path specified in the +`bar_configurations` array. + +## Widgets + +Widgets are placed in the `left_widgets`, `center_widgets`, or `right_widgets` +arrays. Each widget is an object with the widget type as key and its +configuration as value. + +| Widget | Description | +|--------------|--------------------------------------------------------| +| `Komorebi` | Workspaces, layout, focused window, and more | +| `Battery` | Battery level and charging status | +| `Date` | Current date in configurable format | +| `Time` | Current time in configurable format | +| `Media` | Currently playing media information | +| `Memory` | System memory usage | +| `Network` | Network activity and connection status | +| `Storage` | Disk usage information | +| `Update` | Komorebi update notification | +| `Systray` | Windows system tray icons | + +Widgets with dedicated documentation pages: + +- [System Tray](bar-widgets/systray.md) + +> Dedicated pages for the remaining widgets will be added in the future. + +## Schema + +The full configuration schema is available at +[komorebi-bar.lgug2z.com/schema](https://komorebi-bar.lgug2z.com/schema). + +For running a bar on each monitor, see +[Multiple Bar Instances](multiple-bar-instances.md) and +[Multi-Monitor Setup](multi-monitor-setup.md). diff --git a/komorebi-bar/Cargo.toml b/komorebi-bar/Cargo.toml index be2b1443..77d56bf2 100644 --- a/komorebi-bar/Cargo.toml +++ b/komorebi-bar/Cargo.toml @@ -18,6 +18,7 @@ dirs = { workspace = true } dunce = { workspace = true } eframe = { workspace = true } egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" } +egui_extras = { workspace = true } font-loader = "0.11" hotwatch = { workspace = true } image = "0.25" @@ -27,6 +28,7 @@ num = "0.4" num-derive = "0.4" num-traits = "0.2" parking_lot = { workspace = true } +regex = "1" random_word = { version = "0.5", features = ["en"] } reqwest = { version = "0.12", features = ["blocking"] } schemars = { workspace = true, optional = true } @@ -34,6 +36,8 @@ serde = { workspace = true } serde_json = { workspace = true } starship-battery = "0.10" sysinfo = { workspace = true } +systray-util = "0.2.0" +tokio = { version = "1", features = ["rt", "sync", "time"] } tracing = { workspace = true } tracing-subscriber = { workspace = true } which = { workspace = true } diff --git a/komorebi-bar/src/bar.rs b/komorebi-bar/src/bar.rs index 9b418e13..7b797ff8 100644 --- a/komorebi-bar/src/bar.rs +++ b/komorebi-bar/src/bar.rs @@ -18,6 +18,7 @@ use crate::render::Color32Ext; use crate::render::Grouping; use crate::render::RenderConfig; use crate::render::RenderExt; +use crate::take_widget_clicked; use crate::widgets::komorebi::Komorebi; use crate::widgets::komorebi::MonitorInfo; use crate::widgets::widget::BarWidget; @@ -1082,6 +1083,10 @@ impl eframe::App for Komobar { let frame = render_config.change_frame_on_bar(frame, &ctx.style()); CentralPanel::default().frame(frame).show(ctx, |ui| { + // Variable to store command to execute after widgets are rendered + // This allows widgets to mark clicks as consumed before bar processes them + let mut pending_command: Option = None; + if let Some(mouse_config) = &self.config.mouse { let command = if ui .input(|i| i.pointer.button_double_clicked(PointerButton::Primary)) @@ -1182,9 +1187,9 @@ impl eframe::App for Komobar { &None }; - if let Some(command) = command { - command.execute(self.mouse_follows_focus); - } + // Store the command to execute after widgets are rendered + // This allows widgets to mark clicks as consumed + pending_command = command.clone(); } // Apply grouping logic for the bar as a whole @@ -1316,6 +1321,13 @@ impl eframe::App for Komobar { }); }); } + + // Execute the deferred mouse command only if no widget consumed the click + if let Some(command) = pending_command + && !take_widget_clicked() + { + command.execute(self.mouse_follows_focus); + } }); } } diff --git a/komorebi-bar/src/main.rs b/komorebi-bar/src/main.rs index c3b26f63..40671e4e 100644 --- a/komorebi-bar/src/main.rs +++ b/komorebi-bar/src/main.rs @@ -38,6 +38,8 @@ use windows::Win32::UI::WindowsAndMessaging::EnumThreadWindows; use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; use windows_core::BOOL; +use std::sync::atomic::AtomicBool; + pub static MAX_LABEL_WIDTH: AtomicI32 = AtomicI32::new(400); pub static MONITOR_LEFT: AtomicI32 = AtomicI32::new(0); pub static MONITOR_TOP: AtomicI32 = AtomicI32::new(0); @@ -46,6 +48,20 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0); pub static BAR_HEIGHT: f32 = 50.0; pub static DEFAULT_PADDING: f32 = 10.0; +/// Flag to indicate that a widget has consumed a click event this frame. +/// This prevents the bar's global mouse handler from also processing the click. +pub static WIDGET_CLICKED: AtomicBool = AtomicBool::new(false); + +/// Mark that a widget has consumed a click event this frame. +pub fn mark_widget_clicked() { + WIDGET_CLICKED.store(true, Ordering::SeqCst); +} + +/// Check if a widget has consumed a click event this frame and reset the flag. +pub fn take_widget_clicked() -> bool { + WIDGET_CLICKED.swap(false, Ordering::SeqCst) +} + pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0); pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0); diff --git a/komorebi-bar/src/widgets/mod.rs b/komorebi-bar/src/widgets/mod.rs index e75b76d2..9614e759 100644 --- a/komorebi-bar/src/widgets/mod.rs +++ b/komorebi-bar/src/widgets/mod.rs @@ -20,6 +20,8 @@ pub mod media; pub mod memory; pub mod network; pub mod storage; +#[cfg(target_os = "windows")] +pub mod systray; pub mod time; pub mod update; pub mod widget; @@ -92,10 +94,16 @@ impl IconsCache { pub fn insert_image(&self, id: ImageIconId, image: Arc) { self.images.write().unwrap().insert(id, image); } + + /// Removes the cached image and texture for the given icon ID. + pub fn remove(&self, id: &ImageIconId) { + self.images.write().unwrap().remove(id); + self.textures.write().unwrap().1.remove(id); + } } #[inline] -fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage { +pub(crate) fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage { let size = [rgba_image.width() as usize, rgba_image.height() as usize]; let pixels = rgba_image.as_flat_samples(); ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()) @@ -156,6 +164,8 @@ pub enum ImageIconId { Path(Arc), /// Windows HWND handle. Hwnd(isize), + /// System tray icon identifier. + SystrayIcon(String), } impl From<&Path> for ImageIconId { diff --git a/komorebi-bar/src/widgets/systray.rs b/komorebi-bar/src/widgets/systray.rs new file mode 100644 index 00000000..f3f8d878 --- /dev/null +++ b/komorebi-bar/src/widgets/systray.rs @@ -0,0 +1,1301 @@ +use super::ICONS_CACHE; +use super::ImageIcon; +use super::ImageIconId; +use super::rgba_to_color_image; +use crate::bar::Alignment; +use crate::mark_widget_clicked; +use crate::render::RenderConfig; +use crate::selected_frame::SelectableFrame; +use crate::widgets::widget::BarWidget; +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; +use crossbeam_channel::unbounded; +use eframe::egui::CentralPanel; +use eframe::egui::ColorImage; +use eframe::egui::Context; +use eframe::egui::Frame; +use eframe::egui::Image; +use eframe::egui::Label; +use eframe::egui::Margin; +use eframe::egui::ScrollArea; +use eframe::egui::Ui; +use eframe::egui::Vec2; +use eframe::egui::ViewportBuilder; +use eframe::egui::ViewportClass; +use eframe::egui::ViewportId; +use eframe::egui::Window as EguiWindow; +use egui_extras::Column; +use egui_extras::TableBuilder; +use komorebi_client::MatchingStrategy; +use parking_lot::Mutex; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; +use std::time::Instant; +use systray_util::StableId; +use systray_util::Systray as SystrayClient; +use systray_util::SystrayEvent; +use systray_util::SystrayIconAction; +use windows::Win32::Foundation::CloseHandle; +use windows::Win32::Foundation::HWND; +use windows::Win32::System::Threading::OpenProcess; +use windows::Win32::System::Threading::PROCESS_NAME_WIN32; +use windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION; +use windows::Win32::System::Threading::QueryFullProcessImageNameW; +use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics; +use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId; +use windows::Win32::UI::WindowsAndMessaging::IsWindow; +use windows::Win32::UI::WindowsAndMessaging::SM_CXSCREEN; +use windows::Win32::UI::WindowsAndMessaging::SM_CYSCREEN; +use windows::core::PWSTR; + +/// Whether hidden icons are currently shown +static SHOW_HIDDEN_ICONS: AtomicBool = AtomicBool::new(false); + +/// Whether the systray info panel is currently shown +static SHOW_SYSTRAY_INFO: AtomicBool = AtomicBool::new(false); + +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +/// Position of the overflow toggle button +pub enum OverflowTogglePosition { + /// Toggle button appears on the left side (before visible icons) + Left, + /// Toggle button appears on the right side (after visible icons) + #[default] + Right, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +/// Where to place a systray button (refresh, info, shortcuts, etc.) +pub enum ButtonPosition { + /// Show in the main visible area + Visible, + /// Show in the overflow/hidden section + Overflow, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(untagged)] +/// A field value with an optional matching strategy. +/// +/// A plain string uses exact (case-insensitive) matching. +/// An object with `value` and `matching_strategy` uses the specified strategy. +pub enum FieldMatch { + /// Exact case-insensitive match + Exact(String), + /// Match using a specific strategy + WithStrategy { + /// The value to match against + value: String, + /// How to match (Equals, StartsWith, EndsWith, Contains, Regex, etc.) + matching_strategy: MatchingStrategy, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(untagged)] +/// Rule for matching a systray icon to hide +/// +/// A plain string matches the exe name (backward compatible). +/// An object with optional `exe`, `tooltip`, and/or `guid` fields +/// uses AND logic: all specified fields must match. +/// Each field can be a plain string (exact match) or an object with +/// `value` and `matching_strategy` for advanced matching. +pub enum HiddenIconRule { + /// Match by exe name (case-insensitive, exact) + Exe(String), + /// Match by one or more properties (all specified fields must match) + Match { + /// Exe name to match + exe: Option, + /// Tooltip text to match + tooltip: Option, + /// Icon GUID to match (most stable identifier across restarts) + guid: Option, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +/// System tray widget configuration +pub struct SystrayConfig { + /// Enable the System Tray widget + pub enable: bool, + /// A list of rules for icons to hide from the system tray + /// + /// Each entry can be a plain string (matches exe name, case-insensitive) + /// or an object with optional `exe`, `tooltip`, and/or `guid` fields + /// (all specified fields must match, AND logic, case-insensitive). + /// + /// Run komorebi-bar with RUST_LOG=info to see the properties of all + /// systray icons in the log output. + pub hidden_icons: Option>, + /// Position of the overflow toggle button (default: Right) + pub overflow_toggle_position: Option, + /// Show an info button that opens a floating panel listing all systray icons + /// with their exe, tooltip, and GUID. Set to "Visible" to show it in the main + /// area, or "Overflow" to show it in the hidden/overflow section. + pub info_button: Option, + /// Interval in seconds to automatically check for and remove stale icons + /// whose owning process has exited. Clamped between 30 and 600 seconds. + /// Defaults to 60. Set to 0 to disable. + pub stale_icons_check_interval: Option, + /// Show a refresh button that manually triggers stale icon cleanup. + /// Set to "Visible" to show it in the main area, or "Overflow" to + /// show it in the hidden/overflow section. + pub refresh_button: Option, + /// Show a button that toggles komorebi-shortcuts (kills the process if + /// running, starts it otherwise). Set to "Visible" to show it in the main + /// area, or "Overflow" to show it in the hidden/overflow section. + pub shortcuts_button: Option, +} + +/// Command sent from UI to background thread +#[derive(Debug)] +enum SystrayCommand { + SendAction(StableId, SystrayIconAction), +} + +/// A single field condition: pre-lowercased value (except for Regex), +/// the matching strategy, and an optional pre-compiled regex. +#[derive(Clone)] +struct NormalizedField { + value: String, + strategy: MatchingStrategy, + compiled_regex: Option, +} + +impl NormalizedField { + /// Build from a raw value and strategy. Pre-lowercases the value for + /// non-regex strategies and pre-compiles the regex pattern if applicable. + fn new(value: String, strategy: MatchingStrategy) -> Self { + let compiled_regex = if strategy == MatchingStrategy::Regex { + match regex::Regex::new(&value) { + Ok(re) => Some(re), + Err(err) => { + tracing::warn!("invalid regex in hidden_icons rule: {err}"); + None + } + } + } else { + None + }; + + let normalized_value = if strategy == MatchingStrategy::Regex { + value + } else { + value.to_lowercase() + }; + + Self { + value: normalized_value, + strategy, + compiled_regex, + } + } + + /// Build from a `FieldMatch` config value. + fn from_field_match(field: FieldMatch) -> Self { + match field { + FieldMatch::Exact(s) => Self::new(s, MatchingStrategy::Equals), + FieldMatch::WithStrategy { + value, + matching_strategy, + } => Self::new(value, matching_strategy), + } + } + + /// Returns true if this field condition matches the given input string. + /// Non-regex comparisons are case-insensitive (value is pre-lowercased, + /// input is lowercased at match time). + fn matches(&self, input: &str) -> bool { + if self.strategy == MatchingStrategy::Regex { + return self + .compiled_regex + .as_ref() + .is_some_and(|re| re.is_match(input)); + } + + let input_lower = input.to_lowercase(); + match self.strategy { + MatchingStrategy::Legacy | MatchingStrategy::Equals => self.value == input_lower, + MatchingStrategy::StartsWith => input_lower.starts_with(&self.value), + MatchingStrategy::EndsWith => input_lower.ends_with(&self.value), + MatchingStrategy::Contains => input_lower.contains(&self.value), + MatchingStrategy::DoesNotEqual => self.value != input_lower, + MatchingStrategy::DoesNotStartWith => !input_lower.starts_with(&self.value), + MatchingStrategy::DoesNotEndWith => !input_lower.ends_with(&self.value), + MatchingStrategy::DoesNotContain => !input_lower.contains(&self.value), + MatchingStrategy::Regex => unreachable!(), + } + } +} + +/// Pre-normalized hidden icon matching rule with per-field matching strategies. +#[derive(Clone)] +struct NormalizedHiddenRule { + exe: Option, + tooltip: Option, + guid: Option, +} + +impl NormalizedHiddenRule { + /// Returns true if this rule matches the given icon properties (AND logic). + /// All specified (non-None) fields must match. + fn matches(&self, exe_name: &str, tooltip: &str, guid: Option<&str>) -> bool { + let exe_ok = self.exe.as_ref().is_none_or(|f| f.matches(exe_name)); + let tooltip_ok = self.tooltip.as_ref().is_none_or(|f| f.matches(tooltip)); + let guid_ok = self + .guid + .as_ref() + .is_none_or(|f| guid.is_some_and(|actual| f.matches(actual))); + exe_ok && tooltip_ok && guid_ok + } +} + +impl From for NormalizedHiddenRule { + fn from(rule: HiddenIconRule) -> Self { + match rule { + HiddenIconRule::Exe(exe) => Self { + exe: Some(NormalizedField::new(exe, MatchingStrategy::Equals)), + tooltip: None, + guid: None, + }, + HiddenIconRule::Match { exe, tooltip, guid } => Self { + exe: exe.map(NormalizedField::from_field_match), + tooltip: tooltip.map(NormalizedField::from_field_match), + guid: guid.map(NormalizedField::from_field_match), + }, + } + } +} + +/// Cached icon data for display +#[derive(Clone, Debug)] +struct CachedIcon { + stable_id: StableId, + tooltip: String, + exe_name: String, + guid_str: Option, + window_handle: Option, + image_icon: Option, + is_visible: bool, + /// Whether the icon has a callback message and can receive click events + is_clickable: bool, +} + +/// Pre-processed icon data from the background thread. +/// Image conversion (RgbaImage -> ColorImage) is done off the UI thread. +struct PreprocessedIcon { + stable_id: StableId, + tooltip: String, + window_handle: Option, + is_visible: bool, + /// Whether the icon has a callback message and can receive click events + is_clickable: bool, + /// Stable ID as string (pre-computed to avoid repeated allocations) + stable_id_str: String, + /// Cache key for the icon image (stable_id + hash) + image_cache_key: String, + /// Pre-converted ColorImage (conversion done off UI thread) + color_image: Option>, + /// GUID string for logging only + guid_str: Option, +} + +/// Pre-processed event sent from background thread to UI +enum PreprocessedEvent { + IconAddOrUpdate(PreprocessedIcon), + IconRemove(StableId), +} + +/// Global shared state for the systray (UI-side only, no SystrayClient here) +#[derive(Default)] +struct GlobalSystrayState { + /// Cached icons for rendering (shared across all widget instances) + icons: HashMap, + /// Receiver for pre-processed events from the background thread + event_rx: Option>, + /// Sender for commands to the background thread + command_tx: Option>, + /// Whether the background thread has been started + initialized: bool, + /// Last time stale icon cleanup was performed + last_cleanup: Option, +} + +/// Global singleton for the systray state +static SYSTRAY_STATE: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(GlobalSystrayState::default()))); + +/// Pre-process a SystrayEvent in the background thread. +/// Converts RgbaImage -> ColorImage so the UI thread only does the GPU upload. +fn preprocess_event(event: SystrayEvent) -> PreprocessedEvent { + match event { + SystrayEvent::IconAdd(ref icon) | SystrayEvent::IconUpdate(ref icon) => { + let stable_id_str = icon.stable_id.to_string(); + + let image_cache_key = match &icon.icon_image_hash { + Some(hash) => format!("{}_{}", stable_id_str, hash), + None => stable_id_str.clone(), + }; + + let guid_str = icon.guid.map(|g| g.to_string()); + + // Convert RgbaImage -> ColorImage here (background thread) instead of the UI thread + let color_image = icon + .icon_image + .as_ref() + .map(|img| Arc::new(rgba_to_color_image(&img.clone()))); + + let is_clickable = icon.window_handle.is_some() + && icon.uid.is_some() + && icon.callback_message.is_some(); + + PreprocessedEvent::IconAddOrUpdate(PreprocessedIcon { + stable_id: icon.stable_id.clone(), + tooltip: icon.tooltip.clone(), + window_handle: icon.window_handle, + is_visible: icon.is_visible, + is_clickable, + stable_id_str, + image_cache_key, + color_image, + guid_str, + }) + } + SystrayEvent::IconRemove(stable_id) => PreprocessedEvent::IconRemove(stable_id), + } +} + +/// Initialize the global systray background thread (only runs once) +fn ensure_systray_initialized() { + let mut state = SYSTRAY_STATE.lock(); + + if state.initialized { + return; + } + + state.initialized = true; + + let (event_tx, event_rx) = unbounded::(); + let (command_tx, command_rx) = unbounded::(); + + state.event_rx = Some(event_rx); + state.command_tx = Some(command_tx); + + // Drop the lock before spawning the thread + drop(state); + + // Spawn background thread with its own tokio runtime + thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create tokio runtime for systray"); + + rt.block_on(async move { + match SystrayClient::new() { + Ok(mut systray) => { + tracing::info!("Systray client initialized successfully"); + + // Send initial icons (pre-processed in background thread) + for icon in systray.icons() { + let event = SystrayEvent::IconAdd(icon.clone()); + let _ = event_tx.send(preprocess_event(event)); + } + + // Create an async channel receiver for commands + let (async_cmd_tx, mut async_cmd_rx) = + tokio::sync::mpsc::unbounded_channel::(); + + // Spawn a task to bridge crossbeam -> tokio channel + let bridge_tx = async_cmd_tx.clone(); + std::thread::spawn(move || { + while let Ok(cmd) = command_rx.recv() { + if bridge_tx.send(cmd).is_err() { + break; + } + } + }); + + loop { + tokio::select! { + // Handle systray events (pre-process before sending) + event = systray.events() => { + match event { + Some(event) => { + if event_tx.send(preprocess_event(event)).is_err() { + tracing::error!("Failed to send systray event to UI"); + break; + } + } + None => { + tracing::info!("Systray events channel closed"); + break; + } + } + } + // Handle commands from UI + Some(cmd) = async_cmd_rx.recv() => { + match cmd { + SystrayCommand::SendAction(stable_id, action) => { + tracing::debug!( + "Processing systray action for {}: {:?}", + stable_id, + action + ); + if let Err(e) = systray.send_action(&stable_id, &action) { + tracing::error!( + "Failed to send systray action to {}: {:?}", + stable_id, + e + ); + } + } + } + } + } + } + } + Err(e) => { + tracing::error!("Failed to initialize systray client: {:?}", e); + } + } + }); + }); +} + +pub struct Systray { + pub enable: bool, + /// Pre-normalized rules for hiding icons + hidden_icons: Vec, + /// Position of the overflow toggle button + overflow_toggle_position: OverflowTogglePosition, + /// Where to show the info button (None = no button) + info_button: Option, + /// Interval for automatic stale icon cleanup (None = disabled) + stale_icons_check_interval: Option, + /// Where to show the refresh button (None = no button) + refresh_button: Option, + /// Where to show the shortcuts toggle button (None = no button) + shortcuts_button: Option, +} + +impl From<&SystrayConfig> for Systray { + fn from(value: &SystrayConfig) -> Self { + // Initialize the global systray on first widget creation + if value.enable { + ensure_systray_initialized(); + } + + // Normalize rules (lowercase all fields) for case-insensitive matching + let hidden_icons = value + .hidden_icons + .clone() + .unwrap_or_default() + .into_iter() + .map(NormalizedHiddenRule::from) + .collect(); + + // 0 = disabled, None = default 60s, otherwise clamp to [30, 600] + let stale_icons_check_interval = match value.stale_icons_check_interval { + Some(0) => None, + Some(secs) => Some(Duration::from_secs(secs.clamp(30, 600))), + None => Some(Duration::from_secs(60)), + }; + + Self { + enable: value.enable, + hidden_icons, + overflow_toggle_position: value.overflow_toggle_position.unwrap_or_default(), + info_button: value.info_button, + stale_icons_check_interval, + refresh_button: value.refresh_button, + shortcuts_button: value.shortcuts_button, + } + } +} + +impl Systray { + /// Process pending events from the background thread. + /// Returns true if any events were processed. + fn process_events() -> bool { + // 1. Drain events from channel (brief lock) + let events: Vec = { + let state = SYSTRAY_STATE.lock(); + match &state.event_rx { + Some(rx) => rx.try_iter().collect(), + None => return false, + } + }; + + if events.is_empty() { + return false; + } + + // 2. Deduplicate — keep only the last event per stable_id. + // If an icon fires many updates between frames, only the last matters. + let deduped = Self::deduplicate_events(events); + + // 3. Resolve exe names outside the lock. + // Exe names don't change for a given window handle, so we reuse + // any already-known name and only call the Win32 API for new icons. + let known_exe_names: HashMap = { + let state = SYSTRAY_STATE.lock(); + state + .icons + .iter() + .filter(|(_, icon)| !icon.exe_name.is_empty()) + .map(|(id, icon)| (id.clone(), icon.exe_name.clone())) + .collect() + }; + + let mut resolved_exe_names: HashMap = HashMap::new(); + for event in &deduped { + if let PreprocessedEvent::IconAddOrUpdate(picon) = event + && !known_exe_names.contains_key(&picon.stable_id_str) + && !resolved_exe_names.contains_key(&picon.stable_id_str) + { + let exe_name = picon + .window_handle + .and_then(Self::get_exe_from_hwnd) + .unwrap_or_default(); + resolved_exe_names.insert(picon.stable_id_str.clone(), exe_name); + } + } + + // 4. Process deduplicated events with the lock held (fast path only — + // no Win32 calls or image conversion happen here). + let mut state = SYSTRAY_STATE.lock(); + for event in deduped { + match event { + PreprocessedEvent::IconAddOrUpdate(picon) => { + let exe_name = known_exe_names + .get(&picon.stable_id_str) + .or_else(|| resolved_exe_names.get(&picon.stable_id_str)) + .cloned() + .unwrap_or_default(); + + // Evict stale cache entry if the image hash changed + if let Some(old_cached) = state.icons.get(&picon.stable_id_str) + && let Some(old_icon) = &old_cached.image_icon + { + let new_key = ImageIconId::SystrayIcon(picon.image_cache_key.clone()); + if old_icon.id != new_key { + ICONS_CACHE.remove(&old_icon.id); + } + } + + let stable_id_str = picon.stable_id_str.clone(); + let cached = Self::create_cached_icon(picon, exe_name); + state.icons.insert(stable_id_str, cached); + } + PreprocessedEvent::IconRemove(stable_id) => { + let key = stable_id.to_string(); + // Evict cache for removed icons + if let Some(old_cached) = state.icons.get(&key) + && let Some(old_icon) = &old_cached.image_icon + { + ICONS_CACHE.remove(&old_icon.id); + } + state.icons.remove(&key); + } + } + } + + true + } + + /// Deduplicate events, keeping only the last event per stable_id. + /// For rapid-fire icon updates, this collapses N events into 1. + fn deduplicate_events(events: Vec) -> Vec { + let mut last_event: HashMap = HashMap::new(); + + for event in events { + let key = match &event { + PreprocessedEvent::IconAddOrUpdate(picon) => picon.stable_id_str.clone(), + PreprocessedEvent::IconRemove(stable_id) => stable_id.to_string(), + }; + last_event.insert(key, event); + } + + last_event.into_values().collect() + } + + /// Create a CachedIcon from pre-processed data. + /// Image conversion and exe name resolution are already done by the time + /// this is called. + fn create_cached_icon(picon: PreprocessedIcon, exe_name: String) -> CachedIcon { + tracing::info!( + "Systray icon: tooltip={:?}, exe={:?}, guid={:?}, stable_id={}, is_visible={}", + picon.tooltip, + exe_name, + picon.guid_str, + picon.stable_id_str, + picon.is_visible + ); + + // Image already converted by the background thread — + // just insert the ColorImage into the shared cache. + let image_icon = picon.color_image.map(|color_image| { + let id = ImageIconId::SystrayIcon(picon.image_cache_key.clone()); + ICONS_CACHE.insert_image(id.clone(), color_image.clone()); + ImageIcon::new(id, color_image) + }); + + CachedIcon { + stable_id: picon.stable_id, + tooltip: picon.tooltip, + exe_name, + guid_str: picon.guid_str, + window_handle: picon.window_handle, + image_icon, + is_visible: picon.is_visible, + is_clickable: picon.is_clickable, + } + } + + /// Get the executable name from a window handle + fn get_exe_from_hwnd(hwnd: isize) -> Option { + unsafe { + let mut process_id: u32 = 0; + GetWindowThreadProcessId( + HWND(hwnd as *mut _), + Some(std::ptr::addr_of_mut!(process_id)), + ); + + if process_id == 0 { + return None; + } + + let handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, process_id).ok()?; + + let mut len = 260_u32; + let mut path: Vec = vec![0; len as usize]; + let text_ptr = path.as_mut_ptr(); + + let result = + QueryFullProcessImageNameW(handle, PROCESS_NAME_WIN32, PWSTR(text_ptr), &mut len); + + let _ = CloseHandle(handle); + + if result.is_err() { + return None; + } + + let exe_path = String::from_utf16(&path[..len as usize]).ok()?; + + // Extract just the filename from the path + exe_path.rsplit('\\').next().map(|s| s.to_string()) + } + } + + /// Send a click action to an icon + fn send_action(stable_id: &StableId, action: SystrayIconAction) { + let state = SYSTRAY_STATE.lock(); + + if let Some(command_tx) = &state.command_tx + && command_tx + .send(SystrayCommand::SendAction(stable_id.clone(), action)) + .is_err() + { + tracing::error!("Failed to send command to systray thread"); + } + } + + /// Get a snapshot of current icons + fn get_visible_icons() -> Vec { + let state = SYSTRAY_STATE.lock(); + let mut icons: Vec<_> = state + .icons + .values() + .filter(|icon| icon.is_visible) + .cloned() + .collect(); + icons.sort_by(|a, b| a.stable_id.to_string().cmp(&b.stable_id.to_string())); + icons + } + + /// Get a snapshot of all icons (including those not marked as visible by the OS) + fn get_all_icons() -> Vec { + let state = SYSTRAY_STATE.lock(); + let mut icons: Vec<_> = state.icons.values().cloned().collect(); + icons.sort_by(|a, b| a.stable_id.to_string().cmp(&b.stable_id.to_string())); + icons + } + + /// Check if an icon should be hidden based on the configured rules + fn is_icon_hidden(&self, icon: &CachedIcon) -> bool { + self.hidden_icons + .iter() + .any(|rule| rule.matches(&icon.exe_name, &icon.tooltip, icon.guid_str.as_deref())) + } + + /// Remove icons whose owning window no longer exists. + /// Returns true if any stale icons were removed. + fn cleanup_stale_icons() -> bool { + let mut state = SYSTRAY_STATE.lock(); + + let stale_ids: Vec = state + .icons + .iter() + .filter(|(_, icon)| { + icon.window_handle + .is_some_and(|hwnd| unsafe { !IsWindow(Some(HWND(hwnd as *mut _))).as_bool() }) + }) + .map(|(id, _)| id.clone()) + .collect(); + + if stale_ids.is_empty() { + return false; + } + + for id in &stale_ids { + if let Some(old_cached) = state.icons.get(id) + && let Some(old_icon) = &old_cached.image_icon + { + ICONS_CACHE.remove(&old_icon.id); + } + state.icons.remove(id); + } + + tracing::info!("Removed {} stale systray icon(s)", stale_ids.len()); + true + } +} + +impl BarWidget for Systray { + fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { + if !self.enable { + return; + } + + // Process any pending events and request repaint if there were any + if Self::process_events() { + ctx.request_repaint(); + } + + // Periodic stale icon cleanup + if let Some(interval) = self.stale_icons_check_interval { + let should_cleanup = { + let mut state = SYSTRAY_STATE.lock(); + let now = Instant::now(); + let due = state + .last_cleanup + .is_none_or(|last| now.duration_since(last) >= interval); + if due { + state.last_cleanup = Some(now); + } + due + }; + + if should_cleanup && Self::cleanup_stale_icons() { + ctx.request_repaint(); + } + } + + // Render the floating info window before the bar layout so it is not + // clipped by the widget's allocated area. + self.render_info_window(ctx); + + let icon_size = config.icon_font_id.size; + let all_icons = Self::get_visible_icons(); + + // Separate visible and hidden icons + let (visible_icons, hidden_icons): (Vec<_>, Vec<_>) = all_icons + .into_iter() + .partition(|icon| !self.is_icon_hidden(icon)); + + let show_hidden = SHOW_HIDDEN_ICONS.load(Ordering::SeqCst); + let refresh_in_overflow = self.refresh_button == Some(ButtonPosition::Overflow); + let info_in_overflow = self.info_button == Some(ButtonPosition::Overflow); + let shortcuts_in_overflow = self.shortcuts_button == Some(ButtonPosition::Overflow); + let has_overflow_content = !hidden_icons.is_empty() + || refresh_in_overflow + || info_in_overflow + || shortcuts_in_overflow; + + // Check if we're in a right-aligned context (rendering is reversed) + let is_reversed = matches!(config.alignment, Some(Alignment::Right)); + + // Determine effective toggle position (flip if reversed) + let effective_toggle_left = match (self.overflow_toggle_position, is_reversed) { + (OverflowTogglePosition::Left, false) => true, + (OverflowTogglePosition::Right, false) => false, + (OverflowTogglePosition::Left, true) => false, // Flip when reversed + (OverflowTogglePosition::Right, true) => true, // Flip when reversed + }; + + config.apply_on_widget(false, ui, |ui| { + // Render toggle button on the left if configured + if has_overflow_content && effective_toggle_left { + self.render_toggle_button( + ui, + show_hidden, + &hidden_icons, + icon_size, + ctx, + is_reversed, + ); + } + + // Render visible icons + for cached_icon in &visible_icons { + self.render_icon(ctx, ui, cached_icon, icon_size); + } + + // Render visible-area buttons (after icons) + if self.refresh_button == Some(ButtonPosition::Visible) { + Self::render_refresh_button(ui, ctx); + } + if self.info_button == Some(ButtonPosition::Visible) { + Self::render_info_button(ui); + } + if self.shortcuts_button == Some(ButtonPosition::Visible) { + Self::render_shortcuts_button(ui); + } + + // Render toggle button on the right if configured (default) + if has_overflow_content && !effective_toggle_left { + self.render_toggle_button( + ui, + show_hidden, + &hidden_icons, + icon_size, + ctx, + is_reversed, + ); + } + }); + } +} + +impl Systray { + /// Render the toggle button for showing/hiding overflow icons + fn render_toggle_button( + &self, + ui: &mut Ui, + show_hidden: bool, + hidden_icons: &[CachedIcon], + icon_size: f32, + ctx: &Context, + is_reversed: bool, + ) { + // Determine arrow direction: + // - When collapsed: arrow points toward where hidden icons will appear + // - When expanded: arrow points toward visible icons (away from hidden icons) + // + // In left_widgets (not reversed) with toggle on Left: + // Collapsed: ? [visible...] (arrow points left, where hidden will appear) + // Expanded: ? [hidden...] [visible...] (arrow points right, toward visible) + // + // In right_widgets (reversed) with toggle on Left: + // Collapsed: [visible...] ? (arrow points left, where hidden will appear) + // Expanded: [visible...] ? [hidden...] (arrow points right, toward visible) + let toggle_icon = match (self.overflow_toggle_position, is_reversed, show_hidden) { + // Left position, normal rendering + (OverflowTogglePosition::Left, false, false) => egui_phosphor::regular::CARET_LEFT, + (OverflowTogglePosition::Left, false, true) => egui_phosphor::regular::CARET_RIGHT, + // Right position, normal rendering + (OverflowTogglePosition::Right, false, false) => egui_phosphor::regular::CARET_RIGHT, + (OverflowTogglePosition::Right, false, true) => egui_phosphor::regular::CARET_LEFT, + // Left position, reversed rendering (right_widgets) - arrows flipped + (OverflowTogglePosition::Left, true, false) => egui_phosphor::regular::CARET_LEFT, + (OverflowTogglePosition::Left, true, true) => egui_phosphor::regular::CARET_RIGHT, + // Right position, reversed rendering (right_widgets) - arrows flipped + (OverflowTogglePosition::Right, true, false) => egui_phosphor::regular::CARET_RIGHT, + (OverflowTogglePosition::Right, true, true) => egui_phosphor::regular::CARET_LEFT, + }; + + let toggle_response = SelectableFrame::new(show_hidden) + .show(ui, |ui| { + ui.add(Label::new(toggle_icon).selectable(false)); + }) + .on_hover_text_at_pointer(if show_hidden { + "Hide overflow icons" + } else { + "Show overflow icons" + }); + + if toggle_response.clicked() { + mark_widget_clicked(); + SHOW_HIDDEN_ICONS.store(!show_hidden, Ordering::SeqCst); + } + + // If expanded, show the hidden icons and overflow buttons + if show_hidden { + for cached_icon in hidden_icons { + self.render_icon(ctx, ui, cached_icon, icon_size); + } + + if self.refresh_button == Some(ButtonPosition::Overflow) { + Self::render_refresh_button(ui, ctx); + } + if self.info_button == Some(ButtonPosition::Overflow) { + Self::render_info_button(ui); + } + if self.shortcuts_button == Some(ButtonPosition::Overflow) { + Self::render_shortcuts_button(ui); + } + } + } + + /// Render the refresh button that triggers stale icon cleanup + fn render_refresh_button(ui: &mut Ui, ctx: &Context) { + let response = SelectableFrame::new(false) + .show(ui, |ui| { + ui.add(Label::new(egui_phosphor::regular::ARROWS_CLOCKWISE).selectable(false)); + }) + .on_hover_text_at_pointer("Refresh systray icons"); + + if response.clicked() { + mark_widget_clicked(); + if Self::cleanup_stale_icons() { + ctx.request_repaint(); + } + } + } + + /// Render the info button that toggles the info panel + fn render_info_button(ui: &mut Ui) { + let show_info = SHOW_SYSTRAY_INFO.load(Ordering::SeqCst); + + let response = SelectableFrame::new(show_info) + .show(ui, |ui| { + ui.add(Label::new(egui_phosphor::regular::INFO).selectable(false)); + }) + .on_hover_text_at_pointer("Show systray icon details"); + + if response.clicked() { + mark_widget_clicked(); + SHOW_SYSTRAY_INFO.store(!show_info, Ordering::SeqCst); + } + } + + /// Render the shortcuts toggle button. + /// Toggles `komorebi-shortcuts.exe`: kills it if running, starts it otherwise. + fn render_shortcuts_button(ui: &mut Ui) { + let response = SelectableFrame::new(false) + .show(ui, |ui| { + ui.add(Label::new(egui_phosphor::regular::KEYBOARD).selectable(false)); + }) + .on_hover_text_at_pointer("Toggle shortcuts"); + + if response.clicked() { + mark_widget_clicked(); + // Run on a background thread so `taskkill` output() doesn't block the UI + thread::spawn(|| { + let killed = std::process::Command::new("taskkill") + .args(["/F", "/IM", "komorebi-shortcuts.exe"]) + .output() + .is_ok_and(|o| o.status.success()); + + if !killed { + let _ = std::process::Command::new("komorebi-shortcuts.exe").spawn(); + } + }); + } + } + + /// Render the info panel as a separate OS window via a deferred viewport. + /// This avoids being clipped by the bar's thin OS window. + fn render_info_window(&self, ctx: &Context) { + if !SHOW_SYSTRAY_INFO.load(Ordering::SeqCst) { + return; + } + + // Clone the rules into an Arc so the `Send + Sync + 'static` callback + // can use them for the "Hidden (rule)" column. + let rules: Arc> = Arc::new(self.hidden_icons.clone()); + + let window_size = [500.0f32, 300.0]; + // GetSystemMetrics returns physical pixels; ViewportBuilder expects + // logical points, so divide by the current DPI scale factor. + let scale = ctx.pixels_per_point(); + let center = unsafe { + let sw = GetSystemMetrics(SM_CXSCREEN) as f32 / scale; + let sh = GetSystemMetrics(SM_CYSCREEN) as f32 / scale; + [(sw - window_size[0]) / 2.0, (sh - window_size[1]) / 2.0] + }; + + ctx.show_viewport_deferred( + ViewportId::from_hash_of("systray_info"), + ViewportBuilder::default() + .with_title("Systray Icons") + .with_inner_size(window_size) + .with_position(center), + move |ctx, class| { + // Handle the OS window's close button + if ctx.input(|i| i.viewport().close_requested()) { + SHOW_SYSTRAY_INFO.store(false, Ordering::SeqCst); + return; + } + + match class { + ViewportClass::Embedded => { + // Fallback when the backend doesn't support multiple + // OS windows — render inside an egui::Window (clipped + // to the bar, but still functional). + let mut open = true; + EguiWindow::new("Systray Icons") + .open(&mut open) + .resizable(true) + .default_size([500.0, 300.0]) + .show(ctx, |ui| { + Self::render_info_content(ui, ctx, &rules); + }); + if !open { + SHOW_SYSTRAY_INFO.store(false, Ordering::SeqCst); + } + } + ViewportClass::Deferred | ViewportClass::Immediate | ViewportClass::Root => { + CentralPanel::default().show(ctx, |ui| { + Self::render_info_content(ui, ctx, &rules); + }); + } + } + }, + ); + } + + /// Render a small copy-to-clipboard button + fn copy_button(ui: &mut Ui, text: &str) { + if ui + .small_button(egui_phosphor::regular::COPY) + .on_hover_text("Copy to clipboard") + .clicked() + { + ui.ctx().copy_text(text.to_string()); + } + } + + /// Render the info table content (shared between deferred and embedded viewport paths). + /// Uses `egui_extras::TableBuilder` for striped rows with overlines. + fn render_info_content(ui: &mut Ui, ctx: &Context, rules: &[NormalizedHiddenRule]) { + let all_icons = Self::get_all_icons(); + + let line_height = eframe::egui::TextStyle::Body + .resolve(ui.style()) + .size + .max(ui.spacing().interact_size.y); + + ScrollArea::horizontal().show(ui, |ui| { + let available_height = ui.available_height(); + + let table = TableBuilder::new(ui) + .striped(true) + .resizable(true) + .cell_layout(eframe::egui::Layout::left_to_right( + eframe::egui::Align::Center, + )) + .column(Column::auto()) // Icon + .column(Column::auto().resizable(true)) // Exe + .column(Column::auto().resizable(true)) // Tooltip + .column(Column::auto().resizable(true)) // GUID + .column(Column::auto()) // Visible + .column(Column::auto().resizable(true)) // Clickable + .min_scrolled_height(0.0) + .max_scroll_height(available_height); + + table + .header(20.0, |mut header| { + header.col(|ui| { + ui.strong("Icon"); + }); + header.col(|ui| { + ui.strong("Exe"); + }); + header.col(|ui| { + ui.strong("Tooltip"); + }); + header.col(|ui| { + ui.strong("GUID"); + }); + header.col(|ui| { + ui.strong("Visible"); + }); + header.col(|ui| { + ui.strong("Clickable"); + }); + }) + .body(|mut body| { + for icon in &all_icons { + // Size the row to fit multi-line tooltips + let tooltip_lines = icon.tooltip.lines().count().max(1); + let row_height = line_height * tooltip_lines as f32; + + body.row(row_height, |mut row| { + row.set_overline(true); + + row.col(|ui| { + if let Some(image_icon) = &icon.image_icon { + ui.add( + Image::from_texture(&image_icon.texture(ctx)) + .maintain_aspect_ratio(true) + .fit_to_exact_size(Vec2::splat(16.0)), + ); + } else { + ui.allocate_space(Vec2::splat(16.0)); + } + }); + row.col(|ui| { + ui.label(&icon.exe_name); + if !icon.exe_name.is_empty() { + Self::copy_button(ui, &icon.exe_name); + } + }); + row.col(|ui| { + ui.label(&icon.tooltip); + if !icon.tooltip.is_empty() { + Self::copy_button(ui, &icon.tooltip); + } + }); + row.col(|ui| { + let guid = icon.guid_str.as_deref().unwrap_or("—"); + ui.label(guid); + if icon.guid_str.is_some() { + Self::copy_button(ui, guid); + } + }); + row.col(|ui| { + let hidden_by_rule = rules.iter().any(|rule| { + rule.matches( + &icon.exe_name, + &icon.tooltip, + icon.guid_str.as_deref(), + ) + }); + let visibility_text = if !icon.is_visible { + "Hidden (OS)" + } else if hidden_by_rule { + "Hidden (rule)" + } else { + "Yes" + }; + ui.label(visibility_text); + }); + row.col(|ui| { + if let Some(cmd) = + Self::fallback_command(&icon.exe_name, &icon.tooltip) + { + ui.label(format!("Fallback: {cmd}")); + } else if icon.is_clickable { + ui.label("Yes"); + } else { + ui.label("No"); + } + }); + }); + } + }); + }); + } + + /// Render a single systray icon + fn render_icon(&self, ctx: &Context, ui: &mut Ui, cached_icon: &CachedIcon, icon_size: f32) { + let stable_id = cached_icon.stable_id.clone(); + let tooltip = &cached_icon.tooltip; + + let response = SelectableFrame::new(false).show(ui, |ui| { + if let Some(image_icon) = &cached_icon.image_icon { + Frame::NONE + .inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8)) + .show(ui, |ui| { + ui.add( + Image::from_texture(&image_icon.texture(ctx)) + .maintain_aspect_ratio(true) + .fit_to_exact_size(Vec2::splat(icon_size)), + ); + }); + } else { + // Fallback: allocate space with a placeholder + ui.allocate_space(Vec2::splat(icon_size)); + } + }); + + let response = if tooltip.is_empty() { + response + } else { + response.on_hover_text_at_pointer(tooltip) + }; + + // Handle mouse clicks - mark as consumed to prevent bar from also handling. + // Fallback commands take priority: if we've defined a fallback for an icon, + // we know its native click is broken/useless (some icons register a callback + // but don't actually respond to click messages). + let has_fallback = + Self::fallback_command(&cached_icon.exe_name, &cached_icon.tooltip).is_some(); + + // Check double_clicked() before clicked() because egui fires both on + // the second click of a double-click — we want to send only the + // double-click action in that case. + if response.double_clicked() { + mark_widget_clicked(); + if has_fallback { + Self::fallback_click(cached_icon); + } else { + Self::send_action(&stable_id, SystrayIconAction::LeftDoubleClick); + } + } else if response.clicked() { + mark_widget_clicked(); + if has_fallback { + Self::fallback_click(cached_icon); + } else { + Self::send_action(&stable_id, SystrayIconAction::LeftClick); + } + } else if response.secondary_clicked() { + mark_widget_clicked(); + if has_fallback { + Self::fallback_click(cached_icon); + } else { + Self::send_action(&stable_id, SystrayIconAction::RightClick); + } + } else if response.middle_clicked() { + mark_widget_clicked(); + if has_fallback { + Self::fallback_click(cached_icon); + } else { + Self::send_action(&stable_id, SystrayIconAction::MiddleClick); + } + } + } + + /// Returns the fallback command for an icon with known broken/missing click + /// handling, if one is defined. Fallbacks take priority over native clicks. + fn fallback_command(exe_name: &str, tooltip: &str) -> Option<&'static str> { + match exe_name.to_lowercase().as_str() { + "securityhealthsystray.exe" => Some("start windowsdefender://"), + "explorer.exe" if tooltip.ends_with('%') => Some("start ms-settings:apps-volume"), + "explorer.exe" if tooltip.is_empty() => Some("start ms-settings:batterysaver"), + _ => None, + } + } + + /// Execute the fallback command for an icon with broken/missing click handling. + fn fallback_click(icon: &CachedIcon) { + if let Some(cmd) = Self::fallback_command(&icon.exe_name, &icon.tooltip) { + tracing::debug!( + "Fallback click for non-clickable icon: exe={}, cmd={}", + icon.exe_name, + cmd + ); + std::process::Command::new("cmd.exe") + .args(["/C", cmd]) + .spawn() + .ok(); + } else { + tracing::debug!("No fallback for non-clickable icon: exe={}", icon.exe_name); + } + } +} diff --git a/komorebi-bar/src/widgets/widget.rs b/komorebi-bar/src/widgets/widget.rs index 773cba2a..6da0806b 100644 --- a/komorebi-bar/src/widgets/widget.rs +++ b/komorebi-bar/src/widgets/widget.rs @@ -19,6 +19,10 @@ use crate::widgets::network::Network; use crate::widgets::network::NetworkConfig; use crate::widgets::storage::Storage; use crate::widgets::storage::StorageConfig; +#[cfg(target_os = "windows")] +use crate::widgets::systray::Systray; +#[cfg(target_os = "windows")] +use crate::widgets::systray::SystrayConfig; use crate::widgets::time::Time; use crate::widgets::time::TimeConfig; use crate::widgets::update::Update; @@ -66,6 +70,10 @@ pub enum WidgetConfig { /// Storage widget configuration #[cfg_attr(feature = "schemars", schemars(title = "Storage"))] Storage(StorageConfig), + /// System Tray widget configuration (Windows only) + #[cfg(target_os = "windows")] + #[cfg_attr(feature = "schemars", schemars(title = "Systray"))] + Systray(SystrayConfig), /// Time widget configuration #[cfg_attr(feature = "schemars", schemars(title = "Time"))] Time(TimeConfig), @@ -87,6 +95,8 @@ impl WidgetConfig { WidgetConfig::Memory(config) => Box::new(Memory::from(*config)), WidgetConfig::Network(config) => Box::new(Network::from(*config)), WidgetConfig::Storage(config) => Box::new(Storage::from(*config)), + #[cfg(target_os = "windows")] + WidgetConfig::Systray(config) => Box::new(Systray::from(config)), WidgetConfig::Time(config) => Box::new(Time::from(config.clone())), WidgetConfig::Update(config) => Box::new(Update::from(*config)), } @@ -112,6 +122,8 @@ impl WidgetConfig { WidgetConfig::Memory(config) => config.enable, WidgetConfig::Network(config) => config.enable, WidgetConfig::Storage(config) => config.enable, + #[cfg(target_os = "windows")] + WidgetConfig::Systray(config) => config.enable, WidgetConfig::Time(config) => config.enable, WidgetConfig::Update(config) => config.enable, } diff --git a/komorebi/src/core/animation.rs b/komorebi/src/core/animation.rs index 659685a7..83be03cc 100644 --- a/komorebi/src/core/animation.rs +++ b/komorebi/src/core/animation.rs @@ -74,7 +74,7 @@ pub enum AnimationStyle { EaseInOutBounce, #[cfg_attr(feature = "schemars", schemars(title = "CubicBezier"))] #[value(skip)] - /// Custom Cubic Bézier function + /// Custom Cubic Bezier function CubicBezier(f64, f64, f64, f64), } diff --git a/mkdocs.yml b/mkdocs.yml index 35fe0052..dd464c34 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,6 +81,8 @@ nav: - common-workflows/mouse-follows-focus.md - common-workflows/dynamic-layout-switching.md - common-workflows/multiple-bar-instances.md + - common-workflows/bar.md + - common-workflows/bar-widgets/systray.md - common-workflows/multi-monitor-setup.md - CLI reference: - cli/quickstart.md diff --git a/schema.bar.json b/schema.bar.json index 6bc7d36f..13e024c5 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "KomobarConfig", - "description": "The `komorebi.bar.json` configuration file reference for `v0.1.40`", + "description": "The `komorebi.bar.json` configuration file reference for `v0.1.41`", "type": "object", "properties": { "center_widgets": { @@ -353,7 +353,7 @@ }, { "title": "CubicBezier", - "description": "Custom Cubic Bézier function", + "description": "Custom Cubic Bezier function", "type": "object", "properties": { "CubicBezier": { @@ -2158,6 +2158,21 @@ } ] }, + "ButtonPosition": { + "description": "Where to place a systray button (refresh, info, shortcuts, etc.)", + "oneOf": [ + { + "description": "Show in the main visible area", + "type": "string", + "const": "Visible" + }, + { + "description": "Show in the overflow/hidden section", + "type": "string", + "const": "Overflow" + } + ] + }, "Catppuccin": { "description": "Catppuccin palette", "oneOf": [ @@ -2573,6 +2588,33 @@ } ] }, + "FieldMatch": { + "description": "A field value with an optional matching strategy.\n\nA plain string uses exact (case-insensitive) matching.\nAn object with `value` and `matching_strategy` uses the specified strategy.", + "anyOf": [ + { + "description": "Exact case-insensitive match", + "type": "string" + }, + { + "description": "Match using a specific strategy", + "type": "object", + "properties": { + "matching_strategy": { + "description": "How to match (Equals, StartsWith, EndsWith, Contains, Regex, etc.)", + "$ref": "#/$defs/MatchingStrategy" + }, + "value": { + "description": "The value to match against", + "type": "string" + } + }, + "required": [ + "value", + "matching_strategy" + ] + } + ] + }, "FocusFollowsMouseImplementation": { "description": "Focus follows mouse implementation", "oneOf": [ @@ -2802,6 +2844,54 @@ "type": "string", "format": "color-hex" }, + "HiddenIconRule": { + "description": "Rule for matching a systray icon to hide\n\nA plain string matches the exe name (backward compatible).\nAn object with optional `exe`, `tooltip`, and/or `guid` fields\nuses AND logic: all specified fields must match.\nEach field can be a plain string (exact match) or an object with\n`value` and `matching_strategy` for advanced matching.", + "anyOf": [ + { + "description": "Match by exe name (case-insensitive, exact)", + "type": "string" + }, + { + "description": "Match by one or more properties (all specified fields must match)", + "type": "object", + "properties": { + "exe": { + "description": "Exe name to match", + "anyOf": [ + { + "$ref": "#/$defs/FieldMatch" + }, + { + "type": "null" + } + ] + }, + "guid": { + "description": "Icon GUID to match (most stable identifier across restarts)", + "anyOf": [ + { + "$ref": "#/$defs/FieldMatch" + }, + { + "type": "null" + } + ] + }, + "tooltip": { + "description": "Tooltip text to match", + "anyOf": [ + { + "$ref": "#/$defs/FieldMatch" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, "HidingBehaviour": { "description": "Window hiding behaviour", "oneOf": [ @@ -3859,6 +3949,61 @@ } ] }, + "MatchingStrategy": { + "description": "Strategy for matching identifiers", + "oneOf": [ + { + "description": "Should not be used, only kept for backward compatibility", + "type": "string", + "const": "Legacy" + }, + { + "description": "Equals", + "type": "string", + "const": "Equals" + }, + { + "description": "Starts With", + "type": "string", + "const": "StartsWith" + }, + { + "description": "Ends With", + "type": "string", + "const": "EndsWith" + }, + { + "description": "Contains", + "type": "string", + "const": "Contains" + }, + { + "description": "Regex", + "type": "string", + "const": "Regex" + }, + { + "description": "Does not end with", + "type": "string", + "const": "DoesNotEndWith" + }, + { + "description": "Does not start with", + "type": "string", + "const": "DoesNotStartWith" + }, + { + "description": "Does not equal", + "type": "string", + "const": "DoesNotEqual" + }, + { + "description": "Does not contain", + "type": "string", + "const": "DoesNotContain" + } + ] + }, "MediaConfig": { "description": "Media widget configuration", "type": "object", @@ -4301,6 +4446,21 @@ "Down" ] }, + "OverflowTogglePosition": { + "description": "Position of the overflow toggle button", + "oneOf": [ + { + "description": "Toggle button appears on the left side (before visible icons)", + "type": "string", + "const": "Left" + }, + { + "description": "Toggle button appears on the right side (after visible icons)", + "type": "string", + "const": "Right" + } + ] + }, "PathBuf": { "description": "A file system path. Environment variables like %VAR%, $Env:VAR, or $VAR are automatically resolved.", "type": "string" @@ -8147,6 +8307,82 @@ "filter_state_changes" ] }, + "SystrayConfig": { + "description": "System tray widget configuration", + "type": "object", + "properties": { + "enable": { + "description": "Enable the System Tray widget", + "type": "boolean" + }, + "hidden_icons": { + "description": "A list of rules for icons to hide from the system tray\n\nEach entry can be a plain string (matches exe name, case-insensitive)\nor an object with optional `exe`, `tooltip`, and/or `guid` fields\n(all specified fields must match, AND logic, case-insensitive).\n\nRun komorebi-bar with RUST_LOG=info to see the properties of all\nsystray icons in the log output.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/HiddenIconRule" + } + }, + "info_button": { + "description": "Show an info button that opens a floating panel listing all systray icons\nwith their exe, tooltip, and GUID. Set to \"Visible\" to show it in the main\narea, or \"Overflow\" to show it in the hidden/overflow section.", + "anyOf": [ + { + "$ref": "#/$defs/ButtonPosition" + }, + { + "type": "null" + } + ] + }, + "overflow_toggle_position": { + "description": "Position of the overflow toggle button (default: Right)", + "anyOf": [ + { + "$ref": "#/$defs/OverflowTogglePosition" + }, + { + "type": "null" + } + ] + }, + "refresh_button": { + "description": "Show a refresh button that manually triggers stale icon cleanup.\nSet to \"Visible\" to show it in the main area, or \"Overflow\" to\nshow it in the hidden/overflow section.", + "anyOf": [ + { + "$ref": "#/$defs/ButtonPosition" + }, + { + "type": "null" + } + ] + }, + "shortcuts_button": { + "description": "Show a button that toggles komorebi-shortcuts (kills the process if\nrunning, starts it otherwise). Set to \"Visible\" to show it in the main\narea, or \"Overflow\" to show it in the hidden/overflow section.", + "anyOf": [ + { + "$ref": "#/$defs/ButtonPosition" + }, + { + "type": "null" + } + ] + }, + "stale_icons_check_interval": { + "description": "Interval in seconds to automatically check for and remove stale icons\nwhose owning process has exited. Clamped between 30 and 600 seconds.\nDefaults to 60. Set to 0 to disable.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "enable" + ] + }, "TimeConfig": { "description": "Time widget configuration", "type": "object", @@ -8416,6 +8652,20 @@ "Storage" ] }, + { + "title": "Systray", + "description": "System Tray widget configuration (Windows only)", + "type": "object", + "properties": { + "Systray": { + "$ref": "#/$defs/SystrayConfig" + } + }, + "additionalProperties": false, + "required": [ + "Systray" + ] + }, { "title": "Time", "description": "Time widget configuration", diff --git a/schema.json b/schema.json index 45a5fae2..a400c378 100644 --- a/schema.json +++ b/schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "StaticConfig", - "description": "The `komorebi.json` static configuration file reference for `v0.1.40`", + "description": "The `komorebi.json` static configuration file reference for `v0.1.41`", "type": "object", "properties": { "animation": { @@ -703,7 +703,7 @@ }, { "title": "CubicBezier", - "description": "Custom Cubic Bézier function", + "description": "Custom Cubic Bezier function", "type": "object", "properties": { "CubicBezier": {