From 850c695f9157ea30d4fd7a56ed787872801039ab Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Sat, 19 Sep 2026 01:19:34 +0200 Subject: [PATCH] feat: system tray and process state controls (#687) --- .github/workflows/oneclient_nightly.yml | 1 + .github/workflows/oneclient_release.yml | 1 + Cargo.lock | 655 +++++++++++++++++- Cargo.toml | 2 +- distribution/aur/oneclient-bin/PKGBUILD | 2 + distribution/aur/oneclient/PKGBUILD | 2 + distribution/copr/oneclient-bin.spec | 2 + packages/oneclient_app/icons/tray.png | Bin 0 -> 2400 bytes .../src/components/control_center.rs | 13 +- .../src/components/navbar/mod.rs | 7 +- packages/oneclient_app/src/events.rs | 17 +- packages/oneclient_app/src/ipc.rs | 55 +- .../oneclient_app/src/layout/ipc_commands.rs | 46 ++ packages/oneclient_app/src/layout/mod.rs | 2 + .../oneclient_app/src/layout/root_layout.rs | 3 +- packages/oneclient_app/src/main.rs | 34 +- packages/oneclient_app/src/platform.rs | 223 +++++- packages/oneclient_app/src/state.rs | 7 + .../src/view/app/settings/launcher.rs | 67 +- packages/oneclient_app/src/view/relocating.rs | 7 +- packages/oneclient_core/src/lib.rs | 1 + .../oneclient_core/src/settings/launcher.rs | 32 + packages/oneclient_core/src/settings/mod.rs | 2 +- packages/oneclient_core/src/state.rs | 6 + 24 files changed, 1087 insertions(+), 100 deletions(-) create mode 100644 packages/oneclient_app/icons/tray.png create mode 100644 packages/oneclient_app/src/layout/ipc_commands.rs diff --git a/.github/workflows/oneclient_nightly.yml b/.github/workflows/oneclient_nightly.yml index 657cbb10..132b9586 100644 --- a/.github/workflows/oneclient_nightly.yml +++ b/.github/workflows/oneclient_nightly.yml @@ -85,6 +85,7 @@ jobs: libfontconfig-dev libfreetype-dev \ libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \ libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \ + libgtk-3-dev libayatana-appindicator3-dev libxdo-dev \ libfuse2 - name: Install cargo-packager diff --git a/.github/workflows/oneclient_release.yml b/.github/workflows/oneclient_release.yml index fec605a1..bc8e79f5 100644 --- a/.github/workflows/oneclient_release.yml +++ b/.github/workflows/oneclient_release.yml @@ -216,6 +216,7 @@ jobs: libfontconfig-dev libfreetype-dev \ libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \ libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \ + libgtk-3-dev libayatana-appindicator3-dev libxdo-dev \ libfuse2 - name: Install cargo-packager diff --git a/Cargo.lock b/Cargo.lock index 09a26f59..dfda341d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,7 +482,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -721,6 +721,29 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atoi" version = "2.0.0" @@ -1083,6 +1106,31 @@ dependencies = [ "bytes 1.11.1", ] +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "calloop" version = "0.13.0" @@ -1143,7 +1191,7 @@ dependencies = [ "base64 0.22.1", "cargo-packager-utils", "ctor 0.2.9", - "dirs", + "dirs 5.0.1", "dunce", "flate2", "http 1.5.0", @@ -1191,6 +1239,16 @@ dependencies = [ "nom 7.1.3", ] +[[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" @@ -1245,7 +1303,7 @@ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1479,6 +1537,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -1669,6 +1736,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -1743,7 +1819,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1995,6 +2071,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "filetime" version = "0.2.29" @@ -2125,6 +2211,7 @@ dependencies = [ "freya-sdk", "freya-winit", "torin", + "tray-icon", ] [[package]] @@ -2202,7 +2289,7 @@ dependencies = [ "generational-box", "hotpath", "itertools 0.14.0", - "keyboard-types", + "keyboard-types 0.8.3", "mundy", "paste", "pathgraph", @@ -2222,7 +2309,7 @@ dependencies = [ "bitflags 2.13.1", "freya-clipboard", "freya-core", - "keyboard-types", + "keyboard-types 0.8.3", "ropey", "torin", "unicode-segmentation", @@ -2314,12 +2401,12 @@ dependencies = [ "bindgen", "cc", "flate2", - "heck", + "heck 0.5.0", "pkg-config", "regex", "serde_json", "tar", - "toml", + "toml 1.1.4+spec-1.1.0", ] [[package]] @@ -2381,9 +2468,10 @@ dependencies = [ "gl", "glutin", "glutin-winit", + "gtk", "hotpath", "image", - "keyboard-types", + "keyboard-types 0.8.3", "mundy", "objc2 0.6.4", "objc2-app-kit 0.3.2", @@ -2398,6 +2486,7 @@ dependencies = [ "softbuffer", "torin", "tracing", + "tray-icon", "windows-sys 0.60.2", "winit", ] @@ -2520,6 +2609,64 @@ dependencies = [ "tokio-io", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generational-box" version = "0.7.10" @@ -2614,6 +2761,38 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + [[package]] name = "gl" version = "0.14.0" @@ -2634,6 +2813,53 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.4" @@ -2666,7 +2892,7 @@ dependencies = [ "glutin_egl_sys", "glutin_glx_sys", "glutin_wgl_sys", - "libloading", + "libloading 0.8.9", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", @@ -2719,6 +2945,69 @@ dependencies = [ "gl_generator", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "h2" version = "0.4.15" @@ -2784,6 +3073,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -3136,7 +3431,7 @@ dependencies = [ "image-webp", "moxcms", "num-traits", - "png", + "png 0.18.1", "qoi", "ravif", "rayon", @@ -3415,6 +3710,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + [[package]] name = "keyboard-types" version = "0.8.3" @@ -3469,6 +3775,30 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[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 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.189" @@ -3509,6 +3839,16 @@ dependencies = [ "cc", ] +[[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 = "libloading" version = "0.8.9" @@ -3557,6 +3897,25 @@ dependencies = [ "vcpkg", ] +[[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 = "linux-raw-sys" version = "0.4.15" @@ -3781,6 +4140,27 @@ dependencies = [ "pxfm", ] +[[package]] +name = "muda" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types 0.7.0", + "libxdo", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.17.16", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + [[package]] name = "mundy" version = "0.2.3" @@ -4015,7 +4395,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -4606,7 +4986,7 @@ dependencies = [ "sqlx", "thiserror 2.0.18", "tokio", - "toml", + "toml 1.1.4+spec-1.1.0", "tracing", "url", "uuid 1.23.1", @@ -4890,6 +5270,31 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking" version = "2.2.1" @@ -5055,6 +5460,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "png" version = "0.18.1" @@ -5161,13 +5579,57 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -6226,6 +6688,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -6580,7 +7051,7 @@ dependencies = [ "cfg-if", "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "proc-macro2", "quote", @@ -6763,7 +7234,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -6775,6 +7246,16 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -6853,6 +7334,19 @@ 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 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + [[package]] name = "tar" version = "0.4.46" @@ -6864,6 +7358,12 @@ dependencies = [ "xattr", ] +[[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.27.0" @@ -7012,7 +7512,7 @@ checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" dependencies = [ "as-raw-xcb-connection", "ctor 0.10.1", - "libloading", + "libloading 0.8.9", "pkg-config", "tracing", ] @@ -7120,6 +7620,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "toml" version = "1.1.4+spec-1.1.0" @@ -7128,11 +7640,20 @@ checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", ] [[package]] @@ -7144,6 +7665,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.25.13+spec-1.1.0" @@ -7151,9 +7696,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.4", ] [[package]] @@ -7162,7 +7707,7 @@ version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow", + "winnow 1.0.4", ] [[package]] @@ -7336,6 +7881,27 @@ dependencies = [ "windows 0.56.0", ] +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.17.16", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + [[package]] name = "tree_magic_mini" version = "3.2.2" @@ -7574,6 +8140,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -8380,6 +8952,15 @@ dependencies = [ "xkbcommon-dl", ] +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "1.0.4" @@ -8395,7 +8976,7 @@ version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" dependencies = [ - "toml", + "toml 1.1.4+spec-1.1.0", "version_check", ] @@ -8429,6 +9010,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[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 = "x11-clipboard" version = "0.9.3" @@ -8459,7 +9050,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading", + "libloading 0.8.9", "once_cell", "rustix 1.1.4", "x11rb-protocol", @@ -8580,7 +9171,7 @@ dependencies = [ "uds_windows", "uuid 1.23.1", "windows-sys 0.61.2", - "winnow", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -8616,7 +9207,7 @@ version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8632,7 +9223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", - "winnow", + "winnow 1.0.4", "zvariant", ] @@ -8643,7 +9234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" dependencies = [ "serde", - "winnow", + "winnow 1.0.4", "zbus_names", "zvariant", ] @@ -8795,7 +9386,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow", + "winnow 1.0.4", "zvariant_derive", "zvariant_utils", ] @@ -8806,7 +9397,7 @@ version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8823,5 +9414,5 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow", + "winnow 1.0.4", ] diff --git a/Cargo.toml b/Cargo.toml index 9640b86a..49a60566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ syn = {version = "=2.0.117"} thiserror = {version = "=2.0.18"} # ui -freya = {git = "https://github.com/marc2332/freya", branch = "fix/skip-basic-render-driver", features = ["engine", "winit", "router", "remote-asset", "sdk", "query", "radio", "titlebar"]} +freya = {git = "https://github.com/marc2332/freya", branch = "fix/skip-basic-render-driver", features = ["engine", "winit", "router", "remote-asset", "sdk", "query", "radio", "titlebar", "tray"]} freya-core = {git = "https://github.com/marc2332/freya", branch = "fix/skip-basic-render-driver"} freya-testing = {git = "https://github.com/marc2332/freya", branch = "fix/skip-basic-render-driver"} freya-skia-safe = "=0.100.0" # required for the SKSL player renderer diff --git a/distribution/aur/oneclient-bin/PKGBUILD b/distribution/aur/oneclient-bin/PKGBUILD index 93a4c87c..d0e7bab7 100644 --- a/distribution/aur/oneclient-bin/PKGBUILD +++ b/distribution/aur/oneclient-bin/PKGBUILD @@ -18,6 +18,8 @@ depends=( 'wayland' 'dbus' 'gtk3' + 'libayatana-appindicator' + 'xdotool' ) provides=('oneclient') conflicts=('oneclient') diff --git a/distribution/aur/oneclient/PKGBUILD b/distribution/aur/oneclient/PKGBUILD index 7d93ea25..94cb371b 100644 --- a/distribution/aur/oneclient/PKGBUILD +++ b/distribution/aur/oneclient/PKGBUILD @@ -17,6 +17,8 @@ depends=( 'wayland' 'dbus' 'gtk3' + 'libayatana-appindicator' + 'xdotool' ) makedepends=( 'rust' diff --git a/distribution/copr/oneclient-bin.spec b/distribution/copr/oneclient-bin.spec index 4edf422c..e85db99d 100644 --- a/distribution/copr/oneclient-bin.spec +++ b/distribution/copr/oneclient-bin.spec @@ -15,6 +15,8 @@ Source0: %{url}/releases/download/oneclient-%{version}/%{_rpmfile} ExclusiveArch: x86_64 BuildRequires: bsdtar Requires: gtk3 +Requires: libayatana-appindicator-gtk3 +Requires: libxdo Requires: dbus-libs Provides: oneclient = %{version}-%{release} diff --git a/packages/oneclient_app/icons/tray.png b/packages/oneclient_app/icons/tray.png new file mode 100644 index 0000000000000000000000000000000000000000..bd7ede5e29af725895b7bd7fd408c92ba2e7d700 GIT binary patch literal 2400 zcmV-m37__fP)Px#IAvH#W=%~1DgXcg2mk?xX#fNO00031000^Q000000-yo_1ONa40RR91K%fHv z1ONa40RR91KmY&$0A-nAiU0rz)=5M`RCod9n`f*QMHI(*B7%y@0~^>L2~tE+u@g+d z*b)h%Lc%BG2V>+L)<}Y4O;jX-C^1nJ^ozz1CSufpkw`JtNE1;J^$GTd6cLr;?|;2} zGk5pQ?CkEP#gqKs&YU^te`aQ9cgo(^rOW?Z+FIHE-Me@9gzt24Ea(Le0*QRp*4DN- z$%L-(459o2$7UDlE#OD63ZAV_*}hFu*>-R%cpvNpK6xlzQw-ZcZ$m%UM}l54J^;6Y zzG>gRmM8cjU^>_gVq{XfayT6aCK&n?VhFL>0`3Fd(;Ry=PryOoBTz(!rYnW>Ibcd! zuRQu+3y^<|tsa}g;y$BM&ICFi!*{@GQ4aqZZRfJqus;!Cv5v{%z_u(o^nL z7wP~441(!Z(8tSY7p&&RwbLkSRR@T=9lSyvGb7E2+fYS!0K|vj^&%+q;_=jjozk%XYH%_zVT~RjT-!0QJ6zCT?+`p67 zo#Rp{IVVFLQb0Wv{8K1|ZLHZNBbfFEnyOs6Bx8sIgYm9W$+V{0_%#g1^GgPD-L{c2 z-|pyda1HC}xt(u(Kcgc`XfIx*5_e?C>k3F#8bp7hJKFL=KML$Je2S{B zw)h)5*tGguTa?wXe;u7U{h+_fuuoJEH!71#V4!_}Rrvw}|ALM}ow8JwWf15%EF8|VcpnQk#j<$pUPa&241kid=^j5DBeVxy;}U6| zB4w7|s@&5h$PyP#W0QgRkd-n$$FVcZ&Av{M9-d3UI#5=gZQ_Sz1PC2y`T~7P!JQy@ zKtg#3_#VWHI?9|DlZsQW_tkaaJy7%oDf{!pRl8$kmXaQokbAD1(Q8+y^||) z;~EsWHQW}1Q-X%sLLGfyMhqRl1%(*LU@#h-4bG_JIIbaim%@>HrmiCKl4H4>bSF?l zlg8?*U7gCJ1#UHZ0T>YN)NJ?%*=va_jzUuJl%zAx#93+WIo#Q4qB=m!L@ig72J6Fv z?sg@qcdVYZBP$Vg>9P}lokl%e^aiBkQYW1g3>$uQFbSB8uGkK$%>p4~0?0;d zbZu+|t<(kq?XEPH^Z@%(^p02#G&AW8u@h{lf#%>Oy^ZvNDoMMN_9R!et=6vGM?fuSgdEPr9UXcE z^g953i>o@#K~YJzgC=6l7=SN0q9hg$b?!3hnVf~UwEwn>I`+xVx_5Ea6Fw#NPi4LWy(!W&Z!I@ zI$7Redd4ZANm14+*os5ZH;Lb#A+IYa)A3oJ%nSJIBgyPMf}C>uP*g#2Y{jv-hGZYB z5$F#FD}hh8QgFP-SB-zc=U700r^R2>LDH%ZBa6jl1wwyCptAQ{KGNZ%1yEH9e;BC; zYSpK)y915+&}fO?BfgPyv0-0EHQraJCOynrbyXEDTQ%=y1pV~}`I8w#6xeCM(TdM? zZ&>`Mj(|f!>_-ety)xShfb|hTpTS%i?XjKc8kJ1z1S$UWxGI9ftdhZ8H~o;>+^wO1 z%`w!kIFhr8rhY)b(6K9>=JDk?M?6ElKG;dTu9%QxfBnS8Yx#(P5>wx{|N|yJY_Qd^3g~#T435 zXa{TZ@I>hs*j1$6B8ITDPO6jVj8L|^LeO~`!mLb z2Piug>%}=R&1vO>k3izUF#3x0j3i&;>Kn>Bz5TBKsHJIje&F3*rj1kinQ*R4>C9|Y z2h=?u;JVsm$ybItK%HQT&&RTLb)S&A*Hwhl(H%gK>MP3x;GI$efqaem1|Pu^pO0mm z=r)qj?+J8z6(!LfAU23KbzKR5Q@(%<h(eH8Y0kNN-tRZc70Ha0c z|Ghz;Y{lSyFoHVy7E^f~)yk=CA8;$k``6h=tVsABk=COd2YjMmvwZ+`det}WTQuKR z)FEU~h4dpy4c%ivFW`=_PVUkobh!f0P>jxZ40n;%NYe+K{p+8nok|{%$$tSf0n?PJ SK;%0B0000 Self { - self.enabled = false; - self - } - fn on_press(mut self, on_press: impl Into>>) -> Self { self.on_press = Some(on_press.into()); self diff --git a/packages/oneclient_app/src/components/navbar/mod.rs b/packages/oneclient_app/src/components/navbar/mod.rs index 5fba5620..9dde7d8a 100644 --- a/packages/oneclient_app/src/components/navbar/mod.rs +++ b/packages/oneclient_app/src/components/navbar/mod.rs @@ -29,12 +29,7 @@ pub fn window_controls() -> impl IntoElement { }); }; - let close = |_| { - let platform = Platform::get(); - Platform::get().with_window(Platform::window_id(), move |window| { - platform.close_window(window.id()); - }); - }; + let close = |_| crate::platform::close(); rect() .horizontal() diff --git a/packages/oneclient_app/src/events.rs b/packages/oneclient_app/src/events.rs index d21b5c6e..1565cb25 100644 --- a/packages/oneclient_app/src/events.rs +++ b/packages/oneclient_app/src/events.rs @@ -30,6 +30,8 @@ impl EventPump { // Hovering any toast pauses every toast including ones arriving while hovering let mut paused = false; let mut game_flush: Option = None; + let mut launched: HashSet = HashSet::new(); + let mut playing: Option = None; loop { let next_deadline = armed @@ -96,6 +98,10 @@ impl EventPump { if folded.saw_game_log && game_flush.is_none() { game_flush = Some(tokio::time::Instant::now() + GAME_LOG_FLUSH); } + if folded.saw_stage { + launched.extend(&folded.launched); + playing = crate::platform::follow_game(&self.station, &launched, playing); + } } signal = self.signals.recv() => { @@ -139,6 +145,10 @@ impl EventPump { folded.clusters = true; } Event::Game(GameEvent::Stage { cluster_id, stage }) => { + folded.saw_stage = true; + if stage.is_busy() { + folded.launched.push(cluster_id); + } stages.push((cluster_id, stage)); } Event::Game(GameEvent::Log { cluster_id, line }) => { @@ -148,7 +158,10 @@ impl EventPump { Event::Game(GameEvent::Failed { cluster_id, message, - }) => failed = Some((cluster_id, message)), + }) => { + folded.saw_stage = true; + failed = Some((cluster_id, message)); + } Event::Progress(ProgressEvent::Update { id, ref label, @@ -249,6 +262,8 @@ impl EventPump { struct Folded { touched_engine: bool, saw_game_log: bool, + saw_stage: bool, + launched: Vec, clusters: bool, java: bool, } diff --git a/packages/oneclient_app/src/ipc.rs b/packages/oneclient_app/src/ipc.rs index b03b02b7..11772cf8 100644 --- a/packages/oneclient_app/src/ipc.rs +++ b/packages/oneclient_app/src/ipc.rs @@ -1,6 +1,8 @@ +use std::sync::{Mutex, OnceLock}; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; use crate::cli::Cli; @@ -12,6 +14,10 @@ const REPLY_OK: &str = "OK"; pub enum IpcCommand { Launch(String), Focus, + Close, + Logs, + Stop, + Quit, } pub enum Claim { @@ -25,6 +31,10 @@ impl IpcCommand { match self { Self::Launch(folder) => format!("LAUNCH {folder}"), Self::Focus => "FOCUS".to_string(), + Self::Close => "CLOSE".to_string(), + Self::Logs => "LOGS".to_string(), + Self::Stop => "STOP".to_string(), + Self::Quit => "QUIT".to_string(), } } @@ -34,7 +44,14 @@ impl IpcCommand { let folder = folder.trim(); return (!folder.is_empty()).then(|| Self::Launch(folder.to_string())); } - (line == "FOCUS").then_some(Self::Focus) + match line { + "FOCUS" => Some(Self::Focus), + "CLOSE" => Some(Self::Close), + "LOGS" => Some(Self::Logs), + "STOP" => Some(Self::Stop), + "QUIT" => Some(Self::Quit), + _ => None, + } } } @@ -68,15 +85,31 @@ async fn forward(request: &IpcCommand) -> bool { imp::send(&request.encode()).await } -/// The accept loop runs off the caller's thread so a stalled client cannot hold -/// up the next shortcut launch; `on_command` still runs here -pub async fn serve(listener: Listener, on_command: impl Fn(IpcCommand)) { - let (commands, mut incoming) = tokio::sync::mpsc::unbounded_channel(); - tokio::spawn(imp::serve(listener, commands)); +static QUEUE: OnceLock> = OnceLock::new(); +static INBOX: Mutex>> = Mutex::new(None); - while let Some(command) = incoming.recv().await { - on_command(command); - } +fn queue() -> &'static UnboundedSender { + QUEUE.get_or_init(|| { + let (commands, incoming) = unbounded_channel(); + *INBOX.lock().unwrap() = Some(incoming); + commands + }) +} + +pub fn send(command: IpcCommand) { + let _ = queue().send(command); +} + +/// The accept loop runs off the UI thread so a stalled client cannot hold up +/// the next shortcut launch +pub fn listen(listener: Listener) { + tokio::spawn(imp::serve(listener, queue().clone())); +} + +#[must_use] +pub fn take_inbox() -> Option> { + queue(); + INBOX.lock().unwrap().take() } enum BindError { @@ -87,7 +120,7 @@ enum BindError { Io(std::io::Error), } -async fn handle(stream: S, commands: &tokio::sync::mpsc::UnboundedSender) +async fn handle(stream: S, commands: &UnboundedSender) where S: AsyncRead + AsyncWrite + Unpin, { @@ -315,7 +348,7 @@ mod tests { assert_eq!(IpcCommand::decode(""), None); assert_eq!(IpcCommand::decode("LAUNCH"), None); assert_eq!(IpcCommand::decode("LAUNCH "), None); - assert_eq!(IpcCommand::decode("QUIT"), None); + assert_eq!(IpcCommand::decode("RESTART"), None); } #[test] diff --git a/packages/oneclient_app/src/layout/ipc_commands.rs b/packages/oneclient_app/src/layout/ipc_commands.rs new file mode 100644 index 00000000..bcf3672d --- /dev/null +++ b/packages/oneclient_app/src/layout/ipc_commands.rs @@ -0,0 +1,46 @@ +use freya::prelude::*; +use freya::router::*; + +use crate::hooks::use_dispatch; +use crate::ipc::{self, IpcCommand}; +use crate::platform; +use crate::routes::Route; + +pub fn use_ipc_commands() { + let dispatch = use_dispatch(); + + use_hook(move || { + let Some(mut inbox) = ipc::take_inbox() else { + return; + }; + let router = RouterContext::get(); + + spawn_forever(async move { + while let Some(command) = inbox.recv().await { + match command { + IpcCommand::Focus => platform::focus_window(), + IpcCommand::Launch(folder) => { + platform::focus_window(); + dispatch.request_launch_by_folder(folder); + } + IpcCommand::Logs => { + platform::focus_window(); + let running = dispatch.station().peek().game.running_clusters().min(); + if let Some(cluster_id) = running { + let _ = router.push(Route::ProcessLogs { cluster_id }); + } + } + IpcCommand::Stop => { + let running: Vec = + dispatch.station().peek().game.running_clusters().collect(); + for cluster_id in running { + dispatch.kill_cluster(cluster_id); + } + } + IpcCommand::Close => platform::close(), + IpcCommand::Quit => platform::quit(), + } + } + }); + }); +} diff --git a/packages/oneclient_app/src/layout/mod.rs b/packages/oneclient_app/src/layout/mod.rs index 7f2cb598..638ab43a 100644 --- a/packages/oneclient_app/src/layout/mod.rs +++ b/packages/oneclient_app/src/layout/mod.rs @@ -1,6 +1,7 @@ mod animated_outlet; mod app_shell; mod cluster_shell; +mod ipc_commands; mod onboarding_shell; mod pending_launch; mod root_layout; @@ -10,6 +11,7 @@ pub use animated_outlet::AnimatedAppOutlet; pub(crate) use app_shell::gradient_overlay_radial; pub use app_shell::{AppShell, HOME_BACKGROUND_ASSET, HomeArtPrefetch}; pub use cluster_shell::{ClusterShell, cluster_content}; +pub use ipc_commands::use_ipc_commands; pub use onboarding_shell::OnboardingShell; pub use pending_launch::PendingLaunchDriver; pub use root_layout::RootLayout; diff --git a/packages/oneclient_app/src/layout/root_layout.rs b/packages/oneclient_app/src/layout/root_layout.rs index e1dffcd8..011d1101 100644 --- a/packages/oneclient_app/src/layout/root_layout.rs +++ b/packages/oneclient_app/src/layout/root_layout.rs @@ -9,7 +9,7 @@ use crate::components::{ #[cfg(not(target_os = "macos"))] use crate::hooks::use_start_maximized; use crate::hooks::{SplashState, use_provide_overlay_claims, use_provide_splash}; -use crate::layout::{HomeArtPrefetch, PendingLaunchDriver}; +use crate::layout::{HomeArtPrefetch, PendingLaunchDriver, use_ipc_commands}; use crate::motion::AnimationClockDriver; use crate::routes::Route; use crate::theme; @@ -25,6 +25,7 @@ impl Component for RootLayout { use_provide_splash(SplashState { active, home_ready }); use_provide_overlay_claims(); use_provide_tooltips(); + use_ipc_commands(); rect() .width(Size::fill()) diff --git a/packages/oneclient_app/src/main.rs b/packages/oneclient_app/src/main.rs index c1a6ecf5..19a9588e 100644 --- a/packages/oneclient_app/src/main.rs +++ b/packages/oneclient_app/src/main.rs @@ -20,7 +20,6 @@ struct OneClientApp { needs_location: bool, start_maximized: bool, boot_launch: Cell>, - ipc: Cell>, } impl App for OneClientApp { @@ -37,7 +36,6 @@ impl App for OneClientApp { }); let boot_launch = self.boot_launch.take(); - let ipc_listener = self.ipc.take(); let actions = use_hook(move || { let (signals_tx, signals_rx) = tokio::sync::mpsc::unbounded_channel(); @@ -76,17 +74,6 @@ impl App for OneClientApp { actions.request_launch_by_folder(folder); } - if let Some(listener) = ipc_listener { - let served = actions.clone(); - spawn_forever(ipc::serve(listener, move |command| match command { - ipc::IpcCommand::Launch(folder) => { - platform::focus_window(); - served.request_launch_by_folder(folder); - } - ipc::IpcCommand::Focus => platform::focus_window(), - })); - } - actions }); @@ -131,14 +118,11 @@ fn main() { let needs_location = never_set_up && !has_database && !was_damaged; let mut unprotected = None; - let ipc = match rt.block_on(ipc::claim(&cli)) { + match rt.block_on(ipc::claim(&cli)) { Claim::Forwarded => return, - Claim::Primary(listener) => Some(listener), - Claim::Solo(reason) => { - unprotected = Some(reason); - None - } - }; + Claim::Primary(listener) => ipc::listen(listener), + Claim::Solo(reason) => unprotected = Some(reason), + } let settings = rt.block_on(oneclient_core::settings::store::load_settings(None)); @@ -189,12 +173,12 @@ fn main() { oneclient_app::platform::macos::loop_memory_collector(); let start_maximized = settings.start_maximized; + let show_tray_icon = settings.show_tray_icon; let window_config = WindowConfig::new_app(OneClientApp { needs_location, start_maximized, boot_launch: Cell::new(cli.launch), - ipc: Cell::new(ipc), }) .with_title(constants::WINDOW_TITLE) .with_app_id(constants::WINDOW_APP_ID) @@ -210,10 +194,10 @@ fn main() { .with_on_close(|_, _| { if oneclient_core::relocate::in_progress() { tracing::warn!("close request ignored, the data folder is still being moved"); - CloseDecision::KeepOpen } else { - CloseDecision::Close + ipc::send(ipc::IpcCommand::Close); } + CloseDecision::KeepOpen }); #[cfg(target_os = "macos")] @@ -246,6 +230,10 @@ fn main() { ) .with_default_font(theme::DEFAULT_FONT); + if show_tray_icon { + launch_config = launch_config.with_tray(platform::tray::build, platform::tray::handle); + } + for (font, bytes) in theme::load_fonts() { launch_config = launch_config.with_font(font, bytes); } diff --git a/packages/oneclient_app/src/platform.rs b/packages/oneclient_app/src/platform.rs index f36267dc..0799df15 100644 --- a/packages/oneclient_app/src/platform.rs +++ b/packages/oneclient_app/src/platform.rs @@ -1,14 +1,89 @@ +use std::collections::HashSet; + +use freya::prelude::{Platform, WinitPlatformExt, spawn_forever}; +use freya::radio::RadioStation; +use oneclient_core::settings::LaunchBehaviour; + +use crate::state::{AppChannel, AppState}; + const ALLOWED_URL_SCHEMES: [&str; 3] = ["http", "https", "mailto"]; pub fn focus_window() { - use freya::prelude::{Platform, WinitPlatformExt}; - Platform::get().with_window(Platform::window_id(), |win| { + #[cfg(target_os = "macos")] + macos::set_menu_bar_only(false); + + win.set_visible(true); win.set_minimized(false); win.focus_window(); }); } +pub fn hide_window() { + Platform::get().with_window(Platform::window_id(), |win| { + #[cfg(target_os = "macos")] + if !tray::is_active() { + win.set_minimized(true); + return; + } + + win.set_visible(false); + + #[cfg(target_os = "macos")] + macos::set_menu_bar_only(true); + }); +} + +pub fn close() { + let background = + crate::launcher::state().is_ok_and(|state| state.settings.read().run_in_background); + if background { hide_window() } else { quit() } +} + +pub fn quit() { + let platform = Platform::get(); + spawn_forever(async move { + if let Ok(state) = crate::launcher::state() { + oneclient_core::shutdown(&state).await; + } + drop(platform.post_callback(|_, ctx| ctx.exit())); + }); +} + +pub fn follow_game( + station: &RadioStation, + launched: &HashSet, + previous: Option, +) -> Option { + let (current, any, behaviour) = { + let state = station.peek(); + ( + state + .game + .running_clusters() + .filter(|id| launched.contains(id)) + .min(), + state.game.running_clusters().next().is_some(), + state.settings.settings.launch_behaviour, + ) + }; + + tray::set_game_running(any); + + if current == previous { + return previous; + } + + match (behaviour, current.is_some()) { + (LaunchBehaviour::HideWhilePlaying, true) => hide_window(), + (LaunchBehaviour::HideWhilePlaying, false) => focus_window(), + (LaunchBehaviour::CloseLauncher, true) => quit(), + _ => {} + } + + current +} + pub fn open_url(url: &str) { if !has_allowed_scheme(url) { tracing::warn!("refused to open url with a disallowed scheme: {url}"); @@ -65,6 +140,115 @@ pub fn copy_image_to_clipboard(path: std::path::PathBuf) { }); } +pub mod tray { + use std::cell::RefCell; + use std::sync::atomic::{AtomicBool, Ordering}; + + use freya::prelude::{LaunchConfig, RendererContext}; + use freya::tray::menu::{Menu, MenuItem, PredefinedMenuItem}; + use freya::tray::{TrayEvent, TrayIcon, TrayIconBuilder}; + + use crate::constants; + use crate::ipc::{self, IpcCommand}; + + const OPEN_ID: &str = "oneclient.open"; + const PLAY_ID: &str = "oneclient.play"; + const STOP_ID: &str = "oneclient.stop"; + const LOGS_ID: &str = "oneclient.logs"; + const QUIT_ID: &str = "oneclient.quit"; + + #[cfg(target_os = "macos")] + const ICON: &[u8] = include_bytes!("../icons/tray.png"); + #[cfg(not(target_os = "macos"))] + const ICON: &[u8] = include_bytes!("../icons/64x64.png"); + + static ACTIVE: AtomicBool = AtomicBool::new(false); + + thread_local! { + static GAME_ITEMS: RefCell> = const { RefCell::new(Vec::new()) }; + } + + #[cfg_attr(not(target_os = "macos"), allow(dead_code))] + pub(super) fn is_active() -> bool { + ACTIVE.load(Ordering::Relaxed) + } + + pub(super) fn set_game_running(running: bool) { + GAME_ITEMS.with_borrow(|items| { + for item in items { + item.set_enabled(running); + } + }); + } + + pub fn build() -> TrayIcon { + let live_on_linux = cfg!(target_os = "linux"); + let stop = MenuItem::with_id(STOP_ID, "Stop game", live_on_linux, None); + let logs = MenuItem::with_id(LOGS_ID, "Show logs", live_on_linux, None); + + let menu = Menu::with_items(&[ + &MenuItem::with_id(OPEN_ID, "Open OneClient", true, None), + &MenuItem::with_id(PLAY_ID, "Play last version", true, None), + &PredefinedMenuItem::separator(), + &stop, + &logs, + &PredefinedMenuItem::separator(), + &MenuItem::with_id(QUIT_ID, "Quit OneClient", true, None), + ]) + .expect("failed to build the tray menu"); + + GAME_ITEMS.with_borrow_mut(|items| *items = vec![stop, logs]); + ACTIVE.store(true, Ordering::Relaxed); + + TrayIconBuilder::new() + .with_menu(Box::new(menu)) + .with_tooltip(constants::WINDOW_TITLE) + .with_icon(LaunchConfig::tray_icon(ICON)) + .with_icon_as_template(cfg!(target_os = "macos")) + .with_menu_on_left_click(!cfg!(target_os = "windows")) + .build() + .expect("failed to create the tray icon") + } + + pub fn handle(event: TrayEvent, _ctx: RendererContext<'_>) { + let command = match event { + TrayEvent::Menu(menu) => match menu.id.0.as_str() { + OPEN_ID => IpcCommand::Focus, + PLAY_ID => return play_last(), + STOP_ID => IpcCommand::Stop, + LOGS_ID => IpcCommand::Logs, + QUIT_ID => IpcCommand::Quit, + _ => return, + }, + #[cfg(target_os = "windows")] + TrayEvent::Icon(freya::tray::TrayIconEvent::Click { + button: freya::tray::MouseButton::Left, + button_state: freya::tray::MouseButtonState::Up, + .. + }) => IpcCommand::Focus, + TrayEvent::Icon(_) => return, + }; + ipc::send(command); + } + + fn play_last() { + tokio::spawn(async { + let Ok(state) = crate::launcher::state() else { + return; + }; + let Ok(clusters) = state.clusters.list().await else { + return; + }; + if let Some(cluster) = crate::utils::sort_clusters_for_home(clusters) + .into_iter() + .next() + { + ipc::send(IpcCommand::Launch(cluster.folder_name)); + } + }); + } +} + #[cfg(target_os = "macos")] pub mod macos { use std::time::Duration; @@ -88,4 +272,39 @@ pub mod macos { malloc_zone_pressure_relief(core::ptr::null_mut(), 0); } } + + pub fn set_menu_bar_only(menu_bar_only: bool) { + const REGULAR: isize = 0; + const ACCESSORY: isize = 1; + + type Id = *mut core::ffi::c_void; + + unsafe { + unsafe extern "C" { + fn objc_getClass(name: *const core::ffi::c_char) -> Id; + fn sel_registerName(name: *const core::ffi::c_char) -> Id; + fn objc_msgSend(); + } + + let class = objc_getClass(c"NSApplication".as_ptr()); + if class.is_null() { + return; + } + + let send: unsafe extern "C" fn() = objc_msgSend; + let shared: unsafe extern "C" fn(Id, Id) -> Id = core::mem::transmute(send); + let app = shared(class, sel_registerName(c"sharedApplication".as_ptr())); + if app.is_null() { + return; + } + + let set_policy: unsafe extern "C" fn(Id, Id, isize) -> bool = + core::mem::transmute(send); + set_policy( + app, + sel_registerName(c"setActivationPolicy:".as_ptr()), + if menu_bar_only { ACCESSORY } else { REGULAR }, + ); + } + } } diff --git a/packages/oneclient_app/src/state.rs b/packages/oneclient_app/src/state.rs index dc04a493..575e1dc3 100644 --- a/packages/oneclient_app/src/state.rs +++ b/packages/oneclient_app/src/state.rs @@ -214,6 +214,13 @@ impl GameState { self.stage(cluster_id).is_some_and(LaunchStage::is_busy) } + pub fn running_clusters(&self) -> impl Iterator + '_ { + self.stages + .iter() + .filter(|(_, stage)| **stage == LaunchStage::Running) + .map(|(id, _)| *id) + } + #[must_use] pub fn is_running(&self, cluster_id: i64) -> bool { self.stage(cluster_id) == Some(LaunchStage::Running) diff --git a/packages/oneclient_app/src/view/app/settings/launcher.rs b/packages/oneclient_app/src/view/app/settings/launcher.rs index 11d0bb1f..119a424a 100644 --- a/packages/oneclient_app/src/view/app/settings/launcher.rs +++ b/packages/oneclient_app/src/view/app/settings/launcher.rs @@ -3,12 +3,14 @@ use std::path::{Path, PathBuf}; use freya::prelude::*; use freya::router::RouterContext; use oneclient_core::relocate::RelocationPlan; -use oneclient_core::settings::data_dir; +use oneclient_core::settings::{LaunchBehaviour, data_dir}; use oneclient_core::storage::format_bytes; use super::{section_header, settings_page, settings_row}; use crate::Route; -use crate::components::{Button, Icon, IconType, OverlayPopup, open_folder_button, toggle}; +use crate::components::{ + Button, Dropdown, Icon, IconType, OverlayPopup, open_folder_button, toggle, +}; use crate::hooks::{ Actions, DiscardLeftoversKeys, mutation_error, mutation_is_running, try_leftovers, use_discard_leftovers, use_dispatch, use_launcher, use_leftovers, use_settings_snapshot, @@ -39,22 +41,36 @@ impl Component for SettingsLauncher { move || v }); + let run_in_background = use_state({ + let v = settings.run_in_background; + move || v + }); + + let show_tray_icon = use_state({ + let v = settings.show_tray_icon; + move || v + }); + let mut first = use_state(|| true); { - let settings = settings.clone(); + let dispatch = dispatch.clone(); use_side_effect(move || { let discord = *discord_rpc.read(); let crash = *crash_reporting.read(); let maximized = *start_maximized.read(); + let background = *run_in_background.read(); + let tray = *show_tray_icon.read(); if *first.peek() { first.set(false); return; } - let mut next = settings.clone(); - next.discord_enabled = discord; - next.crash_reporting = crash; - next.start_maximized = maximized; - dispatch.set_settings(next); + dispatch.edit_settings(|next| { + next.discord_enabled = discord; + next.crash_reporting = crash; + next.start_maximized = maximized; + next.run_in_background = background; + next.show_tray_icon = tray; + }); }); } @@ -98,12 +114,47 @@ impl Component for SettingsLauncher { consent_summary, review_terms, )) + .child(section_header("SYSTEM")) + .child(settings_row( + IconType::Eye, + "While Playing", + "What the launcher window does once a game is running.", + launch_behaviour_field(settings.launch_behaviour, dispatch), + )) + .child(settings_row( + IconType::Moon01, + "Run in Background", + "Closing the window keeps OneClient running. Open it again from the tray icon or by launching it.", + toggle(run_in_background), + )) + .child(settings_row( + IconType::LayoutTop, + "Show Tray Icon", + "Show OneClient in the system tray or menu bar. Applies on restart.", + toggle(show_tray_icon), + )) .child(section_header("FOLDERS AND FILES")) .child(DataFolder.into_element()) .into_element() } } +fn launch_behaviour_field(selected: LaunchBehaviour, dispatch: Actions) -> impl IntoElement { + let options: Vec = LaunchBehaviour::ALL + .iter() + .map(|behaviour| behaviour.label().to_string()) + .collect(); + + Dropdown::new(selected.label(), options) + .width(Size::px(220.)) + .height(Size::px(34.)) + .on_select(move |idx: usize| { + if let Some(behaviour) = LaunchBehaviour::ALL.get(idx).copied() { + dispatch.edit_settings(|settings| settings.launch_behaviour = behaviour); + } + }) +} + #[derive(PartialEq)] struct DataFolder; diff --git a/packages/oneclient_app/src/view/relocating.rs b/packages/oneclient_app/src/view/relocating.rs index 889e84ef..04538a9a 100644 --- a/packages/oneclient_app/src/view/relocating.rs +++ b/packages/oneclient_app/src/view/relocating.rs @@ -114,12 +114,7 @@ fn moved(outcome: &RelocationOutcome, actions: Actions) -> Element { buttons().child(back_button(actions)).child( Button::new() .primary() - .on_press(|_| { - let platform = Platform::get(); - Platform::get().with_window(Platform::window_id(), move |window| { - platform.close_window(window.id()); - }); - }) + .on_press(|_| crate::platform::quit()) .text("Quit OneClient"), ), ) diff --git a/packages/oneclient_core/src/lib.rs b/packages/oneclient_core/src/lib.rs index 4d1b3818..a925a086 100644 --- a/packages/oneclient_core/src/lib.rs +++ b/packages/oneclient_core/src/lib.rs @@ -65,6 +65,7 @@ pub use settings::ProfileUpdate; pub use state::LauncherServices; pub use state::LauncherState; pub use state::run_startup_tasks; +pub use state::shutdown; pub use tos::{TermsDocument, fetch_terms}; pub use verify::{ClusterVerifyReport, verify_cluster_files}; pub use versions::{ diff --git a/packages/oneclient_core/src/settings/launcher.rs b/packages/oneclient_core/src/settings/launcher.rs index 0bc4ae69..9439193d 100644 --- a/packages/oneclient_core/src/settings/launcher.rs +++ b/packages/oneclient_core/src/settings/launcher.rs @@ -13,6 +13,32 @@ pub enum ViewLayout { List, } +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum LaunchBehaviour { + #[default] + KeepVisible, + HideWhilePlaying, + CloseLauncher, +} + +impl LaunchBehaviour { + pub const ALL: [Self; 3] = [ + Self::KeepVisible, + Self::HideWhilePlaying, + Self::CloseLauncher, + ]; + + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::KeepVisible => "Keep visible", + Self::HideWhilePlaying => "Hide while playing", + Self::CloseLauncher => "Close completely", + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(default)] pub struct ViewState { @@ -33,6 +59,9 @@ pub struct LauncherSettings { pub max_concurrent_requests: usize, pub global_game_settings: GameSettingsProfile, pub allow_parallel_running_clusters: bool, + pub launch_behaviour: LaunchBehaviour, + pub run_in_background: bool, + pub show_tray_icon: bool, pub dynamic_background_enabled: bool, pub start_maximized: bool, pub animations_enabled: bool, @@ -75,6 +104,9 @@ impl Default for LauncherSettings { max_concurrent_requests: 25, global_game_settings: GameSettingsProfile::default_global_profile(), allow_parallel_running_clusters: false, + launch_behaviour: LaunchBehaviour::default(), + run_in_background: false, + show_tray_icon: true, dynamic_background_enabled: true, start_maximized: false, animations_enabled: true, diff --git a/packages/oneclient_core/src/settings/mod.rs b/packages/oneclient_core/src/settings/mod.rs index d4ac67d5..5b33b041 100644 --- a/packages/oneclient_core/src/settings/mod.rs +++ b/packages/oneclient_core/src/settings/mod.rs @@ -2,7 +2,7 @@ mod launcher; pub mod data_dir; -pub use launcher::{LauncherSettings, ViewLayout, ViewState}; +pub use launcher::{LaunchBehaviour, LauncherSettings, ViewLayout, ViewState}; pub use oneclient_cluster::{ GameSettingsProfile, PackageUpdateMode, ProfileUpdate, SettingsOsExtra, }; diff --git a/packages/oneclient_core/src/state.rs b/packages/oneclient_core/src/state.rs index 12bff6fb..17357f95 100644 --- a/packages/oneclient_core/src/state.rs +++ b/packages/oneclient_core/src/state.rs @@ -184,3 +184,9 @@ pub fn run_startup_tasks(state: &Arc) { .signal(oneclient_events::Signal::SyncComplete); }); } + +pub async fn shutdown(state: &LauncherState) { + tracing::info!("shutting the launcher down"); + state.discord.shutdown(); + state.services.db.close().await; +}