From c300deba9a96dc916483585a2fc83795ce7de5d4 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 15 Jun 2026 07:59:21 +0000 Subject: [PATCH] feat(host/windows): Steam library auto-discovery on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Steam `LibraryProvider` keyed off `$HOME` + Linux paths, so the game library was empty on Windows. Add Windows discovery: the default Steam install dirs under Program Files (`ProgramFiles(x86)`/`ProgramFiles`/ `ProgramW6432`), with games on other drives picked up via each root's `libraryfolders.vdf` — whose Windows values are backslash-escaped, so unescape `\\` → `\`. The existing root-scan/dedup logic is shared via a new `steam_roots_existing` helper. The custom store (mgmt JSON CRUD) was already cross-platform; only Steam auto-discovery was Linux-only. Not yet covered: a non-default Steam install dir (the registry `Valve\Steam\InstallPath`). Degrades gracefully — no Steam → empty list. clippy -D warnings + library tests green on Windows and Linux. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/library.rs | 35 +++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/punktfunk-host/src/library.rs b/crates/punktfunk-host/src/library.rs index 75f2a112..7f267d87 100644 --- a/crates/punktfunk-host/src/library.rs +++ b/crates/punktfunk-host/src/library.rs @@ -127,6 +127,7 @@ fn steam_art(appid: u32) -> Artwork { } /// Candidate Steam roots (classic, Flatpak, Deck) that actually exist, canonicalized + deduped. +#[cfg(not(target_os = "windows"))] fn steam_roots() -> Vec { let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else { return Vec::new(); @@ -137,6 +138,25 @@ fn steam_roots() -> Vec { home.join(".steam/root"), home.join(".var/app/com.valvesoftware.Steam/.local/share/Steam"), // Flatpak Steam ]; + steam_roots_existing(candidates) +} + +/// Windows Steam roots: the default install dirs under Program Files. Games installed on other +/// drives are still found via each root's `libraryfolders.vdf` (see [`steam_library_dirs`]). A +/// non-default Steam install dir (registry `Valve\Steam\InstallPath`) isn't covered yet. +#[cfg(target_os = "windows")] +fn steam_roots() -> Vec { + let mut candidates = Vec::new(); + for var in ["ProgramFiles(x86)", "ProgramFiles", "ProgramW6432"] { + if let Some(pf) = std::env::var_os(var) { + candidates.push(PathBuf::from(pf).join("Steam")); + } + } + steam_roots_existing(candidates) +} + +/// Keep only the candidate roots that exist (have a `steamapps` dir), canonicalized + deduped. +fn steam_roots_existing(candidates: impl IntoIterator) -> Vec { let mut seen = HashSet::new(); let mut roots = Vec::new(); for c in candidates { @@ -174,12 +194,21 @@ fn steam_library_dirs() -> Vec { } /// Pull every `"path" ""` value out of a `libraryfolders.vdf`. We don't need a full VDF -/// parser for the two flat fields we read — Linux library paths never contain the `"` or `\` -/// that would require unescaping. +/// parser for the two flat fields we read. On Windows the values are backslash-escaped +/// (`D:\\SteamLibrary`), so unescape `\\` → `\`; Linux paths need no unescaping. fn vdf_paths(text: &str) -> Vec { text.lines() .filter_map(|l| vdf_value(l.trim(), "path")) - .map(str::to_string) + .map(|p| { + #[cfg(target_os = "windows")] + { + p.replace("\\\\", "\\") + } + #[cfg(not(target_os = "windows"))] + { + p.to_string() + } + }) .collect() }