Skip to content

Commit b97ba43

Browse files
committed
fix(windows): load ICO from ASAR via buffer; forge exe metadata
nativeImage.createFromPath and getFileIcon cannot read icons inside app.asar; use fs.readFileSync + nativeImage.createFromBuffer for .ico files and only use getFileIcon on real paths (exe + unpacked ico). Restore maximize on ready-to-show; optional HSP_DISABLE_GPU=1 for shortcut/GPU diagnostics; spellcheck off and explicit setIgnoreMenuShortcuts(false). Log candidate ico paths if the taskbar icon is still empty. Forge packager win32metadata improves exe identity but does not replace Authenticode signing. Made-with: Cursor
1 parent a2b7375 commit b97ba43

2 files changed

Lines changed: 80 additions & 26 deletions

File tree

app/windows/build.cjs

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ const {
99
ipcMain,
1010
nativeImage,
1111
nativeTheme,
12-
screen,
1312
} = require("electron");
13+
// Optional: set HSP_DISABLE_GPU=1 to test whether GPU stack affects shortcuts (e.g. Print Screen) on Windows.
14+
if (process.platform === "win32" && process.env.HSP_DISABLE_GPU === "1") {
15+
try {
16+
app.disableHardwareAcceleration();
17+
} catch (_) {}
18+
}
1419
const path = require("path");
1520
const fs = require("fs");
1621
const crypto = require("crypto");
@@ -581,15 +586,58 @@ function resolveAppIconIcoPath() {
581586
return null;
582587
}
583588

