most basic side of Rust being able to change the key binding.
This commit is contained in:
parent
d36ddfd61c
commit
5bddf49f88
5 changed files with 236 additions and 116 deletions
|
|
@ -1,13 +1,29 @@
|
||||||
mod managers;
|
mod managers;
|
||||||
|
mod settings;
|
||||||
mod shortcut;
|
mod shortcut;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use managers::audio::AudioRecordingManager;
|
use managers::audio::AudioRecordingManager;
|
||||||
use managers::transcription::TranscriptionManager;
|
use managers::transcription::TranscriptionManager;
|
||||||
use std::sync::Arc;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub active_bindings: Mutex<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
// Convenience method to create a new AppState
|
||||||
|
fn new() -> Self {
|
||||||
|
AppState {
|
||||||
|
active_bindings: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
@ -21,6 +37,8 @@ pub fn run() {
|
||||||
.plugin(tauri_plugin_macos_permissions::init())
|
.plugin(tauri_plugin_macos_permissions::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_store::Builder::default().build())
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||||
|
.manage(AppState::new())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
let _tray = TrayIconBuilder::new().build(app)?;
|
let _tray = TrayIconBuilder::new().build(app)?;
|
||||||
|
|
||||||
|
|
@ -64,7 +82,7 @@ pub fn run() {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![])
|
.invoke_handler(tauri::generate_handler![shortcut::set_binding])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)] // Clone is useful
|
||||||
|
pub struct ShortcutBinding {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub default_binding: String,
|
||||||
|
pub current_binding: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct AppSettings {
|
||||||
|
pub bindings: Vec<ShortcutBinding>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,99 +1,20 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
|
||||||
use std::time;
|
|
||||||
|
|
||||||
use rdev::{simulate, EventType, Key, SimulateError};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use tauri::App;
|
use tauri::App;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri::Runtime;
|
||||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||||
use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
||||||
use tauri_plugin_store::{JsonValue, StoreExt};
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
use crate::managers::audio::AudioRecordingManager;
|
use crate::managers::audio::AudioRecordingManager;
|
||||||
use crate::managers::transcription::TranscriptionManager;
|
use crate::managers::transcription::TranscriptionManager;
|
||||||
|
use crate::settings;
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)] // Clone is useful
|
use crate::settings::AppSettings;
|
||||||
pub struct ShortcutBinding {
|
use crate::settings::ShortcutBinding;
|
||||||
id: String,
|
use crate::utils;
|
||||||
name: String,
|
use crate::AppState;
|
||||||
description: String,
|
|
||||||
default_binding: String,
|
|
||||||
current_binding: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct AppSettings {
|
|
||||||
bindings: Vec<ShortcutBinding>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_send_event(event: &EventType) {
|
|
||||||
if let Err(SimulateError) = simulate(event) {
|
|
||||||
println!("We could not send {:?}", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(event: EventType) {
|
|
||||||
try_send_event(&event);
|
|
||||||
thread::sleep(time::Duration::from_millis(60));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_paste() {
|
|
||||||
// Determine the modifier key based on the OS
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let modifier_key = Key::MetaLeft; // Command key on macOS
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let modifier_key = Key::ControlLeft; // Control key on other systems
|
|
||||||
|
|
||||||
// Press both keys
|
|
||||||
send(EventType::KeyPress(modifier_key));
|
|
||||||
send(EventType::KeyPress(Key::KeyV));
|
|
||||||
|
|
||||||
// Release both keys
|
|
||||||
send(EventType::KeyRelease(Key::KeyV));
|
|
||||||
send(EventType::KeyRelease(modifier_key));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paste(text: String, app_handle: AppHandle) {
|
|
||||||
let clipboard = app_handle.clipboard();
|
|
||||||
|
|
||||||
// get the current clipboard content
|
|
||||||
let clipboard_content = clipboard.read_text().unwrap_or_default();
|
|
||||||
|
|
||||||
clipboard.write_text(&text).unwrap();
|
|
||||||
send_paste();
|
|
||||||
|
|
||||||
// restore the clipboard
|
|
||||||
clipboard.write_text(&clipboard_content).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// const HANDY_TAURI_STORE: &str = "handy_tauri_store";
|
|
||||||
|
|
||||||
// let mut shortcut_map: HashMap<String, fn()> = HashMap::new();
|
|
||||||
|
|
||||||
fn transcribe_pressed(app: &AppHandle) {
|
fn transcribe_pressed(app: &AppHandle) {
|
||||||
let rm = app.state::<Arc<AudioRecordingManager>>();
|
let rm = app.state::<Arc<AudioRecordingManager>>();
|
||||||
|
|
@ -111,7 +32,7 @@ fn transcribe_released(app: &AppHandle) {
|
||||||
// Not .await, as transcribe is synchronous
|
// Not .await, as transcribe is synchronous
|
||||||
Ok(transcription) => {
|
Ok(transcription) => {
|
||||||
println!("Global Shortcut Transcription: {}", transcription);
|
println!("Global Shortcut Transcription: {}", transcription);
|
||||||
paste(transcription, ah);
|
utils::paste(transcription, ah);
|
||||||
}
|
}
|
||||||
Err(err) => println!("Global Shortcut Transcription error: {}", err),
|
Err(err) => println!("Global Shortcut Transcription error: {}", err),
|
||||||
}
|
}
|
||||||
|
|
@ -120,38 +41,73 @@ fn transcribe_released(app: &AppHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_shortcuts(app: &App) {
|
pub fn init_shortcuts(app: &App) {
|
||||||
// init store
|
// Initialize store
|
||||||
let kb_store = app
|
let kb_store = app
|
||||||
.store("settings_store.json")
|
.store("settings_store.json")
|
||||||
.expect("Failed to initialize store");
|
.expect("Failed to initialize store");
|
||||||
|
|
||||||
if let Some(bindings) = kb_store.get("settings") {
|
// Get or create settings
|
||||||
// print the bindings that exist
|
let settings = if let Some(settings_value) = kb_store.get("settings") {
|
||||||
println!("Bindings: {:?}", bindings);
|
// 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 {
|
} else {
|
||||||
kb_store.set(
|
// Create default settings
|
||||||
"settings",
|
let default_settings = settings::get_default_settings();
|
||||||
serde_json::to_value(&get_default_settings()).unwrap(),
|
|
||||||
);
|
|
||||||
// create the default bindings
|
|
||||||
}
|
|
||||||
|
|
||||||
// load state from store
|
// Store the settings
|
||||||
|
kb_store.set("settings", serde_json::to_value(&default_settings).unwrap());
|
||||||
|
|
||||||
_register_shortcut_upon_start(
|
default_settings
|
||||||
app,
|
};
|
||||||
"alt+space"
|
|
||||||
.parse::<Shortcut>()
|
// Register shortcuts with the bindings from settings
|
||||||
.expect("Failed to parse shortcut"),
|
_register_shortcuts(app, settings.bindings);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
|
#[tauri::command]
|
||||||
// Initialize global shortcut and set its handler
|
pub fn set_binding(app: AppHandle, id: String, binding: String) {
|
||||||
app.handle()
|
let app_state = app.state::<AppState>();
|
||||||
.plugin(
|
let mut active_bindings = app_state.active_bindings.lock().unwrap();
|
||||||
tauri_plugin_global_shortcut::Builder::new()
|
let old_binding_str = active_bindings.get(&id).cloned(); // Clone to avoid borrowing issues
|
||||||
.with_handler(move |handler_app, scut, event| {
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// register the new binding
|
||||||
|
app.global_shortcut()
|
||||||
|
.on_shortcut(shortcut, move |handler_app, scut, event| {
|
||||||
if scut == &shortcut {
|
if scut == &shortcut {
|
||||||
println!("Global Shortcut pressed! {}", scut.into_string());
|
println!("Global Shortcut pressed! {}", scut.into_string());
|
||||||
if event.state == ShortcutState::Pressed {
|
if event.state == ShortcutState::Pressed {
|
||||||
|
|
@ -161,8 +117,66 @@ fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build(),
|
.expect("couldnt register shortcut");
|
||||||
)
|
|
||||||
.unwrap();
|
// TODO error handling?
|
||||||
app.global_shortcut().register(shortcut).unwrap(); // Register global shortcut
|
active_bindings.insert(id, binding);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error parsing shortcut '{}': {:?}", binding, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
active_bindings.insert(binding.id, binding.current_binding);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Log registration error
|
||||||
|
eprintln!(
|
||||||
|
"Failed to register shortcut {}: {}",
|
||||||
|
binding.current_binding, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Log parsing error
|
||||||
|
eprintln!(
|
||||||
|
"Failed to parse shortcut {}: {}",
|
||||||
|
binding.current_binding, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
use rdev::{simulate, EventType, Key, SimulateError};
|
||||||
|
use std::thread;
|
||||||
|
use std::time;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
|
|
||||||
|
fn try_send_event(event: &EventType) {
|
||||||
|
if let Err(SimulateError) = simulate(event) {
|
||||||
|
println!("We could not send {:?}", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(event: EventType) {
|
||||||
|
try_send_event(&event);
|
||||||
|
thread::sleep(time::Duration::from_millis(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_paste() {
|
||||||
|
// Determine the modifier key based on the OS
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let modifier_key = Key::MetaLeft; // Command key on macOS
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let modifier_key = Key::ControlLeft; // Control key on other systems
|
||||||
|
|
||||||
|
// Press both keys
|
||||||
|
send(EventType::KeyPress(modifier_key));
|
||||||
|
send(EventType::KeyPress(Key::KeyV));
|
||||||
|
|
||||||
|
// Release both keys
|
||||||
|
send(EventType::KeyRelease(Key::KeyV));
|
||||||
|
send(EventType::KeyRelease(modifier_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste(text: String, app_handle: AppHandle) {
|
||||||
|
let clipboard = app_handle.clipboard();
|
||||||
|
|
||||||
|
// get the current clipboard content
|
||||||
|
let clipboard_content = clipboard.read_text().unwrap_or_default();
|
||||||
|
|
||||||
|
clipboard.write_text(&text).unwrap();
|
||||||
|
send_paste();
|
||||||
|
|
||||||
|
// restore the clipboard
|
||||||
|
clipboard.write_text(&clipboard_content).unwrap();
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from "tauri-plugin-macos-permissions-api";
|
} from "tauri-plugin-macos-permissions-api";
|
||||||
import { register } from "@tauri-apps/plugin-global-shortcut";
|
import { register } from "@tauri-apps/plugin-global-shortcut";
|
||||||
import HandyTextLogo from "./components/icons/HandyTextLogo";
|
import HandyTextLogo from "./components/icons/HandyTextLogo";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [hasAccessibility, setHasAccessibility] = useState(false);
|
const [hasAccessibility, setHasAccessibility] = useState(false);
|
||||||
|
|
@ -21,6 +22,11 @@ function App() {
|
||||||
}
|
}
|
||||||
setHasAccessibility(hasPermissions);
|
setHasAccessibility(hasPermissions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// invoke("set_binding", {
|
||||||
|
// id: "test",
|
||||||
|
// binding: "alt+d",
|
||||||
|
// });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue