diff --git a/package-lock.json b/package-lock.json index b172636..567ee6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "drover", - "version": "0.1.23", + "version": "0.1.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "drover", - "version": "0.1.23", + "version": "0.1.24", "license": "MIT", "dependencies": { "@radix-ui/react-alert-dialog": "1.1.23", diff --git a/package.json b/package.json index 514432e..35bbf6b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "drover", "productName": "Drover", - "version": "0.1.23", + "version": "0.1.24", "description": "A native workspace for Herdr-powered agents, terminals, worktrees, and live sessions.", "main": ".vite/build/main.js", "private": true, diff --git a/src/main.ts b/src/main.ts index 1c4e151..be6c9e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -565,6 +565,7 @@ function createWindow(): void { }); mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' })); + mainWindow.webContents.setVisualZoomLevelLimits(1, 3); if (smokeTestMode) { mainWindow.webContents.once('did-finish-load', () => app.exit(0)); mainWindow.webContents.once('did-fail-load', (_event, code, description, url) => { diff --git a/src/main/application-menu.test.ts b/src/main/application-menu.test.ts index 9afd9b4..117d9a4 100644 --- a/src/main/application-menu.test.ts +++ b/src/main/application-menu.test.ts @@ -65,5 +65,35 @@ describe('applicationMenuTemplate', () => { expect(onAction).toHaveBeenNthCalledWith(5, 'new-workspace'); expect(onAction).toHaveBeenNthCalledWith(6, 'focus-pane-left'); expect(onAction).toHaveBeenNthCalledWith(7, 'open-shortcuts'); + const viewMenu = template.find((item) => item.label === 'View'); + const resetZoom = Array.isArray(viewMenu?.submenu) + ? viewMenu.submenu.find( + (item) => + 'role' in item && item.role === 'resetZoom' && item.accelerator === 'CmdOrCtrl+0', + ) + : undefined; + const zoomIn = Array.isArray(viewMenu?.submenu) + ? viewMenu.submenu.find( + (item) => 'role' in item && item.role === 'zoomIn' && item.accelerator === 'CmdOrCtrl+=', + ) + : undefined; + const zoomOut = Array.isArray(viewMenu?.submenu) + ? viewMenu.submenu.find( + (item) => 'role' in item && item.role === 'zoomOut' && item.accelerator === 'CmdOrCtrl+-', + ) + : undefined; + + expect(resetZoom).toMatchObject({ role: 'resetZoom', accelerator: 'CmdOrCtrl+0' }); + expect(zoomIn).toMatchObject({ role: 'zoomIn', accelerator: 'CmdOrCtrl+=' }); + expect(zoomOut).toMatchObject({ role: 'zoomOut', accelerator: 'CmdOrCtrl+-' }); + expect(viewMenu?.submenu).toEqual( + expect.arrayContaining([ + expect.objectContaining({ role: 'resetZoom', accelerator: 'CmdOrCtrl+0' }), + expect.objectContaining({ role: 'zoomIn', accelerator: 'CmdOrCtrl+=' }), + expect.objectContaining({ role: 'zoomIn', accelerator: 'CmdOrCtrl+Plus', visible: false }), + expect.objectContaining({ role: 'zoomOut', accelerator: 'CmdOrCtrl+-' }), + expect.objectContaining({ role: 'zoomOut', accelerator: 'CmdOrCtrl+_', visible: false }), + ]), + ); }); }); diff --git a/src/main/application-menu.ts b/src/main/application-menu.ts index f09d4ff..190769f 100644 --- a/src/main/application-menu.ts +++ b/src/main/application-menu.ts @@ -93,9 +93,14 @@ export function applicationMenuTemplate( { label: 'View', submenu: [ - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, + { role: 'resetZoom', accelerator: 'CmdOrCtrl+0' }, + { role: 'zoomIn', accelerator: 'CmdOrCtrl+=' }, + { role: 'zoomIn', accelerator: 'CmdOrCtrl+Plus', visible: false }, + { role: 'zoomIn', accelerator: 'CmdOrCtrl+numadd', visible: false }, + { role: 'zoomOut', accelerator: 'CmdOrCtrl+-' }, + { role: 'zoomOut', accelerator: 'CmdOrCtrl+_', visible: false }, + { role: 'zoomOut', accelerator: 'CmdOrCtrl+numsub', visible: false }, + { role: 'resetZoom', accelerator: 'CmdOrCtrl+num0', visible: false }, { type: 'separator' }, { role: 'togglefullscreen' }, ], diff --git a/src/renderer/help/ShortcutHelpDialog.test.tsx b/src/renderer/help/ShortcutHelpDialog.test.tsx index 289ca0d..48ea5c2 100644 --- a/src/renderer/help/ShortcutHelpDialog.test.tsx +++ b/src/renderer/help/ShortcutHelpDialog.test.tsx @@ -21,5 +21,11 @@ describe('ShortcutHelpDialog', () => { await user.clear(screen.getByLabelText('Search shortcuts')); await user.type(screen.getByLabelText('Search shortcuts'), '⌘K'); expect(screen.getByText('Open Navigator')).toBeInTheDocument(); + await user.clear(screen.getByLabelText('Search shortcuts')); + await user.type(screen.getByLabelText('Search shortcuts'), 'zoom'); + expect(screen.getByText('Zoom in')).toBeInTheDocument(); + expect(screen.getByText('Zoom out')).toBeInTheDocument(); + expect(screen.getByText('Reset zoom')).toBeInTheDocument(); + expect(screen.getByText('Toggle pane zoom')).toBeInTheDocument(); }); }); diff --git a/src/renderer/help/ShortcutHelpDialog.tsx b/src/renderer/help/ShortcutHelpDialog.tsx index b07a400..e01459c 100644 --- a/src/renderer/help/ShortcutHelpDialog.tsx +++ b/src/renderer/help/ShortcutHelpDialog.tsx @@ -16,7 +16,7 @@ interface ShortcutHelpDialogProps { } interface ShortcutDefinition { - category: 'Session' | 'Spaces' | 'Tabs' | 'Panes' | 'Terminal'; + category: 'Session' | 'Spaces' | 'Tabs' | 'Panes' | 'Terminal' | 'View'; action: string; keys: string; } @@ -40,6 +40,9 @@ export const DESKTOP_SHORTCUTS: readonly ShortcutDefinition[] = [ { category: 'Panes', action: 'Split pane right', keys: '⌘⌥→' }, { category: 'Panes', action: 'Split pane down', keys: '⌘⌥↓' }, { category: 'Panes', action: 'Toggle pane zoom', keys: '⌘⇧Z' }, + { category: 'View', action: 'Zoom in', keys: '⌘+' }, + { category: 'View', action: 'Zoom out', keys: '⌘-' }, + { category: 'View', action: 'Reset zoom', keys: '⌘0' }, { category: 'Terminal', action: 'Search terminal', keys: '⌘F' }, { category: 'Terminal', action: 'Copy terminal selection', keys: '⌘C' }, ] as const; diff --git a/src/renderer/terminal/TerminalPanel.test.tsx b/src/renderer/terminal/TerminalPanel.test.tsx index 9485529..5a0328c 100644 --- a/src/renderer/terminal/TerminalPanel.test.tsx +++ b/src/renderer/terminal/TerminalPanel.test.tsx @@ -434,6 +434,31 @@ describe('TerminalPanel', () => { expect(intercepted).toBe(false); await waitFor(() => expect(terminalControl.paste).toHaveBeenCalledWith('shortcut paste')); }); + it('allows window zoom in, zoom out, and reset zoom shortcuts through the custom key handler', () => { + render(); + + const zoomInEqual = terminalControl.customKeyHandler?.( + new KeyboardEvent('keydown', { key: '=', metaKey: true }), + ); + const zoomInPlus = terminalControl.customKeyHandler?.( + new KeyboardEvent('keydown', { key: '+', ctrlKey: true }), + ); + const zoomOutMinus = terminalControl.customKeyHandler?.( + new KeyboardEvent('keydown', { key: '-', metaKey: true }), + ); + const resetZoomZero = terminalControl.customKeyHandler?.( + new KeyboardEvent('keydown', { key: '0', ctrlKey: true }), + ); + const numpadAdd = terminalControl.customKeyHandler?.( + new KeyboardEvent('keydown', { code: 'NumpadAdd', metaKey: true }), + ); + + expect(zoomInEqual).toBe(false); + expect(zoomInPlus).toBe(false); + expect(zoomOutMinus).toBe(false); + expect(resetZoomZero).toBe(false); + expect(numpadAdd).toBe(false); + }); it('opens only modifier-clicked HTTP links through the injected callback', () => { const onOpenExternal = vi.fn(); diff --git a/src/renderer/terminal/TerminalPanel.tsx b/src/renderer/terminal/TerminalPanel.tsx index b75a2f9..81a5b09 100644 --- a/src/renderer/terminal/TerminalPanel.tsx +++ b/src/renderer/terminal/TerminalPanel.tsx @@ -239,6 +239,24 @@ export function TerminalPanel({ pane, onOpenExternal, onScrollRequest }: Termina setSearchOpen(true); return false; } + if ( + event.type === 'keydown' && + (event.metaKey || event.ctrlKey) && + !event.altKey && + (event.key === '=' || + event.key === '+' || + event.key === '-' || + event.key === '_' || + event.key === '0' || + event.code === 'Equal' || + event.code === 'Minus' || + event.code === 'Digit0' || + event.code === 'NumpadAdd' || + event.code === 'NumpadSubtract' || + event.code === 'Numpad0') + ) { + return false; + } if (event.type === 'keydown' && !event.altKey) { const key = event.key.toLowerCase(); const terminalClipboardShortcut = event.metaKey || (event.ctrlKey && event.shiftKey);