import React, { useEffect, useState, useRef } from "react"; import { load } from "@tauri-apps/plugin-store"; import { BindingResponseSchema, SettingsSchema, ShortcutBindingSchema, ShortcutBindingsMap, } from "../../lib/types"; import { invoke } from "@tauri-apps/api/core"; import { type } from "@tauri-apps/plugin-os"; import { getKeyName } from "../../lib/utils/keyboard"; import ResetIcon from "../icons/ResetIcon"; export const KeyboardShortcuts: React.FC = () => { const [bindings, setBindings] = React.useState({}); const [pttEnabled, setPttEnabled] = React.useState(false); const [audioFeedbackEnabled, setAudioFeedbackEnabled] = React.useState(false); const [keyPressed, setKeyPressed] = useState([]); const [recordedKeys, setRecordedKeys] = useState([]); const [editingShortcutId, setEditingShortcutId] = useState( null, ); const [originalBinding, setOriginalBinding] = useState(""); const [isMacOS, setIsMacOS] = useState(false); const shortcutRefs = useRef>(new Map()); // Check if running on macOS useEffect(() => { const checkOsType = async () => { try { const osType = type(); setIsMacOS(osType === "macos"); } catch (error) { console.error("Error detecting OS type:", error); setIsMacOS(false); } }; checkOsType(); }, []); // Normalize modifier keys (unify left/right variants) const normalizeKey = (key: string): string => { // Handle left/right variants of modifier keys if (key.startsWith("left ") || key.startsWith("right ")) { const parts = key.split(" "); if (parts.length === 2) { // Return just the modifier name without left/right prefix return parts[1]; } } return key; }; // Format keys for macOS display const formatMacOSKeys = (key: string): string => { if (!isMacOS) return key; // Only format for macOS const keyMap: Record = { alt: "option", }; return keyMap[key.toLowerCase()] || key; }; // Format a key combination for display const formatKeyCombination = (combination: string): string => { if (!isMacOS) return combination; // Only format for macOS return combination.split("+").map(formatMacOSKeys).join(" + "); }; useEffect(() => { load("settings_store.json", { autoSave: false }).then((r) => { console.log("loaded store", r); r.get("settings").then((s) => { const settings = SettingsSchema.parse(s); setBindings(settings.bindings); setPttEnabled(settings.push_to_talk); setAudioFeedbackEnabled(settings.audio_feedback); }); }); }, []); useEffect(() => { // Only add event listeners when we're in editing mode if (editingShortcutId === null) return; console.log("keyPressed", keyPressed); // Keyboard event listeners const handleKeyDown = (e: KeyboardEvent) => { e.preventDefault(); // Get the key and normalize it (unify left/right modifiers) const rawKey = getKeyName(e); const key = normalizeKey(rawKey); console.log("You pressed", rawKey, "normalized to", key); 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 = (e: KeyboardEvent) => { e.preventDefault(); // Get the key and normalize it const rawKey = getKeyName(e); 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 if (keyPressed.length === 1 && keyPressed[0] === key) { // Create the shortcut string from all recorded keys const newShortcut = recordedKeys.join("+"); if (editingShortcutId && bindings[editingShortcutId]) { const updatedBinding = { ...bindings[editingShortcutId], current_binding: newShortcut, }; setBindings((prev) => ({ ...prev, [editingShortcutId]: updatedBinding, })); invoke("change_binding", { id: editingShortcutId, binding: newShortcut, }); // Exit editing mode and reset states setEditingShortcutId(null); setKeyPressed([]); setRecordedKeys([]); setOriginalBinding(""); } } }; // Add click outside handler const handleClickOutside = (e: MouseEvent) => { const activeElement = shortcutRefs.current.get(editingShortcutId); if (activeElement && !activeElement.contains(e.target as Node)) { // Cancel shortcut recording and restore original value if (editingShortcutId && bindings[editingShortcutId]) { setBindings((prev) => ({ ...prev, [editingShortcutId]: { ...prev[editingShortcutId], current_binding: originalBinding, }, })); // Reset states setEditingShortcutId(null); setKeyPressed([]); setRecordedKeys([]); setOriginalBinding(""); } } }; window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); window.addEventListener("mousedown", handleClickOutside); return () => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); window.removeEventListener("mousedown", handleClickOutside); }; }, [keyPressed, recordedKeys, editingShortcutId, bindings, originalBinding]); // Start recording a new shortcut const startRecording = (id: string) => { // 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 = () => { if (recordedKeys.length === 0) return "Press keys..."; if (!isMacOS) { return recordedKeys.join("+"); } // Map each key to its macOS-friendly name for display return recordedKeys.map(formatMacOSKeys).join(" + "); }; // Store references to shortcut elements const setShortcutRef = (id: string, ref: HTMLDivElement | null) => { shortcutRefs.current.set(id, ref); }; return (

Push To Talk

Hold to record, release to stop

Audio Feedback

Play sound when recording starts and stops

{Object.entries(bindings).map(([id, binding]) => (

{binding.name}

{binding.description}

{editingShortcutId === id ? (
setShortcutRef(id, 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(id)} > {formatKeyCombination(binding.current_binding)}
)}
))}
); };