modified: README.md

modified:   src/config.rs
	modified:   src/diagnose.rs
	modified:   src/launcher.rs
	modified:   src/main.rs
	new file:   src/setup.rs
	modified:   src/tray.rs
This commit is contained in:
funman300
2026-04-16 21:43:58 -07:00
parent 336c5d908e
commit 7e5ed3d447
7 changed files with 690 additions and 414 deletions
+198 -68
View File
@@ -4,37 +4,154 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Wine prefix directory
pub struct Launcher {
/// Short CLI name (e.g. "battlenet").
pub name: String,
/// Display name for tray / menus.
pub display: String,
/// Wine prefix directory for this launcher.
pub prefix_dir: PathBuf,
/// GE-Proton version string passed to PROTONPATH ("GE-Proton" tracks latest)
pub proton_version: String,
/// umu GAMEID used to look up protonfixes
/// Path to the launcher exe, relative to the prefix's drive_c/.
pub exe_path: PathBuf,
/// umu GAMEID (used to look up protonfixes).
pub gameid: String,
/// Directory where GE-Proton versions are installed
/// pgrep/pkill -f regex matching this launcher's running processes.
pub process_pattern: String,
/// Optional URL of the Windows installer (consumed by `setup`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub installer_url: Option<String>,
/// Optional per-launcher Proton version override (falls back to
/// Config::proton_version).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proton_version: Option<String>,
}
impl Launcher {
/// Absolute path to the launcher exe inside the prefix.
pub fn full_exe_path(&self) -> PathBuf {
self.prefix_dir.join("drive_c").join(&self.exe_path)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// Directory where GE-Proton versions are installed.
#[serde(default = "default_compat_dir")]
pub proton_compat_dir: PathBuf,
/// Default Proton version passed to PROTONPATH.
/// The literal "GE-Proton" makes umu-run fetch/use the latest.
#[serde(default = "default_proton_version")]
pub proton_version: String,
/// Configured launchers.
#[serde(default)]
pub launchers: Vec<Launcher>,
}
fn home_dir() -> PathBuf {
std::env::var("HOME")
.map(PathBuf::from)
.expect("$HOME is not set; cannot determine default paths")
}
fn default_compat_dir() -> PathBuf {
home_dir().join(".local/share/Steam/compatibilitytools.d")
}
fn default_proton_version() -> String {
"GE-Proton".into()
}
/// The six launchers umutray ships out of the box. `exe_path`, `gameid`,
/// and `process_pattern` are best-effort defaults for typical installs —
/// users can adjust per-launcher via `umutray config edit`.
pub fn presets() -> Vec<Launcher> {
let games = home_dir().join("Games");
vec![
Launcher {
name: "battlenet".into(),
display: "Battle.net".into(),
prefix_dir: games.join("battlenet"),
exe_path: PathBuf::from(
"Program Files (x86)/Battle.net/Battle.net Launcher.exe",
),
gameid: "umu-battlenet".into(),
process_pattern: r"Battle\.net".into(),
installer_url: None,
proton_version: None,
},
Launcher {
name: "eaapp".into(),
display: "EA App".into(),
prefix_dir: games.join("eaapp"),
exe_path: PathBuf::from(
"Program Files/Electronic Arts/EA Desktop/EA Desktop/EADesktop.exe",
),
gameid: "umu-eaapp".into(),
process_pattern: r"EADesktop\.exe".into(),
installer_url: None,
proton_version: None,
},
Launcher {
name: "epic".into(),
display: "Epic Games".into(),
prefix_dir: games.join("epic"),
exe_path: PathBuf::from(
"Program Files (x86)/Epic Games/Launcher/Portal/Binaries/Win32/EpicGamesLauncher.exe",
),
gameid: "umu-epicgameslauncher".into(),
process_pattern: r"EpicGamesLauncher\.exe".into(),
installer_url: None,
proton_version: None,
},
Launcher {
name: "ubisoft".into(),
display: "Ubisoft Connect".into(),
prefix_dir: games.join("ubisoft"),
exe_path: PathBuf::from(
"Program Files (x86)/Ubisoft/Ubisoft Game Launcher/UbisoftConnect.exe",
),
gameid: "umu-uplay".into(),
process_pattern: r"UbisoftConnect\.exe|upc\.exe".into(),
installer_url: None,
proton_version: None,
},
Launcher {
name: "gog".into(),
display: "GOG Galaxy".into(),
prefix_dir: games.join("gog"),
exe_path: PathBuf::from(
"Program Files (x86)/GOG Galaxy/GalaxyClient.exe",
),
gameid: "umu-gog".into(),
process_pattern: r"GalaxyClient\.exe".into(),
installer_url: None,
proton_version: None,
},
Launcher {
name: "rockstar".into(),
display: "Rockstar Games".into(),
prefix_dir: games.join("rockstar"),
exe_path: PathBuf::from(
"Program Files/Rockstar Games/Launcher/Launcher.exe",
),
gameid: "umu-rockstar".into(),
process_pattern: r"Rockstar Games.*Launcher\.exe".into(),
installer_url: None,
proton_version: None,
},
]
}
impl Default for Config {
fn default() -> Self {
let home = home_dir();
Self {
prefix_dir: home.join("Games/battlenet-umu"),
proton_version: "GE-Proton".into(),
gameid: "umu-battlenet".into(),
proton_compat_dir: home.join(".local/share/Steam/compatibilitytools.d"),
proton_compat_dir: default_compat_dir(),
proton_version: default_proton_version(),
launchers: presets(),
}
}
}
fn home_dir() -> PathBuf {
// No sensible fallback — writing a Wine prefix to /tmp would be data-loss
// waiting to happen, so surface the misconfiguration instead.
std::env::var("HOME")
.map(PathBuf::from)
.expect("$HOME is not set; cannot determine default paths")
}
impl Config {
pub fn config_path() -> Result<PathBuf> {
let dirs = ProjectDirs::from("co.aleshym", "", "umutray")
@@ -42,7 +159,54 @@ impl Config {
Ok(dirs.config_dir().join("config.toml"))
}
/// Print the config path followed by the serialised current config.
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
if !path.exists() {
let c = Self::default();
c.save().context("Failed to write default config")?;
return Ok(c);
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read config from {path:?}"))?;
match toml::from_str::<Self>(&content) {
Ok(mut c) => {
if c.launchers.is_empty() {
c.launchers = presets();
c.save().context("Failed to write presets")?;
}
Ok(c)
}
Err(e) => {
let bak = path.with_extension("toml.bak");
std::fs::rename(&path, &bak).with_context(|| {
format!("Failed to back up stale config to {bak:?}")
})?;
eprintln!("warning: couldn't parse {}: {e}", path.display());
eprintln!(
" backed up to {} — writing fresh config with presets",
bak.display()
);
let c = Self::default();
c.save()?;
Ok(c)
}
}
}
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write config to {path:?}"))
}
pub fn find(&self, name: &str) -> Option<&Launcher> {
self.launchers.iter().find(|l| l.name == name)
}
pub fn show(&self) -> Result<()> {
let path = Self::config_path()?;
println!("# {}", path.display());
@@ -51,10 +215,8 @@ impl Config {
Ok(())
}
/// Open the config file in $EDITOR (falling back to nano).
/// Ensures the file exists first so the editor isn't launched on nothing.
pub fn edit() -> Result<()> {
let _ = Self::load()?; // writes defaults if missing
let _ = Self::load()?;
let path = Self::config_path()?;
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
@@ -69,59 +231,27 @@ impl Config {
Ok(())
}
/// Update individual fields non-interactively, then save.
pub fn set_fields(
/// Update global fields non-interactively, then save.
/// Use `config edit` for per-launcher changes.
pub fn set_globals(
&mut self,
prefix: Option<PathBuf>,
proton_version: Option<String>,
compat_dir: Option<PathBuf>,
gameid: Option<String>,
) -> Result<()> {
if prefix.is_none() && compat_dir.is_none() && gameid.is_none() {
anyhow::bail!("nothing to set — pass at least one of --prefix / --compat-dir / --gameid");
if proton_version.is_none() && compat_dir.is_none() {
anyhow::bail!(
"nothing to set — pass --proton-version or --compat-dir"
);
}
if let Some(p) = prefix {
self.prefix_dir = p;
if let Some(v) = proton_version {
self.proton_version = v;
}
if let Some(p) = compat_dir {
self.proton_compat_dir = p;
}
if let Some(g) = gameid {
self.gameid = g;
if let Some(d) = compat_dir {
self.proton_compat_dir = d;
}
self.save()?;
println!("\x1b[1;32m✓\x1b[0m Config saved.");
println!();
self.show()?;
Ok(())
}
/// Load config from disk, creating a default one if it doesn't exist.
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
if !path.exists() {
let config = Self::default();
config.save().context("Failed to write default config")?;
return Ok(config);
}
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read config from {path:?}"))?;
toml::from_str(&content).context("Failed to parse config.toml")
}
/// Write config to disk.
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content)
.with_context(|| format!("Failed to write config to {path:?}"))
}
/// Absolute path to the Battle.net Launcher.exe inside the prefix.
pub fn launcher_exe(&self) -> PathBuf {
self.prefix_dir
.join("drive_c/Program Files (x86)/Battle.net/Battle.net Launcher.exe")
self.show()
}
}