Add download progress and graceful tray shutdown

- proton: wrap the download sink in a small ProgressWriter that prints
  percent / MiB to stderr every 1 MiB so the ~600 MB GE-Proton pull isn't
  silent for minutes. No extra deps.
- tray: store the ksni Handle on the tray itself so Quit can call
  shutdown() before exit(), unregistering the SNI item from D-Bus instead
  of leaving a stale entry until the session bus notices the PID is gone.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-16 17:21:15 -07:00
parent 7de6f6d938
commit 8908c15974
2 changed files with 78 additions and 5 deletions
+54 -2
View File
@@ -79,9 +79,12 @@ fn install_version(config: &Config, tag: &str) -> Result<()> {
.context("Download failed")?
.error_for_status()
.context("Download returned an error status")?;
let mut f = std::fs::File::create(&tmp_path)
let total = resp.content_length();
let f = std::fs::File::create(&tmp_path)
.with_context(|| format!("Failed to create temp file {tmp_path:?}"))?;
std::io::copy(&mut resp, &mut f).context("Failed to stream archive to disk")?;
let mut progress = ProgressWriter::new(f, total);
std::io::copy(&mut resp, &mut progress).context("Failed to stream archive to disk")?;
progress.finish();
}
println!("Extracting to {:?}...", config.proton_compat_dir);
@@ -156,6 +159,55 @@ fn print_list(config: &Config) -> Result<()> {
Ok(())
}
/// Wraps a writer to print download progress to stderr, throttled to one
/// update per megabyte so we don't spam the terminal.
struct ProgressWriter<W: Write> {
inner: W,
total: Option<u64>,
written: u64,
last_print: u64,
}
impl<W: Write> ProgressWriter<W> {
fn new(inner: W, total: Option<u64>) -> Self {
Self { inner, total, written: 0, last_print: 0 }
}
fn finish(&mut self) {
let _ = self.inner.flush();
// Clear the progress line — the caller's next println! starts fresh.
eprintln!();
}
}
impl<W: Write> Write for ProgressWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let n = self.inner.write(buf)?;
self.written += n as u64;
if self.written - self.last_print >= 1 << 20 {
self.last_print = self.written;
match self.total {
Some(t) => {
let pct = (self.written as f64 / t as f64) * 100.0;
eprint!(
"\r {:.1}% ({} / {} MiB)",
pct,
self.written >> 20,
t >> 20,
);
}
None => eprint!("\r {} MiB", self.written >> 20),
}
let _ = std::io::stderr().flush();
}
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
fn pick_interactively(config: &Config) -> Result<String> {
let releases = fetch_releases(10)?;
if releases.is_empty() {
+24 -3
View File
@@ -7,6 +7,9 @@ pub struct BattlenetTray {
pub config: Config,
/// Whether Battle.net is currently running; updated by background poller.
pub running: bool,
/// Set after the service spawns so Quit can shut down the SNI item
/// cleanly instead of yanking it off the bus via exit().
pub handle: Option<ksni::Handle<BattlenetTray>>,
}
impl ksni::Tray for BattlenetTray {
@@ -113,8 +116,18 @@ impl ksni::Tray for BattlenetTray {
StandardItem {
label: "Quit".into(),
icon_name: "application-exit".into(),
activate: Box::new(|_this: &mut Self| {
std::process::exit(0);
activate: Box::new(|this: &mut Self| {
if let Some(h) = this.handle.clone() {
// Run shutdown off-thread: we're currently holding
// the tray lock inside update(), and shutdown wants
// the service loop to turn.
thread::spawn(move || {
h.shutdown();
std::process::exit(0);
});
} else {
std::process::exit(0);
}
}),
..Default::default()
}
@@ -130,6 +143,7 @@ pub fn run(config: &Config) -> Result<()> {
let tray = BattlenetTray {
config: config.clone(),
running: launcher::is_running(),
handle: None,
};
let service = ksni::TrayService::new(tray);
@@ -137,10 +151,17 @@ pub fn run(config: &Config) -> Result<()> {
let handle = service.handle();
service.spawn();
// Hand the tray a clone of its own handle so Quit can shut down cleanly.
let handle_for_self = handle.clone();
handle.update(move |t: &mut BattlenetTray| {
t.handle = Some(handle_for_self);
});
// Background thread: poll Battle.net process state every 2 s and update the tray.
let poll_handle = handle;
thread::spawn(move || loop {
let running = launcher::is_running();
handle.update(|tray: &mut BattlenetTray| {
poll_handle.update(|tray: &mut BattlenetTray| {
tray.running = running;
});
thread::sleep(Duration::from_secs(2));