diff --git a/apps/launcher/src/App.vue b/apps/launcher/src/App.vue
index 3919cdede..feb1b10fe 100644
--- a/apps/launcher/src/App.vue
+++ b/apps/launcher/src/App.vue
@@ -387,7 +387,7 @@ async function replaceToolShortcut() {
{{ shortcutMessage }}
diff --git a/apps/launcher/src/fixtures.ts b/apps/launcher/src/fixtures.ts
index 7e092e6c5..b592d91a6 100644
--- a/apps/launcher/src/fixtures.ts
+++ b/apps/launcher/src/fixtures.ts
@@ -4,6 +4,7 @@ import { LEGACY_PRIMARY_PROFILE_ID, parseProfileId } from "@shared/multiple-acco
export const fixtureSnapshot: LauncherSnapshot = {
revision: 1,
+ platform: "macos",
experience: {
installationKind: "migrated-single",
setup: "complete",
diff --git a/forge.config.ts b/forge.config.ts
index 1cdcadb5e..3c2322df0 100644
--- a/forge.config.ts
+++ b/forge.config.ts
@@ -38,11 +38,14 @@ function packagedExecutablePath(
if (platform !== "win32" && platform !== "linux") {
throw new Error(`unsupported package platform: ${platform}`);
}
- const suffix = platform === "win32" ? ".exe" : "";
+ // packageAfterCopy runs before Electron Packager renames the executable.
+ // Its build path is resources/app, so both desktop targets reach the
+ // package root and flip the pre-rename Electron binary.
+ const executable = platform === "win32" ? "electron.exe" : "electron";
return path.resolve(
resourcesPath,
- "..",
- `${channelConfig.productName}${suffix}`,
+ "../..",
+ executable,
);
}
diff --git a/package.json b/package.json
index fdccc229b..9d003ee74 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"packageManager": "pnpm@11.13.1",
"type": "module",
"description": "Guild Wars Reforged for macOS",
+ "author": "gwonmac contributors",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
diff --git a/src/main/launcher-orchestrator.ts b/src/main/launcher-orchestrator.ts
index fe004e5d6..2dce33a79 100644
--- a/src/main/launcher-orchestrator.ts
+++ b/src/main/launcher-orchestrator.ts
@@ -14,6 +14,7 @@ import type {
LauncherSnapshot,
} from "../shared/launcher-contracts.js";
import type { ProfileId } from "../shared/multiple-accounts.js";
+import type { ShortcutPlatform } from "../shared/keyboard-shortcuts.js";
import type { LauncherStateStore } from "./core/launcher-state.js";
import { launcherToolSettings } from "./core/launcher-tools.js";
@@ -36,6 +37,7 @@ export interface LauncherOrchestratorOptions {
readonly getSettings: () => AppSettings;
readonly toolsLoaded: () => boolean;
readonly developmentFixtures: boolean;
+ readonly platform?: ShortcutPlatform;
/** Unpackaged Electron tests only: lets renderer-focused suites bypass client preparation. */
readonly allowUnreadyLaunch?: boolean;
readonly publish: (snapshot: LauncherSnapshot) => void;
@@ -66,6 +68,7 @@ export class LauncherOrchestrator {
const fixture = this.options.developmentFixtures ? "fixture" as const : "placeholder" as const;
return {
revision: this.revision,
+ platform: this.options.platform ?? "macos",
experience: {
installationKind: document.installationKind,
setup: document.setupVersion > 0 ? "complete" : "pending",
diff --git a/src/main/launcher-shortcut-capture.ts b/src/main/launcher-shortcut-capture.ts
index f353307d5..b69521f5b 100644
--- a/src/main/launcher-shortcut-capture.ts
+++ b/src/main/launcher-shortcut-capture.ts
@@ -1,14 +1,19 @@
/**
- * Captures one macOS application shortcut at the launcher boundary. Main owns
+ * Captures one platform-native application shortcut at the launcher boundary. Main owns
* reserved keys and Tool conflicts, so renderer code cannot bypass the policy.
*/
import type { BrowserWindow, Event, Input } from "electron";
import type { AppSettings } from "../shared/contracts.js";
import type { GlobalTool, LauncherShortcutCaptureResult } from "../shared/launcher-contracts.js";
-import { shortcutFromInput, shortcutReserved } from "../shared/keyboard-shortcuts.js";
+import {
+ shortcutFromInput,
+ shortcutPlatform,
+ shortcutReserved,
+} from "../shared/keyboard-shortcuts.js";
import { shortcutOwner } from "./core/launcher-tools.js";
const CAPTURE_TIMEOUT_MS = 30_000;
+const PLATFORM = shortcutPlatform(process.platform);
export function captureLauncherShortcut(
win: BrowserWindow,
@@ -31,7 +36,7 @@ export function captureLauncherShortcut(
finish({ status: "cancelled" });
return;
}
- const binding = shortcutFromInput(input);
+ const binding = shortcutFromInput(input, PLATFORM);
if (!binding) {
finish({ status: "invalid" });
return;
diff --git a/src/main/main.ts b/src/main/main.ts
index 1ffacdf96..5183f1d0d 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -149,6 +149,7 @@ import { LAUNCHER_IPC } from "../shared/launcher-contracts.js";
import type { GlobalTool, LauncherSettingsPatch, ShortcutReplacement } from "../shared/launcher-contracts.js";
import {
DEFAULT_SHORTCUTS,
+ shortcutPlatform,
shortcutReserved,
withShortcutOverride,
} from "../shared/keyboard-shortcuts.js";
@@ -164,6 +165,7 @@ import {
loadWindowsNativeHost,
WindowsCredentialKeychain,
} from "./windows-native-host.js";
+import { windowsAppUserModelId } from "./windows-shell.js";
const nativeHostLayout = {
packaged: app.isPackaged,
@@ -190,6 +192,9 @@ if (!explicitUserData && process.platform === "win32") {
app.setPath("userData", applicationStorageRoots.sessions);
app.setPath("sessionData", applicationStorageRoots.sessions);
}
+if (process.platform === "win32") {
+ app.setAppUserModelId(windowsAppUserModelId(app.getName()));
+}
const primaryInstance = app.requestSingleInstanceLock();
if (!primaryInstance) {
@@ -241,6 +246,9 @@ const windowCoordinator = new WindowCoordinator(
{
...(app.dock ? { dock: app.dock } : {}),
focus: (options) => app.focus(options),
+ ...(process.platform === "win32"
+ ? { requestAttention: (win: BrowserWindow) => win.flashFrame(true) }
+ : {}),
},
windowRegistry,
);
@@ -508,7 +516,12 @@ function buildWindowHost(
};
}
-if (primaryInstance) app.on("second-instance", revealMainWindow);
+if (primaryInstance) app.on("second-instance", () => {
+ if (
+ process.platform !== "win32"
+ || !windowCoordinator.restoreMostRecentWindow()
+ ) revealMainWindow();
+});
if (primaryInstance) void app.whenReady().then(async () => {
if (INJECT_STARTUP_FAILURE) {
@@ -801,6 +814,7 @@ if (primaryInstance) void app.whenReady().then(async () => {
getSettings: () => currentSettings ?? settings,
toolsLoaded: () => enhancementSelection.tools,
developmentFixtures: !app.isPackaged || process.env.GW_LAUNCHER_FIXTURES === "1",
+ platform: shortcutPlatform(process.platform),
allowUnreadyLaunch,
publish: (snapshot) => {
const launcher = windowRegistry.launcherWindow();
diff --git a/src/main/window-coordinator.ts b/src/main/window-coordinator.ts
index a7d7032a4..ae9dd7af9 100644
--- a/src/main/window-coordinator.ts
+++ b/src/main/window-coordinator.ts
@@ -2,8 +2,8 @@
* The one owner for presenting the launcher and profile game windows.
*
* The registry remains the source of window identity. This coordinator keeps
- * macOS reveal, focus, hide, and final-game-close policy in one place without
- * retaining another window map or changing normal BrowserWindow levels.
+ * reveal, focus, hide, attention, and final-game-close policy in one place
+ * without retaining another window map or changing normal BrowserWindow levels.
*/
import type { WindowRegistry, RegisteredWindow } from "./window-registry.js";
@@ -16,9 +16,10 @@ interface PresentableWindow extends RegisteredWindow {
focus(): void;
}
-interface ApplicationPresentation {
+interface ApplicationPresentation
{
readonly dock?: { show(): void };
focus(options: { steal: true }): void;
+ requestAttention?(win: Window): void;
}
interface PreventableClose {
@@ -26,13 +27,13 @@ interface PreventableClose {
}
export class WindowCoordinator {
- readonly #application: ApplicationPresentation;
+ readonly #application: ApplicationPresentation;
readonly #registry: WindowRegistry;
#focusOrder: Window[] = [];
#activationTarget: Window | null = null;
constructor(
- application: ApplicationPresentation,
+ application: ApplicationPresentation,
registry: WindowRegistry,
) {
this.#application = application;
@@ -116,7 +117,10 @@ export class WindowCoordinator {
* then appear inactive before this explicit launch-owner check may focus one.
*/
revealAsyncGameIfLauncherFocused(win: Window): boolean {
- if (!this.#registry.launcherWindow()?.isFocused()) return false;
+ if (!this.#registry.launcherWindow()?.isFocused()) {
+ this.#application.requestAttention?.(win);
+ return false;
+ }
return this.revealGame(win);
}
diff --git a/src/main/window-shortcuts.ts b/src/main/window-shortcuts.ts
index 332ddae71..2dfe31817 100644
--- a/src/main/window-shortcuts.ts
+++ b/src/main/window-shortcuts.ts
@@ -9,6 +9,8 @@ import {
resolveShortcuts,
shortcutFromInput,
shortcutMatches,
+ shortcutPlatform,
+ type ShortcutPlatform,
type ShortcutAction,
type ShortcutCaptureResult,
} from "../shared/keyboard-shortcuts.js";
@@ -60,6 +62,7 @@ const textEditCommand = (input: Electron.Input): GameTextEditCommand | null => {
class WindowShortcuts {
readonly #actions: ShortcutActions;
+ readonly #platform: ShortcutPlatform;
#shortcuts = resolveShortcuts({
"tools.toggle": null,
"trade.toggle": null,
@@ -73,8 +76,10 @@ class WindowShortcuts {
constructor(
win: BrowserWindow,
actions: ShortcutActions,
+ platform: ShortcutPlatform,
) {
this.#actions = actions;
+ this.#platform = platform;
win.webContents.on("before-input-event", (event, input) => {
if (input.type === "keyUp") {
const decision = this.#claimedCodes.get(input.code);
@@ -140,7 +145,7 @@ class WindowShortcuts {
this.#finish({ status: "cleared" });
return;
}
- const binding = shortcutFromInput(input);
+ const binding = shortcutFromInput(input, this.#platform);
this.#finish(binding
? { status: "captured", binding }
: { status: "invalid" });
@@ -223,7 +228,7 @@ class WindowShortcuts {
return;
}
for (const [action, binding] of Object.entries(this.#shortcuts)) {
- if (binding && shortcutMatches(binding, input)) {
+ if (binding && shortcutMatches(binding, input, this.#platform)) {
recordMainInput(win, {
source: 'main', kind: 'native-key', phase: 'down',
key: tracedKey(input.key), repeat: input.isAutoRepeat, decision: 'shortcut',
@@ -343,8 +348,9 @@ const controllers = new WeakMap();
export function installWindowShortcuts(
win: BrowserWindow,
actions: ShortcutActions,
+ platform = shortcutPlatform(process.platform),
): void {
- controllers.set(win, new WindowShortcuts(win, actions));
+ controllers.set(win, new WindowShortcuts(win, actions, platform));
}
export function updateWindowShortcuts(
diff --git a/src/main/windows-shell.ts b/src/main/windows-shell.ts
new file mode 100644
index 000000000..09ad7009e
--- /dev/null
+++ b/src/main/windows-shell.ts
@@ -0,0 +1,26 @@
+/**
+ * The stable Squirrel.Windows identity shared by installer shortcuts and the
+ * running process. Windows groups launcher and game previews under this one
+ * identity; no per-window AppUserModelID creates a second taskbar product.
+ */
+import {
+ DISTRIBUTION_CHANNEL_CONFIG,
+ DISTRIBUTION_CHANNELS,
+ type DistributionChannel,
+} from "../shared/distribution-channel.js";
+
+export function windowsSquirrelPackageId(
+ channel: DistributionChannel,
+): string {
+ if (channel === "release") return "GuildWarsReforged";
+ if (channel === "preview") return "GuildWarsReforgedPreview";
+ return "GuildWarsReforgedDev";
+}
+
+export function windowsAppUserModelId(productName: string): string {
+ const channel = DISTRIBUTION_CHANNELS.find(
+ (candidate) => DISTRIBUTION_CHANNEL_CONFIG[candidate].productName === productName,
+ );
+ if (!channel) throw new Error(`unknown Windows product identity: ${productName}`);
+ return `com.squirrel.${windowsSquirrelPackageId(channel)}.${productName}`;
+}
diff --git a/src/shared/keyboard-shortcuts.ts b/src/shared/keyboard-shortcuts.ts
index b010f1dd1..b09f7f04d 100644
--- a/src/shared/keyboard-shortcuts.ts
+++ b/src/shared/keyboard-shortcuts.ts
@@ -51,6 +51,16 @@ export interface ShortcutInput {
alt: boolean;
}
+export const SHORTCUT_PLATFORMS = ["macos", "windows", "linux"] as const;
+export type ShortcutPlatform = (typeof SHORTCUT_PLATFORMS)[number];
+
+export function shortcutPlatform(platform: string): ShortcutPlatform {
+ if (platform === "darwin") return "macos";
+ if (platform === "win32") return "windows";
+ if (platform === "linux") return "linux";
+ throw new Error(`unsupported shortcut platform: ${platform}`);
+}
+
export function isShortcutBinding(value: unknown): value is ShortcutBinding {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
return false;
@@ -111,17 +121,26 @@ export function shortcutEquals(
export function shortcutMatches(
binding: ShortcutBinding,
input: ShortcutInput,
+ platform: ShortcutPlatform,
): boolean {
- return input.meta
- && !input.control
+ const primary = platform === "macos"
+ ? input.meta && !input.control
+ : input.control && !input.meta && !input.alt;
+ return primary
&& shortcutKey(input.code) === binding.key
&& input.shift === binding.shift
&& input.alt === binding.option;
}
-export function shortcutFromInput(input: ShortcutInput): ShortcutBinding | null {
+export function shortcutFromInput(
+ input: ShortcutInput,
+ platform: ShortcutPlatform,
+): ShortcutBinding | null {
const key = shortcutKey(input.code);
- if (!input.meta || input.control || key === null) return null;
+ const primary = platform === "macos"
+ ? input.meta && !input.control
+ : input.control && !input.meta && !input.alt;
+ if (!primary || key === null) return null;
return { key, shift: input.shift, option: input.alt };
}
@@ -140,7 +159,7 @@ const RESERVED_SHORTCUTS: readonly ShortcutBinding[] = [
})),
{ key: "z", shift: true, option: false },
{ key: "r", shift: false, option: false },
- // Travel owns Command+1…9 for quick-destination assignment.
+ // Travel owns primary-modifier+1…9 for quick-destination assignment.
..."123456789".split("").map((key) => ({
key,
shift: false,
@@ -176,17 +195,26 @@ export function withShortcutOverride(
return next;
}
-export function shortcutAccelerator(binding: ShortcutBinding | null): string | undefined {
+export function shortcutAccelerator(
+ binding: ShortcutBinding | null,
+ platform: ShortcutPlatform,
+): string | undefined {
if (!binding) return undefined;
return [
- "Command",
+ platform === "macos" ? "Command" : "Control",
binding.option ? "Alt" : null,
binding.shift ? "Shift" : null,
binding.key.toUpperCase(),
].filter((part): part is string => part !== null).join("+");
}
-export function shortcutDisplay(binding: ShortcutBinding | null): string {
+export function shortcutDisplay(
+ binding: ShortcutBinding | null,
+ platform: ShortcutPlatform,
+): string {
if (!binding) return "Not set";
- return `⌘${binding.option ? "⌥" : ""}${binding.shift ? "⇧" : ""}${binding.key.toUpperCase()}`;
+ if (platform === "macos") {
+ return `⌘${binding.option ? "⌥" : ""}${binding.shift ? "⇧" : ""}${binding.key.toUpperCase()}`;
+ }
+ return `Ctrl+${binding.shift ? "Shift+" : ""}${binding.key.toUpperCase()}`;
}
diff --git a/src/shared/launcher-contracts.ts b/src/shared/launcher-contracts.ts
index 9143e7614..4eb6e5349 100644
--- a/src/shared/launcher-contracts.ts
+++ b/src/shared/launcher-contracts.ts
@@ -21,7 +21,7 @@ import {
normaliseCartographyPresetLibrary,
type CartographyPresetLibrary,
} from "./cartography-overlay.js";
-import type { ShortcutBinding } from "./keyboard-shortcuts.js";
+import type { ShortcutBinding, ShortcutPlatform } from "./keyboard-shortcuts.js";
import type { ProfileId } from "./multiple-accounts.js";
export const LAUNCHER_IPC = Object.freeze({
@@ -175,6 +175,7 @@ export type LauncherReadiness =
export interface LauncherSnapshot {
readonly revision: number;
+ readonly platform: ShortcutPlatform;
readonly experience: Readonly<{
installationKind: LauncherInstallationKind;
setup: "pending" | "complete";
diff --git a/tests/policy/fuses.test.ts b/tests/policy/fuses.test.ts
index 38aa8c7f2..166480743 100644
--- a/tests/policy/fuses.test.ts
+++ b/tests/policy/fuses.test.ts
@@ -51,5 +51,10 @@ test("no fuse is left to its default", () => {
test("fuses are applied to every packaged platform executable", () => {
assert.match(forge, /packagedExecutablePath\(resourcesPath, platform\)/u);
+ assert.match(
+ forge,
+ /const executable = platform === "win32" \? "electron\.exe" : "electron"/u,
+ );
+ assert.match(forge, /resourcesPath,\s+"\.\.\/\.\.",\s+executable/u);
assert.doesNotMatch(forge, /if \(platform !== "darwin"\) return/u);
});
diff --git a/tests/unit/keyboard-shortcuts.test.ts b/tests/unit/keyboard-shortcuts.test.ts
index 06d4ffd3b..ff754e34b 100644
--- a/tests/unit/keyboard-shortcuts.test.ts
+++ b/tests/unit/keyboard-shortcuts.test.ts
@@ -38,35 +38,50 @@ describe("keyboard shortcuts", () => {
});
});
- it("matches only Command and keeps Control chords in the game", () => {
+ it("matches the platform primary modifier", () => {
const binding = { key: "k", shift: true, option: false };
assert.equal(shortcutMatches(binding, {
code: "KeyK", meta: true, control: false, shift: true, alt: false,
- }), true);
+ }, "macos"), true);
assert.equal(shortcutMatches(binding, {
code: "KeyK", meta: false, control: true, shift: true, alt: false,
- }), false);
+ }, "macos"), false);
+ assert.equal(shortcutMatches(binding, {
+ code: "KeyK", meta: false, control: true, shift: true, alt: false,
+ }, "windows"), true);
assert.equal(shortcutMatches(binding, {
code: "KeyK", meta: true, control: true, shift: true, alt: false,
- }), false);
+ }, "windows"), false);
assert.equal(shortcutMatches(binding, {
code: "KeyK", meta: true, control: false, shift: false, alt: false,
- }), false);
+ }, "macos"), false);
});
it("normalizes physical Command chords across Option-modified layouts", () => {
assert.deepEqual(shortcutFromInput({
code: "KeyK", meta: true, control: false, shift: true, alt: true,
- }), { key: "k", shift: true, option: true });
+ }, "macos"), { key: "k", shift: true, option: true });
assert.equal(shortcutFromInput({
code: "F1", meta: true, control: false, shift: false, alt: false,
- }), null);
+ }, "macos"), null);
assert.equal(shortcutFromInput({
code: "KeyK", meta: false, control: false, shift: false, alt: false,
- }), null);
+ }, "macos"), null);
assert.equal(shortcutFromInput({
code: "KeyK", meta: true, control: true, shift: false, alt: false,
- }), null);
+ }, "macos"), null);
+ });
+
+ it("refuses Windows AltGr while accepting Ctrl and Ctrl-Shift", () => {
+ assert.deepEqual(shortcutFromInput({
+ code: "KeyT", meta: false, control: true, shift: false, alt: false,
+ }, "windows"), { key: "t", shift: false, option: false });
+ assert.deepEqual(shortcutFromInput({
+ code: "KeyT", meta: false, control: true, shift: true, alt: false,
+ }, "windows"), { key: "t", shift: true, option: false });
+ assert.equal(shortcutFromInput({
+ code: "KeyQ", meta: false, control: true, shift: false, alt: true,
+ }, "windows"), null);
});
it("protects editing and lifecycle shortcuts and finds action conflicts", () => {
@@ -83,9 +98,11 @@ describe("keyboard shortcuts", () => {
it("formats the same binding for Electron and for players", () => {
const binding = { key: "c", shift: true, option: false };
- assert.equal(shortcutAccelerator(binding), "Command+Shift+C");
- assert.equal(shortcutDisplay(binding), "⌘⇧C");
- assert.equal(shortcutDisplay(null), "Not set");
+ assert.equal(shortcutAccelerator(binding, "macos"), "Command+Shift+C");
+ assert.equal(shortcutAccelerator(binding, "windows"), "Control+Shift+C");
+ assert.equal(shortcutDisplay(binding, "macos"), "⌘⇧C");
+ assert.equal(shortcutDisplay(binding, "windows"), "Ctrl+Shift+C");
+ assert.equal(shortcutDisplay(null, "windows"), "Not set");
});
it("refuses unknown actions, keys, fields, and modifier types", () => {
diff --git a/tests/unit/window-coordinator.test.ts b/tests/unit/window-coordinator.test.ts
index ff60f1117..ec626d629 100644
--- a/tests/unit/window-coordinator.test.ts
+++ b/tests/unit/window-coordinator.test.ts
@@ -45,7 +45,7 @@ function fakeWindow(id: number) {
};
}
-function setup() {
+function setup(options: { requestAttention?: boolean } = {}) {
const registry = new WindowRegistry>();
const applicationCalls: string[] = [];
const application = {
@@ -54,6 +54,9 @@ function setup() {
assert.deepEqual(options, { steal: true });
applicationCalls.push("app.focus");
},
+ ...(options.requestAttention
+ ? { requestAttention: () => applicationCalls.push("request.attention") }
+ : {}),
};
return {
applicationCalls,
@@ -238,4 +241,20 @@ describe("window coordinator", () => {
registry.unregister(launcher);
assert.equal(coordinator.revealAsyncGameIfLauncherFocused(game), false);
});
+
+ it("requests attention without focusing a delayed Windows game", () => {
+ const { applicationCalls, coordinator, registry } = setup({
+ requestAttention: true,
+ });
+ const launcher = fakeWindow(1);
+ const game = fakeWindow(2);
+ launcher.setVisible(true);
+ game.setVisible(true);
+ registry.register(launcher, { role: "launcher" });
+ registry.register(game, { role: "game", profileId: FIRST }, 2);
+
+ assert.equal(coordinator.revealAsyncGameIfLauncherFocused(game), false);
+ assert.deepEqual(applicationCalls, ["request.attention"]);
+ assert.deepEqual(game.calls, []);
+ });
});
diff --git a/tests/unit/window-shortcuts.test.ts b/tests/unit/window-shortcuts.test.ts
index 254af7c45..881639820 100644
--- a/tests/unit/window-shortcuts.test.ts
+++ b/tests/unit/window-shortcuts.test.ts
@@ -46,7 +46,7 @@ describe("window shortcut input", () => {
return new Promise((resolve) => settleQuitDialogs.push(resolve));
},
recordCommandQ: (phase, reason) => commandQ.push(`${phase}:${reason}`),
- });
+ }, "macos");
updateWindowShortcuts(win, { ...DEFAULT_SETTINGS, gwonmacTools: true });
const dispatch = (input: ShortcutInput) => {
@@ -236,4 +236,49 @@ describe("window shortcut input", () => {
assert.equal(dispatch(keyDown("KeyB", "b")), false);
assert.deepEqual(actions, ["tools.toggle"]);
});
+
+ it("uses Control on Windows without claiming AltGr", () => {
+ let beforeInput: (
+ event: { preventDefault(): void },
+ input: ShortcutInput,
+ ) => void = () => undefined;
+ const win = {
+ webContents: {
+ on(name: string, listener: typeof beforeInput) {
+ if (name === "before-input-event") beforeInput = listener;
+ },
+ },
+ on() { return win; },
+ } as unknown as BrowserWindow;
+ const actions: string[] = [];
+ installWindowShortcuts(win, {
+ run: (action) => actions.push(action),
+ edit: () => undefined,
+ quitOrReload: () => undefined,
+ }, "windows");
+ updateWindowShortcuts(win, { ...DEFAULT_SETTINGS, gwonmacTools: true });
+
+ const dispatch = (overrides: Partial): boolean => {
+ let prevented = false;
+ beforeInput({ preventDefault: () => { prevented = true; } }, {
+ type: "keyDown",
+ code: "KeyB",
+ key: "b",
+ meta: false,
+ control: true,
+ shift: false,
+ alt: false,
+ isAutoRepeat: false,
+ ...overrides,
+ });
+ return prevented;
+ };
+
+ assert.equal(dispatch({}), true);
+ assert.deepEqual(actions, ["tools.toggle"]);
+ releaseWindowShortcutKey(win, "KeyB");
+ assert.equal(dispatch({ control: true, alt: true }), false);
+ assert.equal(dispatch({ control: false, meta: true }), false);
+ assert.deepEqual(actions, ["tools.toggle"]);
+ });
});
diff --git a/tests/unit/windows-shell.test.ts b/tests/unit/windows-shell.test.ts
new file mode 100644
index 000000000..1707355ba
--- /dev/null
+++ b/tests/unit/windows-shell.test.ts
@@ -0,0 +1,27 @@
+/** Pin the one taskbar and Squirrel identity for every distribution channel. */
+import assert from "node:assert/strict";
+import { describe, it } from "node:test";
+import {
+ windowsAppUserModelId,
+ windowsSquirrelPackageId,
+} from "../../src/main/windows-shell.js";
+
+describe("Windows shell identity", () => {
+ it("keeps package identifiers space-free and taskbar identifiers exact", () => {
+ assert.equal(windowsSquirrelPackageId("release"), "GuildWarsReforged");
+ assert.equal(windowsSquirrelPackageId("preview"), "GuildWarsReforgedPreview");
+ assert.equal(windowsSquirrelPackageId("development"), "GuildWarsReforgedDev");
+ assert.equal(
+ windowsAppUserModelId("Guild Wars Reforged"),
+ "com.squirrel.GuildWarsReforged.Guild Wars Reforged",
+ );
+ assert.equal(
+ windowsAppUserModelId("Guild Wars Reforged Preview"),
+ "com.squirrel.GuildWarsReforgedPreview.Guild Wars Reforged Preview",
+ );
+ });
+
+ it("refuses an identity the installer cannot create", () => {
+ assert.throws(() => windowsAppUserModelId("Guild Wars Reforged Custom"));
+ });
+});