From b58b65dabcc48459a4b4fb7ba9619651c51f999c Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Thu, 8 May 2025 17:19:23 -0700 Subject: [PATCH] most basic working being able to change key bindings. --- bun.lockb | Bin 65096 -> 65446 bytes package.json | 1 + src-tauri/src/shortcut.rs | 73 ++++++++-- src/components/settings/KeyboardShortcuts.tsx | 127 +++++++++++++++--- src/lib/types.ts | 7 + 5 files changed, 180 insertions(+), 28 deletions(-) diff --git a/bun.lockb b/bun.lockb index 40415df12ec5bd5fa7799e4354549ec308eabe64..64ec2cf348505d79ad69c6bb7c0ed7c641ec5fa6 100755 GIT binary patch delta 9686 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 delta 9577 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, + error: Option, +} + #[tauri::command] -pub fn change_binding(app: AppHandle, id: String, binding: String) -> ShortcutBinding { +pub fn change_binding( + app: AppHandle, + id: String, + binding: String, +) -> Result { let mut settings = settings::get_settings(&app); // Get the binding to modify - let binding_to_modify = settings.bindings.get(&id).unwrap().clone(); + let binding_to_modify = match settings.bindings.get(&id) { + Some(binding) => binding.clone(), + None => { + return Ok(BindingResponse { + success: false, + binding: None, + error: Some(format!("Binding with id '{}' not found", id)), + }) + } + }; // Unregister the existing binding - _unregister_shortcut(&app, binding_to_modify.clone()); + if let Err(e) = _unregister_shortcut(&app, binding_to_modify.clone()) { + return Ok(BindingResponse { + success: false, + binding: None, + error: Some(format!("Failed to unregister shortcut: {}", e)), + }); + } // Create an updated binding let mut updated_binding = binding_to_modify; updated_binding.current_binding = binding; // Register the new binding - _register_shortcut(&app, updated_binding.clone()); + if let Err(e) = _register_shortcut(&app, updated_binding.clone()) { + return Ok(BindingResponse { + success: false, + binding: None, + error: Some(format!("Failed to register shortcut: {}", e)), + }); + } // Update the binding in the settings settings.bindings.insert(id, updated_binding.clone()); @@ -69,17 +102,26 @@ pub fn change_binding(app: AppHandle, id: String, binding: String) -> ShortcutBi settings::write_settings(&app, settings); // Return the updated binding - updated_binding + Ok(BindingResponse { + success: true, + binding: Some(updated_binding), + error: None, + }) } #[tauri::command] -pub fn reset_binding(app: AppHandle, id: String) -> ShortcutBinding { +pub fn reset_binding(app: AppHandle, id: String) -> Result { let binding = settings::get_stored_binding(&app, &id); + return change_binding(app, id, binding.default_binding); } -fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) { - let shortcut = binding.current_binding.parse::().unwrap(); +fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { + // Parse shortcut and return error if it fails + let shortcut = match binding.current_binding.parse::() { + Ok(s) => s, + Err(e) => return Err(format!("Failed to parse shortcut: {}", e)), + }; app.global_shortcut() .on_shortcut(shortcut, move |handler_app, scut, event| { @@ -92,13 +134,20 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) { } } }) - .expect("couldnt register shortcut"); + .map_err(|e| format!("Couldn't register shortcut: {}", e))?; + + Ok(()) } -fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) { - let shortcut = binding.current_binding.parse::().unwrap(); +fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { + let shortcut = match binding.current_binding.parse::() { + Ok(s) => s, + Err(e) => return Err(format!("Failed to parse shortcut: {}", e)), + }; app.global_shortcut() .unregister(shortcut) - .expect("couldnt unregister shortcut"); + .map_err(|e| format!("Failed to unregister shortcut: {}", e))?; + + Ok(()) } diff --git a/src/components/settings/KeyboardShortcuts.tsx b/src/components/settings/KeyboardShortcuts.tsx index db242af..c466bcd 100644 --- a/src/components/settings/KeyboardShortcuts.tsx +++ b/src/components/settings/KeyboardShortcuts.tsx @@ -1,15 +1,21 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { load } from "@tauri-apps/plugin-store"; import { + BindingResponseSchema, SettingsSchema, - ShortcutBinding, ShortcutBindingSchema, ShortcutBindingsMap, } from "../../lib/types"; import { invoke } from "@tauri-apps/api/core"; +import keycode from "keycode"; export const KeyboardShortcuts: React.FC = () => { const [bindings, setBindings] = React.useState({}); + const [keyPressed, setKeyPressed] = useState([]); + const [recordedKeys, setRecordedKeys] = useState([]); + const [editingShortcutId, setEditingShortcutId] = useState( + null + ); useEffect(() => { load("settings_store.json", { autoSave: false }).then((r) => { @@ -20,17 +26,88 @@ export const KeyboardShortcuts: React.FC = () => { setBindings(settings.bindings); }); }); - - // setTimeout(() => { - // console.log("invoked set binding"); - // invoke("change_binding", { id: "test", binding: "alt+d" }).then((b) => { - // const newBinding = ShortcutBindingSchema.parse(b); - // console.log(bindings); - // setBindings((prev) => ({ ...prev, [newBinding.id]: newBinding })); - // }); - // }, 1000); }, []); + 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(); + + const key = keycode(e).toLowerCase(); + console.log("You pressed", 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(); + + const key = keycode(e).toLowerCase(); + + // 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([]); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }, [keyPressed, recordedKeys, editingShortcutId, bindings]); + + // Start recording a new shortcut + const startRecording = (id: string) => { + setEditingShortcutId(id); + setKeyPressed([]); + setRecordedKeys([]); + }; + + // Format the current shortcut keys being recorded + const formatCurrentKeys = () => { + return recordedKeys.length > 0 ? recordedKeys.join("+") : "Press keys..."; + }; + return (
{Object.entries(bindings).map(([id, binding]) => ( @@ -45,15 +122,33 @@ export const KeyboardShortcuts: React.FC = () => {

{binding.description}

-
- {binding.current_binding} -
+ {editingShortcutId === id ? ( +
+ {formatCurrentKeys()} +
+ ) : ( +
startRecording(id)} + > + {binding.current_binding} +
+ )}