Skip to content

Commit d8506f2

Browse files
committed
fix(windows): window/taskbar icon via shell thumbnail and conditional icon
Use nativeImage.createThumbnailFromPath on the packaged exe and on-disk ico paths before getFileIcon. Omit BrowserWindow icon when no valid NativeImage so an empty image does not override the exe. Re-apply setIcon on ready-to-show and did-finish-load; log resourcesPath when resolution fails. Made-with: Cursor
1 parent b97ba43 commit d8506f2

1 file changed

Lines changed: 44 additions & 6 deletions

File tree

app/windows/build.cjs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,15 +1834,42 @@ function log(msg) {
18341834
} catch (_) {}
18351835
}
18361836

1837-
/** Windows: resolve before the first show() or the taskbar often keeps a blank/generic icon. */
1837+
/** Windows: resolve before the first show(); omit `icon` if empty so the shell can fall back to the exe. */
18381838
async function resolveBrowserWindowIcon() {
18391839
const fromFile = nativeImageFromAppIcon();
18401840
if (fromFile && !fromFile.isEmpty()) return fromFile;
18411841
if (process.platform !== "win32" || !app.isPackaged) return fromFile;
1842+
1843+
const thumbSize = { width: 256, height: 256 };
1844+
const inAsarOnly = (p) => p.includes("app.asar") && !p.includes("app.asar.unpacked");
1845+
1846+
// Shell-backed extraction: often works for the packaged .exe (embedded rcedit icon) when ICO buffer decode fails.
1847+
try {
1848+
if (fs.existsSync(process.execPath)) {
1849+
const img = await nativeImage.createThumbnailFromPath(process.execPath, thumbSize);
1850+
if (img && !img.isEmpty()) return img;
1851+
}
1852+
} catch (e) {
1853+
try {
1854+
log(`createThumbnailFromPath(exe): ${e?.message || e}`);
1855+
} catch (_) {}
1856+
}
1857+
1858+
for (const p of collectAppIconIcoCandidates()) {
1859+
if (!p || !fs.existsSync(p) || inAsarOnly(p)) continue;
1860+
try {
1861+
const img = await nativeImage.createThumbnailFromPath(p, thumbSize);
1862+
if (img && !img.isEmpty()) return img;
1863+
} catch (e) {
1864+
try {
1865+
log(`createThumbnailFromPath(${p}): ${e?.message || e}`);
1866+
} catch (_) {}
1867+
}
1868+
}
1869+
18421870
const shellPaths = [process.execPath, ...collectAppIconIcoCandidates()].filter((p) => {
18431871
if (!p || !fs.existsSync(p)) return false;
1844-
const inAsarArchive = p.includes("app.asar") && !p.includes("app.asar.unpacked");
1845-
return !inAsarArchive;
1872+
return !inAsarOnly(p);
18461873
});
18471874
for (const p of shellPaths) {
18481875
for (const size of ["large", "normal", "small"]) {
@@ -1871,14 +1898,22 @@ async function createWindow() {
18711898
}
18721899

18731900
const windowIcon = await resolveBrowserWindowIcon();
1874-
if (process.platform === "win32" && app.isPackaged && (!windowIcon || windowIcon.isEmpty())) {
1901+
const iconForWindow = windowIcon && !windowIcon.isEmpty() ? windowIcon : undefined;
1902+
if (process.platform === "win32" && app.isPackaged && !iconForWindow) {
18751903
try {
18761904
log(
1877-
`warn: taskbar icon empty; candidates=${collectAppIconIcoCandidates().join(" | ")} exe=${process.execPath}`,
1905+
`warn: window icon unresolved; resourcesPath=${process.resourcesPath} ico=${collectAppIconIcoCandidates().join(" | ")} exe=${process.execPath}`,
18781906
);
18791907
} catch (_) {}
18801908
}
18811909

1910+
const applyWindowIcon = () => {
1911+
if (!iconForWindow || mainWindow.isDestroyed()) return;
1912+
try {
1913+
mainWindow.setIcon(iconForWindow);
1914+
} catch (_) {}
1915+
};
1916+
18821917
// NSIS close-app uses PRODUCT_NAME (package.json → build.productName). The window title must
18831918
// match that string, not a URL — otherwise the installer cannot find/close the running app.
18841919
// Keep in sync with app/package.json "build.productName".
@@ -1888,7 +1923,7 @@ async function createWindow() {
18881923
width: 1200,
18891924
height: 800,
18901925
title: windowTitle,
1891-
icon: windowIcon,
1926+
...(iconForWindow ? { icon: iconForWindow } : {}),
18921927
// Match app dark background (theme.ts); reduces flash and helps menu/client seam blend on Windows.
18931928
backgroundColor: "#111111",
18941929
webPreferences: {
@@ -1904,12 +1939,15 @@ async function createWindow() {
19041939
} catch (_) {}
19051940

19061941
mainWindow.once("ready-to-show", () => {
1942+
applyWindowIcon();
19071943
try {
19081944
mainWindow.maximize();
19091945
} catch (_) {}
19101946
mainWindow.show();
19111947
});
19121948

1949+
mainWindow.webContents.once("did-finish-load", applyWindowIcon);
1950+
19131951
mainWindow.webContents.on("page-title-updated", (e) => {
19141952
e.preventDefault();
19151953
mainWindow.setTitle(windowTitle);

0 commit comments

Comments
 (0)