From cf2a242d7c7b5e0f4e67d14577000ffddb946062 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Tue, 21 Oct 2025 15:55:33 +0530 Subject: [PATCH] linux-rust: add tray icon --- linux-rust/Cargo.lock | 842 ++++++++++++++++++++++++++++++- linux-rust/Cargo.toml | 6 +- linux-rust/src/airpods.rs | 79 ++- linux-rust/src/bluetooth/aacp.rs | 31 +- linux-rust/src/main.rs | 26 +- linux-rust/src/ui/mod.rs | 1 + linux-rust/src/ui/tray.rs | 223 ++++++++ 7 files changed, 1186 insertions(+), 22 deletions(-) create mode 100644 linux-rust/src/ui/mod.rs create mode 100644 linux-rust/src/ui/tray.rs diff --git a/linux-rust/Cargo.lock b/linux-rust/Cargo.lock index 368ee04..13ef431 100644 --- a/linux-rust/Cargo.lock +++ b/linux-rust/Cargo.lock @@ -57,6 +57,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -149,6 +158,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "approx" version = "0.5.1" @@ -158,6 +173,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -339,6 +371,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -354,6 +409,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -366,6 +427,12 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block" version = "0.1.6" @@ -420,7 +487,7 @@ dependencies = [ "libc", "log", "macaddr", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "pin-project", @@ -432,6 +499,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.19.0" @@ -464,6 +537,12 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -514,6 +593,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -581,6 +670,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -851,7 +946,7 @@ dependencies = [ "rust-ini", "web-sys", "winreg", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -1103,6 +1198,26 @@ dependencies = [ "log", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1165,6 +1280,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fast-srgb8" version = "1.0.0" @@ -1177,6 +1307,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1397,8 +1547,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1413,6 +1565,16 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gl_generator" version = "0.14.0" @@ -1773,6 +1935,64 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.0", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imageproc" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d" +dependencies = [ + "ab_glyph", + "approx", + "getrandom 0.2.16", + "image", + "itertools", + "nalgebra", + "num", + "rand", + "rand_distr", + "rayon", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "2.12.0" @@ -1792,12 +2012,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1887,6 +2127,19 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "ksni" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc9a5e60d55371fd681051b05e9b58e1d818f5085f6364afe872c9347311f91" +dependencies = [ + "futures-util", + "paste", + "serde", + "tokio", + "zbus 5.12.0", +] + [[package]] name = "kurbo" version = "0.10.4" @@ -1903,6 +2156,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.177" @@ -1918,6 +2177,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1986,11 +2255,15 @@ dependencies = [ name = "librepods-rust" version = "0.1.0" dependencies = [ + "ab_glyph", "bluer", "dbus", "env_logger", "hex", "iced", + "image", + "imageproc", + "ksni", "libpulse-binding", "log", "tokio", @@ -2030,6 +2303,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.5" @@ -2051,6 +2333,26 @@ dependencies = [ "libc", ] +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.6" @@ -2090,6 +2392,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2111,6 +2419,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "naga" version = "0.19.2" @@ -2131,6 +2449,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "nalgebra" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2170,6 +2503,12 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.29.0" @@ -2183,6 +2522,68 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -2194,6 +2595,37 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2201,6 +2633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2701,6 +3134,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -2751,7 +3197,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -2768,6 +3214,43 @@ name = "profiling" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.107", +] + +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" @@ -2823,6 +3306,16 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "range-alloc" version = "0.1.4" @@ -2835,12 +3328,68 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.11.0" @@ -2944,6 +3493,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + [[package]] name = "roxmltree" version = "0.20.0" @@ -3027,6 +3582,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3121,6 +3685,15 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3147,12 +3720,34 @@ dependencies = [ "libc", ] +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -3385,6 +3980,25 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.23.0" @@ -3427,6 +4041,20 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3438,7 +4066,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -3495,6 +4123,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -3520,6 +4149,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -3529,6 +4179,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.7" @@ -3536,7 +4199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] @@ -3694,6 +4357,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -3941,6 +4621,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + [[package]] name = "wgpu" version = "0.19.4" @@ -4048,6 +4734,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.2.1" @@ -4532,7 +5228,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "rand", "serde", @@ -4543,9 +5239,38 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros 5.12.0", + "zbus_names 4.2.0", + "zvariant 5.8.0", ] [[package]] @@ -4558,7 +5283,22 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.107", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.107", + "zbus_names 4.2.0", + "zvariant 5.8.0", + "zvariant_utils 3.2.1", ] [[package]] @@ -4569,7 +5309,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant 5.8.0", ] [[package]] @@ -4598,6 +5350,30 @@ dependencies = [ "syn 2.0.107", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.2.0" @@ -4608,7 +5384,21 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive 5.8.0", + "zvariant_utils 3.2.1", ] [[package]] @@ -4621,7 +5411,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.107", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.107", + "zvariant_utils 3.2.1", ] [[package]] @@ -4634,3 +5437,16 @@ dependencies = [ "quote", "syn 2.0.107", ] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.107", + "winnow", +] diff --git a/linux-rust/Cargo.toml b/linux-rust/Cargo.toml index 9576bec..2b1c57d 100644 --- a/linux-rust/Cargo.toml +++ b/linux-rust/Cargo.toml @@ -12,4 +12,8 @@ log = "0.4.28" dbus = "0.9.9" hex = "0.4.3" iced = {version = "0.13.1", features = ["tokio", "auto-detect-theme"]} -libpulse-binding = "2.30.1" \ No newline at end of file +libpulse-binding = "2.30.1" +ksni = "0.3.1" +image = "0.25.8" +imageproc = "0.25.0" +ab_glyph = "0.2.32" \ No newline at end of file diff --git a/linux-rust/src/airpods.rs b/linux-rust/src/airpods.rs index f044cfc..1063113 100644 --- a/linux-rust/src/airpods.rs +++ b/linux-rust/src/airpods.rs @@ -1,10 +1,13 @@ use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent}; +use crate::bluetooth::aacp::ControlCommandIdentifiers; use crate::media_controller::MediaController; use bluer::Address; use log::{debug, info}; use std::sync::Arc; +use ksni::Handle; use tokio::sync::Mutex; use tokio::time::{sleep, Duration}; +use crate::ui::tray::MyTray; pub struct AirPodsDevice { pub mac_address: Address, @@ -13,11 +16,13 @@ pub struct AirPodsDevice { } impl AirPodsDevice { - pub async fn new(mac_address: Address) -> Self { + pub async fn new(mac_address: Address, tray_handle: Handle) -> Self { info!("Creating new AirPodsDevice for {}", mac_address); let mut aacp_manager = AACPManager::new(); aacp_manager.connect(mac_address).await; + tray_handle.update(|tray: &mut MyTray| tray.connected = true).await; + info!("Sending handshake"); aacp_manager.send_handshake().await.expect( "Failed to send handshake to AirPods device", @@ -46,8 +51,52 @@ impl AirPodsDevice { let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string()))); let mc_clone = media_controller.clone(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel(); aacp_manager.set_event_channel(tx).await; + tray_handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx)).await; + + let aacp_manager_clone = aacp_manager.clone(); + tokio::spawn(async move { + while let Some((id, value)) = command_rx.recv().await { + if let Err(e) = aacp_manager_clone.send_control_command(id, &value).await { + log::error!("Failed to send control command: {}", e); + } + } + }); + + let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel(); + aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ListeningMode, listening_mode_tx).await; + let tray_handle_clone = tray_handle.clone(); + tokio::spawn(async move { + while let Some(value) = listening_mode_rx.recv().await { + tray_handle_clone.update(|tray: &mut MyTray| { + tray.listening_mode = Some(value[0]); + }).await; + } + }); + + let (allow_off_tx, mut allow_off_rx) = tokio::sync::mpsc::unbounded_channel(); + aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx).await; + let tray_handle_clone = tray_handle.clone(); + tokio::spawn(async move { + while let Some(value) = allow_off_rx.recv().await { + tray_handle_clone.update(|tray: &mut MyTray| { + tray.allow_off_option = Some(value[0]); + }).await; + } + }); + + let (conversation_detect_tx, mut conversation_detect_rx) = tokio::sync::mpsc::unbounded_channel(); + aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ConversationDetectConfig, conversation_detect_tx).await; + let tray_handle_clone = tray_handle.clone(); + tokio::spawn(async move { + while let Some(value) = conversation_detect_rx.recv().await { + tray_handle_clone.update(|tray: &mut MyTray| { + tray.conversation_detect_enabled = Some(value[0] == 0x01); + }).await; + } + }); tokio::spawn(async move { while let Some(event) = rx.recv().await { @@ -58,9 +107,33 @@ impl AirPodsDevice { debug!("Calling handle_ear_detection with old_status: {:?}, new_status: {:?}", old_status, new_status); controller.handle_ear_detection(old_status, new_status).await; } - _ => { - debug!("Received unhandled AACP event: {:?}", event); + AACPEvent::BatteryInfo(battery_info) => { + debug!("Received BatteryInfo event: {:?}", battery_info); + tray_handle.update(|tray: &mut MyTray| { + for b in &battery_info { + match b.component as u8 { + 0x02 => { + tray.battery_l = Some(b.level); + tray.battery_l_status = Some(b.status); + } + 0x04 => { + tray.battery_r = Some(b.level); + tray.battery_r_status = Some(b.status); + } + 0x08 => { + tray.battery_c = Some(b.level); + tray.battery_c_status = Some(b.status); + } + _ => {} + } + } + }).await; + debug!("Updated tray with new battery info"); } + AACPEvent::ControlCommand(status) => { + debug!("Received ControlCommand event: {:?}", status); + } + _ => {} } } }); diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index ac83f9b..47a4eb0 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use tokio::sync::{Mutex, mpsc}; use tokio::task::JoinSet; use tokio::time::{sleep, Instant}; +use std::collections::HashMap; const PSM: u16 = 0x1001; const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); @@ -31,14 +32,14 @@ pub mod opcodes { pub const SEND_CONNECTED_MAC: u8 = 0x14; } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ControlCommandStatus { pub identifier: ControlCommandIdentifiers, pub value: Vec, } #[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ControlCommandIdentifiers { MicMode = 0x01, ButtonSendMode = 0x05, @@ -222,6 +223,7 @@ pub enum AACPEvent { struct AACPManagerState { sender: Option>>, control_command_status_list: Vec, + control_command_subscribers: HashMap>>>, owns: bool, connected_devices: Vec, audio_source: Option, @@ -237,6 +239,7 @@ impl AACPManagerState { AACPManagerState { sender: None, control_command_status_list: Vec::new(), + control_command_subscribers: HashMap::new(), owns: false, connected_devices: Vec::new(), audio_source: None, @@ -352,6 +355,15 @@ impl AACPManager { state.event_tx = Some(tx); } + pub async fn subscribe_to_control_command(&self, identifier: ControlCommandIdentifiers, tx: mpsc::UnboundedSender>) { + let mut state = self.state.lock().await; + state.control_command_subscribers.entry(identifier).or_default().push(tx); + // send initial value if available + if let Some(status) = state.control_command_status_list.iter().find(|s| s.identifier == identifier) { + let _ = state.control_command_subscribers.get(&identifier).unwrap().last().unwrap().send(status.value.clone()); + } + } + pub async fn receive_packet(&self, packet: &[u8]) { if !packet.starts_with(&HEADER_BYTES) { debug!("Received packet does not start with expected header: {}", hex::encode(packet)); @@ -433,6 +445,11 @@ impl AACPManager { if identifier == ControlCommandIdentifiers::OwnsConnection { state.owns = value_bytes[0] != 0; } + if let Some(subscribers) = state.control_command_subscribers.get(&identifier) { + for sub in subscribers { + let _ = sub.send(value.clone()); + } + } if let Some(ref tx) = state.event_tx { let _ = tx.send(AACPEvent::ControlCommand(status)); } @@ -621,6 +638,16 @@ impl AACPManager { packet.extend_from_slice(name_bytes); self.send_data_packet(&packet).await } + + pub async fn send_control_command(&self, identifier: ControlCommandIdentifiers, value: &[u8]) -> Result<()> { + let opcode = [opcodes::CONTROL_COMMAND, 0x00]; + let mut data = vec![identifier as u8]; + for i in 0..4 { + data.push(value.get(i).copied().unwrap_or(0)); + } + let packet = [opcode.as_slice(), data.as_slice()].concat(); + self.send_data_packet(&packet).await + } } async fn recv_thread(manager: AACPManager, sp: Arc) { diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index a355aa9..09fef94 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -1,6 +1,7 @@ mod bluetooth; mod airpods; mod media_controller; +mod ui; use std::env; use log::{debug, info}; @@ -12,6 +13,8 @@ use std::collections::HashMap; use crate::bluetooth::discovery::find_connected_airpods; use crate::airpods::AirPodsDevice; use bluer::Address; +use ksni::TrayMethods; +use crate::ui::tray::MyTray; #[tokio::main] async fn main() -> bluer::Result<()> { @@ -21,6 +24,22 @@ async fn main() -> bluer::Result<()> { env_logger::init(); + + let tray = MyTray { + conversation_detect_enabled: None, + battery_l: None, + battery_l_status: None, + battery_r: None, + battery_r_status: None, + battery_c: None, + battery_c_status: None, + connected: false, + listening_mode: None, + allow_off_option: None, + command_tx: None, + }; + let handle = tray.spawn().await.unwrap(); + let session = bluer::Session::new().await?; let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; @@ -32,7 +51,7 @@ async fn main() -> bluer::Result<()> { Ok(device) => { let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string()); info!("Found connected AirPods: {}, initializing.", name); - let _airpods_device = AirPodsDevice::new(device.address()).await; + let _airpods_device = AirPodsDevice::new(device.address(), handle.clone()).await; } Err(_) => { info!("No connected AirPods found."); @@ -41,7 +60,7 @@ async fn main() -> bluer::Result<()> { let conn = Connection::new_system()?; let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged"); - conn.add_match(rule, |_: (), conn, msg| { + conn.add_match(rule, move |_: (), conn, msg| { let Some(path) = msg.path() else { return true; }; if !path.contains("/org/bluez/hci") || !path.contains("/dev_") { return true; @@ -68,8 +87,9 @@ async fn main() -> bluer::Result<()> { let Ok(addr_str) = proxy.get::("org.bluez.Device1", "Address") else { return true; }; let Ok(addr) = addr_str.parse::
() else { return true; }; info!("AirPods connected: {}, initializing", name); + let handle_clone = handle.clone(); tokio::spawn(async move { - let _airpods_device = AirPodsDevice::new(addr).await; + let _airpods_device = AirPodsDevice::new(addr, handle_clone).await; }); true })?; diff --git a/linux-rust/src/ui/mod.rs b/linux-rust/src/ui/mod.rs new file mode 100644 index 0000000..07825ef --- /dev/null +++ b/linux-rust/src/ui/mod.rs @@ -0,0 +1 @@ +pub mod tray; \ No newline at end of file diff --git a/linux-rust/src/ui/tray.rs b/linux-rust/src/ui/tray.rs new file mode 100644 index 0000000..4b18006 --- /dev/null +++ b/linux-rust/src/ui/tray.rs @@ -0,0 +1,223 @@ +// use ksni::TrayMethods; // provides the spawn method + +use ab_glyph::{Font, ScaleFont}; +use ksni::{Icon, ToolTip}; + +use crate::bluetooth::aacp::ControlCommandIdentifiers; + +#[derive(Debug)] +pub(crate) struct MyTray { + pub(crate) conversation_detect_enabled: Option, + pub(crate) battery_l: Option, + pub(crate) battery_l_status: Option, + pub(crate) battery_r: Option, + pub(crate) battery_r_status: Option, + pub(crate) battery_c: Option, + pub(crate) battery_c_status: Option, + pub(crate) connected: bool, + pub(crate) listening_mode: Option, + pub(crate) allow_off_option: Option, + pub(crate) command_tx: Option)>>, +} + +impl ksni::Tray for MyTray { + fn id(&self) -> String { + env!("CARGO_PKG_NAME").into() + } + fn title(&self) -> String { + "AirPods".into() + } + fn icon_pixmap(&self) -> Vec { + // text to icon pixmap + let text = if self.connected { + let min_battery = match (self.battery_l, self.battery_r) { + (Some(l), Some(r)) => Some(l.min(r)), + (Some(l), None) => Some(l), + (None, Some(r)) => Some(r), + (None, None) => None, + }; + min_battery.map(|b| format!("{}%", b)).unwrap_or("?".to_string()) + } else { + "D".into() + }; + let icon = icon_from_text(&text, true); + vec![icon] + } + fn tool_tip(&self) -> ToolTip { + if self.connected { + let l = self.battery_l.map(|b| format!("L: {}%", b)).unwrap_or("L: ?".to_string()); + let l_status = self.battery_l_status.map(|s| format!(" ({:?})", s)).unwrap_or("".to_string()); + let r = self.battery_r.map(|b| format!("R: {}%", b)).unwrap_or("R: ?".to_string()); + let r_status = self.battery_r_status.map(|s| format!(" ({:?})", s)).unwrap_or("".to_string()); + let c = self.battery_c.map(|b| format!("C: {}%", b)).unwrap_or("C: ?".to_string()); + let c_status = self.battery_c_status.map(|s| format!(" ({:?})", s)).unwrap_or("".to_string()); + ToolTip { + icon_name: "".to_string(), + icon_pixmap: vec![], + title: "Battery Status".to_string(), + description: format!("{}{} {}{} {}{}", l, l_status, r, r_status, c, c_status), + } + } else { + ToolTip { + icon_name: "".to_string(), + icon_pixmap: vec![], + title: "Not Connected".to_string(), + description: "Device is not connected.".to_string(), + } + } + } + fn menu(&self) -> Vec> { + use ksni::menu::*; + let allow_off = self.allow_off_option == Some(0x01); + let options = if allow_off { + vec![ + ("Off", 0x01), + ("ANC", 0x02), + ("Transparency", 0x03), + ("Adaptive", 0x04), + ] + } else { + vec![ + ("ANC", 0x02), + ("Transparency", 0x03), + ("Adaptive", 0x04), + ] + }; + let selected = self.listening_mode.and_then(|mode| { + options.iter().position(|&(_, val)| val == mode) + }).unwrap_or(0); + let options_clone = options.clone(); + vec![ + RadioGroup { + selected, + select: Box::new(move |this: &mut Self, current| { + if let Some(tx) = &this.command_tx { + let value = options_clone.get(current).map(|&(_, val)| val).unwrap_or(0x02); + let _ = tx.send((ControlCommandIdentifiers::ListeningMode, vec![value])); + } + }), + options: options.into_iter().map(|(label, _)| RadioItem { + label: label.into(), + ..Default::default() + }).collect(), + ..Default::default() + } + .into(), + CheckmarkItem { + label: "Conversation Detection".into(), + checked: self.conversation_detect_enabled.unwrap_or(false), + enabled: self.conversation_detect_enabled.is_some(), + activate: Box::new(|this: &mut Self| { + if let Some(tx) = &this.command_tx { + if let Some(is_enabled) = this.conversation_detect_enabled { + let value = if is_enabled { 0x02 } else { 0x01 }; + let _ = tx.send((ControlCommandIdentifiers::ConversationDetectConfig, vec![value])); + } + } + }), + ..Default::default() + } + .into(), + StandardItem { + label: "Exit".into(), + icon_name: "application-exit".into(), + activate: Box::new(|_| std::process::exit(0)), + ..Default::default() + } + .into(), + ] + } +} + +fn icon_from_text(text: &str, text_mode: bool) -> Icon { + use ab_glyph::{FontRef, PxScale}; + use image::{ImageBuffer, Rgba}; + use imageproc::drawing::draw_text_mut; + + let width = 64; + let height = 64; + + let mut img = ImageBuffer::from_fn(width, height, |_, _| Rgba([0u8, 0u8, 0u8, 0u8])); + + if !text_mode { + let percentage = if text.ends_with('%') { + text.trim_end_matches('%').parse::().unwrap_or(0.0) / 100.0 + } else { + 0.0 + }; + + let center_x = width as f32 / 2.0; + let center_y = height as f32 / 2.0; + let inner_radius = 22.0; + let outer_radius = 28.0; + + // ring background + for y in 0..height { + for x in 0..width { + let dx = x as f32 - center_x; + let dy = y as f32 - center_y; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > inner_radius && dist <= outer_radius { + img.put_pixel(x, y, Rgba([128u8, 128u8, 128u8, 255u8])); + } + } + } + + // ring + for y in 0..height { + for x in 0..width { + let dx = x as f32 - center_x; + let dy = y as f32 - center_y; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > inner_radius && dist <= outer_radius { + let angle = dy.atan2(dx); + let angle_from_top = (std::f32::consts::PI / 2.0 - angle).rem_euclid(2.0 * std::f32::consts::PI); + if angle_from_top <= percentage * 2.0 * std::f32::consts::PI { + img.put_pixel(x, y, Rgba([0u8, 255u8, 0u8, 255u8])); + } + } + } + } + } else { + // battery text + let font_data = include_bytes!("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"); + let font = match FontRef::try_from_slice(font_data) { + Ok(f) => f, + Err(_) => { + return Icon { + width: width as i32, + height: height as i32, + data: vec![0u8; (width * height * 4) as usize], + }; + } + }; + + let scale = PxScale::from(28.0); + let color = Rgba([255u8, 255u8, 255u8, 255u8]); + + let scaled_font = font.as_scaled(scale); + let mut text_width = 0.0; + for c in text.chars() { + let glyph_id = font.glyph_id(c); + text_width += scaled_font.h_advance(glyph_id); + } + let x = ((width as f32 - text_width) / 2.0).max(0.0) as i32; + let y = ((height as f32 - scale.y) / 2.0).max(0.0) as i32; + + draw_text_mut(&mut img, color, x, y, scale, &font, text); + } + + let mut data = Vec::with_capacity((width * height * 4) as usize); + for pixel in img.pixels() { + data.push(pixel[3]); + data.push(pixel[0]); + data.push(pixel[1]); + data.push(pixel[2]); + } + + Icon { + width: width as i32, + height: height as i32, + data, + } +} \ No newline at end of file