mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: add debounce to terminal shortcuts and show in keyboard layout
- Add 300ms cooldown to prevent rapid terminal creation when holding keys - Merge DEFAULT_KEYBOARD_SHORTCUTS with user shortcuts so terminal shortcuts (Alt+D, Alt+Shift+D, Alt+W) show in keyboard layout - Fix keyboard map to handle undefined shortcuts from old persisted state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,11 +161,18 @@ interface KeyboardMapProps {
|
|||||||
export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMapProps) {
|
export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMapProps) {
|
||||||
const { keyboardShortcuts } = useAppStore();
|
const { keyboardShortcuts } = useAppStore();
|
||||||
|
|
||||||
|
// Merge with defaults to ensure new shortcuts are always shown
|
||||||
|
const mergedShortcuts = React.useMemo(() => ({
|
||||||
|
...DEFAULT_KEYBOARD_SHORTCUTS,
|
||||||
|
...keyboardShortcuts,
|
||||||
|
}), [keyboardShortcuts]);
|
||||||
|
|
||||||
// Create a reverse map: base key -> list of shortcut names (including info about modifiers)
|
// Create a reverse map: base key -> list of shortcut names (including info about modifiers)
|
||||||
const keyToShortcuts = React.useMemo(() => {
|
const keyToShortcuts = React.useMemo(() => {
|
||||||
const map: Record<string, Array<{ name: keyof KeyboardShortcuts; hasModifiers: boolean }>> = {};
|
const map: Record<string, Array<{ name: keyof KeyboardShortcuts; hasModifiers: boolean }>> = {};
|
||||||
(Object.entries(keyboardShortcuts) as [keyof KeyboardShortcuts, string][]).forEach(
|
(Object.entries(mergedShortcuts) as [keyof KeyboardShortcuts, string][]).forEach(
|
||||||
([shortcutName, shortcutStr]) => {
|
([shortcutName, shortcutStr]) => {
|
||||||
|
if (!shortcutStr) return; // Skip undefined shortcuts
|
||||||
const parsed = parseShortcut(shortcutStr);
|
const parsed = parseShortcut(shortcutStr);
|
||||||
const normalizedKey = parsed.key.toUpperCase();
|
const normalizedKey = parsed.key.toUpperCase();
|
||||||
const hasModifiers = !!(parsed.shift || parsed.cmdCtrl || parsed.alt);
|
const hasModifiers = !!(parsed.shift || parsed.cmdCtrl || parsed.alt);
|
||||||
@@ -176,7 +183,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
return map;
|
return map;
|
||||||
}, [keyboardShortcuts]);
|
}, [mergedShortcuts]);
|
||||||
|
|
||||||
const renderKey = (keyDef: { key: string; label: string; width: number }) => {
|
const renderKey = (keyDef: { key: string; label: string; width: number }) => {
|
||||||
const normalizedKey = keyDef.key.toUpperCase();
|
const normalizedKey = keyDef.key.toUpperCase();
|
||||||
@@ -185,7 +192,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap
|
|||||||
const isBound = shortcuts.length > 0;
|
const isBound = shortcuts.length > 0;
|
||||||
const isSelected = selectedKey?.toUpperCase() === normalizedKey;
|
const isSelected = selectedKey?.toUpperCase() === normalizedKey;
|
||||||
const isModified = shortcuts.some(
|
const isModified = shortcuts.some(
|
||||||
(s) => keyboardShortcuts[s] !== DEFAULT_KEYBOARD_SHORTCUTS[s]
|
(s) => mergedShortcuts[s] !== DEFAULT_KEYBOARD_SHORTCUTS[s]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get category for coloring (use first shortcut's category if multiple)
|
// Get category for coloring (use first shortcut's category if multiple)
|
||||||
@@ -250,7 +257,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap
|
|||||||
<TooltipContent side="top" className="max-w-xs">
|
<TooltipContent side="top" className="max-w-xs">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{shortcuts.map((shortcut) => {
|
{shortcuts.map((shortcut) => {
|
||||||
const shortcutStr = keyboardShortcuts[shortcut];
|
const shortcutStr = mergedShortcuts[shortcut];
|
||||||
const displayShortcut = formatShortcut(shortcutStr, true);
|
const displayShortcut = formatShortcut(shortcutStr, true);
|
||||||
return (
|
return (
|
||||||
<div key={shortcut} className="flex items-center gap-2">
|
<div key={shortcut} className="flex items-center gap-2">
|
||||||
@@ -266,7 +273,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap
|
|||||||
<kbd className="text-xs font-mono bg-sidebar-accent/30 px-1 rounded">
|
<kbd className="text-xs font-mono bg-sidebar-accent/30 px-1 rounded">
|
||||||
{displayShortcut}
|
{displayShortcut}
|
||||||
</kbd>
|
</kbd>
|
||||||
{keyboardShortcuts[shortcut] !== DEFAULT_KEYBOARD_SHORTCUTS[shortcut] && (
|
{mergedShortcuts[shortcut] !== DEFAULT_KEYBOARD_SHORTCUTS[shortcut] && (
|
||||||
<span className="text-xs text-yellow-400">(custom)</span>
|
<span className="text-xs text-yellow-400">(custom)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -353,6 +360,12 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
|
|||||||
const [modifiers, setModifiers] = React.useState({ shift: false, cmdCtrl: false, alt: false });
|
const [modifiers, setModifiers] = React.useState({ shift: false, cmdCtrl: false, alt: false });
|
||||||
const [shortcutError, setShortcutError] = React.useState<string | null>(null);
|
const [shortcutError, setShortcutError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
// Merge with defaults to ensure new shortcuts are always shown
|
||||||
|
const mergedShortcuts = React.useMemo(() => ({
|
||||||
|
...DEFAULT_KEYBOARD_SHORTCUTS,
|
||||||
|
...keyboardShortcuts,
|
||||||
|
}), [keyboardShortcuts]);
|
||||||
|
|
||||||
const groupedShortcuts = React.useMemo(() => {
|
const groupedShortcuts = React.useMemo(() => {
|
||||||
const groups: Record<string, Array<{ key: keyof KeyboardShortcuts; label: string; value: string }>> = {
|
const groups: Record<string, Array<{ key: keyof KeyboardShortcuts; label: string; value: string }>> = {
|
||||||
navigation: [],
|
navigation: [],
|
||||||
@@ -365,13 +378,13 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
|
|||||||
groups[category].push({
|
groups[category].push({
|
||||||
key: shortcut,
|
key: shortcut,
|
||||||
label: SHORTCUT_LABELS[shortcut] ?? shortcut,
|
label: SHORTCUT_LABELS[shortcut] ?? shortcut,
|
||||||
value: keyboardShortcuts[shortcut],
|
value: mergedShortcuts[shortcut],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}, [keyboardShortcuts]);
|
}, [mergedShortcuts]);
|
||||||
|
|
||||||
// Build the full shortcut string from key + modifiers
|
// Build the full shortcut string from key + modifiers
|
||||||
const buildShortcutString = React.useCallback((key: string, mods: typeof modifiers) => {
|
const buildShortcutString = React.useCallback((key: string, mods: typeof modifiers) => {
|
||||||
@@ -385,14 +398,14 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
|
|||||||
|
|
||||||
// Check for conflicts with other shortcuts
|
// Check for conflicts with other shortcuts
|
||||||
const checkConflict = React.useCallback((shortcutStr: string, currentKey: keyof KeyboardShortcuts) => {
|
const checkConflict = React.useCallback((shortcutStr: string, currentKey: keyof KeyboardShortcuts) => {
|
||||||
const conflict = Object.entries(keyboardShortcuts).find(
|
const conflict = Object.entries(mergedShortcuts).find(
|
||||||
([k, v]) => k !== currentKey && v.toUpperCase() === shortcutStr.toUpperCase()
|
([k, v]) => k !== currentKey && v?.toUpperCase() === shortcutStr.toUpperCase()
|
||||||
);
|
);
|
||||||
return conflict ? (SHORTCUT_LABELS[conflict[0] as keyof KeyboardShortcuts] ?? conflict[0]) : null;
|
return conflict ? (SHORTCUT_LABELS[conflict[0] as keyof KeyboardShortcuts] ?? conflict[0]) : null;
|
||||||
}, [keyboardShortcuts]);
|
}, [mergedShortcuts]);
|
||||||
|
|
||||||
const handleStartEdit = (key: keyof KeyboardShortcuts) => {
|
const handleStartEdit = (key: keyof KeyboardShortcuts) => {
|
||||||
const currentValue = keyboardShortcuts[key];
|
const currentValue = mergedShortcuts[key];
|
||||||
const parsed = parseShortcut(currentValue);
|
const parsed = parseShortcut(currentValue);
|
||||||
setEditingShortcut(key);
|
setEditingShortcut(key);
|
||||||
setKeyValue(parsed.key);
|
setKeyValue(parsed.key);
|
||||||
@@ -495,7 +508,7 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
|
|||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{shortcuts.map(({ key, label, value }) => {
|
{shortcuts.map(({ key, label, value }) => {
|
||||||
const isModified = keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
|
const isModified = mergedShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key];
|
||||||
const isEditing = editingShortcut === key;
|
const isEditing = editingShortcut === key;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function TerminalPanel({
|
|||||||
const fitAddonRef = useRef<XFitAddon | null>(null);
|
const fitAddonRef = useRef<XFitAddon | null>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const lastShortcutTimeRef = useRef<number>(0);
|
||||||
const [isTerminalReady, setIsTerminalReady] = useState(false);
|
const [isTerminalReady, setIsTerminalReady] = useState(false);
|
||||||
const [shellName, setShellName] = useState("shell");
|
const [shellName, setShellName] = useState("shell");
|
||||||
|
|
||||||
@@ -182,28 +183,43 @@ export function TerminalPanel({
|
|||||||
|
|
||||||
// Custom key handler to intercept terminal shortcuts
|
// Custom key handler to intercept terminal shortcuts
|
||||||
// Return false to prevent xterm from handling the key
|
// Return false to prevent xterm from handling the key
|
||||||
|
const SHORTCUT_COOLDOWN_MS = 300; // Prevent rapid firing
|
||||||
|
|
||||||
terminal.attachCustomKeyEventHandler((event) => {
|
terminal.attachCustomKeyEventHandler((event) => {
|
||||||
// Only intercept keydown events
|
// Only intercept keydown events
|
||||||
if (event.type !== 'keydown') return true;
|
if (event.type !== 'keydown') return true;
|
||||||
|
|
||||||
|
// Check cooldown to prevent rapid terminal creation
|
||||||
|
const now = Date.now();
|
||||||
|
const canTrigger = now - lastShortcutTimeRef.current > SHORTCUT_COOLDOWN_MS;
|
||||||
|
|
||||||
// Alt+D - Split right
|
// Alt+D - Split right
|
||||||
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'd') {
|
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'd') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (canTrigger) {
|
||||||
|
lastShortcutTimeRef.current = now;
|
||||||
onSplitHorizontalRef.current();
|
onSplitHorizontalRef.current();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alt+Shift+D - Split down
|
// Alt+Shift+D - Split down
|
||||||
if (event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'd') {
|
if (event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'd') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (canTrigger) {
|
||||||
|
lastShortcutTimeRef.current = now;
|
||||||
onSplitVerticalRef.current();
|
onSplitVerticalRef.current();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alt+W - Close terminal
|
// Alt+W - Close terminal
|
||||||
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'w') {
|
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'w') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (canTrigger) {
|
||||||
|
lastShortcutTimeRef.current = now;
|
||||||
onCloseRef.current();
|
onCloseRef.current();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user