Add config and service subcommands
config subcommand — show, path, edit ($EDITOR), and a non-interactive set that takes --prefix / --compat-dir / --gameid. Lets users retarget the Wine prefix without hand-editing TOML. service subcommand — install / uninstall / status for a systemd --user unit that autostarts the tray. install writes ~/.config/systemd/user/ battlenet-manager.service with ExecStart pointing at the current binary, then daemon-reloads and enable --now's the unit. uninstall tears it back down. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ first launch.
|
||||
## Usage
|
||||
|
||||
| Command | What it does |
|
||||
| --------------------------------- | ------------------------------------------------------- |
|
||||
| --------------------------------- | -------------------------------------------------------- |
|
||||
| `battlenet-manager` | Start the tray daemon (default) |
|
||||
| `battlenet-manager launch` | Launch Battle.net and return (for `.desktop` shortcuts) |
|
||||
| `battlenet-manager kill` | SIGTERM → wait 3 s → SIGKILL on all Battle.net procs |
|
||||
@@ -43,6 +43,11 @@ first launch.
|
||||
| `update-proton --latest` | Install newest GE-Proton release |
|
||||
| `update-proton --version X` | Install a specific tag (e.g. `GE-Proton10-34`) |
|
||||
| `update-proton --list` | Show recent releases without installing |
|
||||
| `config show` / `config path` | Print current config or its path |
|
||||
| `config edit` | Open config in `$EDITOR` |
|
||||
| `config set --prefix PATH` | Change the Wine prefix (also `--compat-dir`, `--gameid`) |
|
||||
| `service install` | Write + enable a `systemd --user` unit for autostart |
|
||||
| `service uninstall` / `status` | Remove the unit / show its status |
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
+54
-1
@@ -36,12 +36,65 @@ fn home_dir() -> PathBuf {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn config_path() -> Result<PathBuf> {
|
||||
pub fn config_path() -> Result<PathBuf> {
|
||||
let dirs = ProjectDirs::from("co.aleshym", "", "battlenet-manager")
|
||||
.context("Could not determine config directory")?;
|
||||
Ok(dirs.config_dir().join("config.toml"))
|
||||
}
|
||||
|
||||
/// Print the config path followed by the serialised current config.
|
||||
pub fn show(&self) -> Result<()> {
|
||||
let path = Self::config_path()?;
|
||||
println!("# {}", path.display());
|
||||
let s = toml::to_string_pretty(self)?;
|
||||
print!("{s}");
|
||||
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 path = Self::config_path()?;
|
||||
let editor = std::env::var("EDITOR")
|
||||
.or_else(|_| std::env::var("VISUAL"))
|
||||
.unwrap_or_else(|_| "nano".into());
|
||||
let status = std::process::Command::new(&editor)
|
||||
.arg(&path)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to spawn editor '{editor}'"))?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("Editor '{editor}' exited non-zero");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update individual fields non-interactively, then save.
|
||||
pub fn set_fields(
|
||||
&mut self,
|
||||
prefix: Option<PathBuf>,
|
||||
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 let Some(p) = prefix {
|
||||
self.prefix_dir = p;
|
||||
}
|
||||
if let Some(p) = compat_dir {
|
||||
self.proton_compat_dir = p;
|
||||
}
|
||||
if let Some(g) = gameid {
|
||||
self.gameid = g;
|
||||
}
|
||||
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()?;
|
||||
|
||||
+64
@@ -2,10 +2,12 @@ mod config;
|
||||
mod diagnose;
|
||||
mod launcher;
|
||||
mod proton;
|
||||
mod service;
|
||||
mod tray;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Battle.net launcher manager for Linux via umu/Proton-GE.
|
||||
///
|
||||
@@ -46,6 +48,52 @@ enum Commands {
|
||||
#[arg(long)]
|
||||
list: bool,
|
||||
},
|
||||
|
||||
/// Show or modify configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
},
|
||||
|
||||
/// Manage the systemd --user service that autostarts the tray
|
||||
Service {
|
||||
#[command(subcommand)]
|
||||
action: ServiceAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ConfigAction {
|
||||
/// Print the config file path and current values
|
||||
Show,
|
||||
/// Print just the config file path
|
||||
Path,
|
||||
/// Open the config file in $EDITOR
|
||||
Edit,
|
||||
/// Update one or more fields non-interactively
|
||||
Set {
|
||||
/// Wine prefix directory
|
||||
#[arg(long, value_name = "PATH")]
|
||||
prefix: Option<PathBuf>,
|
||||
|
||||
/// GE-Proton install directory
|
||||
#[arg(long, value_name = "PATH")]
|
||||
compat_dir: Option<PathBuf>,
|
||||
|
||||
/// umu GAMEID (used for protonfixes lookup)
|
||||
#[arg(long, value_name = "ID")]
|
||||
gameid: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ServiceAction {
|
||||
/// Write the unit, daemon-reload, and enable+start the service
|
||||
Install,
|
||||
/// Stop, disable, and remove the unit file
|
||||
Uninstall,
|
||||
/// Show `systemctl --user status` for the service
|
||||
Status,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
@@ -60,6 +108,22 @@ fn main() -> Result<()> {
|
||||
Commands::UpdateProton { latest, version, list } => {
|
||||
proton::run(&config, latest, version, list)?;
|
||||
}
|
||||
Commands::Config { action } => match action {
|
||||
ConfigAction::Show => config.show()?,
|
||||
ConfigAction::Path => {
|
||||
println!("{}", config::Config::config_path()?.display());
|
||||
}
|
||||
ConfigAction::Edit => config::Config::edit()?,
|
||||
ConfigAction::Set { prefix, compat_dir, gameid } => {
|
||||
let mut c = config;
|
||||
c.set_fields(prefix, compat_dir, gameid)?;
|
||||
}
|
||||
},
|
||||
Commands::Service { action } => match action {
|
||||
ServiceAction::Install => service::install()?,
|
||||
ServiceAction::Uninstall => service::uninstall()?,
|
||||
ServiceAction::Status => service::status()?,
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
const UNIT_NAME: &str = "battlenet-manager.service";
|
||||
|
||||
fn unit_path() -> Result<PathBuf> {
|
||||
let home = std::env::var("HOME").context("$HOME is not set")?;
|
||||
Ok(PathBuf::from(home)
|
||||
.join(".config/systemd/user")
|
||||
.join(UNIT_NAME))
|
||||
}
|
||||
|
||||
fn render_unit(exe: &std::path::Path) -> String {
|
||||
format!(
|
||||
"[Unit]\n\
|
||||
Description=Battle.net tray 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 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(" "));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the unit, reload systemd, and enable+start the service.
|
||||
pub fn install() -> Result<()> {
|
||||
let exe = std::env::current_exe()
|
||||
.context("Cannot determine path to own executable")?;
|
||||
let path = unit_path()?;
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {parent:?}"))?;
|
||||
}
|
||||
|
||||
let contents = render_unit(&exe);
|
||||
std::fs::write(&path, &contents)
|
||||
.with_context(|| format!("Failed to write unit file {path:?}"))?;
|
||||
println!("Wrote unit: {}", path.display());
|
||||
println!("ExecStart: {}", exe.display());
|
||||
println!();
|
||||
|
||||
systemctl(&["daemon-reload"])?;
|
||||
systemctl(&["enable", "--now", UNIT_NAME])?;
|
||||
|
||||
println!();
|
||||
println!("\x1b[1;32m✓\x1b[0m Service installed and started.");
|
||||
println!(" Status: systemctl --user status {UNIT_NAME}");
|
||||
println!(" Logs: journalctl --user -u {UNIT_NAME} -f");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop, disable, and remove the unit file.
|
||||
pub fn uninstall() -> Result<()> {
|
||||
let path = unit_path()?;
|
||||
|
||||
// Ignore failures: the unit may already be stopped or unknown to systemd.
|
||||
let _ = systemctl(&["disable", "--now", UNIT_NAME]);
|
||||
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)
|
||||
.with_context(|| format!("Failed to remove {path:?}"))?;
|
||||
println!("Removed {}", path.display());
|
||||
} else {
|
||||
println!("No unit file at {}", path.display());
|
||||
}
|
||||
|
||||
let _ = systemctl(&["daemon-reload"]);
|
||||
println!("\x1b[1;32m✓\x1b[0m Service removed.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pass through `systemctl --user status`.
|
||||
pub fn status() -> Result<()> {
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--user", "status", UNIT_NAME])
|
||||
.status();
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user