import React, { useEffect, useState, useRef } from "react"; import { type } from "@tauri-apps/plugin-os"; import { getKeyName, formatKeyCombination, normalizeKey, type OSType, } from "../../lib/utils/keyboard"; import { ResetButton } from "../ui/ResetButton"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; import { invoke } from "@tauri-apps/api/core"; import { toast } from "sonner"; interface HandyShortcutProps { descriptionMode?: "inline" | "tooltip"; grouped?: boolean; } export const HandyShortcut: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { const { getSetting, updateBinding, resetBinding, isUpdating, isLoading } = useSettings(); const [keyPressed, setKeyPressed] = useState([]); const [recordedKeys, setRecordedKeys] = useState([]); const [editingShortcutId, setEditingShortcutId] = useState( null, ); const [originalBinding, setOriginalBinding] = useState(""); const [osType, setOsType] = useState("unknown"); const shortcutRefs = useRef>(new Map()); const bindings = getSetting("bindings") || {}; // Detect and store OS type useEffect(() => { const detectOsType = async () => { try { const detectedType = type(); let normalizedType: OSType; switch (detectedType) { case "macos": normalizedType = "macos"; break; case "windows": normalizedType = "windows"; break; case "linux": normalizedType = "linux"; break; default: normalizedType = "unknown"; } setOsType(normalizedType); } catch (error) { console.error("Error detecting OS type:", error); setOsType("unknown"); } }; detectOsType(); }, []); useEffect(() => { // Only add event listeners when we're in editing mode if (editingShortcutId === null) return; let cleanup = false; // Keyboard event listeners const handleKeyDown = async (e: KeyboardEvent) => { if (cleanup) return; if (e.repeat) return; // ignore auto-repeat if (e.key === "Escape") { // Cancel recording and restore original binding if (editingShortcutId && originalBinding) { try { await updateBinding(editingShortcutId, originalBinding); await invoke("resume_binding", { id: editingShortcutId }).catch( console.error, ); } catch (error) { console.error("Failed to restore original binding:", error); toast.error("Failed to restore original shortcut"); } } else if (editingShortcutId) { await invoke("resume_binding", { id: editingShortcutId }).catch( console.error, ); } setEditingShortcutId(null); setKeyPressed([]); setRecordedKeys([]); setOriginalBinding(""); return; } e.preventDefault(); // Get the key with OS-specific naming and normalize it const rawKey = getKeyName(e, osType); const key = normalizeKey(rawKey); if (!keyPressed.includes(key)) { setKeyPressed((prev) => [...prev, key]); // Also add to recorded keys if not already there if (!recordedKeys.includes(key)) { setRecordedKeys((prev) => [...prev, key]); } } }; const handleKeyUp = async (e: KeyboardEvent) => { if (cleanup) return; e.preventDefault(); // Get the key with OS-specific naming and normalize it const rawKey = getKeyName(e, osType); const key = normalizeKey(rawKey); // Remove from currently pressed keys setKeyPressed((prev) => prev.filter((k) => k !== key)); // If no keys are pressed anymore, commit the shortcut const updatedKeyPressed = keyPressed.filter((k) => k !== key); if (updatedKeyPressed.length === 0 && recordedKeys.length > 0) { // Create the shortcut string from all recorded keys const newShortcut = recordedKeys.join("+"); if (editingShortcutId && bindings[editingShortcutId]) { try { await updateBinding(editingShortcutId, newShortcut); // Re-register the shortcut now that recording is finished await invoke("resume_binding", { id: editingShortcutId }).catch( console.error, ); } catch (error) { console.error("Failed to change binding:", error); toast.error(`Failed to set shortcut: ${error}`); // Reset to original binding on error if (originalBinding) { try { await updateBinding(editingShortcutId, originalBinding); await invoke("resume_binding", { id: editingShortcutId }).catch( console.error, ); } catch (resetError) { console.error("Failed to reset binding:", resetError); toast.error("Failed to reset shortcut to original value"); } } } // Exit editing mode and reset states setEditingShortcutId(null); setKeyPressed([]); setRecordedKeys([]); setOriginalBinding(""); } } }; // Add click outside handler const handleClickOutside = async (e: MouseEvent) => { if (cleanup) return; const activeElement = shortcutRefs.current.get(editingShortcutId); if (activeElement && !activeElement.contains(e.target as Node)) { // Cancel shortcut recording and restore original binding if (editingShortcutId && originalBinding) { try { await updateBinding(editingShortcutId, originalBinding); await invoke("resume_binding", { id: editingShortcutId }).catch( console.error, ); } catch (error) { console.error("Failed to restore original binding:", error); toast.error("Failed to restore original shortcut"); } } else if (editingShortcutId) { invoke("resume_binding", { id: editingShortcutId }).catch( console.error, ); } setEditingShortcutId(null); setKeyPressed([]); setRecordedKeys([]); setOriginalBinding(""); } }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); window.addEventListener("click", handleClickOutside); return () => { cleanup = true; window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); window.removeEventListener("click", handleClickOutside); }; }, [ keyPressed, recordedKeys, editingShortcutId, bindings, originalBinding, updateBinding, osType, ]); // Start recording a new shortcut const startRecording = async (id: string) => { if (editingShortcutId === id) return; // Already editing this shortcut // Suspend current binding to avoid firing while recording await invoke("suspend_binding", { id }).catch(console.error); // Store the original binding to restore if canceled setOriginalBinding(bindings[id]?.current_binding || ""); setEditingShortcutId(id); setKeyPressed([]); setRecordedKeys([]); }; // Format the current shortcut keys being recorded const formatCurrentKeys = (): string => { if (recordedKeys.length === 0) return "Press keys..."; // Use the same formatting as the display to ensure consistency return formatKeyCombination(recordedKeys.join("+"), osType); }; // Store references to shortcut elements const setShortcutRef = (id: string, ref: HTMLDivElement | null) => { shortcutRefs.current.set(id, ref); }; // If still loading, show loading state if (isLoading) { return (
Loading shortcuts...
); } // If no bindings are loaded, show empty state if (Object.keys(bindings).length === 0) { return (
No shortcuts configured
); } return ( {(() => { const primaryBinding = Object.values(bindings)[0]; const primaryId = Object.keys(bindings)[0]; if (!primaryBinding) { return (
No shortcuts configured
); } return (
{editingShortcutId === primaryId ? (
setShortcutRef(primaryId, ref)} className="px-2 py-1 text-sm font-semibold border border-logo-primary bg-logo-primary/30 rounded min-w-[120px] text-center" > {formatCurrentKeys()}
) : (
startRecording(primaryId)} > {formatKeyCombination(primaryBinding.current_binding, osType)}
)} resetBinding(primaryId)} disabled={isUpdating(`binding_${primaryId}`)} />
); })()}
); };