diff --git a/.gitignore b/.gitignore index 95d8e6f..da861b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /target .vscode/ +.claude/ +CLAUDE.md # Packaging build artifacts packaging/pkg/ diff --git a/Cargo.lock b/Cargo.lock index b93e0eb..19851fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,28 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.4", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.14.0", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -267,6 +289,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" @@ -411,6 +444,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + [[package]] name = "blocking" version = "1.6.2" @@ -614,7 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" dependencies = [ "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -871,7 +913,16 @@ dependencies = [ "rust-ini", "web-sys", "winreg", - "zbus", + "zbus 4.4.0", +] + +[[package]] +name = "dataview" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daba87f72c730b508641c9fb6411fc9bba73939eed2cab611c399500511880d0" +dependencies = [ + "derive_pod", ] [[package]] @@ -911,6 +962,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" +[[package]] +name = "derive_pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -990,6 +1047,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", ] @@ -2525,6 +2584,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "num-traits" version = "0.2.19" @@ -2598,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", @@ -2607,6 +2672,18 @@ dependencies = [ "objc2-quartz-core 0.2.2", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -2614,7 +2691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2626,7 +2703,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2638,7 +2715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2673,7 +2750,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2685,7 +2762,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", "objc2-foundation 0.2.2", @@ -2704,7 +2781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.2", @@ -2738,9 +2815,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] @@ -2751,7 +2828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2763,7 +2840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-metal", @@ -2798,7 +2875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", @@ -2818,7 +2895,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] @@ -2830,7 +2907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", "objc2-foundation 0.2.2", @@ -3036,6 +3113,25 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pelite" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88dccf4bd32294364aeb7bd55d749604450e9db54605887551f21baea7617685" +dependencies = [ + "dataview", + "libc", + "no-std-compat", + "pelite-macros", + "winapi", +] + +[[package]] +name = "pelite-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7cf3f8ecebb0f4895f4892a8be0a0dc81b498f9d56735cb769dc31bf00815b" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3059,7 +3155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.6", ] [[package]] @@ -3166,6 +3262,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -3261,8 +3363,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -3272,7 +3384,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -3284,6 +3406,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "range-alloc" version = "0.1.5" @@ -3427,6 +3558,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -4436,7 +4591,9 @@ dependencies = [ "iced_fonts", "ksni", "owo-colors", + "pelite", "reqwest", + "rfd", "serde", "serde_json", "tokio", @@ -4519,8 +4676,15 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4533,6 +4697,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5232,7 +5407,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.11.1", - "block2", + "block2 0.5.1", "bytemuck", "calloop 0.13.0", "cfg_aliases 0.2.1", @@ -5246,7 +5421,7 @@ dependencies = [ "memmap2", "ndk", "objc2 0.5.2", - "objc2-app-kit", + "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", @@ -5527,7 +5702,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand", + "rand 0.8.6", "serde", "serde_repr", "sha1", @@ -5536,9 +5711,44 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros 5.14.0", + "zbus_names 4.3.1", + "zvariant 5.10.0", ] [[package]] @@ -5551,7 +5761,22 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names 4.3.1", + "zvariant 5.10.0", + "zvariant_utils 3.3.0", ] [[package]] @@ -5562,7 +5787,18 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant 5.10.0", ] [[package]] @@ -5667,7 +5903,22 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.15", + "zvariant_derive 5.10.0", + "zvariant_utils 3.3.0", ] [[package]] @@ -5680,7 +5931,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 3.3.0", ] [[package]] @@ -5693,3 +5957,16 @@ dependencies = [ "quote", "syn 2.0.117", ] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.15", +] diff --git a/Cargo.toml b/Cargo.toml index 5cadc2a..15aa2a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,3 +46,9 @@ reqwest = { version = "0.12", features = ["blocking", "json"] } iced = { version = "0.13", features = ["tokio"] } iced_fonts = { version = "0.1", features = ["bootstrap"] } tokio = { version = "1.52.1", features = ["rt"] } + +# PE exe version info (game name detection) +pelite = "0.10" + +# Native file dialogs via XDG Desktop Portal +rfd = "0.15" diff --git a/README.md b/README.md index 15a7078..8f64300 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Launch / Kill entries. Users can add or remove launchers in `config edit`. (no ~600 MB in-memory buffering), with a progress indicator. - `diagnose` — sanity-checks umu-run, Vulkan, display server, per-launcher prefix / exe / ownership / running state. -- `service` — installs an XDG autostart entry so the tray autostarts with +- `autostart` — manages the XDG autostart entry so the tray starts with the graphical session. - `setup` — graphical wizard (iced) that downloads an installer URL (with progress bar) or accepts a local `.exe`, then runs it via @@ -51,7 +51,7 @@ sudo pacman -S umu-launcher vulkan-tools Then enable autostart: ```sh -umutray service install +umutray autostart install ``` ## Usage @@ -79,9 +79,9 @@ umutray service install | `umutray config add-game …` | Attach a game to a launcher (needs `--exe-path`) | | `umutray config remove-game …` | Drop a game from a launcher | | `umutray config set-game-flags …`| Per-game overlay toggles: gamemode/mangohud/gamescope | -| `umutray service install` | Write XDG autostart entry (tray starts on login) | -| `umutray service uninstall` | Remove the autostart and desktop entries | -| `umutray service status` | Show whether XDG autostart is enabled | +| `umutray autostart install` | Write XDG autostart entry (tray starts on login) | +| `umutray autostart uninstall` | Remove the autostart and desktop entries | +| `umutray autostart status` | Show whether XDG autostart is enabled | ## Config diff --git a/assets/umutray.svg b/assets/umutray.svg new file mode 100644 index 0000000..94d5ac4 --- /dev/null +++ b/assets/umutray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index 050bcdb..3b1ae10 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -20,8 +20,6 @@ license=('MIT') depends=('umu-launcher') makedepends=('rust' 'cargo') optdepends=( - 'zenity: folder picker in setup wizard (GNOME/GTK)' - 'kdialog: folder picker in setup wizard (KDE)' 'gamemode: per-game GameMode support' 'mangohud: per-game MangoHud overlay' 'gamescope: per-game Gamescope compositor' @@ -54,6 +52,9 @@ package() { # App menu entry install -Dm644 umutray.desktop "$pkgdir/usr/share/applications/umutray.desktop" + # Icon + install -Dm644 assets/umutray.svg "$pkgdir/usr/share/icons/hicolor/scalable/apps/umutray.svg" + # License install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" } diff --git a/src/service.rs b/src/autostart.rs similarity index 67% rename from src/service.rs rename to src/autostart.rs index e549b9c..7ecf84a 100644 --- a/src/service.rs +++ b/src/autostart.rs @@ -1,8 +1,9 @@ use anyhow::{Context, Result}; use owo_colors::OwoColorize; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const DESKTOP_NAME: &str = "umutray.desktop"; +const ICON_SVG: &[u8] = include_bytes!("../assets/umutray.svg"); fn home() -> Result { dirs::home_dir().context("Cannot determine home directory") @@ -27,7 +28,7 @@ fn render_desktop(exe: &std::path::Path, autostart: bool) -> String { Name=umutray\n\ Comment=Wine launcher manager for Windows game launchers\n\ Exec={exec}\n\ - Icon=applications-games\n\ + Icon=umutray\n\ Type=Application\n\ Categories=Game;\n\ Keywords=wine;proton;gaming;launcher;\n\ @@ -42,16 +43,57 @@ fn render_desktop(exe: &std::path::Path, autostart: bool) -> String { s } +fn ensure_parent(path: &Path) -> Result<()> { + if let Some(p) = path.parent() { + std::fs::create_dir_all(p) + .with_context(|| format!("Failed to create {}", p.display()))?; + } + Ok(()) +} + +fn write_file(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> { + ensure_parent(path)?; + std::fs::write(path, contents) + .with_context(|| format!("Failed to write {}", path.display())) +} + +fn remove_file(path: &Path) -> Result<()> { + if path.exists() { + std::fs::remove_file(path) + .with_context(|| format!("Failed to remove {}", path.display()))?; + } + Ok(()) +} + +fn icon_path() -> Result { + Ok(home()?.join(".local/share/icons/hicolor/scalable/apps/umutray.svg")) +} + +fn install_icon() -> Result<()> { + write_file(&icon_path()?, ICON_SVG) +} + +/// Ensure the tray icon SVG is present in the XDG icon theme directory. +/// Called automatically on startup so the icon works without needing +/// a separate install step. +pub fn ensure_icon() { + let Ok(path) = icon_path() else { return }; + if !path.exists() { + let _ = install_icon(); + } +} + +fn uninstall_icon() -> Result<()> { + remove_file(&icon_path()?) +} + /// Install only the .desktop file so umutray appears in the app menu. /// Called automatically on first `umutray gui` run. pub fn install_desktop() -> Result<()> { let exe = std::env::current_exe().context("Cannot determine path to own executable")?; let desktop = desktop_path()?; - if let Some(p) = desktop.parent() { - std::fs::create_dir_all(p).with_context(|| format!("Failed to create {}", p.display()))?; - } - std::fs::write(&desktop, render_desktop(&exe, false)) - .with_context(|| format!("Failed to write desktop file {}", desktop.display()))?; + write_file(&desktop, render_desktop(&exe, false))?; + install_icon()?; println!("{} App menu entry written: {}", "✓".green().bold(), desktop.display()); Ok(()) } @@ -60,8 +102,7 @@ pub fn install_desktop() -> Result<()> { pub fn uninstall_desktop() -> Result<()> { let desktop = desktop_path()?; if desktop.exists() { - std::fs::remove_file(&desktop) - .with_context(|| format!("Failed to remove {}", desktop.display()))?; + remove_file(&desktop)?; println!("Removed {}", desktop.display()); } else { println!("No desktop file at {}", desktop.display()); @@ -75,11 +116,7 @@ pub fn install() -> Result<()> { // XDG autostart let autostart = autostart_path()?; - if let Some(p) = autostart.parent() { - std::fs::create_dir_all(p).with_context(|| format!("Failed to create {}", p.display()))?; - } - std::fs::write(&autostart, render_desktop(&exe, true)) - .with_context(|| format!("Failed to write autostart file {}", autostart.display()))?; + write_file(&autostart, render_desktop(&exe, true))?; println!("Wrote autostart: {}", autostart.display()); // App-menu entry @@ -96,14 +133,14 @@ pub fn install() -> Result<()> { pub fn uninstall() -> Result<()> { let autostart = autostart_path()?; if autostart.exists() { - std::fs::remove_file(&autostart) - .with_context(|| format!("Failed to remove {}", autostart.display()))?; + remove_file(&autostart)?; println!("Removed {}", autostart.display()); } else { println!("No autostart file at {}", autostart.display()); } uninstall_desktop()?; + uninstall_icon()?; println!("{} Autostart removed.", "✓".green().bold()); Ok(()) diff --git a/src/detect.rs b/src/detect.rs index 9f185d4..e96d767 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -1,6 +1,8 @@ use crate::config::{Config, Launcher}; use anyhow::Result; use owo_colors::OwoColorize; +use pelite::pe64::Pe as _; +use pelite::pe32::Pe as _; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; @@ -123,8 +125,7 @@ static STORE_TITLES: LazyLock> = /// return a map of absolute install directory → game title. fn build_store_titles() -> HashMap { let mut map = HashMap::new(); - let Ok(home) = std::env::var("HOME") else { return map }; - let home = PathBuf::from(home); + let Some(home) = dirs::home_dir() else { return map }; // Legendary standalone + Heroic's bundled copy (native and Flatpak). let legendary_candidates = [ @@ -279,7 +280,9 @@ fn scan_exe_dir( /// Epic `.egstore/*.item` JSON files at the game's installation root. /// 4. Launcher path — reads the game name from well-known directory /// structures laid down by the launcher (e.g. `Epic Games//`). -/// 5. Nearest non-generic parent directory name, or raw exe stem. +/// 5. PE version info — reads `ProductName` or `FileDescription` from +/// the exe's embedded Windows version resource. +/// 6. Nearest non-generic parent directory name, or raw exe stem. /// No name generation — if the directory name is unknown, it is used /// as-is rather than being fabricated from the exe filename. /// @@ -322,9 +325,14 @@ fn resolve_uncached(exe_path: &Path) -> String { return name; } - // Stage 5 – nearest non-generic parent directory, or raw exe stem. + // Stage 5 – PE version info embedded in the exe itself + if let Some(name) = read_pe_product_name(exe_path) { + return name; + } + + // Stage 6 – nearest non-generic parent directory, or raw exe stem. // No name generation: if we don't know, we say so honestly. - nearest_dir_name(exe_path) + prettify_exe_name(exe_path) } /// Walk up from `exe_path` looking for platform manifest files that record the @@ -418,7 +426,38 @@ fn name_from_launcher_path(exe_path: &Path) -> Option { None } -fn nearest_dir_name(path: &Path) -> String { +/// Read the `ProductName` (preferred) or `FileDescription` from the exe's +/// embedded PE version info resource. This is the same data Windows uses for +/// "Properties → Details" and is present in the vast majority of game exes. +fn read_pe_product_name(exe_path: &Path) -> Option { + let map = pelite::FileMap::open(exe_path).ok()?; + // Try 64-bit first, fall back to 32-bit. + let vi = pelite::pe64::PeFile::from_bytes(&map) + .ok() + .and_then(|pe| pe.resources().ok()?.version_info().ok()) + .or_else(|| { + pelite::pe32::PeFile::from_bytes(&map) + .ok()? + .resources().ok()? + .version_info().ok() + })?; + let lang = *vi.translation().first()?; + // ProductName is the canonical game title; FileDescription is a fallback. + let name = vi + .value(lang, "ProductName") + .or_else(|| vi.value(lang, "FileDescription"))?; + let name = name.trim(); + if name.is_empty() { + return None; + } + Some(name.to_string()) +} + +/// Heuristic last-resort name derivation from an exe path. +/// +/// Walks up parent directories looking for a non-generic name; falls back to +/// inserting spaces into the CamelCase / digit-boundary exe stem. +pub fn prettify_exe_name(path: &Path) -> String { const GENERIC_DIRS: &[&str] = &[ "bin", "binaries", "x64", "x86", "win64", "win32", "retail", "shipping", "game", "runtime", "_retail_", "_commonredist", @@ -473,7 +512,7 @@ pub fn run(config: &Config, extra_dirs: &[PathBuf], apply: bool) -> Result<()> { } fn default_roots() -> Vec { - let Ok(home) = std::env::var("HOME").map(PathBuf::from) else { + let Some(home) = dirs::home_dir() else { return Vec::new(); }; vec![ diff --git a/src/gui.rs b/src/gui.rs index 016faff..aca52a0 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,10 +1,10 @@ use crate::{ - config::Config, detect, diagnose, launcher, proton, service, + config::Config, detect, diagnose, launcher, proton, autostart, theme::{ btn_accent, btn_danger, btn_ghost, card_style, icon, section_heading, sub_card_style, surface_bg, ACCENT, BORDER_CLR, DIM, GREEN, MUTED, NO_SHADOW, RED, }, - util::{async_blocking, pick_file, pick_folder}, + util::{pick_file, pick_folder}, }; use anyhow::Result; use iced::widget::{ @@ -77,9 +77,9 @@ pub enum Message { BrowseCompatDir, BrowseCompatDirDone(Option), SaveSettings, - ServiceInstall, - ServiceUninstall, - ServiceActionDone(Result<(), String>), + AutostartInstall, + AutostartUninstall, + AutostartDone(Result<(), String>), LaunchProtontricks, // Close dialog CloseRequested(iced::window::Id), @@ -112,8 +112,8 @@ struct Dashboard { settings_proton_version: String, settings_compat_dir: String, proton_versions: Vec, - service_busy: bool, - service_status: String, + autostart_busy: bool, + autostart_status: String, // Close dialog close_dialog_open: bool, close_action: Arc>>, @@ -147,8 +147,8 @@ impl Dashboard { settings_proton_version, settings_compat_dir, proton_versions, - service_busy: false, - service_status: String::new(), + autostart_busy: false, + autostart_status: String::new(), close_dialog_open: false, close_action, } @@ -163,19 +163,21 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { .map(|l| (l.name.clone(), l.process_pattern.clone())) .collect(); Task::perform( - async_blocking(move || { - let mut map = HashMap::new(); - for (name, pattern) in launchers { - let running = std::process::Command::new("pgrep") - .args(["-f", &pattern]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .is_ok_and(|s| s.success()); - map.insert(name, running); - } - map - }), + async { + tokio::task::spawn_blocking(move || { + let mut map = HashMap::new(); + for (name, pattern) in launchers { + let running = std::process::Command::new("pgrep") + .args(["-f", &pattern]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .is_ok_and(|s| s.success()); + map.insert(name, running); + } + map + }).await.expect("blocking task panicked") + }, Message::PollDone, ) } @@ -197,8 +199,12 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { } Message::AddLauncher => { state.context_menu = None; - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); - let _ = std::process::Command::new(exe).arg("setup").spawn(); + let config = state.config.clone(); + std::thread::spawn(move || { + if let Err(e) = crate::setup::run_new(&config) { + eprintln!("umutray: setup picker failed: {e}"); + } + }); Task::none() } Message::Launch(name) => { @@ -211,7 +217,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { let l = l.clone(); let name2 = name.clone(); Task::perform( - async_blocking(move || launcher::launch(&config, &l).map_err(|e| e.to_string())), + async { tokio::task::spawn_blocking(move || launcher::launch(&config, &l).map_err(|e| e.to_string())).await.expect("blocking task panicked") }, move |res| Message::LaunchDone(name2.clone(), res), ) } @@ -236,7 +242,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { let name2 = name.clone(); state.running.insert(name, false); Task::perform( - async_blocking(move || { launcher::kill(&l); Ok::<(), String>(()) }), + async { tokio::task::spawn_blocking(move || { launcher::kill(&l); Ok::<(), String>(()) }).await.expect("blocking task panicked") }, move |res| Message::KillDone(name2.clone(), res), ) } @@ -257,9 +263,11 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { let lname2 = lname.clone(); let gname2 = gname.clone(); return Task::perform( - async_blocking(move || { - launcher::play_game(&config, &l, &g).map_err(|e| e.to_string()) - }), + async { + tokio::task::spawn_blocking(move || { + launcher::play_game(&config, &l, &g).map_err(|e| e.to_string()) + }).await.expect("blocking task panicked") + }, move |res| Message::PlayDone(lname2.clone(), gname2.clone(), res), ); } @@ -273,11 +281,16 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { Task::none() } Message::Setup(name) => { - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); - let _ = std::process::Command::new(exe) - .arg("setup") - .arg(&name) - .spawn(); + let config = state.config.clone(); + let name2 = name.clone(); + std::thread::spawn(move || { + if let Some(l) = config.find(&name2) { + let l = l.clone(); + if let Err(e) = crate::setup::run(&config, &l) { + eprintln!("umutray: setup for {} failed: {e}", l.name); + } + } + }); Task::none() } Message::ToggleGameMode(lname, gname) => { @@ -304,9 +317,11 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { state.last_error = None; let config = state.config.clone(); Task::perform( - async_blocking(move || { - crate::proton::install_latest(&config).map_err(|e| e.to_string()) - }), + async { + tokio::task::spawn_blocking(move || { + crate::proton::install_latest(&config).map_err(|e| e.to_string()) + }).await.expect("blocking task panicked") + }, Message::ProtonDone, ) } @@ -346,11 +361,16 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { } Message::RerunSetup(name) => { state.context_menu = None; - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); - let _ = std::process::Command::new(exe) - .arg("setup") - .arg(&name) - .spawn(); + let config = state.config.clone(); + let name2 = name.clone(); + std::thread::spawn(move || { + if let Some(l) = config.find(&name2) { + let l = l.clone(); + if let Err(e) = crate::setup::run(&config, &l) { + eprintln!("umutray: setup for {} failed: {e}", l.name); + } + } + }); Task::none() } Message::RemoveLauncher(name) => { @@ -368,7 +388,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { state.detect_result = "Scanning…".into(); let config = state.config.clone(); Task::perform( - async_blocking(move || detect::scan_for_gui(&config)), + async { tokio::task::spawn_blocking(move || detect::scan_for_gui(&config)).await.expect("blocking task panicked") }, Message::DetectDone, ) } @@ -399,14 +419,16 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { let config = state.config.clone(); let lname = name.clone(); Task::perform( - async_blocking(move || { - diagnose::run_checks(&config, Some(&name)) - .unwrap_or_else(|e| vec![diagnose::CheckResult { - label: "error".into(), - pass: false, - detail: e.to_string(), - }]) - }), + async { + tokio::task::spawn_blocking(move || { + diagnose::run_checks(&config, Some(&name)) + .unwrap_or_else(|e| vec![diagnose::CheckResult { + label: "error".into(), + pass: false, + detail: e.to_string(), + }]) + }).await.expect("blocking task panicked") + }, move |checks| Message::DiagnoseDone(lname.clone(), checks), ) } @@ -476,7 +498,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { let lname2 = lname.clone(); state.scan_busy.insert(lname); return Task::perform( - async_blocking(move || detect::scan_games_in_prefix(&l)), + async { tokio::task::spawn_blocking(move || detect::scan_games_in_prefix(&l)).await.expect("blocking task panicked") }, move |hits| Message::ScanGamesDone(lname2.clone(), hits), ); } @@ -516,7 +538,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { .unwrap_or_default(); let lname2 = lname.clone(); Task::perform( - async_blocking(move || pick_file("Select game executable", &start)), + async { tokio::task::spawn_blocking(move || pick_file("Select game executable", &start)).await.expect("blocking task panicked") }, move |res| Message::BrowseGameExeDone(lname2.clone(), res), ) } @@ -550,7 +572,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { state.settings_compat_dir = state.config.proton_compat_dir.to_string_lossy().into_owned(); state.proton_versions = proton::list_installed(&state.config); - state.service_status = String::new(); + state.autostart_status = String::new(); Task::none() } Message::HideSettings => { @@ -566,7 +588,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { Task::none() } Message::BrowseCompatDir => Task::perform( - async_blocking(|| pick_folder("Choose GE-Proton compat directory")), + async { tokio::task::spawn_blocking(|| pick_folder("Choose GE-Proton compat directory")).await.expect("blocking task panicked") }, Message::BrowseCompatDirDone, ), Message::BrowseCompatDirDone(path) => { @@ -603,7 +625,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { match state.config.set_globals(Some(version_key), Some(compat)) { Ok(()) => { state.proton_versions = proton::list_installed(&state.config); - state.service_status = "Settings saved.".into(); + state.autostart_status = "Settings saved.".into(); } Err(e) => { state.last_error = Some(format!("Save failed: {e}")); @@ -619,34 +641,34 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { }); Task::none() } - Message::ServiceInstall => { - state.service_busy = true; - state.service_status = "Installing autostart…".into(); + Message::AutostartInstall => { + state.autostart_busy = true; + state.autostart_status = "Installing autostart…".into(); Task::perform( - async_blocking(|| service::install().map_err(|e| e.to_string())), - Message::ServiceActionDone, + async { tokio::task::spawn_blocking(|| autostart::install().map_err(|e| e.to_string())).await.expect("blocking task panicked") }, + Message::AutostartDone, ) } - Message::ServiceUninstall => { - state.service_busy = true; - state.service_status = "Removing autostart…".into(); + Message::AutostartUninstall => { + state.autostart_busy = true; + state.autostart_status = "Removing autostart…".into(); Task::perform( - async_blocking(|| service::uninstall().map_err(|e| e.to_string())), - Message::ServiceActionDone, + async { tokio::task::spawn_blocking(|| autostart::uninstall().map_err(|e| e.to_string())).await.expect("blocking task panicked") }, + Message::AutostartDone, ) } - Message::ServiceActionDone(res) => { - state.service_busy = false; + Message::AutostartDone(res) => { + state.autostart_busy = false; match res { Ok(()) => { - state.service_status = if service_is_installed() { + state.autostart_status = if autostart_is_installed() { "Autostart enabled — starts on next login.".into() } else { "Autostart removed.".into() }; } Err(e) => { - state.service_status = format!("Failed: {e}"); + state.autostart_status = format!("Failed: {e}"); } } Task::none() @@ -697,7 +719,7 @@ fn toggle_flag( let _ = config.save(); } -fn service_is_installed() -> bool { +fn autostart_is_installed() -> bool { dirs::home_dir() .is_some_and(|h| h.join(".config/autostart/umutray.desktop").exists()) } @@ -1626,25 +1648,25 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> { ); // ── Autostart section ───────────────────────────────────────────────── - let installed = service_is_installed(); + let installed = autostart_is_installed(); let svc_status_color = if installed { GREEN } else { MUTED }; let svc_status_text = if installed { "Enabled — starts on login" } else { "Disabled" }; let svc_status_icon = if installed { "\u{f26a}" } else { "\u{f28a}" }; let svc_install_btn = button( - row![icon("\u{f64d}", 12), text(if state.service_busy { " Working…" } else { " Enable" }).size(12)] + row![icon("\u{f64d}", 12), text(if state.autostart_busy { " Working…" } else { " Enable" }).size(12)] .align_y(Alignment::Center).spacing(4), ) - .on_press_maybe((!state.service_busy && !installed).then_some(Message::ServiceInstall)) + .on_press_maybe((!state.autostart_busy && !installed).then_some(Message::AutostartInstall)) .style(btn_accent) .padding([7, 14]); let svc_uninstall_btn = button( - row![icon("\u{f659}", 12), text(if state.service_busy { " Working…" } else { " Disable" }).size(12)] + row![icon("\u{f659}", 12), text(if state.autostart_busy { " Working…" } else { " Disable" }).size(12)] .align_y(Alignment::Center).spacing(4), ) - .on_press_maybe((!state.service_busy && installed).then_some(Message::ServiceUninstall)) + .on_press_maybe((!state.autostart_busy && installed).then_some(Message::AutostartUninstall)) .style(btn_danger) .padding([7, 14]); @@ -1659,7 +1681,7 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> { svc_install_btn, svc_uninstall_btn, ].align_y(Alignment::Center).spacing(8), - text(&state.service_status).size(11).style(|_: &Theme| text::Style { + text(&state.autostart_status).size(11).style(|_: &Theme| text::Style { color: Some(DIM), }), ].spacing(6).into(), diff --git a/src/launcher.rs b/src/launcher.rs index a18f8f7..60a76ba 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -34,11 +34,29 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> { let proton_path = resolve_proton_path(config, launcher); - std::process::Command::new("umu-run") - .env("WINEPREFIX", &launcher.prefix_dir) + // Propagate overlay env vars from any configured game so that games + // launched from within the launcher (e.g. Battle.net → Diablo) inherit + // them. gamemoderun wraps umu-run so the whole process tree gets + // gamemode; MANGOHUD=1 is inherited by all child processes. + let any_gamemode = launcher.games.iter().any(|g| g.gamemode); + let any_mangohud = launcher.games.iter().any(|g| g.mangohud); + + let (prog, args): (OsString, Vec) = if any_gamemode { + let mut a = vec![OsString::from("umu-run")]; + a.push(exe.into_os_string()); + ("gamemoderun".into(), a) + } else { + (OsString::from("umu-run"), vec![exe.into_os_string()]) + }; + + let mut cmd = std::process::Command::new(&prog); + cmd.env("WINEPREFIX", &launcher.prefix_dir) .env("GAMEID", &launcher.gameid) - .env("PROTONPATH", &proton_path) - .arg(&exe) + .env("PROTONPATH", &proton_path); + if any_mangohud { + cmd.env("MANGOHUD", "1"); + } + cmd.args(&args) .spawn() .context( "Failed to spawn umu-run. Is it installed?\n\ @@ -48,9 +66,9 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> { Ok(()) } -/// Launch a game installed through `launcher`, wrapped in the per-game -/// overlays (gamescope, gamemoderun, MANGOHUD). The launcher itself is -/// never wrapped — only games run through this path pick up overlays. +/// Launch a game directly via umu-run, wrapped in the per-game overlays +/// (gamescope, gamemoderun, MANGOHUD). Ensures the parent launcher is +/// running first so the game can authenticate online. pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<()> { let exe = game.full_exe_path(launcher); if !exe.exists() { @@ -63,6 +81,14 @@ pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<() ); } + // Start the launcher if it isn't already running so the game has an + // active authentication session (avoids offline-mode). + if !is_running(launcher) { + launch(config, launcher)?; + // Give the launcher a moment to initialise before spawning the game. + thread::sleep(Duration::from_secs(5)); + } + let proton_path = resolve_proton_path(config, launcher); let (prog, args) = build_wrapped_argv(&exe, game); diff --git a/src/main.rs b/src/main.rs index 76b7d15..6d7e060 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ mod diagnose; mod gui; mod launcher; mod proton; -mod service; +mod autostart; mod setup; mod theme; mod tray; @@ -109,10 +109,10 @@ enum Commands { action: ConfigAction, }, - /// Manage the XDG autostart entry that starts the tray on login - Service { + /// Manage the XDG autostart and desktop entries + Autostart { #[command(subcommand)] - action: ServiceAction, + action: AutostartAction, }, } @@ -222,7 +222,7 @@ enum ConfigAction { } #[derive(Subcommand)] -enum ServiceAction { +enum AutostartAction { /// Write ~/.config/autostart/umutray.desktop so the tray starts on login (includes app menu entry) Install, /// Remove the autostart entry and app menu entry @@ -239,6 +239,10 @@ fn main() -> Result<()> { let cli = Cli::parse(); let config = config::Config::load()?; + // Ensure the SVG icon is present in the XDG icon theme so the tray + // and desktop entries can find it without a separate install step. + autostart::ensure_icon(); + match cli.command.unwrap_or(Commands::Tray) { Commands::Tray => tray::run(&config)?, @@ -428,12 +432,12 @@ fn main() -> Result<()> { } }, - Commands::Service { action } => match action { - ServiceAction::Install => service::install()?, - ServiceAction::Uninstall => service::uninstall()?, - ServiceAction::Status => service::status()?, - ServiceAction::InstallDesktop => service::install_desktop()?, - ServiceAction::UninstallDesktop => service::uninstall_desktop()?, + Commands::Autostart { action } => match action { + AutostartAction::Install => autostart::install()?, + AutostartAction::Uninstall => autostart::uninstall()?, + AutostartAction::Status => autostart::status()?, + AutostartAction::InstallDesktop => autostart::install_desktop()?, + AutostartAction::UninstallDesktop => autostart::uninstall_desktop()?, }, } diff --git a/src/setup.rs b/src/setup.rs index f792b33..d5be97d 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -4,7 +4,7 @@ use crate::{ btn_accent, btn_ghost, card_style, icon, sub_card_style, surface_bg, ACCENT, DIM, GREEN, MUTED, RED, SURFACE_RAISED, }, - util::{async_blocking, pick_folder}, + util::pick_folder, }; use anyhow::Result; use iced::widget::{ @@ -154,7 +154,7 @@ fn update(state: &mut State, message: Message) -> Task { Task::none() } Message::BrowsePrefix => Task::perform( - async_blocking(|| pick_folder("Choose install location (Wine prefix)")), + async { tokio::task::spawn_blocking(|| pick_folder("Choose install location (Wine prefix)")).await.expect("blocking task panicked") }, Message::BrowsePrefixDone, ), Message::BrowsePrefixDone(path) => { @@ -204,7 +204,7 @@ fn update(state: &mut State, message: Message) -> Task { .clone(); let progress = state.download.clone(); return Task::perform( - async_blocking(move || download_blocking(&url, &name, progress)), + async { tokio::task::spawn_blocking(move || download_blocking(&url, &name, progress)).await.expect("blocking task panicked") }, Message::PrepareDone, ); } @@ -259,7 +259,7 @@ fn update(state: &mut State, message: Message) -> Task { .clone(); let progress = state.download.clone(); Task::perform( - async_blocking(move || download_blocking(&url, &name, progress)), + async { tokio::task::spawn_blocking(move || download_blocking(&url, &name, progress)).await.expect("blocking task panicked") }, Message::PrepareDone, ) } @@ -303,7 +303,7 @@ fn update(state: &mut State, message: Message) -> Task { .clone(); let progress = state.download.clone(); Task::perform( - async_blocking(move || download_blocking(&src, &name, progress)), + async { tokio::task::spawn_blocking(move || download_blocking(&src, &name, progress)).await.expect("blocking task panicked") }, Message::PrepareDone, ) } @@ -346,7 +346,7 @@ fn update(state: &mut State, message: Message) -> Task { .expect("launcher set before install"); let log = state.log.clone(); Task::perform( - async_blocking(move || run_installer(&config, &launcher, &installer, log)), + async { tokio::task::spawn_blocking(move || run_installer(&config, &launcher, &installer, log)).await.expect("blocking task panicked") }, Message::InstallDone, ) } diff --git a/src/tray.rs b/src/tray.rs index 7f5f9d5..b2e56e8 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,33 +1,39 @@ use crate::{config::Config, launcher}; use anyhow::Result; use std::collections::HashMap; -use std::path::PathBuf; use std::thread; use std::time::Duration; -fn spawn_setup(name: &str) { - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); - if let Err(e) = std::process::Command::new(exe) - .arg("setup") - .arg(name) - .spawn() - { - eprintln!("umutray: failed to launch setup for {name}: {e}"); - } +fn spawn_setup(config: &Config, name: &str) { + let config = config.clone(); + let name = name.to_owned(); + thread::spawn(move || { + if let Some(l) = config.find(&name) { + let l = l.clone(); + if let Err(e) = crate::setup::run(&config, &l) { + eprintln!("umutray: setup for {name} failed: {e}"); + } + } + }); } -fn spawn_gui() { - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); - if let Err(e) = std::process::Command::new(exe).arg("gui").spawn() { - eprintln!("umutray: failed to launch dashboard: {e}"); - } +fn spawn_gui(config: &Config) { + let config = config.clone(); + thread::spawn(move || { + match crate::gui::run(&config) { + Ok(_) => {} + Err(e) => eprintln!("umutray: failed to launch dashboard: {e}"), + } + }); } -fn spawn_setup_picker() { - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); - if let Err(e) = std::process::Command::new(exe).arg("setup").spawn() { - eprintln!("umutray: failed to launch setup picker: {e}"); - } +fn spawn_setup_picker(config: &Config) { + let config = config.clone(); + thread::spawn(move || { + if let Err(e) = crate::setup::run_new(&config) { + eprintln!("umutray: failed to launch setup picker: {e}"); + } + }); } enum GameFlag { @@ -79,7 +85,7 @@ pub struct UmuTray { pub config: Config, /// Per-launcher running state keyed by launcher.name pub running: HashMap, - /// Set after the service spawns so Quit can shut down the SNI item + /// Set after the tray spawns so Quit can shut down the SNI item /// cleanly instead of yanking it off the bus via exit(). pub handle: Option>, } @@ -90,7 +96,7 @@ impl ksni::Tray for UmuTray { } fn icon_name(&self) -> String { - "applications-games".into() + "umutray".into() } fn title(&self) -> String { @@ -107,7 +113,7 @@ impl ksni::Tray for UmuTray { StandardItem { label: "Open Dashboard".into(), icon_name: "applications-games".into(), - activate: Box::new(|_: &mut Self| spawn_gui()), + activate: Box::new(|this: &mut Self| spawn_gui(&this.config)), ..Default::default() } .into(), @@ -126,8 +132,8 @@ impl ksni::Tray for UmuTray { StandardItem { label: format!("Setup {display}…"), icon_name: "document-new".into(), - activate: Box::new(move |_this: &mut Self| { - spawn_setup(&setup_name); + activate: Box::new(move |this: &mut Self| { + spawn_setup(&this.config, &setup_name); }), ..Default::default() } @@ -245,7 +251,7 @@ impl ksni::Tray for UmuTray { StandardItem { label: "Add Launcher…".into(), icon_name: "list-add".into(), - activate: Box::new(|_: &mut Self| spawn_setup_picker()), + activate: Box::new(|this: &mut Self| spawn_setup_picker(&this.config)), ..Default::default() } .into(), diff --git a/src/util.rs b/src/util.rs index 0b0b758..cd41b56 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,59 +1,18 @@ -/// Run a blocking closure on the tokio blocking thread pool and await its result. -/// Used to offload blocking work (HTTP, disk, process spawning) without -/// stalling the iced event loop. -pub async fn async_blocking(f: F) -> T -where - T: Send + 'static, - F: FnOnce() -> T + Send + 'static, -{ - tokio::task::spawn_blocking(f) - .await - .expect("blocking task panicked") -} - /// Open a native folder picker dialog and return the chosen path, or None if -/// the user cancelled. Tries zenity (GNOME/GTK) then kdialog (KDE) in order. +/// the user cancelled. Uses XDG Desktop Portal where available. pub fn pick_folder(title: &str) -> Option { - for (cmd, args) in [ - ("zenity", vec!["--file-selection", "--directory", "--title", title]), - ("kdialog", vec!["--getexistingdirectory", "/home", "--title", title]), - ] { - let Ok(out) = std::process::Command::new(cmd).args(&args).output() else { - continue; - }; - if out.status.success() { - let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if !s.is_empty() { - return Some(s); - } - } - } - None + rfd::FileDialog::new() + .set_title(title) + .pick_folder() + .map(|p| p.to_string_lossy().into_owned()) } /// Open a native file picker dialog starting in `start_dir`, or None if -/// the user cancelled. Tries zenity then kdialog. +/// the user cancelled. Uses XDG Desktop Portal where available. pub fn pick_file(title: &str, start_dir: &str) -> Option { - // zenity uses --filename with a trailing slash to open a directory - let start_slash = format!("{}/", start_dir.trim_end_matches('/')); - let zenity_args = vec![ - "--file-selection", - "--title", - title, - "--filename", - &start_slash, - ]; - let kdialog_args = vec!["--getopenfilename", start_dir, "--title", title]; - for (cmd, args) in [("zenity", zenity_args.as_slice()), ("kdialog", kdialog_args.as_slice())] { - let Ok(out) = std::process::Command::new(cmd).args(args).output() else { - continue; - }; - if out.status.success() { - let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if !s.is_empty() { - return Some(s); - } - } - } - None + rfd::FileDialog::new() + .set_title(title) + .set_directory(start_dir) + .pick_file() + .map(|p| p.to_string_lossy().into_owned()) } diff --git a/umutray.desktop b/umutray.desktop index eee941f..b3a1ab8 100644 --- a/umutray.desktop +++ b/umutray.desktop @@ -2,7 +2,7 @@ Name=umutray Comment=Wine launcher manager for Windows game launchers Exec=umutray gui -Icon=applications-games +Icon=umutray Type=Application Categories=Game; Keywords=wine;proton;gaming;launcher;