Skip to content

Commit

Permalink
Add dynamic OS-specific shortcut system
Browse files Browse the repository at this point in the history
- Implement shortcut definitions with OS-specific display values
- Create useShortcuts hook for managing shortcuts and their descriptions
- Update IconsSidebar to use dynamic shortcut descriptions
- Integrate with electron to detect OS and handle shortcut actions fixes #234
  • Loading branch information
subin-chella committed Oct 15, 2024
1 parent 05ae423 commit 172f98d
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 10 deletions.
8 changes: 7 additions & 1 deletion electron/main/common/windowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ class WindowsManager {

watcher: chokidar.FSWatcher | undefined

async createWindow(store: Store<StoreSchema>, preload: string, url: string | undefined, indexHtml: string) {
async createWindow(
store: Store<StoreSchema>,
preload: string,
url: string | undefined,
indexHtml: string,
): Promise<BrowserWindow> {
const { x, y } = this.getNextWindowPosition()
const { width, height } = this.getWindowSize()

Expand Down Expand Up @@ -68,6 +73,7 @@ class WindowsManager {
win.webContents.send('error-to-display-in-window', errorStrToSendWindow)
})
})
return win
}

getAndSetupDirectoryForWindowFromPreviousAppSession(
Expand Down
12 changes: 10 additions & 2 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import registerFileHandlers from './filesystem/ipcHandlers'
import { ollamaService, registerLLMSessionHandlers } from './llm/ipcHandlers'
import registerPathHandlers from './path/ipcHandlers'
import { registerDBSessionHandlers } from './vector-database/ipcHandlers'
import { registerGlobalShortcuts, unregisterGlobalShortcuts } from '../../src/components/shortcuts/shortcutManager'

Sentry.init({
dsn: 'https://a764a6135d25ba91f0b25c0252be52f3@o4507840138903552.ingest.us.sentry.io/4507840140410880',
Expand Down Expand Up @@ -43,10 +44,17 @@ if (!app.requestSingleInstanceLock()) {
const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html')

app.whenReady().then(async () => {
await ollamaService.init()
windowsManager.createWindow(store, preload, url, indexHtml)
const window = await windowsManager.createWindow(store, preload, url, indexHtml)
window.webContents.on('did-finish-load', () => {
registerGlobalShortcuts(window)
})
})

app.on('will-quit', () => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((window) => unregisterGlobalShortcuts(window))
})

app.on('window-all-closed', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/components/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FileProvider, useFileContext } from '@/contexts/FileContext'
import ModalProvider from '@/contexts/ModalContext'
import CustomContextMenu from './Common/CustomContextMenu'
import CommonModals from './Common/CommonModals'
import useShortcuts from './shortcuts/use-shortcut'

const MainPageContent: React.FC = () => {
const [showSimilarFiles, setShowSimilarFiles] = useState(false)
Expand All @@ -24,6 +25,7 @@ const MainPageContent: React.FC = () => {

const { showChatbot } = useChatContext()

useShortcuts()
return (
<div className="relative overflow-x-hidden">
<TitleBar
Expand Down
37 changes: 30 additions & 7 deletions src/components/Sidebars/IconsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HiOutlinePencilAlt } from 'react-icons/hi'
import { useModalOpeners } from '../../contexts/ModalContext'
import { useChatContext } from '@/contexts/ChatContext'
import { useContentContext } from '@/contexts/ContentContext'
import useShortcuts from '@/components/shortcuts/use-shortcut'

const IconsSidebar: React.FC = () => {
const { sidebarShowing, setSidebarShowing } = useChatContext()
Expand All @@ -35,6 +36,8 @@ const IconsSidebar: React.FC = () => {
window.ipcRenderer.receive('sb-compact-changed', handleSettingsChange)
}, [])

const { getShortcutDescription } = useShortcuts()

return (
<div
className="flex size-full flex-col items-center justify-between gap-1 bg-neutral-800"
Expand All @@ -49,7 +52,7 @@ const IconsSidebar: React.FC = () => {
className="mx-auto text-gray-200"
color={sidebarShowing === 'files' ? 'white' : 'gray'}
size={18}
title="Files"
title={getShortcutDescription('open-files')}
/>
</div>
</div>
Expand All @@ -62,7 +65,7 @@ const IconsSidebar: React.FC = () => {
color={sidebarShowing === 'chats' ? 'white' : 'gray'}
className="cursor-pointer text-gray-100 "
size={18}
title={sidebarShowing === 'chats' ? 'Close Chatbot' : 'Open Chatbot'}
title={getShortcutDescription('open-chat-bot')}
/>
</div>
</div>
Expand All @@ -75,7 +78,7 @@ const IconsSidebar: React.FC = () => {
color={sidebarShowing === 'search' ? 'white' : 'gray'}
size={18}
className="text-gray-200"
title="Semantic Search"
title={getShortcutDescription('open-search')}
/>
</div>
</div>
Expand All @@ -84,23 +87,38 @@ const IconsSidebar: React.FC = () => {
onClick={() => createUntitledNote()}
>
<div className="flex size-4/5 items-center justify-center rounded hover:bg-neutral-700">
<HiOutlinePencilAlt className="text-gray-200" color="gray" size={22} title="New Note" />
<HiOutlinePencilAlt
className="text-gray-200"
color="gray"
size={22}
title={getShortcutDescription('open-new-note')}
/>
</div>
</div>
<div
className="mt-[2px] flex h-8 w-full cursor-pointer items-center justify-center border-none bg-transparent "
onClick={() => setIsNewDirectoryModalOpen(true)}
>
<div className="flex size-4/5 items-center justify-center rounded hover:bg-neutral-700">
<VscNewFolder className="text-gray-200" color="gray" size={18} title="New Directory" />
<VscNewFolder
className="text-gray-200"
color="gray"
size={18}
title={getShortcutDescription('open-new-directory-modal')}
/>
</div>
</div>
<div
className="flex h-8 w-full cursor-pointer items-center justify-center border-none bg-transparent "
onClick={() => setIsFlashcardModeOpen(true)}
>
<div className="flex size-4/5 items-center justify-center rounded hover:bg-neutral-700">
<MdOutlineQuiz className="text-gray-200" color="gray" size={19} title="Flashcard quiz" />
<MdOutlineQuiz
className="text-gray-200"
color="gray"
size={19}
title={getShortcutDescription('open-flashcard-quiz-modal')}
/>
</div>
</div>

Expand All @@ -117,7 +135,12 @@ const IconsSidebar: React.FC = () => {
type="button"
aria-label="Open Settings"
>
<MdSettings color="gray" size={18} className="mb-3 size-6 text-gray-100" title="Settings" />
<MdSettings
color="gray"
size={18}
className="mb-3 size-6 text-gray-100"
title={getShortcutDescription('open-settings-modal')}
/>
</button>
</div>
)
Expand Down
54 changes: 54 additions & 0 deletions src/components/shortcuts/shortcutDefinitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export interface Shortcut {
key: string
action: string
description: string
displayValue: {
mac: string
other: string
}
}

export const shortcuts: Shortcut[] = [
{
key: 'CommandOrControl+O',
action: 'open-files',
description: 'Open Files',
displayValue: { mac: 'Cmd+O', other: 'Ctrl+O' },
},
{
key: 'CommandOrControl+N',
action: 'open-new-note',
description: 'New Note',
displayValue: { mac: 'Cmd+N', other: 'Ctrl+N' },
},
{
key: 'CommandOrControl+P',
action: 'open-search',
description: 'Semantic Search',
displayValue: { mac: 'Cmd+P', other: 'Ctrl+P' },
},
{
key: 'CommandOrControl+T',
action: 'open-chat-bot',
description: 'Open Chatbot',
displayValue: { mac: 'Cmd+T', other: 'Ctrl+T' },
},
{
key: 'CommandOrControl+D',
action: 'open-new-directory-modal',
description: 'New Directory',
displayValue: { mac: 'Cmd+D', other: 'Ctrl+D' },
},
{
key: 'CommandOrControl+Q',
action: 'open-flashcard-quiz-modal',
description: 'Flashcard quiz',
displayValue: { mac: 'Cmd+Q', other: 'Ctrl+Q' },
},
{
key: 'CommandOrControl+,',
action: 'open-settings-modal',
description: 'Settings',
displayValue: { mac: 'Cmd+,', other: 'Ctrl+,' },
},
]
38 changes: 38 additions & 0 deletions src/components/shortcuts/shortcutManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable import/no-extraneous-dependencies */
import { BrowserWindow } from 'electron'
import { shortcuts } from './shortcutDefinitions'

function isShortcutMatch(input: Electron.Input, shortcutKey: string): boolean {
const keys = shortcutKey.split('+')
const mainKey = keys.pop()?.toLowerCase()

const modifiers = {
control: keys.includes('Control') || keys.includes('CommandOrControl'),
alt: keys.includes('Alt'),
shift: keys.includes('Shift'),
meta: keys.includes('Meta') || keys.includes('CommandOrControl'),
}

return (
input.key.toLowerCase() === mainKey &&
input.control === (modifiers.control || modifiers.meta) &&
input.alt === modifiers.alt &&
input.shift === modifiers.shift &&
(input.meta === modifiers.meta || input.control === modifiers.meta)
)
}

export function registerGlobalShortcuts(mainWindow: BrowserWindow) {
mainWindow.webContents.on('before-input-event', (event, input) => {
shortcuts.forEach((shortcut) => {
if (input.type === 'keyDown' && isShortcutMatch(input, shortcut.key)) {
event.preventDefault()
mainWindow.webContents.send(shortcut.action)
}
})
})
}

export function unregisterGlobalShortcuts(mainWindow: BrowserWindow) {
mainWindow.webContents.removeAllListeners('before-input-event')
}
83 changes: 83 additions & 0 deletions src/components/shortcuts/use-shortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useEffect, useState, useCallback, useRef } from 'react'
import { shortcuts, Shortcut } from './shortcutDefinitions'
import { useModalOpeners } from '../../contexts/ModalContext'
import { useChatContext } from '../../contexts/ChatContext'
import { useContentContext } from '@/contexts/ContentContext'

function useShortcuts() {
const { setIsNewDirectoryModalOpen, setIsFlashcardModeOpen, setIsSettingsModalOpen } = useModalOpeners()
const { setSidebarShowing } = useChatContext()
const { createUntitledNote } = useContentContext()
const [shortcutMap, setShortcutMap] = useState<Record<string, string>>({})
const listenersRef = useRef<(() => void)[]>([])

const handleShortcut = useCallback(
async (action: string) => {
switch (action) {
case 'open-new-note':
await createUntitledNote()
break
case 'open-new-directory-modal':
setIsNewDirectoryModalOpen(true)
break
case 'open-search':
setSidebarShowing('search')
break
case 'open-files':
setSidebarShowing('files')
break
case 'open-chat-bot':
setSidebarShowing('chats')
break
case 'open-flashcard-quiz-modal':
setIsFlashcardModeOpen(true)
break
case 'open-settings-modal':
setIsSettingsModalOpen(true)
break
default:
// Default do nothing
break
}
},
[setSidebarShowing, setIsNewDirectoryModalOpen, setIsSettingsModalOpen, setIsFlashcardModeOpen, createUntitledNote],
)

useEffect(() => {
async function setupShortcuts() {
const platform = await window.electronUtils.getPlatform()
const map: Record<string, string> = {}

shortcuts.forEach((shortcut: Shortcut) => {
const displayValue = platform === 'darwin' ? shortcut.displayValue.mac : shortcut.displayValue.other
map[shortcut.action] = `${shortcut.description} (${displayValue})`

const handler = () => {
handleShortcut(shortcut.action)
}
const removeListener = window.ipcRenderer.receive(shortcut.action, handler)
listenersRef.current.push(removeListener)
})

setShortcutMap(map)
}

setupShortcuts()

return () => {
listenersRef.current.forEach((removeListener) => removeListener())
listenersRef.current = []
}
}, [handleShortcut])

const getShortcutDescription = useCallback(
(action: string) => {
return shortcutMap[action] || ''
},
[shortcutMap],
)

return { getShortcutDescription }
}

export default useShortcuts

0 comments on commit 172f98d

Please sign in to comment.