most basic working being able to change key bindings.

This commit is contained in:
CJ Pais 2025-05-08 17:19:23 -07:00
parent 9b5f1c5a04
commit b58b65dabc
5 changed files with 180 additions and 28 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -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",

View file

@ -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<ShortcutBinding>,
error: Option<String>,
}
#[tauri::command]
pub fn change_binding(app: AppHandle, id: String, binding: String) -> ShortcutBinding {
pub fn change_binding(
app: AppHandle,
id: String,
binding: String,
) -> Result<BindingResponse, String> {
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<BindingResponse, String> {
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::<Shortcut>().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::<Shortcut>() {
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::<Shortcut>().unwrap();
fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
let shortcut = match binding.current_binding.parse::<Shortcut>() {
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(())
}

View file

@ -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<ShortcutBindingsMap>({});
const [keyPressed, setKeyPressed] = useState<string[]>([]);
const [recordedKeys, setRecordedKeys] = useState<string[]>([]);
const [editingShortcutId, setEditingShortcutId] = useState<string | null>(
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 (
<div className="space-y-4">
{Object.entries(bindings).map(([id, binding]) => (
@ -45,15 +122,33 @@ export const KeyboardShortcuts: React.FC = () => {
<p className="text-sm text-gray-500">{binding.description}</p>
</div>
<div className="flex items-center space-x-1">
<div className="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded">
{binding.current_binding}
</div>
{editingShortcutId === id ? (
<div className="px-2 py-1 text-sm font-semibold text-blue-600 bg-blue-50 border border-blue-200 rounded min-w-[100px] text-center">
{formatCurrentKeys()}
</div>
) : (
<div
className="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded cursor-pointer hover:bg-gray-200"
onClick={() => startRecording(id)}
>
{binding.current_binding}
</div>
)}
<button
className="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded hover:bg-gray-50"
onClick={() => {
invoke("reset_binding", { id }).then((b) => {
const newBinding = ShortcutBindingSchema.parse(b);
setBindings({ ...bindings, [newBinding.id]: newBinding });
console.log("reset");
const newBinding = BindingResponseSchema.parse(b);
if (!newBinding.success) {
console.error("Error resetting binding:", newBinding.error);
return;
}
const binding = newBinding.binding!;
setBindings({ ...bindings, [binding.id]: binding });
});
}}
>

View file

@ -17,6 +17,13 @@ export const SettingsSchema = z.object({
bindings: ShortcutBindingsMapSchema,
});
export const BindingResponseSchema = z.object({
success: z.boolean(),
binding: ShortcutBindingSchema.nullable(),
error: z.string().nullable(),
});
export type BindingResponse = z.infer<typeof BindingResponseSchema>;
export type ShortcutBinding = z.infer<typeof ShortcutBindingSchema>;
export type ShortcutBindingsMap = z.infer<typeof ShortcutBindingsMapSchema>;
export type Settings = z.infer<typeof SettingsSchema>;