diff --git a/bun.lockb b/bun.lockb index 40415df..64ec2cf 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d0ecf24..d4f3972 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tauri-apps/plugin-store": "~2", "@tauri-apps/plugin-stronghold": "~2", "@tauri-apps/plugin-upload": "~2", + "keycode": "^2.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", "tailwindcss": "^4.0.2", diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index c9680c7..0b08c7a 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use serde::Serialize; use tauri::App; use tauri::AppHandle; use tauri::Manager; @@ -45,22 +46,54 @@ pub fn init_shortcuts(app: &App) { } } +#[derive(Serialize)] +pub struct BindingResponse { + success: bool, + binding: Option, + 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} +
+ )}