diff --git a/Cargo.lock b/Cargo.lock
index 3248cad..2b3c47c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1866,6 +1866,20 @@ dependencies = [
"bytemuck",
]
+[[package]]
+name = "embed-resource"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml",
+ "vswhom",
+ "winreg",
+]
+
[[package]]
name = "endi"
version = "1.1.1"
@@ -4387,6 +4401,7 @@ dependencies = [
"eframe",
"egui",
"egui-phosphor",
+ "embed-resource",
"futures",
"image",
"parking_lot",
@@ -6248,6 +6263,26 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -7161,6 +7196,16 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "wit-bindgen"
version = "0.57.1"
diff --git a/Cargo.toml b/Cargo.toml
index 9d91509..63fe8b3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -103,6 +103,8 @@ dbus = "0.9"
windows-sys = "0.61"
# Toasts on Windows. Linux notifications go over the `dbus` above.
tauri-winrt-notification = "0.8"
+# The exe's own icon resource, which is what Explorer and a taskbar pin draw.
+embed-resource = "3"
rpassword = "7"
libloading = "0.8"
lru = "0.18"
diff --git a/crates/pstr-app/Cargo.toml b/crates/pstr-app/Cargo.toml
index b40587e..7580863 100644
--- a/crates/pstr-app/Cargo.toml
+++ b/crates/pstr-app/Cargo.toml
@@ -34,6 +34,9 @@ directories.workspace = true
chrono.workspace = true
souvlaki.workspace = true
+[build-dependencies]
+embed-resource.workspace = true
+
[dev-dependencies]
skrifa.workspace = true
diff --git a/crates/pstr-app/build.rs b/crates/pstr-app/build.rs
new file mode 100644
index 0000000..31d5fa8
--- /dev/null
+++ b/crates/pstr-app/build.rs
@@ -0,0 +1,15 @@
+//! Compiles `proton-stream.rc` into the Windows exe; nothing on any other
+//! target. `rc.exe` on an MSVC build, mingw's `windres` on the cross build
+//! from Linux.
+
+fn main() {
+ println!("cargo:rerun-if-changed=proton-stream.rc");
+ println!("cargo:rerun-if-changed=../../packaging/windows/proton-stream.ico");
+ if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
+ return;
+ }
+ // Only the GUI gets the icon; `pstr` is a console tool.
+ embed_resource::compile_for("proton-stream.rc", ["proton-stream"], embed_resource::NONE)
+ .manifest_optional()
+ .expect("compile the icon resource");
+}
diff --git a/crates/pstr-app/proton-stream.rc b/crates/pstr-app/proton-stream.rc
new file mode 100644
index 0000000..3951549
--- /dev/null
+++ b/crates/pstr-app/proton-stream.rc
@@ -0,0 +1,4 @@
+// The exe's icon. The window sets one at runtime too, but that only lasts as
+// long as the window: Explorer, the Start menu and a taskbar pin read the icon
+// out of the file, and without this one they fall back to the generic one.
+1 ICON "../../packaging/windows/proton-stream.ico"
diff --git a/docs/BUGS.md b/docs/BUGS.md
index 4e647cc..83fe739 100644
--- a/docs/BUGS.md
+++ b/docs/BUGS.md
@@ -146,6 +146,54 @@ and [B44](#b44--r8-broke-the-app-under-instrumentation-in-five-places).
## Fixed
+### B63 — a taskbar pin on Windows lost its icon
+
+**Symptom.** Proton Stream pinned to the Windows taskbar showed its icon at
+first, then after a while — reliably after an update — the generic program
+icon.
+
+**Cause.** Two, each enough on its own. `proton-stream.exe` carried no icon
+resource: the window sets one at runtime, which is what a pin shows while the
+icon cache still holds it, but a pin made from the running window points at
+the exe, and once the cache is rebuilt the exe has nothing to give. A pin made
+from the Start Menu shortcut instead took the shortcut's `Icon`, which Windows
+Installer keeps in `C:\Windows\Installer\{ProductCode}`. Every upgrade has a
+new ProductCode and deletes the old product's folder, icon included.
+
+**Fix.** `crates/pstr-app/build.rs` compiles `proton-stream.rc`, which embeds
+`packaging/windows/proton-stream.ico`, into the GUI exe: `rc.exe` on MSVC,
+mingw's `windres` on the cross build. Both installer definitions drop the
+shortcut's `Icon` (and the WiX one `Advertise`), so the shortcut shows the
+exe's icon too. A pin that already lost its icon keeps the dead reference;
+unpin and pin again once.
+
+**Verified.** `objdump -p` on the cross-built exe lists the `RT_ICON` entries,
+all seven sizes. `msiinfo export … Shortcut` on a wixl build has an empty
+`Icon_` column. Not yet checked on a Windows taskbar across an upgrade.
+
+### B62 — an MSI upgrade left the app without libmpv
+
+**Symptom.** Installing a new `.msi` over an old one ended with an app that
+would not start for want of mpv. Installing the same `.msi` a second time
+fixed it.
+
+**Cause.** Both installers remove the old product early — the wixl one at
+`RemoveExistingProducts` after `InstallInitialize`, WiX's `MajorUpgrade` by
+default after `InstallValidate` — but costing runs before either, at
+`CostFinalize`. Costing found the old `libmpv-2.dll` on disk with the same
+version as the new one, since mpv is pinned, and decided not to copy it. Then
+removing the old product deleted it. The second install found no DLL on disk,
+so it copied one.
+
+**Fix.** `REINSTALLMODE=amus` in both installer definitions, which copies every
+file whatever is on disk. Every file is private to the install directory, so
+the forced overwrite can downgrade nothing shared.
+
+**Verified.** `msiinfo export … InstallExecuteSequence` on a wixl build shows
+`FileCost` 900 and `CostFinalize` 1000 ahead of `RemoveExistingProducts` 1501,
+and `msiinfo export … Property` has `REINSTALLMODE amus`. Not yet checked as an
+upgrade on Windows.
+
### B61 — later seasons were captioned with season one's episode names
**Symptom.** On a 42-title AniList library, Attack on Titan seasons 2–4 and
diff --git a/packaging/README.md b/packaging/README.md
index 7cd2f7a..87917e0 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -124,7 +124,7 @@ The `.icns` is rasterized from `io.narl.proton-stream.svg` when `rsvg-convert` o
| `io.narl.proton-stream.desktop` / `.svg` | Freedesktop entry and icon |
| `macos/Info.plist.in` | `.app` bundle plist; `@VERSION@`/`@BIN@`/`@APP_ID@` substituted at build time |
| `windows/proton-stream.wxs` | WiX v4+ installer definition |
-| `windows/proton-stream.ico` | MSI and shortcut icon, rasterized from the SVG |
+| `windows/proton-stream.ico` | Exe and Add/Remove Programs icon, rasterized from the SVG; `crates/pstr-app/build.rs` compiles it into `proton-stream.exe` |
| `windows/license.rtf` | Fallback for the installer's license dialog; the repo's own `LICENSE` is preferred when it exists |
The Debian `control` file has no template — `scripts/build.sh` writes it, because
diff --git a/packaging/windows/proton-stream.wixl.wxs b/packaging/windows/proton-stream.wixl.wxs
index 94b7e74..59f8189 100644
--- a/packaging/windows/proton-stream.wixl.wxs
+++ b/packaging/windows/proton-stream.wixl.wxs
@@ -78,6 +78,15 @@
+
+
+
@@ -98,14 +107,19 @@
and writes the advertised form anyway, with the feature name in
Shortcut/Target. What selects the non-advertised form is being
a child of the Component rather than of the File, plus an
- explicit formatted Target. Keep it out of the . -->
+ explicit formatted Target. Keep it out of the .
+
+ No Icon either. A shortcut's Icon is copied to
+ C:\Windows\Installer\{ProductCode}, and every upgrade has a
+ new ProductCode and deletes the old folder — taking the icon
+ of every taskbar pin made from the shortcut with it. Without
+ one, the shortcut shows the exe's own icon resource. -->
+ WorkingDirectory="INSTALLDIR" />
+
+
@@ -45,12 +54,16 @@
+
+ Target="[#AppExe]"
+ WorkingDirectory="INSTALLFOLDER" />