From 4e662d8a60d2836414384cfc4c1e3314b7b7a66b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:29:34 +0100 Subject: [PATCH] fix: give "Show the app" a key the composer doesn't use, and refuse the app's own keys for global shortcuts --- components/settings/desktop-os-settings.tsx | 16 ++++++---------- lib/desktop/__tests__/os-window.test.tsx | 11 +++++++++++ lib/desktop/__tests__/shortcut-keys.test.ts | 15 ++++++++++++++- lib/desktop/shortcut-keys.ts | 17 ++++++++++++++++- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/components/settings/desktop-os-settings.tsx b/components/settings/desktop-os-settings.tsx index cd3851cca..9213d62e0 100644 --- a/components/settings/desktop-os-settings.tsx +++ b/components/settings/desktop-os-settings.tsx @@ -5,16 +5,10 @@ import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { getDesktopBridge } from "@/lib/desktop/bridge"; import type { DesktopOsDefaultAppStatus, DesktopOsSettings as Settings, DesktopOsStatus } from "@/lib/desktop/types"; +import { describeAccelerator, showShortcutDefault } from "@/lib/desktop/shortcut-keys"; import { DesktopQuickComposeSetting } from "./desktop-quick-compose-setting"; import { SettingItem, SettingsSection, ToggleSwitch } from "./settings-section"; -/** On Windows and Linux this is what a press of the keys shows; macOS maps it to Cmd. */ -const SHOW_SHORTCUT = "CommandOrControl+Shift+B"; - -function shortcutLabel(platform: string | undefined): string { - return platform === "darwin" ? "⇧⌘B" : "Ctrl+Shift+B"; -} - /** * The desktop app's own settings: only where there's an app to set. A browser never sees * them, and they render nothing until the bridge has answered. The General page has how @@ -46,6 +40,8 @@ export function DesktopOsSettings({ part = "app" }: { part?: "app" | "notificati if (!os || !status) return null; const { settings } = status; + const platform = bridge?.platform; + const showKeys = describeAccelerator(settings.showShortcut ?? showShortcutDefault(platform), platform); const change = (patch: Partial) => { void os.setSettings(patch).then(setStatus); }; @@ -103,12 +99,12 @@ export function DesktopOsSettings({ part = "app" }: { part?: "app" | "notificati change({ showShortcut: on ? SHOW_SHORTCUT : null })} + onChange={(on) => change({ showShortcut: on ? showShortcutDefault(platform) : null })} /> {status.quickCompose && ( diff --git a/lib/desktop/__tests__/os-window.test.tsx b/lib/desktop/__tests__/os-window.test.tsx index caac76876..d7f06d8c7 100644 --- a/lib/desktop/__tests__/os-window.test.tsx +++ b/lib/desktop/__tests__/os-window.test.tsx @@ -431,6 +431,17 @@ describe('the settings section', () => { await waitFor(() => expect(os.openDefaultAppSettings).toHaveBeenCalled()); }); + it('switching "Show the app" on takes a key the composer doesn\'t use', async () => { + const { DesktopOsSettings } = await import('@/components/settings/desktop-os-settings'); + const os = bridgeFor(status()); + setBridge(os as never); + render(); + expect(await screen.findByText('show_shortcut_label')).toBeInTheDocument(); + // Close to tray, start with the computer, then this one. + fireEvent.click(screen.getAllByRole('switch')[2]); + await waitFor(() => expect(os.setSettings).toHaveBeenCalledWith({ showShortcut: 'CommandOrControl+Alt+F11' })); + }); + it('says when another app holds the shortcut', async () => { const { DesktopOsSettings } = await import('@/components/settings/desktop-os-settings'); setBridge(bridgeFor(status({ showShortcut: 'CommandOrControl+Shift+B' })) as never); diff --git a/lib/desktop/__tests__/shortcut-keys.test.ts b/lib/desktop/__tests__/shortcut-keys.test.ts index 423225010..678406f51 100644 --- a/lib/desktop/__tests__/shortcut-keys.test.ts +++ b/lib/desktop/__tests__/shortcut-keys.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { describeAccelerator, readShortcutPress, shortcutProblem, type ShortcutKeyEvent } from '../shortcut-keys'; +import { describeAccelerator, readShortcutPress, shortcutProblem, showShortcutDefault, type ShortcutKeyEvent } from '../shortcut-keys'; const press = (key: string, code: string, modifiers: Partial = {}): ShortcutKeyEvent => ({ key, @@ -108,4 +108,17 @@ describe('shortcutProblem', () => { expect(shortcutProblem('CommandOrControl+Control+N', 'win32')).toBe('incomplete'); expect(shortcutProblem('CommandOrControl+Control+N', 'darwin')).toBeNull(); }); + + it('refuses the app\'s own keys: a global one would take them from the composer and the menus', () => { + for (const keys of ['CommandOrControl+Shift+B', 'CommandOrControl+Shift+C', 'CommandOrControl+Shift+Enter', 'CommandOrControl+Shift+M', 'CommandOrControl+Shift+N', 'CommandOrControl+Shift+S', 'CommandOrControl+Shift+R', 'CommandOrControl+Shift+F']) { + for (const platform of ['win32', 'darwin', 'linux']) expect(shortcutProblem(keys, platform), `${keys} on ${platform}`).toBe('reserved'); + } + expect(readShortcutPress(press('B', 'KeyB', { ctrlKey: true, shiftKey: true }), 'win32')).toEqual({ refused: 'reserved' }); + }); + + it('"Show the app" by default is none of them, and on Windows types nothing with AltGr', () => { + for (const platform of ['win32', 'darwin', 'linux']) expect(shortcutProblem(showShortcutDefault(platform), platform)).toBeNull(); + expect(showShortcutDefault('win32')).toBe('CommandOrControl+Alt+F11'); + expect(showShortcutDefault('linux')).toBe('CommandOrControl+Alt+B'); + }); }); diff --git a/lib/desktop/shortcut-keys.ts b/lib/desktop/shortcut-keys.ts index 323fa048d..c2a6f4b20 100644 --- a/lib/desktop/shortcut-keys.ts +++ b/lib/desktop/shortcut-keys.ts @@ -10,6 +10,15 @@ */ export const QUICK_COMPOSE_DEFAULT = 'CommandOrControl+Alt+N'; +/** + * What "Show the app with a keyboard shortcut" takes when switched on: Ctrl+Alt+B (⌥⌘B), and + * on Windows Ctrl+Alt+F11, since Ctrl+Alt is AltGr there and B types on many layouts. The + * shell's showShortcutBuiltIn, which also moves anyone still on the old Ctrl+Shift+B to it. + */ +export function showShortcutDefault(platform: string | undefined): string { + return platform === 'win32' ? 'CommandOrControl+Alt+F11' : 'CommandOrControl+Alt+B'; +} + const MAC_SYMBOLS: Record = { control: '⌃', ctrl: '⌃', @@ -67,6 +76,12 @@ export interface ShortcutKeyEvent { * shell's. `cmd` is Cmd on a Mac and Ctrl elsewhere. */ const RESERVED = new Set(['cmd+shift+A', 'cmd+shift+C', 'cmd+shift+V', 'cmd+shift+X', 'cmd+shift+Z']); +/** + * The app's own keys with two modifiers, which a global key would take from Bulwark itself: + * the composer's Cc, Bcc and schedule send, and the menus' new message, new window, Save as + * PDF, reply all and forward. Same list as APP_KEYS in the shell's os-rules.ts. + */ +const APP_KEYS = new Set(['cmd+shift+B', 'cmd+shift+C', 'cmd+shift+ENTER', 'cmd+shift+M', 'cmd+shift+N', 'cmd+shift+S', 'cmd+shift+R', 'cmd+shift+F']); const RESERVED_ON_MAC = new Set([ 'cmd+shift+3', 'cmd+shift+4', 'cmd+shift+5', 'cmd+shift+Q', 'cmd+ctrl+Q', 'cmd+ctrl+F', @@ -119,7 +134,7 @@ export function shortcutProblem(accelerator: string, platform: string | undefine if (modifiers.has('super')) return 'reserved'; if (modifiers.size < 2 || !(modifiers.has('cmd') || modifiers.has('ctrl'))) return 'incomplete'; const named = `${[...modifiers].sort().join('+')}+${key}`; - return RESERVED.has(named) || (mac && RESERVED_ON_MAC.has(named)) ? 'reserved' : null; + return RESERVED.has(named) || APP_KEYS.has(`${[...modifiers].sort().join('+')}+${key.toUpperCase()}`) || (mac && RESERVED_ON_MAC.has(named)) ? 'reserved' : null; } /**