Skip to content

Commit cc5b0ba

Browse files
authored
Merge pull request #460 from subin-chella/keyboard-shortcut-with-library
Keyboard shortcut on renderer process
2 parents 393805d + 06cb84e commit cc5b0ba

File tree

6 files changed

+197
-20
lines changed

6 files changed

+197
-20
lines changed

electron/main/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ if (!app.requestSingleInstanceLock()) {
4545
const preload = join(__dirname, '../preload/index.js')
4646
const url = process.env.VITE_DEV_SERVER_URL
4747
const indexHtml = join(process.env.DIST, 'index.html')
48-
4948
app.whenReady().then(async () => {
5049
await ollamaService.init()
5150
windowsManager.createWindow(store, preload, url, indexHtml)

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/MainPage.tsx

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react'
1+
import React, { useCallback, useEffect, useState } from 'react'
22

33
import '../styles/global.css'
44
import ChatComponent from './Chat'
@@ -15,13 +15,25 @@ import { ChatProvider, useChatContext } from '@/contexts/ChatContext'
1515
import { FileProvider, useFileContext } from '@/contexts/FileContext'
1616
import ModalProvider from '@/contexts/ModalContext'
1717
import CommonModals from './Common/CommonModals'
18+
import useAppShortcuts from './shortcuts/use-shortcut'
1819

1920
const MainPageContent: React.FC = () => {
2021
const [showSimilarFiles, setShowSimilarFiles] = useState(false)
21-
22+
const [isNewDirectoryModalOpen, setIsNewDirectoryModalOpen] = useState(false)
2223
const { currentlyOpenFilePath } = useFileContext()
2324

2425
const { showChatbot } = useChatContext()
26+
const { getShortcutDescription } = useAppShortcuts()
27+
const openNewDirectoryModal = useCallback(() => {
28+
setIsNewDirectoryModalOpen(true)
29+
}, [])
30+
31+
useEffect(() => {
32+
window.addEventListener('open-new-directory-modal', openNewDirectoryModal)
33+
return () => {
34+
window.removeEventListener('open-new-directory-modal', openNewDirectoryModal)
35+
}
36+
}, [openNewDirectoryModal])
2537

2638
return (
2739
<div className="relative overflow-x-hidden">
@@ -33,7 +45,11 @@ const MainPageContent: React.FC = () => {
3345
/>
3446
<div className="flex h-below-titlebar">
3547
<div className="border-y-0 border-l-0 border-r-[0.001px] border-solid border-neutral-700 pt-2.5">
36-
<IconsSidebar />
48+
<IconsSidebar
49+
getShortcutDescription={getShortcutDescription}
50+
isNewDirectoryModalOpen={isNewDirectoryModalOpen}
51+
setIsNewDirectoryModalOpen={setIsNewDirectoryModalOpen}
52+
/>
3753
</div>
3854

3955
<ResizableComponent resizeSide="right">

src/components/Sidebars/IconsSidebar.tsx

+41-14
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,21 @@ import { useChatContext } from '@/contexts/ChatContext'
1313
import { useContentContext } from '@/contexts/ContentContext'
1414
import NewDirectoryComponent from '../File/NewDirectory'
1515

16-
const IconsSidebar: React.FC = () => {
16+
export interface IconsSidebarProps {
17+
readonly getShortcutDescription: (action: string) => string
18+
19+
readonly isNewDirectoryModalOpen: boolean
20+
21+
readonly setIsNewDirectoryModalOpen: React.Dispatch<React.SetStateAction<boolean>>
22+
}
23+
24+
const IconsSidebar: React.FC<IconsSidebarProps> = ({
25+
getShortcutDescription,
26+
isNewDirectoryModalOpen,
27+
setIsNewDirectoryModalOpen,
28+
}) => {
1729
const { sidebarShowing, setSidebarShowing } = useChatContext()
1830
const [sidebarWidth, setSidebarWidth] = useState<number>(40)
19-
const [isNewDirectoryModalOpen, setIsNewDirectoryModalOpen] = useState(false)
2031

2132
const { isSettingsModalOpen, setIsSettingsModalOpen, setIsFlashcardModeOpen } = useModalOpeners()
2233
const { createUntitledNote } = useContentContext()
@@ -50,7 +61,7 @@ const IconsSidebar: React.FC = () => {
5061
className="mx-auto text-gray-200"
5162
color={sidebarShowing === 'files' ? 'white' : 'gray'}
5263
size={18}
53-
title="Files"
64+
title={getShortcutDescription('open-files') || 'Open Files'}
5465
/>
5566
</div>
5667
</div>
@@ -63,7 +74,7 @@ const IconsSidebar: React.FC = () => {
6374
color={sidebarShowing === 'chats' ? 'white' : 'gray'}
6475
className="cursor-pointer text-gray-100 "
6576
size={18}
66-
title={sidebarShowing === 'chats' ? 'Close Chatbot' : 'Open Chatbot'}
77+
title={getShortcutDescription('open-chat-bot') || 'Open Chatbot'}
6778
/>
6879
</div>
6980
</div>
@@ -76,7 +87,7 @@ const IconsSidebar: React.FC = () => {
7687
color={sidebarShowing === 'search' ? 'white' : 'gray'}
7788
size={18}
7889
className="text-gray-200"
79-
title="Semantic Search"
90+
title={getShortcutDescription('open-search') || 'Semantic Search'}
8091
/>
8192
</div>
8293
</div>
@@ -85,23 +96,38 @@ const IconsSidebar: React.FC = () => {
8596
onClick={() => createUntitledNote()}
8697
>
8798
<div className="flex size-4/5 items-center justify-center rounded hover:bg-neutral-700">
88-
<HiOutlinePencilAlt className="text-gray-200" color="gray" size={22} title="New Note" />
99+
<HiOutlinePencilAlt
100+
className="text-gray-200"
101+
color="gray"
102+
size={22}
103+
title={getShortcutDescription('open-new-note') || 'New Note'}
104+
/>
89105
</div>
90106
</div>
91107
<div
92108
className="mt-[2px] flex h-8 w-full cursor-pointer items-center justify-center border-none bg-transparent "
93109
onClick={() => setIsNewDirectoryModalOpen(true)}
94110
>
95111
<div className="flex size-4/5 items-center justify-center rounded hover:bg-neutral-700">
96-
<VscNewFolder className="text-gray-200" color="gray" size={18} title="New Directory" />
112+
<VscNewFolder
113+
className="text-gray-200"
114+
color="gray"
115+
size={18}
116+
title={getShortcutDescription('open-new-directory-modal') || 'New Directory'}
117+
/>
97118
</div>
98119
</div>
99120
<div
100121
className="flex h-8 w-full cursor-pointer items-center justify-center border-none bg-transparent "
101122
onClick={() => setIsFlashcardModeOpen(true)}
102123
>
103124
<div className="flex size-4/5 items-center justify-center rounded hover:bg-neutral-700">
104-
<MdOutlineQuiz className="text-gray-200" color="gray" size={19} title="Flashcard quiz" />
125+
<MdOutlineQuiz
126+
className="text-gray-200"
127+
color="gray"
128+
size={19}
129+
title={getShortcutDescription('open-flashcard-quiz-modal') || 'Flashcard quiz'}
130+
/>
105131
</div>
106132
</div>
107133

@@ -118,13 +144,14 @@ const IconsSidebar: React.FC = () => {
118144
type="button"
119145
aria-label="Open Settings"
120146
>
121-
<MdSettings color="gray" size={18} className="mb-3 size-6 text-gray-100" title="Settings" />
147+
<MdSettings
148+
color="gray"
149+
size={18}
150+
className="mb-3 size-6 text-gray-100"
151+
title={getShortcutDescription('open-settings-modal') || 'Settings'}
152+
/>
122153
</button>
123-
<NewDirectoryComponent
124-
isOpen={isNewDirectoryModalOpen}
125-
onClose={() => setIsNewDirectoryModalOpen(false)}
126-
// parentDirectoryPath={parentDirectoryPathForNewDirectory}
127-
/>
154+
<NewDirectoryComponent isOpen={isNewDirectoryModalOpen} onClose={() => setIsNewDirectoryModalOpen(false)} />
128155
</div>
129156
)
130157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export interface Shortcut {
2+
key: string
3+
action: string
4+
description: string
5+
displayValue: {
6+
mac: string
7+
other: string
8+
}
9+
}
10+
11+
export const shortcuts: Shortcut[] = [
12+
{
13+
key: 'mod+O',
14+
action: 'open-files',
15+
description: 'Open Files',
16+
displayValue: { mac: 'Cmd+O', other: 'Ctrl+O' },
17+
},
18+
{
19+
key: 'mod+N',
20+
action: 'open-new-note',
21+
description: 'New Note',
22+
displayValue: { mac: 'Cmd+N', other: 'Ctrl+N' },
23+
},
24+
{
25+
key: 'mod+P',
26+
action: 'open-search',
27+
description: 'Semantic Search',
28+
displayValue: { mac: 'Cmd+P', other: 'Ctrl+P' },
29+
},
30+
{
31+
key: 'mod+T',
32+
action: 'open-chat-bot',
33+
description: 'Open Chatbot',
34+
displayValue: { mac: 'Cmd+T', other: 'Ctrl+T' },
35+
},
36+
{
37+
key: 'mod+D',
38+
action: 'open-new-directory-modal',
39+
description: 'New Directory',
40+
displayValue: { mac: 'Cmd+D', other: 'Ctrl+D' },
41+
},
42+
{
43+
key: 'mod+L',
44+
action: 'open-flashcard-quiz-modal',
45+
description: 'Flashcard quiz',
46+
displayValue: { mac: 'Cmd+L', other: 'Ctrl+L' },
47+
},
48+
{
49+
key: 'mod+,',
50+
action: 'open-settings-modal',
51+
description: 'Settings',
52+
displayValue: { mac: 'Cmd+,', other: 'Ctrl+,' },
53+
},
54+
]
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useCallback, useEffect, useRef } from 'react'
2+
import { useDebouncedCallback } from 'use-debounce'
3+
import { useModalOpeners } from '../../contexts/ModalContext'
4+
import { useChatContext } from '../../contexts/ChatContext'
5+
import { useContentContext } from '@/contexts/ContentContext'
6+
import { shortcuts } from './shortcutDefinitions'
7+
8+
function useAppShortcuts() {
9+
const { setIsFlashcardModeOpen, setIsSettingsModalOpen } = useModalOpeners()
10+
const { setSidebarShowing, openNewChat } = useChatContext()
11+
const { createUntitledNote } = useContentContext()
12+
13+
const handleShortcut = useCallback(
14+
(action: string) => {
15+
switch (action) {
16+
case 'open-new-note':
17+
createUntitledNote()
18+
break
19+
case 'open-new-directory-modal':
20+
window.dispatchEvent(new CustomEvent('open-new-directory-modal'))
21+
break
22+
case 'open-search':
23+
setSidebarShowing('search')
24+
break
25+
case 'open-files':
26+
setSidebarShowing('files')
27+
break
28+
case 'open-chat-bot':
29+
openNewChat()
30+
break
31+
case 'open-flashcard-quiz-modal':
32+
setIsFlashcardModeOpen(true)
33+
break
34+
case 'open-settings-modal':
35+
setIsSettingsModalOpen(true)
36+
break
37+
default:
38+
// No other cases
39+
break
40+
}
41+
},
42+
[createUntitledNote, setSidebarShowing, setIsFlashcardModeOpen, setIsSettingsModalOpen, openNewChat],
43+
)
44+
45+
const handleShortcutRef = useRef(handleShortcut)
46+
handleShortcutRef.current = handleShortcut
47+
48+
const debouncedHandleKeyDown = useDebouncedCallback((event: KeyboardEvent) => {
49+
const modifierPressed = event.ctrlKey || event.metaKey
50+
const keyPressed = event.key.toLowerCase()
51+
52+
const triggeredShortcut = shortcuts.find((s) => {
53+
const [mod, key] = s.key.toLowerCase().split('+')
54+
return mod === 'mod' && modifierPressed && key === keyPressed
55+
})
56+
57+
if (triggeredShortcut) {
58+
event.preventDefault()
59+
handleShortcutRef.current(triggeredShortcut.action)
60+
}
61+
}, 100)
62+
63+
useEffect(() => {
64+
window.addEventListener('keydown', debouncedHandleKeyDown)
65+
return () => {
66+
window.removeEventListener('keydown', debouncedHandleKeyDown)
67+
debouncedHandleKeyDown.cancel()
68+
}
69+
}, [debouncedHandleKeyDown])
70+
71+
const getShortcutDescription = useCallback((action: string) => {
72+
const shortcut = shortcuts.find((s) => s.action === action)
73+
if (!shortcut) return ''
74+
const platform = navigator.platform.toLowerCase().includes('mac') ? 'mac' : 'other'
75+
return `${shortcut.description} (${shortcut.displayValue[platform]})`
76+
}, [])
77+
78+
return { getShortcutDescription }
79+
}
80+
81+
export default useAppShortcuts

0 commit comments

Comments
 (0)