Stijnus
commited on
Commit
Β·
8035a76
1
Parent(s):
84f45dd
Bug fix for the Keyboard Shortcuts
Browse filesMAC OS SHORTCUTS:
- Toggle Terminal: β + `
- Toggle Theme: β + β₯ + β§ + D
- Toggle Chat: β + β₯ + J
- Toggle Settings: β + β₯ + S
WINDOWS/LINUX SHORTCUTS:
- Toggle Terminal: Ctrl + `
- Toggle Theme: Win + Alt + Shift + D
- Toggle Chat: Ctrl + Alt + J
- Toggle Settings: Ctrl + Alt + S
- app/components/@settings/tabs/settings/SettingsTab.tsx +40 -9
- app/lib/hooks/useShortcuts.ts +36 -33
- app/lib/stores/settings.ts +15 -4
- app/utils/os.ts +4 -0
app/components/@settings/tabs/settings/SettingsTab.tsx
CHANGED
@@ -7,6 +7,32 @@ import { themeStore, kTheme } from '~/lib/stores/theme';
|
|
7 |
import type { UserProfile } from '~/components/@settings/core/types';
|
8 |
import { useStore } from '@nanostores/react';
|
9 |
import { shortcutsStore } from '~/lib/stores/settings';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
export default function SettingsTab() {
|
12 |
const [currentTimezone, setCurrentTimezone] = useState('');
|
@@ -237,37 +263,42 @@ export default function SettingsTab() {
|
|
237 |
key={name}
|
238 |
className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A] hover:bg-purple-50 dark:hover:bg-purple-500/10 transition-colors"
|
239 |
>
|
240 |
-
<
|
241 |
-
|
242 |
-
|
|
|
|
|
|
|
|
|
|
|
243 |
<div className="flex items-center gap-1">
|
244 |
{shortcut.ctrlOrMetaKey && (
|
245 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
246 |
-
{
|
247 |
</kbd>
|
248 |
)}
|
249 |
{shortcut.ctrlKey && (
|
250 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
251 |
-
|
252 |
</kbd>
|
253 |
)}
|
254 |
{shortcut.metaKey && (
|
255 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
256 |
-
|
257 |
</kbd>
|
258 |
)}
|
259 |
{shortcut.altKey && (
|
260 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
261 |
-
{
|
262 |
</kbd>
|
263 |
)}
|
264 |
{shortcut.shiftKey && (
|
265 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
266 |
-
|
267 |
</kbd>
|
268 |
)}
|
269 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
270 |
-
{shortcut.key
|
271 |
</kbd>
|
272 |
</div>
|
273 |
</div>
|
|
|
7 |
import type { UserProfile } from '~/components/@settings/core/types';
|
8 |
import { useStore } from '@nanostores/react';
|
9 |
import { shortcutsStore } from '~/lib/stores/settings';
|
10 |
+
import { isMac } from '~/utils/os';
|
11 |
+
|
12 |
+
// Helper to format shortcut key display
|
13 |
+
const formatShortcutKey = (key: string) => {
|
14 |
+
if (key === '`') {
|
15 |
+
return '`';
|
16 |
+
}
|
17 |
+
|
18 |
+
return key.toUpperCase();
|
19 |
+
};
|
20 |
+
|
21 |
+
// Helper to get modifier key symbols/text
|
22 |
+
const getModifierSymbol = (modifier: string): string => {
|
23 |
+
switch (modifier) {
|
24 |
+
case 'meta':
|
25 |
+
return isMac ? 'β' : 'Win';
|
26 |
+
case 'alt':
|
27 |
+
return isMac ? 'β₯' : 'Alt';
|
28 |
+
case 'ctrl':
|
29 |
+
return isMac ? 'β' : 'Ctrl';
|
30 |
+
case 'shift':
|
31 |
+
return 'β§';
|
32 |
+
default:
|
33 |
+
return modifier;
|
34 |
+
}
|
35 |
+
};
|
36 |
|
37 |
export default function SettingsTab() {
|
38 |
const [currentTimezone, setCurrentTimezone] = useState('');
|
|
|
263 |
key={name}
|
264 |
className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A] hover:bg-purple-50 dark:hover:bg-purple-500/10 transition-colors"
|
265 |
>
|
266 |
+
<div className="flex flex-col">
|
267 |
+
<span className="text-sm text-bolt-elements-textPrimary capitalize">
|
268 |
+
{name.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
269 |
+
</span>
|
270 |
+
{shortcut.description && (
|
271 |
+
<span className="text-xs text-bolt-elements-textSecondary">{shortcut.description}</span>
|
272 |
+
)}
|
273 |
+
</div>
|
274 |
<div className="flex items-center gap-1">
|
275 |
{shortcut.ctrlOrMetaKey && (
|
276 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
277 |
+
{getModifierSymbol(isMac ? 'meta' : 'ctrl')}
|
278 |
</kbd>
|
279 |
)}
|
280 |
{shortcut.ctrlKey && (
|
281 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
282 |
+
{getModifierSymbol('ctrl')}
|
283 |
</kbd>
|
284 |
)}
|
285 |
{shortcut.metaKey && (
|
286 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
287 |
+
{getModifierSymbol('meta')}
|
288 |
</kbd>
|
289 |
)}
|
290 |
{shortcut.altKey && (
|
291 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
292 |
+
{getModifierSymbol('alt')}
|
293 |
</kbd>
|
294 |
)}
|
295 |
{shortcut.shiftKey && (
|
296 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
297 |
+
{getModifierSymbol('shift')}
|
298 |
</kbd>
|
299 |
)}
|
300 |
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
|
301 |
+
{formatShortcutKey(shortcut.key)}
|
302 |
</kbd>
|
303 |
</div>
|
304 |
</div>
|
app/lib/hooks/useShortcuts.ts
CHANGED
@@ -1,6 +1,10 @@
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
import { useEffect } from 'react';
|
3 |
import { shortcutsStore, type Shortcuts } from '~/lib/stores/settings';
|
|
|
|
|
|
|
|
|
4 |
|
5 |
class ShortcutEventEmitter {
|
6 |
#emitter = new EventTarget();
|
@@ -25,55 +29,54 @@ export function useShortcuts(): void {
|
|
25 |
|
26 |
useEffect(() => {
|
27 |
const handleKeyDown = (event: KeyboardEvent): void => {
|
28 |
-
//
|
29 |
-
console.log('Key pressed:', {
|
30 |
-
key: event.key,
|
31 |
-
code: event.code, // This gives us the physical key regardless of modifiers
|
32 |
-
ctrlKey: event.ctrlKey,
|
33 |
-
shiftKey: event.shiftKey,
|
34 |
-
altKey: event.altKey,
|
35 |
-
metaKey: event.metaKey,
|
36 |
-
});
|
37 |
-
|
38 |
-
/*
|
39 |
-
* Check for theme toggle shortcut first (Option + Command + Shift + D)
|
40 |
-
* Use event.code to check for the physical D key regardless of the character produced
|
41 |
-
*/
|
42 |
if (
|
43 |
-
|
44 |
-
|
45 |
-
event.altKey && //
|
46 |
-
event.
|
47 |
-
!event.ctrlKey
|
48 |
) {
|
49 |
-
event.preventDefault();
|
50 |
-
event.stopPropagation();
|
51 |
-
shortcuts.toggleTheme.action();
|
52 |
-
|
53 |
return;
|
54 |
}
|
55 |
|
56 |
-
//
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
|
|
|
|
|
|
|
|
|
|
63 |
|
64 |
-
|
|
|
65 |
const keyMatches =
|
66 |
shortcut.key.toLowerCase() === event.key.toLowerCase() || `Key${shortcut.key.toUpperCase()}` === event.code;
|
67 |
|
|
|
|
|
|
|
|
|
|
|
68 |
const modifiersMatch =
|
|
|
69 |
(shortcut.ctrlKey === undefined || shortcut.ctrlKey === event.ctrlKey) &&
|
70 |
(shortcut.metaKey === undefined || shortcut.metaKey === event.metaKey) &&
|
71 |
(shortcut.shiftKey === undefined || shortcut.shiftKey === event.shiftKey) &&
|
72 |
(shortcut.altKey === undefined || shortcut.altKey === event.altKey);
|
73 |
|
74 |
if (keyMatches && modifiersMatch) {
|
75 |
-
|
76 |
-
|
|
|
|
|
|
|
|
|
77 |
shortcutEventEmitter.dispatch(name as keyof Shortcuts);
|
78 |
shortcut.action();
|
79 |
break;
|
|
|
1 |
import { useStore } from '@nanostores/react';
|
2 |
import { useEffect } from 'react';
|
3 |
import { shortcutsStore, type Shortcuts } from '~/lib/stores/settings';
|
4 |
+
import { isMac } from '~/utils/os';
|
5 |
+
|
6 |
+
// List of keys that should not trigger shortcuts when typing in input/textarea
|
7 |
+
const INPUT_ELEMENTS = ['input', 'textarea'];
|
8 |
|
9 |
class ShortcutEventEmitter {
|
10 |
#emitter = new EventTarget();
|
|
|
29 |
|
30 |
useEffect(() => {
|
31 |
const handleKeyDown = (event: KeyboardEvent): void => {
|
32 |
+
// Don't trigger shortcuts when typing in input fields
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
if (
|
34 |
+
document.activeElement &&
|
35 |
+
INPUT_ELEMENTS.includes(document.activeElement.tagName.toLowerCase()) &&
|
36 |
+
!event.altKey && // Allow Alt combinations even in input fields
|
37 |
+
!event.metaKey && // Allow Cmd/Win combinations even in input fields
|
38 |
+
!event.ctrlKey // Allow Ctrl combinations even in input fields
|
39 |
) {
|
|
|
|
|
|
|
|
|
40 |
return;
|
41 |
}
|
42 |
|
43 |
+
// Debug logging in development only
|
44 |
+
if (process.env.NODE_ENV === 'development') {
|
45 |
+
console.log('Key pressed:', {
|
46 |
+
key: event.key,
|
47 |
+
code: event.code,
|
48 |
+
ctrlKey: event.ctrlKey,
|
49 |
+
shiftKey: event.shiftKey,
|
50 |
+
altKey: event.altKey,
|
51 |
+
metaKey: event.metaKey,
|
52 |
+
target: event.target,
|
53 |
+
});
|
54 |
+
}
|
55 |
|
56 |
+
// Handle shortcuts
|
57 |
+
for (const [name, shortcut] of Object.entries(shortcuts)) {
|
58 |
const keyMatches =
|
59 |
shortcut.key.toLowerCase() === event.key.toLowerCase() || `Key${shortcut.key.toUpperCase()}` === event.code;
|
60 |
|
61 |
+
// Handle ctrlOrMetaKey based on OS
|
62 |
+
const ctrlOrMetaKeyMatches = shortcut.ctrlOrMetaKey
|
63 |
+
? (isMac && event.metaKey) || (!isMac && event.ctrlKey)
|
64 |
+
: true;
|
65 |
+
|
66 |
const modifiersMatch =
|
67 |
+
ctrlOrMetaKeyMatches &&
|
68 |
(shortcut.ctrlKey === undefined || shortcut.ctrlKey === event.ctrlKey) &&
|
69 |
(shortcut.metaKey === undefined || shortcut.metaKey === event.metaKey) &&
|
70 |
(shortcut.shiftKey === undefined || shortcut.shiftKey === event.shiftKey) &&
|
71 |
(shortcut.altKey === undefined || shortcut.altKey === event.altKey);
|
72 |
|
73 |
if (keyMatches && modifiersMatch) {
|
74 |
+
// Prevent default browser behavior if specified
|
75 |
+
if (shortcut.isPreventDefault) {
|
76 |
+
event.preventDefault();
|
77 |
+
event.stopPropagation();
|
78 |
+
}
|
79 |
+
|
80 |
shortcutEventEmitter.dispatch(name as keyof Shortcuts);
|
81 |
shortcut.action();
|
82 |
break;
|
app/lib/stores/settings.ts
CHANGED
@@ -21,6 +21,8 @@ export interface Shortcut {
|
|
21 |
metaKey?: boolean;
|
22 |
ctrlOrMetaKey?: boolean;
|
23 |
action: () => void;
|
|
|
|
|
24 |
}
|
25 |
|
26 |
export interface Shortcuts {
|
@@ -35,32 +37,41 @@ export const LOCAL_PROVIDERS = ['OpenAILike', 'LMStudio', 'Ollama'];
|
|
35 |
|
36 |
export type ProviderSetting = Record<string, IProviderConfig>;
|
37 |
|
|
|
38 |
export const shortcutsStore = map<Shortcuts>({
|
39 |
toggleTerminal: {
|
40 |
key: '`',
|
41 |
ctrlOrMetaKey: true,
|
42 |
action: () => workbenchStore.toggleTerminal(),
|
|
|
|
|
43 |
},
|
44 |
toggleTheme: {
|
45 |
key: 'd',
|
46 |
-
metaKey: true,
|
47 |
-
altKey: true,
|
48 |
shiftKey: true,
|
49 |
action: () => toggleTheme(),
|
|
|
|
|
50 |
},
|
51 |
toggleChat: {
|
52 |
-
key: 'k'
|
53 |
ctrlOrMetaKey: true,
|
|
|
54 |
action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
|
|
|
|
|
55 |
},
|
56 |
toggleSettings: {
|
57 |
key: 's',
|
58 |
ctrlOrMetaKey: true,
|
59 |
altKey: true,
|
60 |
action: () => {
|
61 |
-
// This will be connected to the settings panel toggle
|
62 |
document.dispatchEvent(new CustomEvent('toggle-settings'));
|
63 |
},
|
|
|
|
|
64 |
},
|
65 |
});
|
66 |
|
|
|
21 |
metaKey?: boolean;
|
22 |
ctrlOrMetaKey?: boolean;
|
23 |
action: () => void;
|
24 |
+
description?: string; // Description of what the shortcut does
|
25 |
+
isPreventDefault?: boolean; // Whether to prevent default browser behavior
|
26 |
}
|
27 |
|
28 |
export interface Shortcuts {
|
|
|
37 |
|
38 |
export type ProviderSetting = Record<string, IProviderConfig>;
|
39 |
|
40 |
+
// Define safer shortcuts that don't conflict with browser defaults
|
41 |
export const shortcutsStore = map<Shortcuts>({
|
42 |
toggleTerminal: {
|
43 |
key: '`',
|
44 |
ctrlOrMetaKey: true,
|
45 |
action: () => workbenchStore.toggleTerminal(),
|
46 |
+
description: 'Toggle terminal',
|
47 |
+
isPreventDefault: true,
|
48 |
},
|
49 |
toggleTheme: {
|
50 |
key: 'd',
|
51 |
+
metaKey: true,
|
52 |
+
altKey: true,
|
53 |
shiftKey: true,
|
54 |
action: () => toggleTheme(),
|
55 |
+
description: 'Toggle theme',
|
56 |
+
isPreventDefault: true,
|
57 |
},
|
58 |
toggleChat: {
|
59 |
+
key: 'j', // Changed from 'k' to 'j' to avoid conflicts
|
60 |
ctrlOrMetaKey: true,
|
61 |
+
altKey: true, // Added alt key to make it more unique
|
62 |
action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
|
63 |
+
description: 'Toggle chat',
|
64 |
+
isPreventDefault: true,
|
65 |
},
|
66 |
toggleSettings: {
|
67 |
key: 's',
|
68 |
ctrlOrMetaKey: true,
|
69 |
altKey: true,
|
70 |
action: () => {
|
|
|
71 |
document.dispatchEvent(new CustomEvent('toggle-settings'));
|
72 |
},
|
73 |
+
description: 'Toggle settings',
|
74 |
+
isPreventDefault: true,
|
75 |
},
|
76 |
});
|
77 |
|
app/utils/os.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Helper to detect OS
|
2 |
+
export const isMac = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase().includes('mac') : false;
|
3 |
+
export const isWindows = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase().includes('win') : false;
|
4 |
+
export const isLinux = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase().includes('linux') : false;
|