From 42bc4ba1ec2a201bb23216140a151f08f4ef3e99 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:07:11 -0600 Subject: [PATCH] fix(windows): do not panic in souvlaki without an HWND; open folders via open::that souvlaki's Windows backend does config.hwnd.expect() at platform/windows/mod.rs:57, so the None we passed was a panic inside a tokio task on the first session, not the Err the match was written for. The comment on that field claimed init would merely fail; it was read from the comment, not the mechanism. No HWND is wired yet, so spawn() bows out on Windows and the body moves to spawn_native, which is compiled out there. Phase A ships without media keys and says so. The offline-folder opener spawned xdg-open on every non-macOS host, Windows included, where it does not exist. Route it through the open crate like the other eleven sites in this crate; Linux and macOS reach the same two binaries as before. cc --- crates/qbz-media-controls/src/platform.rs | 38 +++++++++++++++++++---- crates/qbz-qt/src/offline_cache_qt.rs | 16 +++++----- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/crates/qbz-media-controls/src/platform.rs b/crates/qbz-media-controls/src/platform.rs index a9eff1fab..7169a11ea 100644 --- a/crates/qbz-media-controls/src/platform.rs +++ b/crates/qbz-media-controls/src/platform.rs @@ -8,14 +8,17 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; -use souvlaki::{ - MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig, - SeekDirection, -}; +use souvlaki::{MediaControls, MediaMetadata, MediaPlayback, MediaPosition}; +// Only `spawn_native` and `map_event` touch these, and both are compiled out +// on Windows (souvlaki's SMTC backend panics without an HWND — see `spawn`). +#[cfg(not(target_os = "windows"))] +use souvlaki::{MediaControlEvent, PlatformConfig, SeekDirection}; use crate::types::{MediaEvent, MediaIntegration, PlaybackStatus, TrackMeta}; /// Default step for a magnitude-less MPRIS-style `Seek` (5 seconds, micros). +// Only `map_event` reads it, and that is compiled out on Windows. +#[cfg(not(target_os = "windows"))] const SEEK_STEP_MICROS: i64 = 5_000_000; type EventCb = Arc; @@ -60,6 +63,8 @@ impl MediaIntegration for PlatformHandle { } } +// Only `spawn_native` installs it, and that is compiled out on Windows. +#[cfg(not(target_os = "windows"))] fn map_event(e: MediaControlEvent) -> Option { Some(match e { MediaControlEvent::Play => MediaEvent::Play, @@ -86,11 +91,32 @@ fn map_event(e: MediaControlEvent) -> Option { } pub fn spawn(on_event: EventCb) -> Option { + // souvlaki's Windows backend does NOT degrade gracefully without an HWND: + // `config.hwnd.expect(...)` at souvlaki-0.8.3/src/platform/windows/mod.rs:57-59 + // PANICS, and `MediaControls::new` runs inside a tokio task, so the first + // session would abort the task rather than return the Err the match below + // is written for. (The old comment on `hwnd: None` claimed the opposite; + // it was read from the comment, not the mechanism.) No HWND is wired yet + // — that is plan A-2's "SMTC real" task — so bow out here on Windows. + #[cfg(target_os = "windows")] + { + let _ = on_event; + log::warn!("[media-controls] SMTC disabled: no window handle wired yet (Phase A)"); + return None; + } + #[cfg(not(target_os = "windows"))] + { + return spawn_native(on_event); + } +} + +#[cfg(not(target_os = "windows"))] +fn spawn_native(on_event: EventCb) -> Option { let config = PlatformConfig { dbus_name: "com.blitzfc.qbz", display_name: "QBZ", - // macOS: unused. Windows SMTC needs the window HWND; not shipped/tested, - // so left None (init may fail there → None handle, no media controls). + // macOS: unused. Windows: unreachable — see the early return in + // `spawn` above. hwnd: None, }; diff --git a/crates/qbz-qt/src/offline_cache_qt.rs b/crates/qbz-qt/src/offline_cache_qt.rs index 39e24d8f6..286c6ccb2 100644 --- a/crates/qbz-qt/src/offline_cache_qt.rs +++ b/crates/qbz-qt/src/offline_cache_qt.rs @@ -377,21 +377,19 @@ pub fn remove_album(album_id: String) { /// Open the cache directory in the desktop file manager (Slint `open_folder`). /// -/// `xdg-open` / `open` by hand rather than the `rfd` used elsewhere in the -/// port: rfd opens a PICKER, and this row is "show me the folder". Same two -/// binaries the reference shells out to. +/// Not the `rfd` used elsewhere in the port: rfd opens a PICKER, and this row +/// is "show me the folder". Routed through the `open` crate like the other +/// eleven sites in this crate — the hand-rolled `xdg-open` fallback ran on +/// every non-macOS host, Windows included, where it does not exist. `open` +/// uses ShellExecuteW there and xdg-open/open on Linux/macOS, so the two +/// supported platforms behave exactly as before. pub fn open_folder() { crate::spawn(async move { let Some(off) = offline_qt::get().await else { return; }; let path = off.get_cache_path(); - let opener = if cfg!(target_os = "macos") { - "open" - } else { - "xdg-open" - }; - if let Err(e) = std::process::Command::new(opener).arg(&path).spawn() { + if let Err(e) = open::that(&path) { log::warn!("[qbz-qt] open offline folder failed: {e}"); } });