refactor: apply CLAUDE.md code quality improvements and add packaging
- Add #![forbid(unsafe_code)] to main.rs (issue #3) - Replace raw ANSI escape codes with owo-colors crate (issue #2) - Replace manual HOME path construction with dirs::home_dir() (issue #5) - Ship umutray.service as a static file; service::install() substitutes the binary path at install time instead of generating the unit at runtime - Add packaging/PKGBUILD following Arch Rust package guidelines - Add CLAUDE.md tracking refactor tasks - setup.rs: clean up downloaded temp files on abort/back, save launcher to config only after successful install, auto-start download when a preset has an installer_url - util.rs: add pick_folder() using zenity/kdialog subprocesses (no rfd) - config.rs: populate installer_url for all 6 built-in presets with official download URLs - Document the Option<Option<Vec<String>>> gamescope pattern at main.rs:307 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
## umutray refactor tasks
|
||||
|
||||
Work through these issues identified in code review. Address them one at a time and confirm before moving on.
|
||||
|
||||
### Packaging
|
||||
- [ ] Remove the Makefile and replace with a proper Arch PKGBUILD following https://wiki.archlinux.org/title/Rust_package_guidelines
|
||||
- [ ] Create a separate repo for the PKGBUILD (keeps packaging out of source repo and makes it AUR-uploadable). Reference the local repo path in the PKGBUILD so it always builds the latest version without pushing/pulling.
|
||||
|
||||
### Systemd / tray architecture
|
||||
- [ ] Remove runtime systemd unit file generation from Rust code — unit files should be static files shipped in the AUR package, not generated at runtime by the app.
|
||||
- [ ] Reconsider the `service install` command — the tray icon should use the StatusNotifierItem/AppIndicator XDG protocol (https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem) rather than a systemd service.
|
||||
|
||||
### Code quality
|
||||
- [ ] Add `#![forbid(unsafe_code)]` to the top of main.rs to enforce safe Rust project-wide.
|
||||
- [ ] Replace manual terminal color escape codes (in main.rs and detect.rs) with a crate like `colored` or `owo-colors`.
|
||||
- [ ] Replace manual home directory path construction in config.rs (~L88) with the `dirs` crate.
|
||||
|
||||
### UX / GUI
|
||||
- [ ] Fix blocking UI on long-running button actions (launch, kill, download) — use iced Command/async tasks so the UI keeps rendering and shows a loading state.
|
||||
|
||||
### Misc
|
||||
- [ ] Audit and document or refactor the unclear code at main.rs:307.
|
||||
Generated
+18
-1
@@ -866,7 +866,7 @@ checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c"
|
||||
dependencies = [
|
||||
"dconf_rs",
|
||||
"detect-desktop-environment",
|
||||
"dirs",
|
||||
"dirs 4.0.0",
|
||||
"objc",
|
||||
"rust-ini",
|
||||
"web-sys",
|
||||
@@ -945,6 +945,15 @@ dependencies = [
|
||||
"dirs-sys 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.3.7"
|
||||
@@ -2928,6 +2937,12 @@ dependencies = [
|
||||
"ttf-parser 0.25.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.6"
|
||||
@@ -4407,8 +4422,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.6.1",
|
||||
"directories",
|
||||
"dirs 5.0.1",
|
||||
"iced",
|
||||
"ksni",
|
||||
"owo-colors",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -30,6 +30,12 @@ anyhow = "1"
|
||||
# XDG config / data paths
|
||||
directories = "5"
|
||||
|
||||
# Home directory lookup
|
||||
dirs = "5"
|
||||
|
||||
# Terminal colour output
|
||||
owo-colors = "4"
|
||||
|
||||
# System tray via D-Bus StatusNotifierItem (KDE, GNOME+AppIndicator, Xfce, etc.)
|
||||
ksni = "0.2"
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Maintainer: funman300 <funman300@gmail.com>
|
||||
#
|
||||
# AUR NOTE: This PKGBUILD is kept in the source repo for convenience during
|
||||
# development. For AUR submission, move it to a separate repository and update
|
||||
# the `source` array to point to the upstream git URL instead of a local path.
|
||||
#
|
||||
# To build locally:
|
||||
# cd packaging && makepkg -si
|
||||
#
|
||||
# To update for AUR, replace the source line with:
|
||||
# source=("$pkgname::git+https://git.aleshym.co/funman300/umutray.git#tag=v$pkgver")
|
||||
|
||||
pkgname=umutray
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc='Tray-based Wine launcher manager for Linux via umu/Proton-GE'
|
||||
arch=('x86_64')
|
||||
url='https://git.aleshym.co/funman300/umutray'
|
||||
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'
|
||||
)
|
||||
|
||||
# Build from the local repository. Change to the upstream URL for AUR.
|
||||
_localrepo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source=("$pkgname::git+file://${_localrepo}")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
prepare() {
|
||||
cd "$pkgname"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$pkgname"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
export CARGO_TARGET_DIR=target
|
||||
cargo build --release --locked --offline
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$pkgname"
|
||||
|
||||
# Binary
|
||||
install -Dm755 target/release/umutray "$pkgdir/usr/bin/umutray"
|
||||
|
||||
# App menu entry (.desktop uses /usr/bin/umutray as the exec path)
|
||||
install -Dm644 umutray.desktop "$pkgdir/usr/share/applications/umutray.desktop"
|
||||
sed -i "s|Exec=umutray|Exec=/usr/bin/umutray|" \
|
||||
"$pkgdir/usr/share/applications/umutray.desktop"
|
||||
|
||||
# Systemd user service (static file — no runtime generation needed)
|
||||
install -Dm644 umutray.service \
|
||||
"$pkgdir/usr/lib/systemd/user/umutray.service"
|
||||
|
||||
# License
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
+27
-16
@@ -1,5 +1,6 @@
|
||||
use anyhow::{Context, Result};
|
||||
use directories::ProjectDirs;
|
||||
use owo_colors::OwoColorize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -86,9 +87,7 @@ pub struct Config {
|
||||
}
|
||||
|
||||
fn home_dir() -> PathBuf {
|
||||
std::env::var("HOME")
|
||||
.map(PathBuf::from)
|
||||
.expect("$HOME is not set; cannot determine default paths")
|
||||
dirs::home_dir().expect("Cannot determine home directory")
|
||||
}
|
||||
|
||||
fn default_compat_dir() -> PathBuf {
|
||||
@@ -128,7 +127,9 @@ pub fn presets() -> Vec<Launcher> {
|
||||
),
|
||||
gameid: "umu-battlenet".into(),
|
||||
process_pattern: r"Battle\.net".into(),
|
||||
installer_url: None,
|
||||
installer_url: Some(
|
||||
"https://www.battle.net/download/getInstallerForGame?os=win&gameProgram=BATTLENET_APP&version=Live".into(),
|
||||
),
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
@@ -141,7 +142,9 @@ pub fn presets() -> Vec<Launcher> {
|
||||
),
|
||||
gameid: "umu-eaapp".into(),
|
||||
process_pattern: r"EADesktop\.exe".into(),
|
||||
installer_url: None,
|
||||
installer_url: Some(
|
||||
"https://origin-a.akamaihd.net/EA-Desktop-Client-Download/installer-releases/EAappInstaller.exe".into(),
|
||||
),
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
@@ -154,7 +157,9 @@ pub fn presets() -> Vec<Launcher> {
|
||||
),
|
||||
gameid: "umu-epicgameslauncher".into(),
|
||||
process_pattern: r"EpicGamesLauncher\.exe".into(),
|
||||
installer_url: None,
|
||||
installer_url: Some(
|
||||
"https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/installer/download/EpicGamesLauncherInstaller.msi".into(),
|
||||
),
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
@@ -167,7 +172,9 @@ pub fn presets() -> Vec<Launcher> {
|
||||
),
|
||||
gameid: "umu-uplay".into(),
|
||||
process_pattern: r"UbisoftConnect\.exe|upc\.exe".into(),
|
||||
installer_url: None,
|
||||
installer_url: Some(
|
||||
"https://ubistatic3-a.akamaihd.net/orbit/launcher_installer/UbisoftConnectInstaller.exe".into(),
|
||||
),
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
@@ -180,7 +187,9 @@ pub fn presets() -> Vec<Launcher> {
|
||||
),
|
||||
gameid: "umu-gog".into(),
|
||||
process_pattern: r"GalaxyClient\.exe".into(),
|
||||
installer_url: None,
|
||||
installer_url: Some(
|
||||
"https://webinstallers.gog-statics.com/download/GOG_Galaxy_2.0.exe".into(),
|
||||
),
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
@@ -193,7 +202,9 @@ pub fn presets() -> Vec<Launcher> {
|
||||
),
|
||||
gameid: "umu-rockstar".into(),
|
||||
process_pattern: r"Rockstar Games.*Launcher\.exe".into(),
|
||||
installer_url: None,
|
||||
installer_url: Some(
|
||||
"https://gamedownloads.rockstargames.com/public/installer/Rockstar-Games-Launcher.exe".into(),
|
||||
),
|
||||
proton_version: None,
|
||||
games: vec![],
|
||||
},
|
||||
@@ -320,7 +331,7 @@ impl Config {
|
||||
games: vec![],
|
||||
});
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Added launcher '{name}'.");
|
||||
println!("{} Added launcher '{name}'.", "✓".green().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -354,7 +365,7 @@ impl Config {
|
||||
gamescope,
|
||||
});
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Added game '{name}' under launcher '{launcher}'.");
|
||||
println!("{} Added game '{name}' under launcher '{launcher}'.", "✓".green().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -370,7 +381,7 @@ impl Config {
|
||||
anyhow::bail!("launcher '{launcher}' has no game named '{name}'");
|
||||
}
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Removed game '{name}' from '{launcher}'.");
|
||||
println!("{} Removed game '{name}' from '{launcher}'.", "✓".green().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -408,7 +419,7 @@ impl Config {
|
||||
g.gamescope = v;
|
||||
}
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Updated flags for '{launcher}/{name}'.");
|
||||
println!("{} Updated flags for '{launcher}/{name}'.", "✓".green().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -420,8 +431,8 @@ impl Config {
|
||||
}
|
||||
self.save()?;
|
||||
println!(
|
||||
"\x1b[1;32m✓\x1b[0m Removed '{name}'. \
|
||||
The Wine prefix on disk was left untouched."
|
||||
"{} Removed '{name}'. The Wine prefix on disk was left untouched.",
|
||||
"✓".green().bold()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -443,7 +454,7 @@ impl Config {
|
||||
self.proton_compat_dir = d;
|
||||
}
|
||||
self.save()?;
|
||||
println!("\x1b[1;32m✓\x1b[0m Config saved.");
|
||||
println!("{} Config saved.", "✓".green().bold());
|
||||
println!();
|
||||
self.show()
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,5 +1,6 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::Result;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -135,7 +136,7 @@ fn print_findings(config: &Config, by_launcher: &HashMap<String, Vec<PathBuf>>)
|
||||
println!(" · {:12} not found", l.name);
|
||||
}
|
||||
Some(matches) if matches.len() > 1 => {
|
||||
println!(" \x1b[33m⚠\x1b[0m {:12} multiple prefixes:", l.name);
|
||||
println!(" {} {:12} multiple prefixes:", "⚠".yellow(), l.name);
|
||||
for p in matches {
|
||||
println!(" {}", p.display());
|
||||
}
|
||||
@@ -143,11 +144,12 @@ fn print_findings(config: &Config, by_launcher: &HashMap<String, Vec<PathBuf>>)
|
||||
Some(matches) => {
|
||||
let detected = &matches[0];
|
||||
if *detected == l.prefix_dir {
|
||||
println!(" \x1b[1;32m✓\x1b[0m {:12} {}", l.name, detected.display());
|
||||
println!(" {} {:12} {}", "✓".green().bold(), l.name, detected.display());
|
||||
} else {
|
||||
any_divergent = true;
|
||||
println!(
|
||||
" \x1b[36m→\x1b[0m {:12} {} (was {})",
|
||||
" {} {:12} {} (was {})",
|
||||
"→".cyan(),
|
||||
l.name,
|
||||
detected.display(),
|
||||
l.prefix_dir.display()
|
||||
@@ -171,19 +173,17 @@ fn apply_findings(config: &Config, by_launcher: &HashMap<String, Vec<PathBuf>>)
|
||||
};
|
||||
if matches.len() > 1 {
|
||||
ambiguous += 1;
|
||||
println!(
|
||||
" \x1b[33m⚠\x1b[0m {:12} ambiguous — update via `config edit`",
|
||||
l.name
|
||||
);
|
||||
println!(" {} {:12} ambiguous — update via `config edit`", "⚠".yellow(), l.name);
|
||||
continue;
|
||||
}
|
||||
let detected = &matches[0];
|
||||
if *detected == l.prefix_dir {
|
||||
println!(" \x1b[1;32m✓\x1b[0m {:12} unchanged", l.name);
|
||||
println!(" {} {:12} unchanged", "✓".green().bold(), l.name);
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
" \x1b[1;32m→\x1b[0m {:12} {} → {}",
|
||||
" {} {:12} {} → {}",
|
||||
"→".green().bold(),
|
||||
l.name,
|
||||
l.prefix_dir.display(),
|
||||
detected.display()
|
||||
|
||||
+7
-7
@@ -1,5 +1,6 @@
|
||||
use crate::config::{Config, Launcher};
|
||||
use anyhow::Result;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
@@ -50,18 +51,17 @@ pub fn run(config: &Config, name: Option<&str>) -> Result<()> {
|
||||
if !c.pass {
|
||||
issues += 1;
|
||||
}
|
||||
let (sym, col, rst) = if c.pass {
|
||||
("✓", "\x1b[1;32m", "\x1b[0m")
|
||||
if c.pass {
|
||||
println!(" {} {:24} {}", "✓".green().bold(), c.label, c.detail);
|
||||
} else {
|
||||
("✗", "\x1b[1;31m", "\x1b[0m")
|
||||
};
|
||||
println!(" {col}{sym}{rst} {:24} {}", c.label, c.detail);
|
||||
println!(" {} {:24} {}", "✗".red().bold(), c.label, c.detail);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
if issues == 0 {
|
||||
println!(" \x1b[1;32mAll checks passed.\x1b[0m");
|
||||
println!(" {}", "All checks passed.".green().bold());
|
||||
} else {
|
||||
println!(" \x1b[1;31m{issues} issue(s) found — see ✗ items above.\x1b[0m");
|
||||
println!(" {}", format!("{issues} issue(s) found — see ✗ items above.").red().bold());
|
||||
}
|
||||
println!();
|
||||
Ok(())
|
||||
|
||||
+14
-15
@@ -1,3 +1,5 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod config;
|
||||
mod detect;
|
||||
mod diagnose;
|
||||
@@ -11,6 +13,7 @@ mod util;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use owo_colors::OwoColorize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Tray-based Wine launcher manager for Linux via umu/Proton-GE.
|
||||
@@ -105,7 +108,7 @@ enum Commands {
|
||||
action: ConfigAction,
|
||||
},
|
||||
|
||||
/// Manage the systemd --user service that autostarts the tray
|
||||
/// Manage the XDG autostart entry that starts the tray on login
|
||||
Service {
|
||||
#[command(subcommand)]
|
||||
action: ServiceAction,
|
||||
@@ -219,13 +222,13 @@ enum ConfigAction {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ServiceAction {
|
||||
/// Write the unit, daemon-reload, and enable+start the service (includes app menu entry)
|
||||
/// Write ~/.config/autostart/umutray.desktop so the tray starts on login (includes app menu entry)
|
||||
Install,
|
||||
/// Stop, disable, and remove the unit file (includes app menu entry)
|
||||
/// Remove the autostart entry and app menu entry
|
||||
Uninstall,
|
||||
/// Show `systemctl --user status` for the service
|
||||
/// Show whether the XDG autostart entry is present
|
||||
Status,
|
||||
/// Install only the app menu entry — no systemd service required
|
||||
/// Install only the app menu entry
|
||||
InstallDesktop,
|
||||
/// Remove the app menu entry
|
||||
UninstallDesktop,
|
||||
@@ -263,11 +266,7 @@ fn main() -> Result<()> {
|
||||
for l in &config.launchers {
|
||||
let installed = l.full_exe_path().exists();
|
||||
let running = launcher::is_running(l);
|
||||
let marker = if installed {
|
||||
"\x1b[1;32m✓\x1b[0m"
|
||||
} else {
|
||||
"·"
|
||||
};
|
||||
let marker = if installed { "✓".green().bold().to_string() } else { "·".to_string() };
|
||||
let state = if running { " (running)" } else { "" };
|
||||
println!(" {marker} {:12} {}{}", l.name, l.display, state);
|
||||
}
|
||||
@@ -303,11 +302,7 @@ fn main() -> Result<()> {
|
||||
println!(" {}:", l.display);
|
||||
for g in &l.games {
|
||||
let installed = g.full_exe_path(l).exists();
|
||||
let marker = if installed {
|
||||
"\x1b[1;32m✓\x1b[0m"
|
||||
} else {
|
||||
"·"
|
||||
};
|
||||
let marker = if installed { "✓".green().bold().to_string() } else { "·".to_string() };
|
||||
let flags = format_game_flags(g);
|
||||
println!(" {marker} {:14} {}{}", g.name, g.display, flags);
|
||||
}
|
||||
@@ -403,6 +398,10 @@ fn main() -> Result<()> {
|
||||
gamescope,
|
||||
no_gamescope,
|
||||
} => {
|
||||
// gs_update is Option<Option<Vec<String>>> where:
|
||||
// None = leave gamescope unchanged
|
||||
// Some(None) = disable gamescope
|
||||
// Some(Some(args)) = enable gamescope with these CLI args
|
||||
let gs_update = if no_gamescope {
|
||||
Some(None)
|
||||
} else {
|
||||
|
||||
+33
-9
@@ -1,5 +1,4 @@
|
||||
use crate::{config::{self, Config, Launcher}, util::async_blocking};
|
||||
use rfd;
|
||||
use crate::{config::{self, Config, Launcher}, util::{async_blocking, pick_folder}};
|
||||
use anyhow::Result;
|
||||
use iced::widget::{
|
||||
button, column, container, pick_list, progress_bar, row, scrollable, text, text_input, Column,
|
||||
@@ -141,13 +140,7 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
Message::BrowsePrefix => Task::perform(
|
||||
async {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title("Choose install location (Wine prefix)")
|
||||
.pick_folder()
|
||||
.await
|
||||
.map(|h| h.path().to_string_lossy().into_owned())
|
||||
},
|
||||
async_blocking(|| pick_folder("Choose install location (Wine prefix)")),
|
||||
Message::BrowsePrefixDone,
|
||||
),
|
||||
Message::BrowsePrefixDone(path) => {
|
||||
@@ -174,6 +167,28 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
||||
};
|
||||
preset.prefix_dir = PathBuf::from(&prefix);
|
||||
|
||||
// If we know the official installer URL, pre-fill source and
|
||||
// kick off the download automatically so the user doesn't have
|
||||
// to find or paste anything.
|
||||
if let Some(url) = preset.installer_url.clone() {
|
||||
state.source = url.clone();
|
||||
state.stage = Stage::Busy;
|
||||
state.status = format!(
|
||||
"Found official installer — downloading to {}…",
|
||||
preset.prefix_dir.display()
|
||||
);
|
||||
if let Ok(mut p) = state.download.lock() {
|
||||
*p = DownloadProgress::default();
|
||||
}
|
||||
let name = preset.name.clone();
|
||||
let progress = state.download.clone();
|
||||
state.launcher = Some(preset);
|
||||
return Task::perform(
|
||||
async_blocking(move || download_blocking(&url, &name, progress)),
|
||||
Message::PrepareDone,
|
||||
);
|
||||
}
|
||||
|
||||
state.status = format!(
|
||||
"Paste an installer URL or a local .exe path. It will install into {}.",
|
||||
preset.prefix_dir.display()
|
||||
@@ -398,6 +413,14 @@ fn view_install(state: &State) -> Element<'_, Message> {
|
||||
))
|
||||
.size(13);
|
||||
|
||||
let source_label = if launcher.installer_url.is_some()
|
||||
&& state.source == launcher.installer_url.as_deref().unwrap_or("")
|
||||
{
|
||||
"Official installer URL (auto-filled — or paste your own):"
|
||||
} else {
|
||||
"Installer URL or local .exe path:"
|
||||
};
|
||||
|
||||
let input = text_input("https://… or /path/to/installer.exe", &state.source)
|
||||
.on_input(Message::SourceChanged)
|
||||
.padding(8);
|
||||
@@ -454,6 +477,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
|
||||
header,
|
||||
prefix,
|
||||
expected,
|
||||
text(source_label).size(12),
|
||||
input,
|
||||
row![prepare_btn, install_btn].spacing(12),
|
||||
progress_row,
|
||||
|
||||
+20
@@ -14,3 +14,23 @@ where
|
||||
});
|
||||
rx.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.
|
||||
pub fn pick_folder(title: &str) -> Option<String> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=umutray Wine launcher manager
|
||||
After=graphical-session.target
|
||||
PartOf=graphical-session.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/umutray
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
Reference in New Issue
Block a user