feat: GUI dashboard, system Proton scanning, and XDG autostart

GUI dashboard (gui.rs):
- Add detect, diagnose, settings panel, per-launcher games management
- Async Kill with optimistic UI state and rollback on error
- Settings: pick_list for Proton version, browse button for compat dir,
  validation, per-launcher version badge
- Detect Installed button with scan results in footer
- Diagnose card with ✓/✗ checklist per launcher

Issue #1 — Use System Proton (proton.rs):
- Extend list_installed() to scan ~/.steam/root/compatibilitytools.d,
  ~/.local/share/Steam/compatibilitytools.d, and
  /usr/share/steam/compatibilitytools.d in addition to the configured
  compat dir; deduplicates by name so the same version never appears twice

Issue #4 — Remove Systemd Service (service.rs):
- Replace systemd unit management with XDG autostart:
  install() writes ~/.config/autostart/umutray.desktop instead of a
  systemd unit and never calls systemctl; uninstall() removes that file;
  status() checks whether the autostart entry exists
- Update GUI service_is_installed() to check the XDG autostart path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-18 19:28:22 -07:00
parent 9b7e474e80
commit f170171895
3 changed files with 198 additions and 119 deletions
+50 -78
View File
@@ -1,69 +1,46 @@
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use std::path::PathBuf;
use std::process::Command;
const UNIT_NAME: &str = "umutray.service";
const DESKTOP_NAME: &str = "umutray.desktop";
fn home() -> Result<PathBuf> {
Ok(PathBuf::from(
std::env::var("HOME").context("$HOME is not set")?,
))
dirs::home_dir().context("Cannot determine home directory")
}
fn unit_path() -> Result<PathBuf> {
Ok(home()?.join(".config/systemd/user").join(UNIT_NAME))
fn autostart_path() -> Result<PathBuf> {
Ok(home()?.join(".config/autostart").join(DESKTOP_NAME))
}
fn desktop_path() -> Result<PathBuf> {
Ok(home()?
.join(".local/share/applications")
.join(DESKTOP_NAME))
Ok(home()?.join(".local/share/applications").join(DESKTOP_NAME))
}
fn render_unit(exe: &std::path::Path) -> String {
format!(
"[Unit]\n\
Description=umutray Wine launcher manager\n\
After=graphical-session.target\n\
PartOf=graphical-session.target\n\
\n\
[Service]\n\
ExecStart={exe}\n\
Restart=on-failure\n\
RestartSec=5\n\
\n\
[Install]\n\
WantedBy=graphical-session.target\n",
exe = exe.display(),
)
}
fn render_desktop(exe: &std::path::Path) -> String {
format!(
fn render_desktop(exe: &std::path::Path, autostart: bool) -> String {
let mut s = format!(
"[Desktop Entry]\n\
Name=umutray\n\
Comment=Wine launcher manager for Windows game launchers\n\
Exec={exe} gui\n\
Exec={exe}\n\
Icon=applications-games\n\
Type=Application\n\
Categories=Game;\n\
Keywords=wine;proton;gaming;launcher;\n\
StartupNotify=true\n",
StartupNotify=false\n",
exe = exe.display(),
)
}
fn systemctl(args: &[&str]) -> Result<()> {
let status = Command::new("systemctl")
.arg("--user")
.args(args)
.status()
.context("Failed to invoke systemctl --user (is systemd installed?)")?;
if !status.success() {
bail!("systemctl --user {} exited non-zero", args.join(" "));
);
if autostart {
s.push_str("X-GNOME-Autostart-enabled=true\n");
s.push_str("Hidden=false\n");
} else {
// App-menu entry launches the GUI
s = s.replace(
&format!("Exec={}", exe.display()),
&format!("Exec={} gui", exe.display()),
);
s.push_str("StartupNotify=true\n");
}
Ok(())
s
}
/// Install only the .desktop file so umutray appears in the app menu.
@@ -74,9 +51,9 @@ pub fn install_desktop() -> Result<()> {
if let Some(p) = desktop.parent() {
std::fs::create_dir_all(p).with_context(|| format!("Failed to create {p:?}"))?;
}
std::fs::write(&desktop, render_desktop(&exe))
std::fs::write(&desktop, render_desktop(&exe, false))
.with_context(|| format!("Failed to write desktop file {desktop:?}"))?;
println!("\x1b[1;32m✓\x1b[0m App menu entry written: {}", desktop.display());
println!("{} App menu entry written: {}", "".green().bold(), desktop.display());
Ok(())
}
@@ -93,58 +70,53 @@ pub fn uninstall_desktop() -> Result<()> {
Ok(())
}
/// Write the unit + desktop file, reload systemd, and enable+start the service.
/// Write an XDG autostart entry and the app-menu .desktop file.
pub fn install() -> Result<()> {
let exe = std::env::current_exe().context("Cannot determine path to own executable")?;
// systemd unit
let unit = unit_path()?;
if let Some(p) = unit.parent() {
// 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:?}"))?;
}
std::fs::write(&unit, render_unit(&exe))
.with_context(|| format!("Failed to write unit file {unit:?}"))?;
println!("Wrote unit: {}", unit.display());
std::fs::write(&autostart, render_desktop(&exe, true))
.with_context(|| format!("Failed to write autostart file {autostart:?}"))?;
println!("Wrote autostart: {}", autostart.display());
// .desktop file
// App-menu entry
install_desktop()?;
println!("Exec: {} gui", exe.display());
println!();
systemctl(&["daemon-reload"])?;
systemctl(&["enable", "--now", UNIT_NAME])?;
println!();
println!("\x1b[1;32m✓\x1b[0m Service installed and started.");
println!(" umutray autostarts with your session and is in the app menu.");
println!(" Status: systemctl --user status {UNIT_NAME}");
println!(" Logs: journalctl --user -u {UNIT_NAME} -f");
println!("{} Autostart installed.", "".green().bold());
println!(" umutray will start with your next graphical session.");
println!(" To start now: {}", exe.display());
Ok(())
}
/// Stop, disable, and remove the unit + desktop files.
/// Remove the XDG autostart entry and the app-menu .desktop file.
pub fn uninstall() -> Result<()> {
let _ = systemctl(&["disable", "--now", UNIT_NAME]);
let unit = unit_path()?;
if unit.exists() {
std::fs::remove_file(&unit).with_context(|| format!("Failed to remove {unit:?}"))?;
println!("Removed {}", unit.display());
let autostart = autostart_path()?;
if autostart.exists() {
std::fs::remove_file(&autostart)
.with_context(|| format!("Failed to remove {autostart:?}"))?;
println!("Removed {}", autostart.display());
} else {
println!("No unit file at {}", unit.display());
println!("No autostart file at {}", autostart.display());
}
uninstall_desktop()?;
let _ = systemctl(&["daemon-reload"]);
println!("\x1b[1;32m✓\x1b[0m Service removed.");
println!("{} Autostart removed.", "".green().bold());
Ok(())
}
/// Pass through `systemctl --user status`.
/// Show whether the XDG autostart entry is present.
pub fn status() -> Result<()> {
let _ = Command::new("systemctl")
.args(["--user", "status", UNIT_NAME])
.status();
let autostart = autostart_path()?;
if autostart.exists() {
println!("{} Autostart enabled: {}", "".green().bold(), autostart.display());
} else {
println!("{} Autostart not installed ({})", "".red().bold(), autostart.display());
}
Ok(())
}