diff --git a/src/main.ts b/src/main.ts index 15f910b..367d804 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,10 +9,18 @@ let mainWindow: BrowserWindow | null = null; let spotlightWindow: BrowserWindow | null = null; let settingsWindow: BrowserWindow | null = null; +// Default keyboard shortcuts +const DEFAULT_KEYBOARD_SHORTCUTS = { + spotlight: 'CommandOrControl+Shift+C', + newConversation: 'CommandOrControl+N', + toggleSidebar: 'CommandOrControl+B', +}; + // Default settings const DEFAULT_SETTINGS: SettingsSchema = { spotlightKeybind: 'CommandOrControl+Shift+C', spotlightPersistHistory: true, + keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, }; // Get settings with defaults @@ -27,23 +35,57 @@ function saveSettings(settings: Partial) { store.set('settings', { ...current, ...settings }); } -// Register spotlight shortcut -function registerSpotlightShortcut() { +// Register all keyboard shortcuts +function registerKeyboardShortcuts() { globalShortcut.unregisterAll(); const settings = getSettings(); - const keybind = settings.spotlightKeybind || DEFAULT_SETTINGS.spotlightKeybind; + const shortcuts = settings.keyboardShortcuts || DEFAULT_KEYBOARD_SHORTCUTS; + // Register spotlight shortcut + const spotlightKey = shortcuts.spotlight || settings.spotlightKeybind || DEFAULT_KEYBOARD_SHORTCUTS.spotlight; try { - globalShortcut.register(keybind, () => { + globalShortcut.register(spotlightKey, () => { createSpotlightWindow(); }); } catch (e) { - // Fallback to default if custom keybind fails - console.error('Failed to register keybind:', keybind, e); - globalShortcut.register(DEFAULT_SETTINGS.spotlightKeybind, () => { + console.error('Failed to register spotlight keybind:', spotlightKey, e); + globalShortcut.register(DEFAULT_KEYBOARD_SHORTCUTS.spotlight, () => { createSpotlightWindow(); }); } + + // Register new conversation shortcut + const newConvKey = shortcuts.newConversation || DEFAULT_KEYBOARD_SHORTCUTS.newConversation; + try { + globalShortcut.register(newConvKey, () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('new-conversation'); + mainWindow.show(); + mainWindow.focus(); + } + }); + } catch (e) { + console.error('Failed to register new conversation keybind:', newConvKey, e); + } + + // Register toggle sidebar shortcut + const sidebarKey = shortcuts.toggleSidebar || DEFAULT_KEYBOARD_SHORTCUTS.toggleSidebar; + try { + globalShortcut.register(sidebarKey, () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('toggle-sidebar'); + mainWindow.show(); + mainWindow.focus(); + } + }); + } catch (e) { + console.error('Failed to register toggle sidebar keybind:', sidebarKey, e); + } +} + +// Legacy function for compatibility +function registerSpotlightShortcut() { + registerKeyboardShortcuts(); } // Create spotlight search window diff --git a/src/preload.ts b/src/preload.ts index c7a55ea..f77e43c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -111,9 +111,17 @@ contextBridge.exposeInMainWorld('claude', { ipcRenderer.on('toggle-search-modal', () => callback()); }, + // Global keyboard shortcuts + onNewConversation: (callback: () => void) => { + ipcRenderer.on('new-conversation', () => callback()); + }, + onToggleSidebar: (callback: () => void) => { + ipcRenderer.on('toggle-sidebar', () => callback()); + }, + // Settings functions openSettings: () => ipcRenderer.invoke('open-settings'), getSettings: () => ipcRenderer.invoke('get-settings'), - saveSettings: (settings: { spotlightKeybind?: string; spotlightPersistHistory?: boolean }) => + saveSettings: (settings: { spotlightKeybind?: string; spotlightPersistHistory?: boolean; keyboardShortcuts?: { spotlight?: string; newConversation?: string; toggleSidebar?: string } }) => ipcRenderer.invoke('save-settings', settings), }); diff --git a/src/renderer/main.ts b/src/renderer/main.ts index b0f6efd..aa33bc2 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -26,6 +26,8 @@ declare global { onMessageToolResult: (callback: (data: ToolResultData) => void) => void; onMessageStream: (callback: (data: StreamData) => void) => void; onMessageComplete: (callback: (data: CompleteData) => void) => void; + onNewConversation: (callback: () => void) => void; + onToggleSidebar: (callback: () => void) => void; }; } } @@ -1576,6 +1578,15 @@ async function init() { if (inputEl) inputEl.focus(); } }); + + // Global keyboard shortcut handlers + window.claude.onNewConversation(() => { + newChat(); + }); + + window.claude.onToggleSidebar(() => { + toggleSidebar(); + }); } // Set up event listeners diff --git a/src/renderer/settings.ts b/src/renderer/settings.ts index 6522d2e..7e58b54 100644 --- a/src/renderer/settings.ts +++ b/src/renderer/settings.ts @@ -2,18 +2,30 @@ const claude = (window as any).claude; +interface KeyboardShortcuts { + spotlight: string; + newConversation: string; + toggleSidebar: string; +} + interface Settings { spotlightKeybind: string; spotlightPersistHistory: boolean; + keyboardShortcuts: KeyboardShortcuts; } +// Default keyboard shortcuts +const DEFAULT_SHORTCUTS: KeyboardShortcuts = { + spotlight: 'CommandOrControl+Shift+C', + newConversation: 'CommandOrControl+N', + toggleSidebar: 'CommandOrControl+B', +}; + // DOM Elements -const keybindInput = document.getElementById('keybind-input') as HTMLElement; -const keybindDisplay = document.getElementById('keybind-display') as HTMLElement; const persistHistoryCheckbox = document.getElementById('persist-history') as HTMLInputElement; -let isRecordingKeybind = false; let currentSettings: Settings | null = null; +let activeRecordingInput: HTMLElement | null = null; let pendingKeybind: string | null = null; // Detect if we're on macOS @@ -31,23 +43,6 @@ function formatKeybind(keybind: string): string { .replace(/\+/g, ' + '); } -// Build accelerator string from current modifier state -function buildAcceleratorFromModifiers(e: KeyboardEvent): string { - const parts: string[] = []; - - if (e.metaKey || e.ctrlKey) { - parts.push('CommandOrControl'); - } - if (e.shiftKey) { - parts.push('Shift'); - } - if (e.altKey) { - parts.push('Alt'); - } - - return parts.join('+'); -} - // Convert key event to Electron accelerator format function keyEventToAccelerator(e: KeyboardEvent): { accelerator: string; isComplete: boolean } { const parts: string[] = []; @@ -99,22 +94,75 @@ function keyEventToAccelerator(e: KeyboardEvent): { accelerator: string; isCompl }; } +// Get shortcut value by key +function getShortcutValue(key: keyof KeyboardShortcuts): string { + if (!currentSettings) return DEFAULT_SHORTCUTS[key]; + const shortcuts = currentSettings.keyboardShortcuts || DEFAULT_SHORTCUTS; + return shortcuts[key] || DEFAULT_SHORTCUTS[key]; +} + +// Update all display values +function updateDisplayValues() { + if (!currentSettings) return; + + // Update spotlight display + const spotlightDisplay = document.getElementById('spotlight-keybind-display'); + if (spotlightDisplay) { + spotlightDisplay.textContent = formatKeybind(getShortcutValue('spotlight')); + } + + // Update new conversation display + const newConvDisplay = document.getElementById('new-conv-keybind-display'); + if (newConvDisplay) { + newConvDisplay.textContent = formatKeybind(getShortcutValue('newConversation')); + } + + // Update toggle sidebar display + const sidebarDisplay = document.getElementById('sidebar-keybind-display'); + if (sidebarDisplay) { + sidebarDisplay.textContent = formatKeybind(getShortcutValue('toggleSidebar')); + } + + // Update persist history + if (persistHistoryCheckbox) { + persistHistoryCheckbox.checked = currentSettings.spotlightPersistHistory; + } +} + // Load settings async function loadSettings() { currentSettings = await claude.getSettings(); - if (currentSettings) { - keybindDisplay.textContent = formatKeybind(currentSettings.spotlightKeybind); - persistHistoryCheckbox.checked = currentSettings.spotlightPersistHistory; + // Ensure keyboardShortcuts exists with defaults + if (currentSettings && !currentSettings.keyboardShortcuts) { + currentSettings.keyboardShortcuts = { ...DEFAULT_SHORTCUTS }; + // Migrate old spotlightKeybind if exists + if (currentSettings.spotlightKeybind) { + currentSettings.keyboardShortcuts.spotlight = currentSettings.spotlightKeybind; + } } + + updateDisplayValues(); } -// Save keybind -async function saveKeybind(keybind: string) { +// Save keyboard shortcut +async function saveKeyboardShortcut(shortcutKey: keyof KeyboardShortcuts, keybind: string) { if (!currentSettings) return; - currentSettings = await claude.saveSettings({ spotlightKeybind: keybind }); - keybindDisplay.textContent = formatKeybind(keybind); + const shortcuts = currentSettings.keyboardShortcuts || { ...DEFAULT_SHORTCUTS }; + shortcuts[shortcutKey] = keybind; + + // Also update spotlightKeybind for backwards compatibility + if (shortcutKey === 'spotlight') { + currentSettings = await claude.saveSettings({ + keyboardShortcuts: shortcuts, + spotlightKeybind: keybind + }); + } else { + currentSettings = await claude.saveSettings({ keyboardShortcuts: shortcuts }); + } + + updateDisplayValues(); } // Save persist history @@ -126,71 +174,95 @@ async function savePersistHistory(value: boolean) { // Stop recording and save if we have a valid keybind function stopRecording(save: boolean) { - if (!isRecordingKeybind) return; + if (!activeRecordingInput) return; + + const shortcutKey = activeRecordingInput.dataset.shortcut as keyof KeyboardShortcuts; + const displayEl = activeRecordingInput.querySelector('.keybind-display') as HTMLElement; - isRecordingKeybind = false; - keybindInput.classList.remove('recording'); + activeRecordingInput.classList.remove('recording'); - if (save && pendingKeybind) { - saveKeybind(pendingKeybind); - } else if (currentSettings) { - keybindDisplay.textContent = formatKeybind(currentSettings.spotlightKeybind); + if (save && pendingKeybind && shortcutKey) { + saveKeyboardShortcut(shortcutKey, pendingKeybind); + } else if (currentSettings && displayEl) { + displayEl.textContent = formatKeybind(getShortcutValue(shortcutKey)); } + activeRecordingInput = null; pendingKeybind = null; } -// Keybind recording -keybindInput.addEventListener('click', () => { - if (!isRecordingKeybind) { - isRecordingKeybind = true; +// Set up keybind input handlers +function setupKeybindInput(inputEl: HTMLElement) { + const shortcutKey = inputEl.dataset.shortcut as keyof KeyboardShortcuts; + const displayEl = inputEl.querySelector('.keybind-display') as HTMLElement; + + inputEl.addEventListener('click', () => { + if (activeRecordingInput === inputEl) return; + + // Stop any existing recording + stopRecording(false); + + activeRecordingInput = inputEl; pendingKeybind = null; - keybindInput.classList.add('recording'); - keybindDisplay.textContent = 'Press keys...'; - keybindInput.focus(); - } -}); + inputEl.classList.add('recording'); + if (displayEl) displayEl.textContent = 'Press keys...'; + inputEl.focus(); + }); -keybindInput.addEventListener('keydown', (e) => { - if (!isRecordingKeybind) return; + inputEl.addEventListener('keydown', (e) => { + if (activeRecordingInput !== inputEl) return; - e.preventDefault(); - e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); - // Handle Escape to cancel - if (e.key === 'Escape') { - stopRecording(false); - return; - } + // Handle Escape to cancel + if (e.key === 'Escape') { + stopRecording(false); + return; + } - // Handle Enter to confirm - if (e.key === 'Enter' && pendingKeybind) { - stopRecording(true); - return; - } + // Handle Enter to confirm + if (e.key === 'Enter' && pendingKeybind) { + stopRecording(true); + return; + } - const result = keyEventToAccelerator(e); + const result = keyEventToAccelerator(e); - // Update display to show current keys being pressed - if (result.accelerator) { - keybindDisplay.textContent = formatKeybind(result.accelerator); + // Update display to show current keys being pressed + if (result.accelerator && displayEl) { + displayEl.textContent = formatKeybind(result.accelerator); - // If we have a complete combo (modifier + key), store it as pending - if (result.isComplete) { - pendingKeybind = result.accelerator; + // If we have a complete combo (modifier + key), store it as pending + if (result.isComplete) { + pendingKeybind = result.accelerator; + } } - } -}); + }); -keybindInput.addEventListener('blur', () => { - // Save pending keybind on blur (clicking away) - stopRecording(!!pendingKeybind); -}); + inputEl.addEventListener('blur', () => { + // Save pending keybind on blur (clicking away) + if (activeRecordingInput === inputEl) { + stopRecording(!!pendingKeybind); + } + }); +} + +// Initialize all keybind inputs +function initializeKeybindInputs() { + const keybindInputs = document.querySelectorAll('.keybind-input'); + keybindInputs.forEach(input => { + setupKeybindInput(input as HTMLElement); + }); +} // Persist history toggle -persistHistoryCheckbox.addEventListener('change', () => { +persistHistoryCheckbox?.addEventListener('change', () => { savePersistHistory(persistHistoryCheckbox.checked); }); // Load settings on page load -window.addEventListener('load', loadSettings); +window.addEventListener('load', () => { + loadSettings(); + initializeKeybindInputs(); +}); diff --git a/src/types/index.ts b/src/types/index.ts index e80e56a..0054c32 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,15 @@ +// Keyboard shortcuts configuration +export interface KeyboardShortcuts { + spotlight: string; + newConversation: string; + toggleSidebar: string; +} + // Settings schema export interface SettingsSchema { spotlightKeybind: string; spotlightPersistHistory: boolean; + keyboardShortcuts: KeyboardShortcuts; } // Store schema for electron-store diff --git a/static/settings.html b/static/settings.html index d26dc77..0b4bb27 100644 --- a/static/settings.html +++ b/static/settings.html @@ -13,21 +13,48 @@

Settings

- +
-

Spotlight

+

Keyboard Shortcuts

- - Global shortcut to open spotlight + + Global shortcut to open spotlight search
-
- ⌘ + Shift + Space +
+ Loading... Click to change
+
+
+ + Global shortcut to start a new conversation +
+
+ Loading... + Click to change +
+
+ +
+
+ + Global shortcut to toggle the sidebar +
+ +
+
+ + +
+

Spotlight

+