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:
+198
-68
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user