ugly but working unified settings store.

This commit is contained in:
CJ Pais 2025-05-08 16:43:51 -07:00
parent 5bddf49f88
commit 9b5f1c5a04
5 changed files with 197 additions and 164 deletions

View file

@ -82,7 +82,10 @@ pub fn run() {
}
_ => {}
})
.invoke_handler(tauri::generate_handler![shortcut::set_binding])
.invoke_handler(tauri::generate_handler![
shortcut::change_binding,
shortcut::reset_binding
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,7 +1,9 @@
use serde::Deserialize;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
use tauri::{App, AppHandle, Runtime};
use tauri_plugin_store::{Store, StoreExt};
#[derive(Serialize, Deserialize, Debug, Clone)] // Clone is useful
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ShortcutBinding {
pub id: String,
pub name: String,
@ -10,28 +12,106 @@ pub struct ShortcutBinding {
pub current_binding: String,
}
#[derive(Serialize, Deserialize, Debug)]
/* still handy for composing the initial JSON in the store ------------- */
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppSettings {
pub bindings: Vec<ShortcutBinding>,
pub bindings: HashMap<String, ShortcutBinding>,
}
pub const SETTINGS_STORE_PATH: &str = "settings_store.json";
pub fn get_default_settings() -> AppSettings {
AppSettings {
bindings: vec![
ShortcutBinding {
id: "transcribe".to_string(),
name: "Transcribe".to_string(),
description: "Converts your speech into text.".to_string(),
default_binding: "alt+space".to_string(),
current_binding: "alt+space".to_string(),
},
ShortcutBinding {
id: "test".to_string(),
name: "Test".to_string(),
description: "This is a test binding.".to_string(),
default_binding: "ctrl+d".to_string(),
current_binding: "ctrl+d".to_string(),
},
],
}
let mut bindings = HashMap::new();
bindings.insert(
"transcribe".to_string(),
ShortcutBinding {
id: "transcribe".to_string(),
name: "Transcribe".to_string(),
description: "Converts your speech into text.".to_string(),
default_binding: "alt+space".to_string(),
current_binding: "alt+space".to_string(),
},
);
bindings.insert(
"test".to_string(),
ShortcutBinding {
id: "test".to_string(),
name: "Test".to_string(),
description: "This is a test binding.".to_string(),
default_binding: "ctrl+d".to_string(),
current_binding: "ctrl+d".to_string(),
},
);
AppSettings { bindings }
}
pub fn load_or_create_app_settings(app: &App) -> AppSettings {
// Initialize store
let store = app
.store(SETTINGS_STORE_PATH)
.expect("Failed to initialize store");
let settings = if let Some(settings_value) = store.get("settings") {
// Parse the entire settings object
match serde_json::from_value::<AppSettings>(settings_value) {
Ok(settings) => {
println!("Found existing settings: {:?}", settings);
settings
}
Err(e) => {
println!("Failed to parse settings: {}", e);
// Fall back to default settings if parsing fails
let default_settings = get_default_settings();
// Store the default settings
store.set("settings", serde_json::to_value(&default_settings).unwrap());
default_settings
}
}
} else {
// Create default settings
let default_settings = get_default_settings();
// Store the settings
store.set("settings", serde_json::to_value(&default_settings).unwrap());
default_settings
};
settings
}
pub fn get_settings(app: &AppHandle) -> AppSettings {
let store = app
.store(SETTINGS_STORE_PATH)
.expect("Failed to initialize store");
let settings = serde_json::from_value::<AppSettings>(store.get("settings").unwrap()).unwrap();
settings
}
pub fn write_settings(app: &AppHandle, settings: AppSettings) {
let store = app
.store(SETTINGS_STORE_PATH)
.expect("Failed to initialize store");
store.set("settings", serde_json::to_value(&settings).unwrap());
}
pub fn get_bindings(app: &AppHandle) -> HashMap<String, ShortcutBinding> {
let settings = get_settings(app);
settings.bindings
}
pub fn get_stored_binding(app: &AppHandle, id: &str) -> ShortcutBinding {
let bindings = get_bindings(app);
let binding = bindings.get(id).unwrap().clone();
binding
}

View file

@ -3,18 +3,14 @@ use std::sync::Arc;
use tauri::App;
use tauri::AppHandle;
use tauri::Manager;
use tauri::Runtime;
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
use tauri_plugin_store::StoreExt;
use crate::managers::audio::AudioRecordingManager;
use crate::managers::transcription::TranscriptionManager;
use crate::settings;
use crate::settings::AppSettings;
use crate::settings::ShortcutBinding;
use crate::utils;
use crate::AppState;
fn transcribe_pressed(app: &AppHandle) {
let rm = app.state::<Arc<AudioRecordingManager>>();
@ -41,142 +37,68 @@ fn transcribe_released(app: &AppHandle) {
}
pub fn init_shortcuts(app: &App) {
// Initialize store
let kb_store = app
.store("settings_store.json")
.expect("Failed to initialize store");
// Get or create settings
let settings = if let Some(settings_value) = kb_store.get("settings") {
// Parse the entire settings object
match serde_json::from_value::<AppSettings>(settings_value) {
Ok(settings) => {
println!("Found existing settings: {:?}", settings);
settings
}
Err(e) => {
println!("Failed to parse settings: {}", e);
// Fall back to default settings if parsing fails
let default_settings = settings::get_default_settings();
// Store the default settings
kb_store.set("settings", serde_json::to_value(&default_settings).unwrap());
default_settings
}
}
} else {
// Create default settings
let default_settings = settings::get_default_settings();
// Store the settings
kb_store.set("settings", serde_json::to_value(&default_settings).unwrap());
default_settings
};
let settings = settings::load_or_create_app_settings(app);
// Register shortcuts with the bindings from settings
_register_shortcuts(app, settings.bindings);
for (_id, binding) in settings.bindings {
_register_shortcut(app.handle(), binding);
}
}
#[tauri::command]
pub fn set_binding(app: AppHandle, id: String, binding: String) {
let app_state = app.state::<AppState>();
let mut active_bindings = app_state.active_bindings.lock().unwrap();
let old_binding_str = active_bindings.get(&id).cloned(); // Clone to avoid borrowing issues
pub fn change_binding(app: AppHandle, id: String, binding: String) -> ShortcutBinding {
let mut settings = settings::get_settings(&app);
match binding.parse::<Shortcut>() {
Ok(shortcut) => {
println!("Shortcut '{}' is valid", shortcut);
// unregister the existing binding
if let Some(old_binding_str) = old_binding_str {
match old_binding_str.parse::<Shortcut>() {
Ok(old_binding) => {
app.global_shortcut()
.unregister(old_binding)
.expect("Failed to unregister shortcut");
}
Err(e) => {
eprintln!("Error parsing old shortcut '{}': {:?}", old_binding_str, e);
}
}
} else {
println!("No existing shortcut to unregister");
}
// Get the binding to modify
let binding_to_modify = settings.bindings.get(&id).unwrap().clone();
// register the new binding
app.global_shortcut()
.on_shortcut(shortcut, move |handler_app, scut, event| {
if scut == &shortcut {
println!("Global Shortcut pressed! {}", scut.into_string());
if event.state == ShortcutState::Pressed {
transcribe_pressed(handler_app);
} else if event.state == ShortcutState::Released {
transcribe_released(handler_app);
}
}
})
.expect("couldnt register shortcut");
// Unregister the existing binding
_unregister_shortcut(&app, binding_to_modify.clone());
// TODO error handling?
active_bindings.insert(id, binding);
}
Err(e) => {
eprintln!("Error parsing shortcut '{}': {:?}", binding, 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());
// Update the binding in the settings
settings.bindings.insert(id, updated_binding.clone());
// Save the settings
settings::write_settings(&app, settings);
// Return the updated binding
updated_binding
}
fn _register_shortcuts(app: &App, bindings: Vec<ShortcutBinding>) {
// get bindings from state
let app_state = app.state::<AppState>();
let mut active_bindings = app_state.active_bindings.lock().unwrap();
#[tauri::command]
pub fn reset_binding(app: AppHandle, id: String) -> ShortcutBinding {
let binding = settings::get_stored_binding(&app, &id);
return change_binding(app, id, binding.default_binding);
}
// iterate through bindings
for binding in bindings {
// Parse the shortcut, handling errors gracefully
match binding.current_binding.parse::<Shortcut>() {
Ok(shortcut) => {
// Try to register the shortcut
match app.global_shortcut().on_shortcut(
shortcut,
move |handler_app, scut, event| {
if scut == &shortcut {
println!("Global Shortcut pressed! {}", scut.into_string());
if event.state == ShortcutState::Pressed {
transcribe_pressed(handler_app);
} else if event.state == ShortcutState::Released {
transcribe_released(handler_app);
}
}
},
) {
Ok(_) => {
// Additional actions on successful registration
println!(
"Successfully registered shortcut: {}",
binding.current_binding
);
fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) {
let shortcut = binding.current_binding.parse::<Shortcut>().unwrap();
active_bindings.insert(binding.id, binding.current_binding);
}
Err(err) => {
// Log registration error
eprintln!(
"Failed to register shortcut {}: {}",
binding.current_binding, err
);
}
app.global_shortcut()
.on_shortcut(shortcut, move |handler_app, scut, event| {
if scut == &shortcut {
println!("Global Shortcut pressed! {}", scut.into_string());
if event.state == ShortcutState::Pressed {
transcribe_pressed(handler_app);
} else if event.state == ShortcutState::Released {
transcribe_released(handler_app);
}
}
Err(err) => {
// Log parsing error
eprintln!(
"Failed to parse shortcut {}: {}",
binding.current_binding, err
);
}
}
}
})
.expect("couldnt register shortcut");
}
fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) {
let shortcut = binding.current_binding.parse::<Shortcut>().unwrap();
app.global_shortcut()
.unregister(shortcut)
.expect("couldnt unregister shortcut");
}

View file

@ -1,9 +1,15 @@
import React, { useEffect } from "react";
import { load } from "@tauri-apps/plugin-store";
import { SettingsSchema, ShortcutBinding } from "../../lib/types";
import {
SettingsSchema,
ShortcutBinding,
ShortcutBindingSchema,
ShortcutBindingsMap,
} from "../../lib/types";
import { invoke } from "@tauri-apps/api/core";
export const KeyboardShortcuts: React.FC = () => {
const [bindings, setBindings] = React.useState<ShortcutBinding[]>([]);
const [bindings, setBindings] = React.useState<ShortcutBindingsMap>({});
useEffect(() => {
load("settings_store.json", { autoSave: false }).then((r) => {
@ -14,13 +20,22 @@ 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);
}, []);
return (
<div className="space-y-4">
{bindings.map((binding) => (
{Object.entries(bindings).map(([id, binding]) => (
<div
key={binding.id}
key={id}
className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:bg-gray-50"
>
<div>
@ -30,13 +45,20 @@ export const KeyboardShortcuts: React.FC = () => {
<p className="text-sm text-gray-500">{binding.description}</p>
</div>
<div className="flex items-center space-x-1">
<React.Fragment>
{/* <kbd className="px-2 py-1 text-sm font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded"> */}
<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>
{/* </kbd> */}
</React.Fragment>
<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>
<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 });
});
}}
>
reset
</button>
</div>
</div>
))}

View file

@ -8,9 +8,15 @@ export const ShortcutBindingSchema = z.object({
current_binding: z.string(),
});
export const ShortcutBindingsMapSchema = z.record(
z.string(),
ShortcutBindingSchema
);
export const SettingsSchema = z.object({
bindings: z.array(ShortcutBindingSchema),
bindings: ShortcutBindingsMapSchema,
});
export type ShortcutBinding = z.infer<typeof ShortcutBindingSchema>;
export type ShortcutBindingsMap = z.infer<typeof ShortcutBindingsMapSchema>;
export type Settings = z.infer<typeof SettingsSchema>;