mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 08:02:35 +00:00
Fix terminal zoom shortcuts on Linux layouts (#347)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user