From b08eccf5324fe313169c1140db86d3733e8660d0 Mon Sep 17 00:00:00 2001 From: Mat4m0 Date: Mon, 31 Aug 2026 01:02:41 +0200 Subject: [PATCH] feat(windows): align shortcuts and taskbar behavior --- apps/launcher/src/App.vue | 2 +- apps/launcher/src/fixtures.ts | 1 + forge.config.ts | 9 +++-- package.json | 1 + src/main/launcher-orchestrator.ts | 3 ++ src/main/launcher-shortcut-capture.ts | 11 +++++-- src/main/main.ts | 16 ++++++++- src/main/window-coordinator.ts | 16 +++++---- src/main/window-shortcuts.ts | 12 +++++-- src/main/windows-shell.ts | 26 +++++++++++++++ src/shared/keyboard-shortcuts.ts | 46 +++++++++++++++++++++----- src/shared/launcher-contracts.ts | 3 +- tests/policy/fuses.test.ts | 5 +++ tests/unit/keyboard-shortcuts.test.ts | 41 ++++++++++++++++------- tests/unit/window-coordinator.test.ts | 21 +++++++++++- tests/unit/window-shortcuts.test.ts | 47 ++++++++++++++++++++++++++- tests/unit/windows-shell.test.ts | 27 +++++++++++++++ 17 files changed, 246 insertions(+), 41 deletions(-) create mode 100644 src/main/windows-shell.ts create mode 100644 tests/unit/windows-shell.test.ts 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")); + }); +});