refactor: rename service to autostart, fix fork bomb, add rfd file picker, use dirs crate, auto-start launcher, propagate overlays, ensure icon

- Rename service module to autostart (no systemd service is used)
- Fix fork bomb: replace subprocess spawning with thread::spawn
- Replace zenity/kdialog with rfd crate for XDG Portal file picker
- Use dirs crate instead of env::var("HOME")
- Auto-start launcher before game launch for online auth
- Propagate gamemode/mangohud env vars to launcher process
- Auto-install SVG icon on startup via ensure_icon()
- Add assets/umutray.svg
- Remove stale zenity/kdialog optdepends from PKGBUILD
- Update .gitignore for .claude/ and CLAUDE.md
This commit is contained in:
funman300
2026-04-19 13:02:32 -07:00
parent 2f4f1c64d2
commit c1893f9f64
15 changed files with 620 additions and 240 deletions
+2
View File
@@ -1,5 +1,7 @@
/target /target
.vscode/ .vscode/
.claude/
CLAUDE.md
# Packaging build artifacts # Packaging build artifacts
packaging/pkg/ packaging/pkg/
Generated
+307 -30
View File
@@ -189,6 +189,28 @@ dependencies = [
"libloading 0.7.4", "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]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@@ -267,6 +289,17 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "async-process" name = "async-process"
version = "2.5.0" version = "2.5.0"
@@ -411,6 +444,15 @@ dependencies = [
"objc2 0.5.2", "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]] [[package]]
name = "blocking" name = "blocking"
version = "1.6.2" version = "1.6.2"
@@ -614,7 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f"
dependencies = [ dependencies = [
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -871,7 +913,16 @@ dependencies = [
"rust-ini", "rust-ini",
"web-sys", "web-sys",
"winreg", "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]] [[package]]
@@ -911,6 +962,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
[[package]]
name = "derive_pod"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8"
[[package]] [[package]]
name = "detect-desktop-environment" name = "detect-desktop-environment"
version = "0.2.0" version = "0.2.0"
@@ -990,6 +1047,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2 0.6.2",
"libc",
"objc2 0.6.4", "objc2 0.6.4",
] ]
@@ -2525,6 +2584,12 @@ dependencies = [
"memoffset", "memoffset",
] ]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2598,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-data", "objc2-core-data",
@@ -2607,6 +2672,18 @@ dependencies = [
"objc2-quartz-core 0.2.2", "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]] [[package]]
name = "objc2-cloud-kit" name = "objc2-cloud-kit"
version = "0.2.2" version = "0.2.2"
@@ -2614,7 +2691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -2626,7 +2703,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2638,7 +2715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2673,7 +2750,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal", "objc2-metal",
@@ -2685,7 +2762,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-contacts", "objc2-contacts",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -2704,7 +2781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"dispatch", "dispatch",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
@@ -2738,9 +2815,9 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2751,7 +2828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2763,7 +2840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal", "objc2-metal",
@@ -2798,7 +2875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-cloud-kit", "objc2-cloud-kit",
"objc2-core-data", "objc2-core-data",
@@ -2818,7 +2895,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2830,7 +2907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -3036,6 +3113,25 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -3059,7 +3155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [ dependencies = [
"phf_shared", "phf_shared",
"rand", "rand 0.8.6",
] ]
[[package]] [[package]]
@@ -3166,6 +3262,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.5" version = "0.1.5"
@@ -3261,8 +3363,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "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]] [[package]]
@@ -3272,7 +3384,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "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]] [[package]]
@@ -3284,6 +3406,15 @@ dependencies = [
"getrandom 0.2.17", "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]] [[package]]
name = "range-alloc" name = "range-alloc"
version = "0.1.5" version = "0.1.5"
@@ -3427,6 +3558,30 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@@ -4436,7 +4591,9 @@ dependencies = [
"iced_fonts", "iced_fonts",
"ksni", "ksni",
"owo-colors", "owo-colors",
"pelite",
"reqwest", "reqwest",
"rfd",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -4519,8 +4676,15 @@ dependencies = [
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde", "serde",
"serde_derive",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf8_iter" name = "utf8_iter"
version = "1.0.4" version = "1.0.4"
@@ -4533,6 +4697,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 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]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@@ -5232,7 +5407,7 @@ dependencies = [
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"bytemuck", "bytemuck",
"calloop 0.13.0", "calloop 0.13.0",
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
@@ -5246,7 +5421,7 @@ dependencies = [
"memmap2", "memmap2",
"ndk", "ndk",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-ui-kit", "objc2-ui-kit",
"orbclient", "orbclient",
@@ -5527,7 +5702,7 @@ dependencies = [
"hex", "hex",
"nix", "nix",
"ordered-stream", "ordered-stream",
"rand", "rand 0.8.6",
"serde", "serde",
"serde_repr", "serde_repr",
"sha1", "sha1",
@@ -5536,9 +5711,44 @@ dependencies = [
"uds_windows", "uds_windows",
"windows-sys 0.52.0", "windows-sys 0.52.0",
"xdg-home", "xdg-home",
"zbus_macros", "zbus_macros 4.4.0",
"zbus_names", "zbus_names 3.0.0",
"zvariant", "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]] [[package]]
@@ -5551,7 +5761,22 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "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]] [[package]]
@@ -5562,7 +5787,18 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [ dependencies = [
"serde", "serde",
"static_assertions", "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]] [[package]]
@@ -5667,7 +5903,22 @@ dependencies = [
"enumflags2", "enumflags2",
"serde", "serde",
"static_assertions", "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]] [[package]]
@@ -5680,7 +5931,20 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "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]] [[package]]
@@ -5693,3 +5957,16 @@ dependencies = [
"quote", "quote",
"syn 2.0.117", "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",
]
+6
View File
@@ -46,3 +46,9 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
iced = { version = "0.13", features = ["tokio"] } iced = { version = "0.13", features = ["tokio"] }
iced_fonts = { version = "0.1", features = ["bootstrap"] } iced_fonts = { version = "0.1", features = ["bootstrap"] }
tokio = { version = "1.52.1", features = ["rt"] } 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"
+5 -5
View File
@@ -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. (no ~600 MB in-memory buffering), with a progress indicator.
- `diagnose` — sanity-checks umu-run, Vulkan, display server, per-launcher - `diagnose` — sanity-checks umu-run, Vulkan, display server, per-launcher
prefix / exe / ownership / running state. 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. the graphical session.
- `setup` — graphical wizard (iced) that downloads an installer URL - `setup` — graphical wizard (iced) that downloads an installer URL
(with progress bar) or accepts a local `.exe`, then runs it via (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: Then enable autostart:
```sh ```sh
umutray service install umutray autostart install
``` ```
## Usage ## Usage
@@ -79,9 +79,9 @@ umutray service install
| `umutray config add-game …` | Attach a game to a launcher (needs `--exe-path`) | | `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 remove-game …` | Drop a game from a launcher |
| `umutray config set-game-flags …`| Per-game overlay toggles: gamemode/mangohud/gamescope | | `umutray config set-game-flags …`| Per-game overlay toggles: gamemode/mangohud/gamescope |
| `umutray service install` | Write XDG autostart entry (tray starts on login) | | `umutray autostart install` | Write XDG autostart entry (tray starts on login) |
| `umutray service uninstall` | Remove the autostart and desktop entries | | `umutray autostart uninstall` | Remove the autostart and desktop entries |
| `umutray service status` | Show whether XDG autostart is enabled | | `umutray autostart status` | Show whether XDG autostart is enabled |
## Config ## Config
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8" ?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" viewBox="0 0 256 256"><path fill="#6A2DD0" transform="scale(0.25 0.25)" d="M316.83 489.317C317.107 484.743 317.387 484.045 319.792 480.106C331.825 460.403 344.692 441.083 357.09 421.576L426.676 310.088L460.178 256.008C465.12 247.892 470.692 238.565 475.589 230.368C485.332 214.061 494.15 201.062 515.652 207.922C526.275 211.312 534.508 228.056 540.153 237.267L561.887 272.304L651.315 415.661L675.648 454.536C681.1 463.353 686.925 473.61 693.188 481.796C693.471 484.01 693.683 486.232 693.823 488.46C694.069 495.055 694.774 499.229 691.701 505.13C687.363 508.231 685.162 509.801 679.583 507.794C672.444 505.226 666.114 500.865 659.488 497.208L606.838 468.035C598.995 463.67 590.498 457.656 582.318 454.761C579.585 463.73 577.836 472.577 575.242 481.379L533.513 622.349C529.194 636.653 518.538 676.017 511.284 687.126C506.749 689.685 504.339 689.506 499.435 688.833C498.47 688.213 497.545 687.533 496.665 686.797C494.438 684.872 492.749 682.401 491.764 679.627C487.819 668.528 484.428 654.073 481.349 642.721L460.784 565.328L441.896 494.692C439.173 484.508 432.807 464.261 431.753 453.965C422.177 456.49 412.453 463.896 403.931 468.831C397.648 472.47 391.092 475.633 384.73 479.133C375.143 484.408 335.982 507.269 328.008 508.414C323.524 509.058 320.824 506.354 317.583 503.743C316.948 499.784 316.946 493.481 316.83 489.317Z"/><path fill="#0DE3F9" transform="scale(0.25 0.25)" d="M316.83 489.317C326.713 487.058 361.145 466.68 372.023 460.761L411.657 439.075C419.599 434.76 435.454 426.313 442.416 421.007C445.878 429.66 449.515 445.075 451.785 454.299C455.003 467.441 458.333 480.554 461.774 493.639L488.319 592.414C493.49 612.086 498.3 634.105 503.702 653.315C507.136 645.111 510.74 630.302 513.341 621.249L530.525 561.547L563.101 448.881C565.546 440.194 569.012 430.374 571.228 421.862C590.026 431.246 682.27 488.827 693.823 488.46C694.069 495.055 694.774 499.229 691.701 505.13C687.363 508.231 685.162 509.801 679.583 507.794C672.444 505.226 666.114 500.865 659.488 497.208L606.838 468.035C598.995 463.67 590.498 457.656 582.318 454.761C579.585 463.73 577.836 472.577 575.242 481.379L533.513 622.349C529.194 636.653 518.538 676.017 511.284 687.126C506.749 689.685 504.339 689.506 499.435 688.833C498.47 688.213 497.545 687.533 496.665 686.797C494.438 684.872 492.749 682.401 491.764 679.627C487.819 668.528 484.428 654.073 481.349 642.721L460.784 565.328L441.896 494.692C439.173 484.508 432.807 464.261 431.753 453.965C422.177 456.49 412.453 463.896 403.931 468.831C397.648 472.47 391.092 475.633 384.73 479.133C375.143 484.408 335.982 507.269 328.008 508.414C323.524 509.058 320.824 506.354 317.583 503.743C316.948 499.784 316.946 493.481 316.83 489.317Z"/><path fill="#6A2DD0" transform="scale(0.25 0.25)" d="M587.904 557.75C594.63 558.35 636.107 580.289 645.251 584.753L707.547 615.372C722.827 622.791 739.073 629.822 753.378 639.13C758.309 642.764 762.436 647.947 765.177 653.232C775.79 673.694 769.385 696.993 750.511 709.963C742.787 715.271 735.331 718.229 726.892 722.364L691.26 739.957C670.813 749.813 648.337 761.827 628.013 772.246L565.409 803.747C552.588 810.117 539.24 817.98 525.599 821.676C501.993 828.073 478.772 820.911 458.344 808.937C455.519 807.534 452.539 805.546 449.614 804.348C437.518 799.393 426.435 793.024 414.839 787.176L345.748 752.392L296.932 728.286C278.678 719.455 255.575 711.06 246.053 692.148C241.346 682.798 240.626 669.13 243.911 659.221C249.793 641.473 265.715 634.257 281.362 626.711L363.987 585.171C378.809 577.73 404.312 564.178 419.551 558.967C420.161 563.911 423.45 572.363 425.066 577.529C429.662 592.226 435.14 605.968 439.405 620.812C433.705 622.439 418.179 630.443 412.013 633.482L361.205 658.483L339.266 668.935C335.952 670.529 334.485 669.629 334.151 673.003C333.569 672.051 415.662 713.259 423.407 717.013C442.821 726.191 462.067 735.718 481.137 745.59C487.422 748.819 500.053 754.835 505.208 759.089C507.704 759.259 514.834 754.663 517.462 753.24C524.303 749.591 531.199 746.044 538.147 742.6L637.624 692.709C650.159 686.478 665.981 677.686 678.576 672.23C664.077 663.98 648.275 657.584 633.456 649.828C621.72 643.686 609.236 637.96 597.176 632.47C587.431 628.034 575.228 620.477 564.906 618.184C566.354 611.945 570.995 601.874 573.401 595.696C578.278 583.174 583.569 570.468 587.904 557.75Z"/></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+3 -2
View File
@@ -20,8 +20,6 @@ license=('MIT')
depends=('umu-launcher') depends=('umu-launcher')
makedepends=('rust' 'cargo') makedepends=('rust' 'cargo')
optdepends=( optdepends=(
'zenity: folder picker in setup wizard (GNOME/GTK)'
'kdialog: folder picker in setup wizard (KDE)'
'gamemode: per-game GameMode support' 'gamemode: per-game GameMode support'
'mangohud: per-game MangoHud overlay' 'mangohud: per-game MangoHud overlay'
'gamescope: per-game Gamescope compositor' 'gamescope: per-game Gamescope compositor'
@@ -54,6 +52,9 @@ package() {
# App menu entry # App menu entry
install -Dm644 umutray.desktop "$pkgdir/usr/share/applications/umutray.desktop" 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 # License
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
} }
+53 -16
View File
@@ -1,8 +1,9 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use std::path::PathBuf; use std::path::{Path, PathBuf};
const DESKTOP_NAME: &str = "umutray.desktop"; const DESKTOP_NAME: &str = "umutray.desktop";
const ICON_SVG: &[u8] = include_bytes!("../assets/umutray.svg");
fn home() -> Result<PathBuf> { fn home() -> Result<PathBuf> {
dirs::home_dir().context("Cannot determine home directory") 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\ Name=umutray\n\
Comment=Wine launcher manager for Windows game launchers\n\ Comment=Wine launcher manager for Windows game launchers\n\
Exec={exec}\n\ Exec={exec}\n\
Icon=applications-games\n\ Icon=umutray\n\
Type=Application\n\ Type=Application\n\
Categories=Game;\n\ Categories=Game;\n\
Keywords=wine;proton;gaming;launcher;\n\ Keywords=wine;proton;gaming;launcher;\n\
@@ -42,16 +43,57 @@ fn render_desktop(exe: &std::path::Path, autostart: bool) -> String {
s 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<PathBuf> {
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. /// Install only the .desktop file so umutray appears in the app menu.
/// Called automatically on first `umutray gui` run. /// Called automatically on first `umutray gui` run.
pub fn install_desktop() -> Result<()> { pub fn install_desktop() -> Result<()> {
let exe = std::env::current_exe().context("Cannot determine path to own executable")?; let exe = std::env::current_exe().context("Cannot determine path to own executable")?;
let desktop = desktop_path()?; let desktop = desktop_path()?;
if let Some(p) = desktop.parent() { write_file(&desktop, render_desktop(&exe, false))?;
std::fs::create_dir_all(p).with_context(|| format!("Failed to create {}", p.display()))?; install_icon()?;
}
std::fs::write(&desktop, render_desktop(&exe, false))
.with_context(|| format!("Failed to write desktop file {}", desktop.display()))?;
println!("{} App menu entry written: {}", "".green().bold(), desktop.display()); println!("{} App menu entry written: {}", "".green().bold(), desktop.display());
Ok(()) Ok(())
} }
@@ -60,8 +102,7 @@ pub fn install_desktop() -> Result<()> {
pub fn uninstall_desktop() -> Result<()> { pub fn uninstall_desktop() -> Result<()> {
let desktop = desktop_path()?; let desktop = desktop_path()?;
if desktop.exists() { if desktop.exists() {
std::fs::remove_file(&desktop) remove_file(&desktop)?;
.with_context(|| format!("Failed to remove {}", desktop.display()))?;
println!("Removed {}", desktop.display()); println!("Removed {}", desktop.display());
} else { } else {
println!("No desktop file at {}", desktop.display()); println!("No desktop file at {}", desktop.display());
@@ -75,11 +116,7 @@ pub fn install() -> Result<()> {
// XDG autostart // XDG autostart
let autostart = autostart_path()?; let autostart = autostart_path()?;
if let Some(p) = autostart.parent() { write_file(&autostart, render_desktop(&exe, true))?;
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()))?;
println!("Wrote autostart: {}", autostart.display()); println!("Wrote autostart: {}", autostart.display());
// App-menu entry // App-menu entry
@@ -96,14 +133,14 @@ pub fn install() -> Result<()> {
pub fn uninstall() -> Result<()> { pub fn uninstall() -> Result<()> {
let autostart = autostart_path()?; let autostart = autostart_path()?;
if autostart.exists() { if autostart.exists() {
std::fs::remove_file(&autostart) remove_file(&autostart)?;
.with_context(|| format!("Failed to remove {}", autostart.display()))?;
println!("Removed {}", autostart.display()); println!("Removed {}", autostart.display());
} else { } else {
println!("No autostart file at {}", autostart.display()); println!("No autostart file at {}", autostart.display());
} }
uninstall_desktop()?; uninstall_desktop()?;
uninstall_icon()?;
println!("{} Autostart removed.", "".green().bold()); println!("{} Autostart removed.", "".green().bold());
Ok(()) Ok(())
+46 -7
View File
@@ -1,6 +1,8 @@
use crate::config::{Config, Launcher}; use crate::config::{Config, Launcher};
use anyhow::Result; use anyhow::Result;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use pelite::pe64::Pe as _;
use pelite::pe32::Pe as _;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex}; use std::sync::{LazyLock, Mutex};
@@ -123,8 +125,7 @@ static STORE_TITLES: LazyLock<HashMap<PathBuf, String>> =
/// return a map of absolute install directory → game title. /// return a map of absolute install directory → game title.
fn build_store_titles() -> HashMap<PathBuf, String> { fn build_store_titles() -> HashMap<PathBuf, String> {
let mut map = HashMap::new(); let mut map = HashMap::new();
let Ok(home) = std::env::var("HOME") else { return map }; let Some(home) = dirs::home_dir() else { return map };
let home = PathBuf::from(home);
// Legendary standalone + Heroic's bundled copy (native and Flatpak). // Legendary standalone + Heroic's bundled copy (native and Flatpak).
let legendary_candidates = [ let legendary_candidates = [
@@ -279,7 +280,9 @@ fn scan_exe_dir(
/// Epic `.egstore/*.item` JSON files at the game's installation root. /// Epic `.egstore/*.item` JSON files at the game's installation root.
/// 4. Launcher path — reads the game name from well-known directory /// 4. Launcher path — reads the game name from well-known directory
/// structures laid down by the launcher (e.g. `Epic Games/<Name>/`). /// structures laid down by the launcher (e.g. `Epic Games/<Name>/`).
/// 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 /// No name generation — if the directory name is unknown, it is used
/// as-is rather than being fabricated from the exe filename. /// as-is rather than being fabricated from the exe filename.
/// ///
@@ -322,9 +325,14 @@ fn resolve_uncached(exe_path: &Path) -> String {
return name; 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. // 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 /// 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<String> {
None 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<String> {
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] = &[ const GENERIC_DIRS: &[&str] = &[
"bin", "binaries", "x64", "x86", "win64", "win32", "retail", "bin", "binaries", "x64", "x86", "win64", "win32", "retail",
"shipping", "game", "runtime", "_retail_", "_commonredist", "shipping", "game", "runtime", "_retail_", "_commonredist",
@@ -473,7 +512,7 @@ pub fn run(config: &Config, extra_dirs: &[PathBuf], apply: bool) -> Result<()> {
} }
fn default_roots() -> Vec<PathBuf> { fn default_roots() -> Vec<PathBuf> {
let Ok(home) = std::env::var("HOME").map(PathBuf::from) else { let Some(home) = dirs::home_dir() else {
return Vec::new(); return Vec::new();
}; };
vec![ vec![
+99 -77
View File
@@ -1,10 +1,10 @@
use crate::{ use crate::{
config::Config, detect, diagnose, launcher, proton, service, config::Config, detect, diagnose, launcher, proton, autostart,
theme::{ theme::{
btn_accent, btn_danger, btn_ghost, card_style, icon, section_heading, sub_card_style, 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, 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 anyhow::Result;
use iced::widget::{ use iced::widget::{
@@ -77,9 +77,9 @@ pub enum Message {
BrowseCompatDir, BrowseCompatDir,
BrowseCompatDirDone(Option<String>), BrowseCompatDirDone(Option<String>),
SaveSettings, SaveSettings,
ServiceInstall, AutostartInstall,
ServiceUninstall, AutostartUninstall,
ServiceActionDone(Result<(), String>), AutostartDone(Result<(), String>),
LaunchProtontricks, LaunchProtontricks,
// Close dialog // Close dialog
CloseRequested(iced::window::Id), CloseRequested(iced::window::Id),
@@ -112,8 +112,8 @@ struct Dashboard {
settings_proton_version: String, settings_proton_version: String,
settings_compat_dir: String, settings_compat_dir: String,
proton_versions: Vec<String>, proton_versions: Vec<String>,
service_busy: bool, autostart_busy: bool,
service_status: String, autostart_status: String,
// Close dialog // Close dialog
close_dialog_open: bool, close_dialog_open: bool,
close_action: Arc<Mutex<Option<CloseAction>>>, close_action: Arc<Mutex<Option<CloseAction>>>,
@@ -147,8 +147,8 @@ impl Dashboard {
settings_proton_version, settings_proton_version,
settings_compat_dir, settings_compat_dir,
proton_versions, proton_versions,
service_busy: false, autostart_busy: false,
service_status: String::new(), autostart_status: String::new(),
close_dialog_open: false, close_dialog_open: false,
close_action, close_action,
} }
@@ -163,19 +163,21 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
.map(|l| (l.name.clone(), l.process_pattern.clone())) .map(|l| (l.name.clone(), l.process_pattern.clone()))
.collect(); .collect();
Task::perform( Task::perform(
async_blocking(move || { async {
let mut map = HashMap::new(); tokio::task::spawn_blocking(move || {
for (name, pattern) in launchers { let mut map = HashMap::new();
let running = std::process::Command::new("pgrep") for (name, pattern) in launchers {
.args(["-f", &pattern]) let running = std::process::Command::new("pgrep")
.stdout(std::process::Stdio::null()) .args(["-f", &pattern])
.stderr(std::process::Stdio::null()) .stdout(std::process::Stdio::null())
.status() .stderr(std::process::Stdio::null())
.is_ok_and(|s| s.success()); .status()
map.insert(name, running); .is_ok_and(|s| s.success());
} map.insert(name, running);
map }
}), map
}).await.expect("blocking task panicked")
},
Message::PollDone, Message::PollDone,
) )
} }
@@ -197,8 +199,12 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
} }
Message::AddLauncher => { Message::AddLauncher => {
state.context_menu = None; state.context_menu = None;
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); let config = state.config.clone();
let _ = std::process::Command::new(exe).arg("setup").spawn(); std::thread::spawn(move || {
if let Err(e) = crate::setup::run_new(&config) {
eprintln!("umutray: setup picker failed: {e}");
}
});
Task::none() Task::none()
} }
Message::Launch(name) => { Message::Launch(name) => {
@@ -211,7 +217,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
let l = l.clone(); let l = l.clone();
let name2 = name.clone(); let name2 = name.clone();
Task::perform( 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), move |res| Message::LaunchDone(name2.clone(), res),
) )
} }
@@ -236,7 +242,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
let name2 = name.clone(); let name2 = name.clone();
state.running.insert(name, false); state.running.insert(name, false);
Task::perform( 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), move |res| Message::KillDone(name2.clone(), res),
) )
} }
@@ -257,9 +263,11 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
let lname2 = lname.clone(); let lname2 = lname.clone();
let gname2 = gname.clone(); let gname2 = gname.clone();
return Task::perform( return Task::perform(
async_blocking(move || { async {
launcher::play_game(&config, &l, &g).map_err(|e| e.to_string()) 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), move |res| Message::PlayDone(lname2.clone(), gname2.clone(), res),
); );
} }
@@ -273,11 +281,16 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
Task::none() Task::none()
} }
Message::Setup(name) => { Message::Setup(name) => {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); let config = state.config.clone();
let _ = std::process::Command::new(exe) let name2 = name.clone();
.arg("setup") std::thread::spawn(move || {
.arg(&name) if let Some(l) = config.find(&name2) {
.spawn(); let l = l.clone();
if let Err(e) = crate::setup::run(&config, &l) {
eprintln!("umutray: setup for {} failed: {e}", l.name);
}
}
});
Task::none() Task::none()
} }
Message::ToggleGameMode(lname, gname) => { Message::ToggleGameMode(lname, gname) => {
@@ -304,9 +317,11 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
state.last_error = None; state.last_error = None;
let config = state.config.clone(); let config = state.config.clone();
Task::perform( Task::perform(
async_blocking(move || { async {
crate::proton::install_latest(&config).map_err(|e| e.to_string()) tokio::task::spawn_blocking(move || {
}), crate::proton::install_latest(&config).map_err(|e| e.to_string())
}).await.expect("blocking task panicked")
},
Message::ProtonDone, Message::ProtonDone,
) )
} }
@@ -346,11 +361,16 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
} }
Message::RerunSetup(name) => { Message::RerunSetup(name) => {
state.context_menu = None; state.context_menu = None;
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); let config = state.config.clone();
let _ = std::process::Command::new(exe) let name2 = name.clone();
.arg("setup") std::thread::spawn(move || {
.arg(&name) if let Some(l) = config.find(&name2) {
.spawn(); let l = l.clone();
if let Err(e) = crate::setup::run(&config, &l) {
eprintln!("umutray: setup for {} failed: {e}", l.name);
}
}
});
Task::none() Task::none()
} }
Message::RemoveLauncher(name) => { Message::RemoveLauncher(name) => {
@@ -368,7 +388,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
state.detect_result = "Scanning…".into(); state.detect_result = "Scanning…".into();
let config = state.config.clone(); let config = state.config.clone();
Task::perform( 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, Message::DetectDone,
) )
} }
@@ -399,14 +419,16 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
let config = state.config.clone(); let config = state.config.clone();
let lname = name.clone(); let lname = name.clone();
Task::perform( Task::perform(
async_blocking(move || { async {
diagnose::run_checks(&config, Some(&name)) tokio::task::spawn_blocking(move || {
.unwrap_or_else(|e| vec![diagnose::CheckResult { diagnose::run_checks(&config, Some(&name))
label: "error".into(), .unwrap_or_else(|e| vec![diagnose::CheckResult {
pass: false, label: "error".into(),
detail: e.to_string(), pass: false,
}]) detail: e.to_string(),
}), }])
}).await.expect("blocking task panicked")
},
move |checks| Message::DiagnoseDone(lname.clone(), checks), move |checks| Message::DiagnoseDone(lname.clone(), checks),
) )
} }
@@ -476,7 +498,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
let lname2 = lname.clone(); let lname2 = lname.clone();
state.scan_busy.insert(lname); state.scan_busy.insert(lname);
return Task::perform( 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), move |hits| Message::ScanGamesDone(lname2.clone(), hits),
); );
} }
@@ -516,7 +538,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
.unwrap_or_default(); .unwrap_or_default();
let lname2 = lname.clone(); let lname2 = lname.clone();
Task::perform( 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), move |res| Message::BrowseGameExeDone(lname2.clone(), res),
) )
} }
@@ -550,7 +572,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
state.settings_compat_dir = state.settings_compat_dir =
state.config.proton_compat_dir.to_string_lossy().into_owned(); state.config.proton_compat_dir.to_string_lossy().into_owned();
state.proton_versions = proton::list_installed(&state.config); state.proton_versions = proton::list_installed(&state.config);
state.service_status = String::new(); state.autostart_status = String::new();
Task::none() Task::none()
} }
Message::HideSettings => { Message::HideSettings => {
@@ -566,7 +588,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
Task::none() Task::none()
} }
Message::BrowseCompatDir => Task::perform( 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,
), ),
Message::BrowseCompatDirDone(path) => { Message::BrowseCompatDirDone(path) => {
@@ -603,7 +625,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
match state.config.set_globals(Some(version_key), Some(compat)) { match state.config.set_globals(Some(version_key), Some(compat)) {
Ok(()) => { Ok(()) => {
state.proton_versions = proton::list_installed(&state.config); state.proton_versions = proton::list_installed(&state.config);
state.service_status = "Settings saved.".into(); state.autostart_status = "Settings saved.".into();
} }
Err(e) => { Err(e) => {
state.last_error = Some(format!("Save failed: {e}")); state.last_error = Some(format!("Save failed: {e}"));
@@ -619,34 +641,34 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
}); });
Task::none() Task::none()
} }
Message::ServiceInstall => { Message::AutostartInstall => {
state.service_busy = true; state.autostart_busy = true;
state.service_status = "Installing autostart…".into(); state.autostart_status = "Installing autostart…".into();
Task::perform( Task::perform(
async_blocking(|| service::install().map_err(|e| e.to_string())), async { tokio::task::spawn_blocking(|| autostart::install().map_err(|e| e.to_string())).await.expect("blocking task panicked") },
Message::ServiceActionDone, Message::AutostartDone,
) )
} }
Message::ServiceUninstall => { Message::AutostartUninstall => {
state.service_busy = true; state.autostart_busy = true;
state.service_status = "Removing autostart…".into(); state.autostart_status = "Removing autostart…".into();
Task::perform( Task::perform(
async_blocking(|| service::uninstall().map_err(|e| e.to_string())), async { tokio::task::spawn_blocking(|| autostart::uninstall().map_err(|e| e.to_string())).await.expect("blocking task panicked") },
Message::ServiceActionDone, Message::AutostartDone,
) )
} }
Message::ServiceActionDone(res) => { Message::AutostartDone(res) => {
state.service_busy = false; state.autostart_busy = false;
match res { match res {
Ok(()) => { Ok(()) => {
state.service_status = if service_is_installed() { state.autostart_status = if autostart_is_installed() {
"Autostart enabled — starts on next login.".into() "Autostart enabled — starts on next login.".into()
} else { } else {
"Autostart removed.".into() "Autostart removed.".into()
}; };
} }
Err(e) => { Err(e) => {
state.service_status = format!("Failed: {e}"); state.autostart_status = format!("Failed: {e}");
} }
} }
Task::none() Task::none()
@@ -697,7 +719,7 @@ fn toggle_flag(
let _ = config.save(); let _ = config.save();
} }
fn service_is_installed() -> bool { fn autostart_is_installed() -> bool {
dirs::home_dir() dirs::home_dir()
.is_some_and(|h| h.join(".config/autostart/umutray.desktop").exists()) .is_some_and(|h| h.join(".config/autostart/umutray.desktop").exists())
} }
@@ -1626,25 +1648,25 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
); );
// ── Autostart section ───────────────────────────────────────────────── // ── Autostart section ─────────────────────────────────────────────────
let installed = service_is_installed(); let installed = autostart_is_installed();
let svc_status_color = if installed { GREEN } else { MUTED }; let svc_status_color = if installed { GREEN } else { MUTED };
let svc_status_text = if installed { "Enabled — starts on login" } else { "Disabled" }; 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_status_icon = if installed { "\u{f26a}" } else { "\u{f28a}" };
let svc_install_btn = button( 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), .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) .style(btn_accent)
.padding([7, 14]); .padding([7, 14]);
let svc_uninstall_btn = button( 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), .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) .style(btn_danger)
.padding([7, 14]); .padding([7, 14]);
@@ -1659,7 +1681,7 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
svc_install_btn, svc_install_btn,
svc_uninstall_btn, svc_uninstall_btn,
].align_y(Alignment::Center).spacing(8), ].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), color: Some(DIM),
}), }),
].spacing(6).into(), ].spacing(6).into(),
+33 -7
View File
@@ -34,11 +34,29 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> {
let proton_path = resolve_proton_path(config, launcher); let proton_path = resolve_proton_path(config, launcher);
std::process::Command::new("umu-run") // Propagate overlay env vars from any configured game so that games
.env("WINEPREFIX", &launcher.prefix_dir) // 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<OsString>) = 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("GAMEID", &launcher.gameid)
.env("PROTONPATH", &proton_path) .env("PROTONPATH", &proton_path);
.arg(&exe) if any_mangohud {
cmd.env("MANGOHUD", "1");
}
cmd.args(&args)
.spawn() .spawn()
.context( .context(
"Failed to spawn umu-run. Is it installed?\n\ "Failed to spawn umu-run. Is it installed?\n\
@@ -48,9 +66,9 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> {
Ok(()) Ok(())
} }
/// Launch a game installed through `launcher`, wrapped in the per-game /// Launch a game directly via umu-run, wrapped in the per-game overlays
/// overlays (gamescope, gamemoderun, MANGOHUD). The launcher itself is /// (gamescope, gamemoderun, MANGOHUD). Ensures the parent launcher is
/// never wrapped — only games run through this path pick up overlays. /// running first so the game can authenticate online.
pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<()> { pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<()> {
let exe = game.full_exe_path(launcher); let exe = game.full_exe_path(launcher);
if !exe.exists() { 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 proton_path = resolve_proton_path(config, launcher);
let (prog, args) = build_wrapped_argv(&exe, game); let (prog, args) = build_wrapped_argv(&exe, game);
+15 -11
View File
@@ -6,7 +6,7 @@ mod diagnose;
mod gui; mod gui;
mod launcher; mod launcher;
mod proton; mod proton;
mod service; mod autostart;
mod setup; mod setup;
mod theme; mod theme;
mod tray; mod tray;
@@ -109,10 +109,10 @@ enum Commands {
action: ConfigAction, action: ConfigAction,
}, },
/// Manage the XDG autostart entry that starts the tray on login /// Manage the XDG autostart and desktop entries
Service { Autostart {
#[command(subcommand)] #[command(subcommand)]
action: ServiceAction, action: AutostartAction,
}, },
} }
@@ -222,7 +222,7 @@ enum ConfigAction {
} }
#[derive(Subcommand)] #[derive(Subcommand)]
enum ServiceAction { enum AutostartAction {
/// Write ~/.config/autostart/umutray.desktop so the tray starts on login (includes app menu entry) /// Write ~/.config/autostart/umutray.desktop so the tray starts on login (includes app menu entry)
Install, Install,
/// Remove the autostart entry and app menu entry /// Remove the autostart entry and app menu entry
@@ -239,6 +239,10 @@ fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
let config = config::Config::load()?; 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) { match cli.command.unwrap_or(Commands::Tray) {
Commands::Tray => tray::run(&config)?, Commands::Tray => tray::run(&config)?,
@@ -428,12 +432,12 @@ fn main() -> Result<()> {
} }
}, },
Commands::Service { action } => match action { Commands::Autostart { action } => match action {
ServiceAction::Install => service::install()?, AutostartAction::Install => autostart::install()?,
ServiceAction::Uninstall => service::uninstall()?, AutostartAction::Uninstall => autostart::uninstall()?,
ServiceAction::Status => service::status()?, AutostartAction::Status => autostart::status()?,
ServiceAction::InstallDesktop => service::install_desktop()?, AutostartAction::InstallDesktop => autostart::install_desktop()?,
ServiceAction::UninstallDesktop => service::uninstall_desktop()?, AutostartAction::UninstallDesktop => autostart::uninstall_desktop()?,
}, },
} }
+6 -6
View File
@@ -4,7 +4,7 @@ use crate::{
btn_accent, btn_ghost, card_style, icon, sub_card_style, surface_bg, ACCENT, btn_accent, btn_ghost, card_style, icon, sub_card_style, surface_bg, ACCENT,
DIM, GREEN, MUTED, RED, SURFACE_RAISED, DIM, GREEN, MUTED, RED, SURFACE_RAISED,
}, },
util::{async_blocking, pick_folder}, util::pick_folder,
}; };
use anyhow::Result; use anyhow::Result;
use iced::widget::{ use iced::widget::{
@@ -154,7 +154,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
Task::none() Task::none()
} }
Message::BrowsePrefix => Task::perform( 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,
), ),
Message::BrowsePrefixDone(path) => { Message::BrowsePrefixDone(path) => {
@@ -204,7 +204,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
.clone(); .clone();
let progress = state.download.clone(); let progress = state.download.clone();
return Task::perform( 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, Message::PrepareDone,
); );
} }
@@ -259,7 +259,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
.clone(); .clone();
let progress = state.download.clone(); let progress = state.download.clone();
Task::perform( 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, Message::PrepareDone,
) )
} }
@@ -303,7 +303,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
.clone(); .clone();
let progress = state.download.clone(); let progress = state.download.clone();
Task::perform( 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, Message::PrepareDone,
) )
} }
@@ -346,7 +346,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
.expect("launcher set before install"); .expect("launcher set before install");
let log = state.log.clone(); let log = state.log.clone();
Task::perform( 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, Message::InstallDone,
) )
} }
+32 -26
View File
@@ -1,33 +1,39 @@
use crate::{config::Config, launcher}; use crate::{config::Config, launcher};
use anyhow::Result; use anyhow::Result;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
fn spawn_setup(name: &str) { fn spawn_setup(config: &Config, name: &str) {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); let config = config.clone();
if let Err(e) = std::process::Command::new(exe) let name = name.to_owned();
.arg("setup") thread::spawn(move || {
.arg(name) if let Some(l) = config.find(&name) {
.spawn() let l = l.clone();
{ if let Err(e) = crate::setup::run(&config, &l) {
eprintln!("umutray: failed to launch setup for {name}: {e}"); eprintln!("umutray: setup for {name} failed: {e}");
} }
}
});
} }
fn spawn_gui() { fn spawn_gui(config: &Config) {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); let config = config.clone();
if let Err(e) = std::process::Command::new(exe).arg("gui").spawn() { thread::spawn(move || {
eprintln!("umutray: failed to launch dashboard: {e}"); match crate::gui::run(&config) {
} Ok(_) => {}
Err(e) => eprintln!("umutray: failed to launch dashboard: {e}"),
}
});
} }
fn spawn_setup_picker() { fn spawn_setup_picker(config: &Config) {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("umutray")); let config = config.clone();
if let Err(e) = std::process::Command::new(exe).arg("setup").spawn() { thread::spawn(move || {
eprintln!("umutray: failed to launch setup picker: {e}"); if let Err(e) = crate::setup::run_new(&config) {
} eprintln!("umutray: failed to launch setup picker: {e}");
}
});
} }
enum GameFlag { enum GameFlag {
@@ -79,7 +85,7 @@ pub struct UmuTray {
pub config: Config, pub config: Config,
/// Per-launcher running state keyed by launcher.name /// Per-launcher running state keyed by launcher.name
pub running: HashMap<String, bool>, pub running: HashMap<String, bool>,
/// 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(). /// cleanly instead of yanking it off the bus via exit().
pub handle: Option<ksni::Handle<UmuTray>>, pub handle: Option<ksni::Handle<UmuTray>>,
} }
@@ -90,7 +96,7 @@ impl ksni::Tray for UmuTray {
} }
fn icon_name(&self) -> String { fn icon_name(&self) -> String {
"applications-games".into() "umutray".into()
} }
fn title(&self) -> String { fn title(&self) -> String {
@@ -107,7 +113,7 @@ impl ksni::Tray for UmuTray {
StandardItem { StandardItem {
label: "Open Dashboard".into(), label: "Open Dashboard".into(),
icon_name: "applications-games".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() ..Default::default()
} }
.into(), .into(),
@@ -126,8 +132,8 @@ impl ksni::Tray for UmuTray {
StandardItem { StandardItem {
label: format!("Setup {display}…"), label: format!("Setup {display}…"),
icon_name: "document-new".into(), icon_name: "document-new".into(),
activate: Box::new(move |_this: &mut Self| { activate: Box::new(move |this: &mut Self| {
spawn_setup(&setup_name); spawn_setup(&this.config, &setup_name);
}), }),
..Default::default() ..Default::default()
} }
@@ -245,7 +251,7 @@ impl ksni::Tray for UmuTray {
StandardItem { StandardItem {
label: "Add Launcher…".into(), label: "Add Launcher…".into(),
icon_name: "list-add".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() ..Default::default()
} }
.into(), .into(),
+11 -52
View File
@@ -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<T, F>(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 /// 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<String> { pub fn pick_folder(title: &str) -> Option<String> {
for (cmd, args) in [ rfd::FileDialog::new()
("zenity", vec!["--file-selection", "--directory", "--title", title]), .set_title(title)
("kdialog", vec!["--getexistingdirectory", "/home", "--title", title]), .pick_folder()
] { .map(|p| p.to_string_lossy().into_owned())
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
} }
/// Open a native file picker dialog starting in `start_dir`, or None if /// 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<String> { pub fn pick_file(title: &str, start_dir: &str) -> Option<String> {
// zenity uses --filename with a trailing slash to open a directory rfd::FileDialog::new()
let start_slash = format!("{}/", start_dir.trim_end_matches('/')); .set_title(title)
let zenity_args = vec![ .set_directory(start_dir)
"--file-selection", .pick_file()
"--title", .map(|p| p.to_string_lossy().into_owned())
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
} }
+1 -1
View File
@@ -2,7 +2,7 @@
Name=umutray Name=umutray
Comment=Wine launcher manager for Windows game launchers Comment=Wine launcher manager for Windows game launchers
Exec=umutray gui Exec=umutray gui
Icon=applications-games Icon=umutray
Type=Application Type=Application
Categories=Game; Categories=Game;
Keywords=wine;proton;gaming;launcher; Keywords=wine;proton;gaming;launcher;