From 20f8ac318790ef49117c646bababf31510d785de Mon Sep 17 00:00:00 2001 From: Dastan <88332432+dastansam@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:14:52 +0200 Subject: [PATCH] Use `tray_icon` crate for better tray icon support (#288) * Use `tray_icon` crate for better tray icon support * Install atk in github actions * Use different crate for Linux and platform generic tray icon struct * Introduce abstract, platform generic tray icon * Remove unnecessary GTK3 dependency from CI workflow * Move tray icon implementations into submodules with no other changes * Minor refactoring * Refactor tray icon implementation to a simple spawn function * Update `tray-icon` dependency * Fix compilation dependencies on Linux * Fix exit mapped to incorrect command in new tray icon implementation * Fix icon rendering on Linux * Handle left mouse click on the icon on Windows to show an app * Make tray icon clicking toggle window visibility instead of just showing * Rename second tray icon menu item to Quit --------- Co-authored-by: Nazar Mokrynskyi --- .github/workflows/release.yml | 2 +- .github/workflows/rust.yml | 4 +- Cargo.lock | 563 +++++++++++++++++++++--- Cargo.toml | 8 +- README.md | 7 +- res/translations/de-DE/messages.ftl | 3 +- res/translations/en/messages.ftl | 2 +- res/translations/rs/messages.ftl | 3 +- res/translations/ru-RU/messages.ftl | 2 +- res/translations/zh-CN/messages.ftl | 3 +- src/frontend.rs | 76 ++-- src/frontend/tray_icon.rs | 21 + src/frontend/tray_icon/unix.rs | 109 +++++ src/frontend/tray_icon/windows_macos.rs | 82 ++++ 14 files changed, 768 insertions(+), 117 deletions(-) create mode 100644 src/frontend/tray_icon.rs create mode 100644 src/frontend/tray_icon/unix.rs create mode 100644 src/frontend/tray_icon/windows_macos.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f4b17e2..9cdf87d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: - name: Install GTK4 and libfuse2 (Linux) # libfuse2 is needed for AppImage to run - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libfuse2 + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libdbus-1-dev libfuse2 if: runner.os == 'Linux' - name: Configure GTK4 cache (Windows) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 129ae90f..7272989d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -64,7 +64,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install GTK4 (Ubuntu) - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libdbus-1-dev if: runner.os == 'Linux' - name: Install GTK4 (macOS) @@ -165,7 +165,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install GTK4 (Ubuntu) - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libgtk-4-dev libdbus-1-dev if: runner.os == 'Linux' - name: Install GTK4 (macOS) diff --git a/Cargo.lock b/Cargo.lock index c363ddb6..9c3d673b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,29 @@ dependencies = [ "pin-project-lite 0.2.14", ] +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic" version = "0.5.3" @@ -1271,25 +1294,6 @@ dependencies = [ "serde", ] -[[package]] -name = "betrayer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88df312fbb13c845a05ede7e91ab7b9fea442d9176d092bbdb954ae6db55ad5d" -dependencies = [ - "async-io", - "block2", - "flume", - "icrate", - "log", - "objc2", - "once_cell", - "parking_lot 0.12.3", - "png", - "windows 0.56.0", - "zbus", -] - [[package]] name = "bincode" version = "1.3.3" @@ -1332,6 +1336,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitstream-io" @@ -1441,22 +1448,12 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "block-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" -dependencies = [ - "objc-sys", -] - [[package]] name = "block2" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "block-sys", "objc2", ] @@ -1815,6 +1812,21 @@ dependencies = [ "inout 0.2.0-rc.0", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + [[package]] name = "clap" version = "3.2.25" @@ -1829,7 +1841,7 @@ dependencies = [ "once_cell", "strsim 0.10.0", "termcolor", - "textwrap", + "textwrap 0.16.1", ] [[package]] @@ -1911,9 +1923,25 @@ checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ "bitflags 1.3.2", "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.6.0", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "foreign-types", "libc", "objc", @@ -1927,8 +1955,22 @@ checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation", - "core-graphics-types", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.6.0", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "libc", "objc", ] @@ -2060,11 +2102,21 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" @@ -2073,8 +2125,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "foreign-types", "libc", ] @@ -2086,7 +2151,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", "libc", ] @@ -2304,6 +2380,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -2491,6 +2576,37 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-codegen" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49da9fdfbe872d4841d56605dc42efa5e6ca3291299b87f44e1cde91a28617c" +dependencies = [ + "clap 2.34.0", + "dbus", + "xml-rs", +] + +[[package]] +name = "dbus-tree" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f456e698ae8e54575e19ddb1f9b7bce2298568524f215496b248eb9498b4f508" +dependencies = [ + "dbus", +] + [[package]] name = "dconf_rs" version = "0.3.0" @@ -2823,6 +2939,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + [[package]] name = "dtoa" version = "1.0.9" @@ -3872,6 +3994,21 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + [[package]] name = "gdk-pixbuf" version = "0.18.5" @@ -3898,6 +4035,23 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "gdk4" version = "0.7.3" @@ -4202,6 +4356,58 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.1.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "gtk4" version = "0.7.3" @@ -4769,16 +4975,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icrate" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642" -dependencies = [ - "block2", - "objc2", -] - [[package]] name = "icu_locid" version = "1.5.0" @@ -4839,7 +5035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b0422c86d7ce0e97169cc42e04ae643caf278874a7a3c87b8150a220dc7e1e" dependencies = [ "async-io", - "core-foundation", + "core-foundation 0.9.4", "fnv", "futures", "if-addrs", @@ -5389,12 +5585,35 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.6.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "keystream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" +[[package]] +name = "ksni" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934310bdd016e55725482b8d35ac0c16fd058c1b955d8959aa2d953b918c85b" +dependencies = [ + "dbus", + "dbus-codegen", + "dbus-tree", + "thiserror", +] + [[package]] name = "kvdb" version = "0.13.0" @@ -5441,12 +5660,45 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.7" @@ -5458,6 +5710,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libm" version = "0.2.8" @@ -6404,6 +6666,25 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + [[package]] name = "libz-sys" version = "1.1.18" @@ -6959,6 +7240,25 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "muda" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" +dependencies = [ + "cocoa 0.26.0", + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png", + "thiserror", + "windows-sys 0.59.0", +] + [[package]] name = "multiaddr" version = "0.17.1" @@ -7161,8 +7461,8 @@ checksum = "84e7038885d2aeab236bd60da9e159a5967b47cde3292da3b15ff1bec27c039f" dependencies = [ "ascii", "block", - "cocoa", - "core-foundation", + "cocoa 0.25.0", + "core-foundation 0.9.4", "dirs-next", "objc", "objc-foundation", @@ -7521,12 +7821,89 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "objc2-encode" version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -9462,7 +9839,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e3beb939bcd33c269f4bf946cc829fcd336370267c4a927ac0399c84a3151a1" dependencies = [ - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "jni", "log", @@ -10717,7 +11094,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "num-bigint", @@ -12048,7 +12425,6 @@ dependencies = [ "async-oneshot", "async-trait", "backoff", - "betrayer", "bytesize", "clap 4.5.8", "dark-light", @@ -12066,6 +12442,8 @@ dependencies = [ "futures-timer", "gtk4", "hex", + "image", + "ksni", "mimalloc", "names", "native-dialog", @@ -12124,6 +12502,7 @@ dependencies = [ "tracing-panic", "tracing-subscriber", "tracker", + "tray-icon", "winres", ] @@ -12216,6 +12595,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.10.0" @@ -12764,7 +13149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -12852,6 +13237,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -13302,6 +13696,26 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "tray-icon" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131a65b2cef2081bc14dbcd414c906edbfa3bb5323dd7e748cc298614681196b" +dependencies = [ + "core-graphics 0.24.0", + "crossbeam-channel", + "dirs 5.0.1", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "thiserror", + "windows-sys 0.59.0", +] + [[package]] name = "trie-db" version = "0.29.1" @@ -13590,6 +14004,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -13689,6 +14109,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.2.0" @@ -14313,6 +14739,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -14571,6 +15006,16 @@ dependencies = [ "tap", ] +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x25519-dalek" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index 57af1283..fd6c3bf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ async-lock = "3.4.0" async-oneshot = "0.5.9" async-trait = "0.1.80" backoff = { version = "0.4.0", features = ["futures", "tokio"] } -betrayer = { version = "0.2.0" } bytesize = "1.3.0" clap = { version = "4.5.8", features = ["derive"] } dark-light = "1.1.1" @@ -54,6 +53,7 @@ futures = "0.3.30" futures-timer = "3.0.3" gtk = { version = "0.7.3", package = "gtk4" } hex = "0.4.3" +image = { version = "0.25", features = ["png"], default-features = false } mimalloc = "0.1.41" names = "0.14.0" notify-rust = { version = "4.11.1", features = ["images"] } @@ -115,6 +115,12 @@ tracker = "0.2.2" native-dialog = "0.7.0" tracing-panic = "0.1.2" +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +tray-icon = "0.16.0" + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +ksni = "0.2.2" + [build-dependencies] fluent-static-codegen = "0.3.2" diff --git a/README.md b/README.md index 434551c0..d8978a5f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,12 @@ Application supports bare minimum configuration and doesn't support operator fun In order to build this app you'll need to install both dependencies necessary for building [Subspace](https://github.com/autonomys/subspace) and [GTK4](https://github.com/gtk-rs/gtk4-rs), including the optional -`librsvg`. Follow their documentation for details, otherwise `cargo run` will get you where to want to be. +`librsvg`. Follow their documentation for details. + +On Linux D-Bus development headers also need to be installed, can be done with `sudo apt-get install libdbus-1-dev` on +Ubuntu. + +Then simply `cargo run` will get you where to want to be. ## Contribution diff --git a/res/translations/de-DE/messages.ftl b/res/translations/de-DE/messages.ftl index 4df43c24..660e2dd0 100644 --- a/res/translations/de-DE/messages.ftl +++ b/res/translations/de-DE/messages.ftl @@ -197,7 +197,8 @@ about_system_information = Datenverzeichnis (einschließlich Protokolle): {$data_directory} tray_icon_open = öffnen -tray_icon_close = schließen +# TODO: Check translation +tray_icon_quit = Aufhören notification_app_minimized_to_tray = Space Acres wurde in die Taskleiste minimiert .body = Du kannst es wieder öffnen oder komplett beenden, indem du das Menü des Tray-Symbols verwendest diff --git a/res/translations/en/messages.ftl b/res/translations/en/messages.ftl index bbc116ee..fe355617 100644 --- a/res/translations/en/messages.ftl +++ b/res/translations/en/messages.ftl @@ -196,7 +196,7 @@ about_system_information = Data directory (including logs): {$data_directory} tray_icon_open = Open -tray_icon_close = Close +tray_icon_quit = Quit notification_app_minimized_to_tray = Space Acres was minimized to tray .body = You can open it again or exit completely using tray icon menu diff --git a/res/translations/rs/messages.ftl b/res/translations/rs/messages.ftl index e9a03c7a..0ccb47b5 100644 --- a/res/translations/rs/messages.ftl +++ b/res/translations/rs/messages.ftl @@ -199,7 +199,8 @@ about_system_information = Direktorijum podataka (uključujući dnevnike): {$data_directory} tray_icon_open = Otvori -tray_icon_close = Zatvori +# TODO: Check translation +tray_icon_quit = Izlaz notification_app_minimized_to_tray = Space Acres je minimiziran u sistemsku traku .body = Možete ga ponovo otvoriti ili potpuno izaći koristeći meni ikone u sistemskoj traci diff --git a/res/translations/ru-RU/messages.ftl b/res/translations/ru-RU/messages.ftl index c2e43193..b6c62b96 100644 --- a/res/translations/ru-RU/messages.ftl +++ b/res/translations/ru-RU/messages.ftl @@ -196,7 +196,7 @@ about_system_information = Директория данных (включая журнал): {$data_directory} tray_icon_open = Открыть -tray_icon_close = Закрыть +tray_icon_quit = Выход notification_app_minimized_to_tray = Space Acres был свернут .body = Вы можете открыть его снова или полностью выйти, используя значок в области уведомлений diff --git a/res/translations/zh-CN/messages.ftl b/res/translations/zh-CN/messages.ftl index 7600a5df..d260c041 100644 --- a/res/translations/zh-CN/messages.ftl +++ b/res/translations/zh-CN/messages.ftl @@ -198,7 +198,8 @@ about_system_information = 数据目录 (包括日志): {$data_directory} tray_icon_open = 打开 -tray_icon_close = 关闭 +# TODO: Check translation +tray_icon_quit = 退出 notification_app_minimized_to_tray = Space Acres已最小化到托盘 .body = 你可以关闭或从托盘中重新打开 diff --git a/src/frontend.rs b/src/frontend.rs index 3c998462..d5166a54 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -3,6 +3,7 @@ pub mod loading; pub mod new_version; pub mod running; pub mod translations; +mod tray_icon; mod widgets; use crate::backend::config::RawConfig; @@ -14,9 +15,6 @@ use crate::frontend::new_version::NewVersion; use crate::frontend::running::{RunningInit, RunningInput, RunningOutput, RunningView}; use crate::frontend::translations::{AsDefaultStr, T}; use crate::AppStatusCode; -#[cfg(any(target_os = "linux", windows))] -use betrayer::Icon; -use betrayer::{Menu, MenuItem, TrayEvent, TrayIcon, TrayIconBuilder}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use gtk::glib; @@ -26,6 +24,7 @@ use relm4::actions::{RelmAction, RelmActionGroup}; use relm4::prelude::*; use relm4::{Sender, ShutdownReceiver}; use relm4_icons::icon_name; +use std::any::Any; use std::cell::{Cell, LazyCell}; use std::future::Future; use std::path::PathBuf; @@ -34,7 +33,6 @@ use std::{env, fmt}; use tracing::{debug, error, warn}; pub const GLOBAL_CSS: &str = include_str!("../res/app.css"); -#[cfg(all(unix, not(target_os = "macos")))] const ICON: &[u8] = include_bytes!("../res/icon.png"); const ABOUT_IMAGE: &[u8] = include_bytes!("../res/about.png"); @@ -105,12 +103,6 @@ static PIXBUF_ABOUT_IMG: LazyCell = LazyCell::new(|| { gtk::gdk_pixbuf::Pixbuf::from_read(ABOUT_IMAGE).expect("Statically correct image; qed") }); -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum TrayMenuSignal { - Open, - Close, -} - #[derive(Debug)] pub enum AppInput { Configuration(ConfigurationOutput), @@ -126,12 +118,17 @@ pub enum AppInput { CloseStatusBarWarning, HideWindow, ShowWindow, + ShowHideToggle, ShutDown, } #[derive(Debug)] pub enum AppCommandOutput { BackendNotification(BackendNotification), + ShowWindow, + // Only used in tray icon on some platforms + #[cfg_attr(not(any(windows, target_os = "macos")), allow(dead_code))] + ShowHideToggle, Restart, Quit, } @@ -266,7 +263,7 @@ pub struct App { backend_fut: Option + Send>>, // Keep it around so it doesn't disappear #[do_not_track] - _tray_icon: Option>, + _tray_icon: Option>, } #[relm4::component(pub async)] @@ -589,45 +586,13 @@ impl AsyncComponent for App { }) .transient_for(&root) .build(); + about_dialog.connect_close_request(|about_dialog| { - about_dialog.hide(); + about_dialog.set_visible(false); glib::Propagation::Stop }); - // TODO: Re-enable macOS once https://github.com/subspace/space-acres/issues/183 and/or - // https://github.com/subspace/space-acres/issues/222 are resolved - let tray_icon = if cfg!(target_os = "macos") { - None - } else { - let tray_icon = TrayIconBuilder::new(); - #[cfg(target_os = "linux")] - let tray_icon = tray_icon - .with_icon(Icon::from_png_bytes(ICON).expect("Statically correct image; qed")); - #[cfg(windows)] - let tray_icon = tray_icon - .with_icon(Icon::from_resource(1, None).expect("Tray icon is a valid ICO; qed")); - tray_icon - .with_tooltip("Space Acres") - .with_menu(Menu::new([ - MenuItem::button(T.tray_icon_open(), TrayMenuSignal::Open), - MenuItem::button(T.tray_icon_close(), TrayMenuSignal::Close), - ])) - .build({ - let sender = sender.clone(); - move |tray_event| { - if let TrayEvent::Menu(signal) = tray_event { - match signal { - TrayMenuSignal::Open => sender.input(AppInput::ShowWindow), - TrayMenuSignal::Close => sender.input(AppInput::ShutDown), - } - } - } - }) - .map_err(|error| { - warn!(%error, "Unable to create tray icon"); - }) - .ok() - }; + let tray_icon = tray_icon::spawn(&sender).await; let has_tray_icon = tray_icon.is_some(); let model = Self { @@ -698,7 +663,7 @@ impl AsyncComponent for App { if minimize_on_start { if has_tray_icon { - root.hide() + root.set_visible(false); } else { root.minimize() } @@ -724,6 +689,7 @@ impl AsyncComponent for App { glib::Propagation::Stop } }); + if has_tray_icon { root.connect_hide({ let sender = sender.clone(); @@ -822,11 +788,19 @@ impl AsyncComponent for App { self.set_status_bar_contents(StatusBarContents::None); } AppInput::HideWindow => { - root.hide(); + root.set_visible(false); } AppInput::ShowWindow => { root.present(); } + AppInput::ShowHideToggle => { + if root.is_visible() { + root.set_visible(false); + } else { + root.present(); + + } + } AppInput::ShutDown => { self.set_current_view(View::ShuttingDown); // Make sure user sees that shutdown is happening in case it is called from tray @@ -935,6 +909,12 @@ impl App { AppCommandOutput::BackendNotification(notification) => { self.process_backend_notification(notification, sender); } + AppCommandOutput::ShowWindow => { + sender.input(AppInput::ShowWindow); + } + AppCommandOutput::ShowHideToggle => { + sender.input(AppInput::ShowHideToggle); + } AppCommandOutput::Restart => { sender.input(AppInput::Restart); } diff --git a/src/frontend/tray_icon.rs b/src/frontend/tray_icon.rs new file mode 100644 index 00000000..7c28a66f --- /dev/null +++ b/src/frontend/tray_icon.rs @@ -0,0 +1,21 @@ +// TODO: `tray-icon` crate on Linux pulls GTK3 legacy dependency and is undesirable because of it: +// https://github.com/tauri-apps/tray-icon/issues/107 +// If `ksni` works well, we should upstream it into `tray-icon` itself, then we can use a single +// crate again. +#[cfg(all(unix, not(target_os = "macos")))] +mod unix; +#[cfg(any(target_os = "windows", target_os = "macos"))] +mod windows_macos; + +use crate::frontend::ICON; +use image::{ImageBuffer, Rgba}; +#[cfg(all(unix, not(target_os = "macos")))] +pub(super) use unix::spawn; +#[cfg(any(target_os = "windows", target_os = "macos"))] +pub(super) use windows_macos::spawn; + +fn load_icon() -> ImageBuffer, Vec> { + image::load_from_memory_with_format(ICON, image::ImageFormat::Png) + .expect("Statically correct image; qed") + .to_rgba8() +} diff --git a/src/frontend/tray_icon/unix.rs b/src/frontend/tray_icon/unix.rs new file mode 100644 index 00000000..154a1e65 --- /dev/null +++ b/src/frontend/tray_icon/unix.rs @@ -0,0 +1,109 @@ +use crate::frontend::tray_icon::load_icon; +use crate::frontend::{App, AppCommandOutput, T}; +use futures::channel::oneshot; +use ksni::menu::{MenuItem, StandardItem}; +use ksni::{Icon, ToolTip, Tray, TrayService}; +use relm4::{AsyncComponentSender, Sender}; +use std::any::Any; +use std::cell::RefCell; +use tracing::{error, warn}; + +pub(in super::super) async fn spawn(sender: &AsyncComponentSender) -> Option> { + let (initialized_sender, initialized_receiver) = oneshot::channel(); + + sender.spawn_command(move |sender| { + let icon = TrayIcon { + sender, + initialized: RefCell::new(Some(initialized_sender)), + }; + + let tray_service = TrayService::new(icon); + + if let Err(error) = tray_service.run() { + warn!(%error, "Tray icon error"); + } + }); + + initialized_receiver + .await + .unwrap_or_default() + .then_some(Box::new(())) +} + +pub(in super::super) struct TrayIcon { + sender: Sender, + initialized: RefCell>>, +} + +impl Tray for TrayIcon { + fn id(&self) -> String { + env!("CARGO_PKG_NAME").to_string() + } + + fn title(&self) -> String { + "Space Acres".to_string() + } + + fn icon_pixmap(&self) -> Vec { + let icon_img = load_icon(); + let width = icon_img.width() as i32; + let height = icon_img.height() as i32; + + vec![Icon { + width, + height, + data: icon_img.into_raw(), + }] + } + + fn tool_tip(&self) -> ToolTip { + ToolTip { + title: "Space Acres".to_string(), + ..Default::default() + } + } + + fn menu(&self) -> Vec> { + vec![ + StandardItem { + label: T.tray_icon_open().to_string(), + activate: Box::new(|this: &mut Self| { + if let Err(error) = this.sender.send(AppCommandOutput::ShowWindow) { + error!(?error, "Failed to send tray icon notification"); + } + }), + ..StandardItem::default() + } + .into(), + StandardItem { + label: T.tray_icon_quit().to_string(), + activate: Box::new(|this: &mut Self| { + if let Err(error) = this.sender.send(AppCommandOutput::Quit) { + error!(?error, "Failed to send tray icon notification"); + } + }), + ..StandardItem::default() + } + .into(), + ] + } + + fn watcher_online(&self) { + if let Some(initialized) = self.initialized.borrow_mut().take() { + if let Err(_error) = initialized.send(true) { + warn!("Failed to send initialized notification"); + } + } + } + + fn watcher_offine(&self) -> bool { + warn!("Tray icon not supported on this platform"); + + if let Some(initialized) = self.initialized.borrow_mut().take() { + if let Err(_error) = initialized.send(false) { + warn!("Failed to send initialized notification"); + } + } + false + } +} diff --git a/src/frontend/tray_icon/windows_macos.rs b/src/frontend/tray_icon/windows_macos.rs new file mode 100644 index 00000000..601bb362 --- /dev/null +++ b/src/frontend/tray_icon/windows_macos.rs @@ -0,0 +1,82 @@ +use crate::frontend::tray_icon::load_icon; +use crate::frontend::{App, AppCommandOutput, T}; +use relm4::AsyncComponentSender; +use std::any::Any; +use std::error::Error; +use tracing::warn; +use tray_icon::menu::{Menu, MenuEvent, MenuItem}; +use tray_icon::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent}; + +pub(in super::super) async fn spawn(sender: &AsyncComponentSender) -> Option> { + let init_result: Result> = try { + let icon_img = load_icon(); + let width = icon_img.width(); + let height = icon_img.height(); + let icon = tray_icon::Icon::from_rgba(icon_img.into_raw(), width, height) + .expect("Statically correct image; qed"); + + let menu_open = &MenuItem::new(&*T.tray_icon_open(), true, None); + let menu_open_id = menu_open.id().clone(); + let menu_close = &MenuItem::new(&*T.tray_icon_quit(), true, None); + let menu_close_id = menu_close.id().clone(); + + let menu = Menu::with_items(&[menu_open, menu_close]) + .map_err(|error| format!("Failed to create tray icon menu: {error}"))?; + + let icon = TrayIconBuilder::new() + .with_tooltip("Space Acres") + .with_icon(icon) + .with_menu(Box::new(menu)) + .build() + .map_err(|error| format!("Failed to create tray icon: {error}"))?; + + let menu_events = MenuEvent::receiver(); + sender.spawn_command(move |sender| { + while let Ok(event) = menu_events.recv() { + let output = if event.id == menu_open_id { + AppCommandOutput::ShowWindow + } else if event.id == menu_close_id { + AppCommandOutput::Quit + } else { + continue; + }; + + if let Err(error) = sender.send(output) { + warn!(?error, "Failed to send tray icon notification"); + break; + } + } + }); + + let tray_icon_events = TrayIconEvent::receiver(); + sender.spawn_command(move |sender| { + while let Ok(event) = tray_icon_events.recv() { + let output = if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + AppCommandOutput::ShowHideToggle + } else { + continue; + }; + + if let Err(error) = sender.send(output) { + warn!(?error, "Failed to send tray icon notification"); + break; + } + } + }); + + icon + }; + + match init_result { + Ok(tray_icon) => Some(Box::new(tray_icon)), + Err(error) => { + warn!(%error, "Tray icon error"); + None + } + } +}