Keyboard short cuts

This commit is contained in:
Douwe Osinga
2026-01-26 09:42:45 -05:00
parent 698382c5ac
commit aa8992d5de
3 changed files with 197 additions and 100 deletions
@@ -9,9 +9,10 @@ import ConfigSettings from './config/ConfigSettings';
import PromptsSettingsSection from './PromptsSettingsSection';
import { ExtensionConfig } from '../../api';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { Bot, Share2, Monitor, MessageSquare, FileText } from 'lucide-react';
import { Bot, Share2, Monitor, MessageSquare, FileText, Keyboard } from 'lucide-react';
import { useState, useEffect, useRef } from 'react';
import ChatSettingsSection from './chat/ChatSettingsSection';
import KeyboardShortcutsSection from './keyboard/KeyboardShortcutsSection';
import { CONFIGURATION_ENABLED } from '../../updates';
import { trackSettingsTabViewed } from '../../utils/analytics';
@@ -52,6 +53,7 @@ export default function SettingsView({
app: 'app',
chat: 'chat',
prompts: 'prompts',
keyboard: 'keyboard',
};
const targetTab = sectionToTab[viewOptions.section];
@@ -130,6 +132,14 @@ export default function SettingsView({
<FileText className="h-4 w-4" />
Prompts
</TabsTrigger>
<TabsTrigger
value="keyboard"
className="flex gap-2"
data-testid="settings-keyboard-tab"
>
<Keyboard className="h-4 w-4" />
Keyboard
</TabsTrigger>
<TabsTrigger value="app" className="flex gap-2" data-testid="settings-app-tab">
<Monitor className="h-4 w-4" />
App
@@ -169,6 +179,13 @@ export default function SettingsView({
<PromptsSettingsSection />
</TabsContent>
<TabsContent
value="keyboard"
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
>
<KeyboardShortcutsSection />
</TabsContent>
<TabsContent
value="app"
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
+147 -99
View File
@@ -35,6 +35,7 @@ import {
loadSettings,
saveSettings,
updateEnvironmentVariables,
getKeyboardShortcuts,
} from './utils/settings';
import * as crypto from 'crypto';
// import electron from "electron";
@@ -1266,11 +1267,23 @@ ipcMain.handle('get-settings', () => {
ipcMain.handle('save-settings', (_event, settings) => {
try {
const oldSettings = loadSettings();
saveSettings(settings);
return true;
// Check if keyboard shortcuts changed
const oldShortcuts = getKeyboardShortcuts(oldSettings);
const newShortcuts = getKeyboardShortcuts(settings);
const shortcutsChanged = JSON.stringify(oldShortcuts) !== JSON.stringify(newShortcuts);
// Re-register global shortcuts if they changed
if (shortcutsChanged) {
registerGlobalShortcuts();
}
return { success: true, shortcutsChanged };
} catch (error) {
console.error('Error saving settings:', error);
return false;
return { success: false, shortcutsChanged: false };
}
});
@@ -1806,6 +1819,37 @@ const focusWindow = () => {
}
};
// Register global shortcuts based on settings
const registerGlobalShortcuts = () => {
// Unregister all existing shortcuts first
globalShortcut.unregisterAll();
const settings = loadSettings();
const shortcuts = getKeyboardShortcuts(settings);
// Register focus window shortcut (global)
if (shortcuts.focusWindow) {
try {
globalShortcut.register(shortcuts.focusWindow, () => {
focusWindow();
});
} catch (e) {
console.error('Error registering focus window hotkey:', e);
}
}
// Register quick launcher shortcut (global)
if (shortcuts.quickLauncher) {
try {
globalShortcut.register(shortcuts.quickLauncher, () => {
createLauncher();
});
} catch (e) {
console.error('Error registering launcher hotkey:', e);
}
}
};
async function appMain() {
await configureProxy();
@@ -1872,21 +1916,8 @@ async function appMain() {
});
});
try {
globalShortcut.register('CommandOrControl+Alt+Shift+G', () => {
createLauncher();
});
} catch (e) {
console.error('Error registering launcher hotkey:', e);
}
try {
globalShortcut.register('CommandOrControl+Alt+G', () => {
focusWindow();
});
} catch (e) {
console.error('Error registering focus window hotkey:', e);
}
// Register global shortcuts based on settings
registerGlobalShortcuts();
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
details.requestHeaders['Origin'] = 'http://localhost:5173';
@@ -1936,20 +1967,24 @@ async function appMain() {
const menu = Menu.getApplicationMenu();
const shortcuts = getKeyboardShortcuts(settings);
const appMenu = menu?.items.find((item) => item.label === 'Goose');
if (appMenu?.submenu) {
appMenu.submenu.insert(1, new MenuItem({ type: 'separator' }));
appMenu.submenu.insert(
1,
new MenuItem({
label: 'Settings',
accelerator: 'CmdOrCtrl+,',
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('set-view', 'settings');
},
})
);
if (shortcuts.settings) {
appMenu.submenu.insert(
1,
new MenuItem({
label: 'Settings',
accelerator: shortcuts.settings,
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('set-view', 'settings');
},
})
);
}
appMenu.submenu.insert(1, new MenuItem({ type: 'separator' }));
}
@@ -1960,7 +1995,7 @@ async function appMain() {
const findSubmenu = Menu.buildFromTemplate([
{
label: 'Find…',
accelerator: process.platform === 'darwin' ? 'Command+F' : 'Control+F',
accelerator: shortcuts.find || undefined,
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('find-command');
@@ -1968,7 +2003,7 @@ async function appMain() {
},
{
label: 'Find Next',
accelerator: process.platform === 'darwin' ? 'Command+G' : 'Control+G',
accelerator: shortcuts.findNext || undefined,
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('find-next');
@@ -1976,7 +2011,7 @@ async function appMain() {
},
{
label: 'Find Previous',
accelerator: process.platform === 'darwin' ? 'Shift+Command+G' : 'Shift+Control+G',
accelerator: shortcuts.findPrevious || undefined,
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('find-previous');
@@ -2005,37 +2040,43 @@ async function appMain() {
const fileMenu = menu?.items.find((item) => item.label === 'File');
if (fileMenu?.submenu) {
fileMenu.submenu.insert(
0,
new MenuItem({
label: 'New Chat',
accelerator: 'CmdOrCtrl+T',
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('new-chat');
},
})
);
if (shortcuts.newChat) {
fileMenu.submenu.insert(
0,
new MenuItem({
label: 'New Chat',
accelerator: shortcuts.newChat,
click() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('new-chat');
},
})
);
}
fileMenu.submenu.insert(
1,
new MenuItem({
label: 'New Chat Window',
accelerator: process.platform === 'darwin' ? 'Cmd+N' : 'Ctrl+N',
click() {
ipcMain.emit('create-chat-window');
},
})
);
if (shortcuts.newChatWindow) {
fileMenu.submenu.insert(
1,
new MenuItem({
label: 'New Chat Window',
accelerator: shortcuts.newChatWindow,
click() {
ipcMain.emit('create-chat-window');
},
})
);
}
fileMenu.submenu.insert(
2,
new MenuItem({
label: 'Open Directory...',
accelerator: 'CmdOrCtrl+O',
click: () => openDirectoryDialog(),
})
);
if (shortcuts.openDirectory) {
fileMenu.submenu.insert(
2,
new MenuItem({
label: 'Open Directory...',
accelerator: shortcuts.openDirectory,
click: () => openDirectoryDialog(),
})
);
}
const recentFilesSubmenu = buildRecentFilesMenu();
if (recentFilesSubmenu.length > 0) {
@@ -2050,25 +2091,30 @@ async function appMain() {
fileMenu.submenu.insert(4, new MenuItem({ type: 'separator' }));
fileMenu.submenu.append(
new MenuItem({
label: 'Focus Goose Window',
accelerator: 'CmdOrCtrl+Alt+G',
click() {
focusWindow();
},
})
);
// Add global shortcuts from settings
if (shortcuts.focusWindow) {
fileMenu.submenu.append(
new MenuItem({
label: 'Focus Goose Window',
accelerator: shortcuts.focusWindow,
click() {
focusWindow();
},
})
);
}
fileMenu.submenu.append(
new MenuItem({
label: 'Quick Launcher',
accelerator: 'CmdOrCtrl+Alt+Shift+G',
click() {
createLauncher();
},
})
);
if (shortcuts.quickLauncher) {
fileMenu.submenu.append(
new MenuItem({
label: 'Quick Launcher',
accelerator: shortcuts.quickLauncher,
click() {
createLauncher();
},
})
);
}
}
if (menu) {
@@ -2089,29 +2135,31 @@ async function appMain() {
}
if (windowMenu.submenu) {
windowMenu.submenu.append(
new MenuItem({
label: 'Always on Top',
type: 'checkbox',
accelerator: process.platform === 'darwin' ? 'Cmd+Shift+T' : 'Ctrl+Shift+T',
click(menuItem) {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
const isAlwaysOnTop = menuItem.checked;
if (shortcuts.alwaysOnTop) {
windowMenu.submenu.append(
new MenuItem({
label: 'Always on Top',
type: 'checkbox',
accelerator: shortcuts.alwaysOnTop,
click(menuItem) {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
const isAlwaysOnTop = menuItem.checked;
if (process.platform === 'darwin') {
focusedWindow.setAlwaysOnTop(isAlwaysOnTop, 'floating');
} else {
focusedWindow.setAlwaysOnTop(isAlwaysOnTop);
if (process.platform === 'darwin') {
focusedWindow.setAlwaysOnTop(isAlwaysOnTop, 'floating');
} else {
focusedWindow.setAlwaysOnTop(isAlwaysOnTop);
}
console.log(
`[Main] Set always-on-top to ${isAlwaysOnTop} for window ${focusedWindow.id}`
);
}
console.log(
`[Main] Set always-on-top to ${isAlwaysOnTop} for window ${focusedWindow.id}`
);
}
},
})
);
},
})
);
}
}
}
+32
View File
@@ -1,6 +1,7 @@
import { app } from 'electron';
import fs from 'fs';
import path from 'path';
import { defaultKeyboardShortcuts } from './keyboardShortcutDefaults';
export interface EnvToggles {
GOOSE_SERVER__MEMORY: boolean;
@@ -13,6 +14,19 @@ export interface ExternalGoosedConfig {
secret: string;
}
export interface KeyboardShortcuts {
focusWindow: string | null; // null means disabled
quickLauncher: string | null;
newChat: string | null;
newChatWindow: string | null;
openDirectory: string | null;
settings: string | null;
find: string | null;
findNext: string | null;
findPrevious: string | null;
alwaysOnTop: string | null;
}
export interface Settings {
envToggles: EnvToggles;
showMenuBarIcon: boolean;
@@ -20,6 +34,8 @@ export interface Settings {
enableWakelock: boolean;
spellcheckEnabled: boolean;
externalGoosed?: ExternalGoosedConfig;
globalShortcut?: string | null; // Deprecated: use keyboardShortcuts.focusWindow
keyboardShortcuts?: KeyboardShortcuts;
}
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
@@ -33,6 +49,8 @@ const defaultSettings: Settings = {
showDockIcon: true,
enableWakelock: false,
spellcheckEnabled: true,
globalShortcut: 'CommandOrControl+Alt+G', // Deprecated: kept for backwards compatibility
keyboardShortcuts: defaultKeyboardShortcuts,
};
// Settings management
@@ -69,3 +87,17 @@ export function updateEnvironmentVariables(envToggles: EnvToggles): void {
delete process.env.GOOSE_SERVER__COMPUTER_CONTROLLER;
}
}
export function getKeyboardShortcuts(settings: Settings): KeyboardShortcuts {
// Migrate from old globalShortcut field if needed
if (!settings.keyboardShortcuts && settings.globalShortcut !== undefined) {
return {
...defaultKeyboardShortcuts,
focusWindow: settings.globalShortcut,
quickLauncher: settings.globalShortcut
? settings.globalShortcut.replace(/\+G$/i, '+Shift+G')
: null,
};
}
return settings.keyboardShortcuts || defaultKeyboardShortcuts;
}