Fix terminal zoom shortcuts on Linux layouts (#347)

This commit is contained in:
Matheus Nogueira
2026-04-06 20:42:55 -03:00
committed by GitHub
parent 697c963986
commit e42ae3f0f0
5 changed files with 165 additions and 7 deletions
+9
View File
@@ -67,6 +67,15 @@ app.whenReady().then(async () => {
onCheckForUpdates: () => checkForUpdatesFromMenu(),
onOpenSettings: () => {
mainWindow?.webContents.send('ui:openSettings')
},
onZoomIn: () => {
mainWindow?.webContents.send('terminal:zoom', 'in')
},
onZoomOut: () => {
mainWindow?.webContents.send('terminal:zoom', 'out')
},
onZoomReset: () => {
mainWindow?.webContents.send('terminal:zoom', 'reset')
}
})
registerCoreHandlers(store, runtime)
+23 -5
View File
@@ -3,11 +3,17 @@ import { Menu, app } from 'electron'
type RegisterAppMenuOptions = {
onOpenSettings: () => void
onCheckForUpdates: () => void
onZoomIn: () => void
onZoomOut: () => void
onZoomReset: () => void
}
export function registerAppMenu({
onOpenSettings,
onCheckForUpdates
onCheckForUpdates,
onZoomIn,
onZoomOut,
onZoomReset
}: RegisterAppMenuOptions): void {
const template: Electron.MenuItemConstructorOptions[] = [
{
@@ -53,19 +59,31 @@ export function registerAppMenu({
{ role: 'toggleDevTools' },
{ type: 'separator' },
{
label: 'Actual Size',
label: 'Reset Size',
accelerator: 'CmdOrCtrl+0',
registerAccelerator: false
// Why: Some keyboard layouts/platforms intercept Cmd/Ctrl+zoom chords
// before before-input-event fires. Binding the menu accelerator gives
// us a reliable cross-platform fallback path.
click: () => onZoomReset()
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
registerAccelerator: false
click: () => onZoomIn()
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
registerAccelerator: false
click: () => onZoomOut()
},
{
label: 'Zoom Out (Shift Alias)',
// Why: Some Linux keyboard layouts report the top-row minus chord as
// an underscore accelerator. Keep this hidden alias so Ctrl+- and
// Ctrl+_ can both route to terminal zoom out.
accelerator: 'CmdOrCtrl+_',
visible: false,
click: () => onZoomOut()
},
{ type: 'separator' },
{ role: 'togglefullscreen' }
+83
View File
@@ -96,4 +96,87 @@ describe('createMainWindow', () => {
expect(fileNavigationPreventDefault).toHaveBeenCalledTimes(1)
expect(openExternalMock).toHaveBeenCalledTimes(4)
})
it('supports all minus key variants for terminal zoom out', () => {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
setZoomLevel: vi.fn(),
setWindowOpenHandler: vi.fn(),
send: vi.fn()
}
const browserWindowInstance = {
webContents,
on: vi.fn(),
maximize: vi.fn(),
show: vi.fn(),
loadFile: vi.fn(),
loadURL: vi.fn()
}
browserWindowMock.mockImplementation(function () {
return browserWindowInstance
})
createMainWindow(null)
const beforeInputEvent = windowHandlers['before-input-event']
for (const input of [
{ type: 'keyDown', control: true, meta: false, alt: false, key: '-' },
{ type: 'keyDown', control: true, meta: false, alt: false, key: '_' },
{ type: 'keyDown', control: true, meta: false, alt: false, key: 'Minus' },
{ type: 'keyDown', control: true, meta: false, alt: false, key: 'Subtract' },
{ type: 'keyDown', control: true, meta: false, alt: false, key: '', code: 'Minus' },
{ type: 'keyDown', control: true, meta: false, alt: false, key: '', code: 'NumpadSubtract' }
]) {
const preventDefault = vi.fn()
beforeInputEvent({ preventDefault } as never, input as never)
expect(preventDefault).toHaveBeenCalledTimes(1)
}
expect(webContents.send).toHaveBeenCalledTimes(6)
expect(webContents.send).toHaveBeenNthCalledWith(1, 'terminal:zoom', 'out')
expect(webContents.send).toHaveBeenNthCalledWith(2, 'terminal:zoom', 'out')
expect(webContents.send).toHaveBeenNthCalledWith(3, 'terminal:zoom', 'out')
expect(webContents.send).toHaveBeenNthCalledWith(4, 'terminal:zoom', 'out')
expect(webContents.send).toHaveBeenNthCalledWith(5, 'terminal:zoom', 'out')
expect(webContents.send).toHaveBeenNthCalledWith(6, 'terminal:zoom', 'out')
})
it('routes Electron zoom command events to terminal zoom', () => {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {
on: vi.fn((event, handler) => {
windowHandlers[event] = handler
}),
setZoomLevel: vi.fn(),
setWindowOpenHandler: vi.fn(),
send: vi.fn()
}
const browserWindowInstance = {
webContents,
on: vi.fn(),
maximize: vi.fn(),
show: vi.fn(),
loadFile: vi.fn(),
loadURL: vi.fn()
}
browserWindowMock.mockImplementation(function () {
return browserWindowInstance
})
createMainWindow(null)
const onZoomChanged = windowHandlers['zoom-changed']
const preventDefault = vi.fn()
onZoomChanged({ preventDefault } as never, 'out')
onZoomChanged({ preventDefault } as never, 'in')
expect(preventDefault).toHaveBeenCalledTimes(2)
expect(webContents.send).toHaveBeenCalledTimes(2)
expect(webContents.send).toHaveBeenNthCalledWith(1, 'terminal:zoom', 'out')
expect(webContents.send).toHaveBeenNthCalledWith(2, 'terminal:zoom', 'in')
})
})
+35 -2
View File
@@ -25,6 +25,27 @@ function normalizeExternalUrl(rawUrl: string): string | null {
}
}
function isZoomInShortcut(input: Electron.Input): boolean {
return input.key === '=' || input.key === '+' || input.code === 'NumpadAdd'
}
function isZoomOutShortcut(input: Electron.Input): boolean {
// Why: Electron reports Cmd/Ctrl+Minus differently across layouts and devices:
// some emit '-' while shifted layouts emit '_', and other layouts/devices
// report symbolic names like "Minus"/"Subtract" in either key or code.
// We accept all known variants so zoom out remains reachable everywhere.
const key = (input.key ?? '').toLowerCase()
const code = (input.code ?? '').toLowerCase()
return (
key === '-' ||
key === '_' ||
key.includes('minus') ||
key.includes('subtract') ||
code.includes('minus') ||
code.includes('subtract')
)
}
export function createMainWindow(store: Store | null): BrowserWindow {
const mainWindow = new BrowserWindow({
width: 1200,
@@ -103,10 +124,10 @@ export function createMainWindow(store: Store | null): BrowserWindow {
return
}
if (input.key === '=' || input.key === '+') {
if (isZoomInShortcut(input)) {
event.preventDefault()
mainWindow.webContents.send('terminal:zoom', 'in')
} else if (input.key === '-') {
} else if (isZoomOutShortcut(input)) {
event.preventDefault()
mainWindow.webContents.send('terminal:zoom', 'out')
} else if (input.key === '0' && !input.shift) {
@@ -115,6 +136,18 @@ export function createMainWindow(store: Store | null): BrowserWindow {
}
})
mainWindow.webContents.on('zoom-changed', (event, zoomDirection) => {
// Why: Some keyboard layouts/platforms consume Ctrl/Cmd+Minus before
// before-input-event fires, but still emit Electron's zoom command. We
// reroute that command to terminal zoom so zoom-out remains reachable.
event.preventDefault()
if (zoomDirection === 'in') {
mainWindow.webContents.send('terminal:zoom', 'in')
} else if (zoomDirection === 'out') {
mainWindow.webContents.send('terminal:zoom', 'out')
}
})
// Intercept window close so the renderer can show a confirmation dialog
// when terminals with running processes would be killed. The renderer
// replies with 'window:confirm-close' to proceed, or does nothing to cancel.
@@ -72,6 +72,21 @@ const SHORTCUT_GROUP_DEFINITIONS: ShortcutGroupDefinition[] = [
action: 'Toggle Source Control',
searchKeywords: ['shortcut', 'source control'],
keys: ({ mod, shift }) => [mod, shift, 'G']
},
{
action: 'Zoom In',
searchKeywords: ['shortcut', 'zoom', 'in', 'scale'],
keys: ({ mod, shift }) => (mod === 'Ctrl' ? [mod, shift, '+'] : [mod, '+'])
},
{
action: 'Zoom Out',
searchKeywords: ['shortcut', 'zoom', 'out', 'scale'],
keys: ({ mod, shift }) => (mod === 'Ctrl' ? [mod, shift, '-'] : [mod, '-'])
},
{
action: 'Reset Size',
searchKeywords: ['shortcut', 'zoom', 'reset', 'size', 'actual'],
keys: ({ mod }) => [mod, '0']
}
]
},