589+
/** All existing .ico paths (packaged app may have the file only inside app.asar — see nativeImage note below). */
590+
function collectAppIconIcoCandidates() {
591+
const candidates = [
592+
process.resourcesPath && path.join(process.resourcesPath, "app.asar.unpacked", "assets", "icon.ico"),
593+
process.resourcesPath && path.join(process.resourcesPath, "assets", "icon.ico"),
594+
app.getAppPath && path.join(app.getAppPath(), "assets", "icon.ico"),
595+
path.join(__dirname, "..", "assets", "icon.ico"),
596+
].filter(Boolean);
597+
const out = [];
598+
const seen = new Set();
599+
for (const p of candidates) {
600+
try {
601+
if (!p || !fs.existsSync(p)) continue;
602+
const n = path.normalize(p);
603+
if (seen.has(n)) continue;
604+
seen.add(n);
605+
out.push(n);
606+
} catch (_) {}
607+
}
608+
return out;
609+
}
610+
611+
/**
612+
* Paths under app.asar\... are real for Node (readFileSync) but not for nativeImage.createFromPath /
613+
* app.getFileIcon (shell/GDI cannot read inside the asar archive). Always prefer readFileSync + createFromBuffer for .ico.
614+
*/
615+
function nativeImageFromIcoFilePath(p) {
616+
if (!p) return null;
617+
try {
618+
if (!fs.existsSync(p)) return null;
619+
const buf = fs.readFileSync(p);
620+
const img = nativeImage.createFromBuffer(buf);
621+
return img.isEmpty() ? null : img;
622+
} catch (_) {
623+
return null;
624+
}
625+
}
626+
584627
function nativeImageFromAppIcon() {
585-
const p = resolveAppIconIcoPath();
586-
if (p) {
628+
const paths = collectAppIconIcoCandidates();
629+
for (const p of paths) {
630+
const img = nativeImageFromIcoFilePath(p);
631+
if (img) return img;
632+
}
633+
for (const p of paths) {
587634
try {
635+
const inAsarArchive = p.includes("app.asar") && !p.includes("app.asar.unpacked");
636+
if (inAsarArchive) continue;
588637
const img = nativeImage.createFromPath(p);
589638
if (!img.isEmpty()) return img;
590639
} catch (_) {}
591640
}
592-
// Packaged app: .ico can fail to load from ASAR paths; Windows still reads icons embedded in the exe (packager --icon).
593641
if (process.platform === "win32" && app.isPackaged) {
594642
try {
595643
const img = nativeImage.createFromPath(process.execPath);
@@ -1788,13 +1836,16 @@ function log(msg) {
17881836

17891837
/** Windows: resolve before the first show() or the taskbar often keeps a blank/generic icon. */
17901838
async function resolveBrowserWindowIcon() {
1791-
const fallback = nativeImageFromAppIcon();
1792-
if (process.platform !== "win32" || !app.isPackaged) return fallback;
1793-
const iconPaths = [resolveAppIconIcoPath(), process.execPath].filter(
1794-
(p) => p && (p === process.execPath || fs.existsSync(p)),
1795-
);
1796-
for (const p of iconPaths) {
1797-
for (const size of ["large", "normal"]) {
1839+
const fromFile = nativeImageFromAppIcon();
1840+
if (fromFile && !fromFile.isEmpty()) return fromFile;
1841+
if (process.platform !== "win32" || !app.isPackaged) return fromFile;
1842+
const shellPaths = [process.execPath, ...collectAppIconIcoCandidates()].filter((p) => {
1843+
if (!p || !fs.existsSync(p)) return false;
1844+
const inAsarArchive = p.includes("app.asar") && !p.includes("app.asar.unpacked");
1845+
return !inAsarArchive;
1846+
});
1847+
for (const p of shellPaths) {
1848+
for (const size of ["large", "normal", "small"]) {
17981849
try {
17991850
const img = await app.getFileIcon(p, { size });
18001851
if (!img.isEmpty()) return img;
@@ -1805,7 +1856,7 @@ async function resolveBrowserWindowIcon() {
18051856
}
18061857
}
18071858
}
1808-
return fallback;
1859+
return fromFile;
18091860
}
18101861

18111862
async function createWindow() {
@@ -1822,7 +1873,9 @@ async function createWindow() {
18221873
const windowIcon = await resolveBrowserWindowIcon();
18231874
if (process.platform === "win32" && app.isPackaged && (!windowIcon || windowIcon.isEmpty())) {
18241875
try {
1825-
log("warn: taskbar icon: resolveBrowserWindowIcon returned empty");
1876+
log(
1877+
`warn: taskbar icon empty; candidates=${collectAppIconIcoCandidates().join(" | ")} exe=${process.execPath}`,
1878+
);
18261879
} catch (_) {}
18271880
}
18281881

@@ -1841,25 +1894,19 @@ async function createWindow() {
18411894
webPreferences: {
18421895
nodeIntegration: false,
18431896
contextIsolation: true,
1897+
spellcheck: false,
18441898
},
18451899
show: false,
18461900
});
18471901

1902+
try {
1903+
mainWindow.webContents.setIgnoreMenuShortcuts(false);
1904+
} catch (_) {}
1905+
18481906
mainWindow.once("ready-to-show", () => {
1849-
// Edge-to-edge without WS_MAXIMIZE: on Windows, maximize() can interact badly with shell icons
1850-
// and some system shortcuts (e.g. Print Screen); filling the work area matches prior "fullscreen" intent.
18511907
try {
1852-
if (process.platform === "win32") {
1853-
const wa = screen.getPrimaryDisplay().workArea;
1854-
mainWindow.setBounds(wa);
1855-
} else {
1856-
mainWindow.maximize();
1857-
}
1858-
} catch (_) {
1859-
try {
1860-
mainWindow.maximize();
1861-
} catch (_) {}
1862-
}
1908+
mainWindow.maximize();
1909+
} catch (_) {}
18631910
mainWindow.show();
18641911
});
18651912

app/windows/forge/forge.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ export default {
8686
icon: ICON_PATH,
8787
// electron-packager reads this as the main process entry.
8888
main: MAIN_PROCESS_FILE_REL,
89+
// Improves exe identity in Properties / some SmartScreen heuristics (does not replace Authenticode signing).
90+
win32metadata: {
91+
CompanyName: typeof packageJson.author === "string" ? packageJson.author : "sraibaby",
92+
FileDescription: forgeProductName,
93+
ProductName: forgeProductName,
94+
OriginalFilename: `${forgeProductName}.exe`,
95+
},
8996

9097
// Match your electron-builder asar strategy.
9198
asar: {

0 commit comments

Comments
 (0)