347 lines
12 KiB
Rust
347 lines
12 KiB
Rust
use serde::Serialize;
|
|
use tauri::{App, AppHandle, Emitter, Manager};
|
|
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
|
use tauri_plugin_global_shortcut::{Shortcut, ShortcutState};
|
|
|
|
use crate::actions::ACTION_MAP;
|
|
use crate::settings::ShortcutBinding;
|
|
use crate::settings::{self, get_settings, OverlayPosition};
|
|
use crate::ManagedToggleState;
|
|
|
|
pub fn init_shortcuts(app: &App) {
|
|
let settings = settings::load_or_create_app_settings(app);
|
|
|
|
// Register shortcuts with the bindings from settings
|
|
for (_id, binding) in settings.bindings {
|
|
// Pass app.handle() which is &AppHandle
|
|
if let Err(e) = _register_shortcut(app.handle(), binding) {
|
|
eprintln!("Failed to register shortcut {} during init: {}", _id, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
) -> Result<BindingResponse, String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
|
|
// Get the binding to modify
|
|
let binding_to_modify = match settings.bindings.get(&id) {
|
|
Some(binding) => binding.clone(),
|
|
None => {
|
|
let error_msg = format!("Binding with id '{}' not found", id);
|
|
eprintln!("change_binding error: {}", error_msg);
|
|
return Ok(BindingResponse {
|
|
success: false,
|
|
binding: None,
|
|
error: Some(error_msg),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Unregister the existing binding
|
|
if let Err(e) = _unregister_shortcut(&app, binding_to_modify.clone()) {
|
|
let error_msg = format!("Failed to unregister shortcut: {}", e);
|
|
eprintln!("change_binding error: {}", error_msg);
|
|
}
|
|
|
|
// Validate the new shortcut before we touch the current registration
|
|
if let Err(e) = validate_shortcut_string(&binding) {
|
|
eprintln!("change_binding validation error: {}", e);
|
|
return Err(e);
|
|
}
|
|
|
|
// Create an updated binding
|
|
let mut updated_binding = binding_to_modify;
|
|
updated_binding.current_binding = binding;
|
|
|
|
// Register the new binding
|
|
if let Err(e) = _register_shortcut(&app, updated_binding.clone()) {
|
|
let error_msg = format!("Failed to register shortcut: {}", e);
|
|
eprintln!("change_binding error: {}", error_msg);
|
|
return Ok(BindingResponse {
|
|
success: false,
|
|
binding: None,
|
|
error: Some(error_msg),
|
|
});
|
|
}
|
|
|
|
// 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
|
|
Ok(BindingResponse {
|
|
success: true,
|
|
binding: Some(updated_binding),
|
|
error: None,
|
|
})
|
|
}
|
|
|
|
#[tauri::command]
|
|
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);
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
|
|
// TODO if the setting is currently false, we probably want to
|
|
// cancel any ongoing recordings or actions
|
|
settings.push_to_talk = enabled;
|
|
|
|
settings::write_settings(&app, settings);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
settings.audio_feedback = enabled;
|
|
settings::write_settings(&app, settings);
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_translate_to_english_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
settings.translate_to_english = enabled;
|
|
settings::write_settings(&app, settings);
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_selected_language_setting(app: AppHandle, language: String) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
settings.selected_language = language;
|
|
settings::write_settings(&app, settings);
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_overlay_position_setting(app: AppHandle, position: String) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
let parsed = match position.as_str() {
|
|
"none" => OverlayPosition::None,
|
|
"top" => OverlayPosition::Top,
|
|
"bottom" => OverlayPosition::Bottom,
|
|
other => {
|
|
eprintln!("Invalid overlay position '{}', defaulting to bottom", other);
|
|
OverlayPosition::Bottom
|
|
}
|
|
};
|
|
settings.overlay_position = parsed;
|
|
settings::write_settings(&app, settings);
|
|
|
|
// Update overlay position without recreating window
|
|
crate::utils::update_overlay_position(&app);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_debug_mode_setting(app: AppHandle, enabled: bool) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
settings.debug_mode = enabled;
|
|
settings::write_settings(&app, settings);
|
|
|
|
// Emit event to notify frontend of debug mode change
|
|
let _ = app.emit(
|
|
"settings-changed",
|
|
serde_json::json!({
|
|
"setting": "debug_mode",
|
|
"value": enabled
|
|
}),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn update_custom_words(app: AppHandle, words: Vec<String>) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
settings.custom_words = words;
|
|
settings::write_settings(&app, settings);
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn change_word_correction_threshold_setting(
|
|
app: AppHandle,
|
|
threshold: f64,
|
|
) -> Result<(), String> {
|
|
let mut settings = settings::get_settings(&app);
|
|
settings.word_correction_threshold = threshold;
|
|
settings::write_settings(&app, settings);
|
|
Ok(())
|
|
}
|
|
|
|
/// Determine whether a shortcut string contains at least one non-modifier key.
|
|
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
|
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
|
fn validate_shortcut_string(raw: &str) -> Result<(), String> {
|
|
let modifiers = [
|
|
"ctrl", "control", "shift", "alt", "option", "meta", "command", "cmd", "super", "win",
|
|
"windows",
|
|
];
|
|
let has_non_modifier = raw
|
|
.split('+')
|
|
.any(|part| !modifiers.contains(&part.trim().to_lowercase().as_str()));
|
|
if has_non_modifier {
|
|
Ok(())
|
|
} else {
|
|
Err("Shortcut must contain at least one non-modifier key".into())
|
|
}
|
|
}
|
|
|
|
/// Temporarily unregister a binding while the user is editing it in the UI.
|
|
/// This avoids firing the action while keys are being recorded.
|
|
#[tauri::command]
|
|
pub fn suspend_binding(app: AppHandle, id: String) -> Result<(), String> {
|
|
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
|
if let Err(e) = _unregister_shortcut(&app, b) {
|
|
eprintln!("suspend_binding error for id '{}': {}", id, e);
|
|
return Err(e);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Re-register the binding after the user has finished editing.
|
|
#[tauri::command]
|
|
pub fn resume_binding(app: AppHandle, id: String) -> Result<(), String> {
|
|
if let Some(b) = settings::get_bindings(&app).get(&id).cloned() {
|
|
if let Err(e) = _register_shortcut(&app, b) {
|
|
eprintln!("resume_binding error for id '{}': {}", id, e);
|
|
return Err(e);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
|
// Validate human-level rules first
|
|
if let Err(e) = validate_shortcut_string(&binding.current_binding) {
|
|
eprintln!(
|
|
"_register_shortcut validation error for binding '{}': {}",
|
|
binding.current_binding, e
|
|
);
|
|
return Err(e);
|
|
}
|
|
|
|
// Parse shortcut and return error if it fails
|
|
let shortcut = match binding.current_binding.parse::<Shortcut>() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
let error_msg = format!(
|
|
"Failed to parse shortcut '{}': {}",
|
|
binding.current_binding, e
|
|
);
|
|
eprintln!("_register_shortcut parse error: {}", error_msg);
|
|
return Err(error_msg);
|
|
}
|
|
};
|
|
|
|
// Prevent duplicate registrations that would silently shadow one another
|
|
if app.global_shortcut().is_registered(shortcut) {
|
|
let error_msg = format!("Shortcut '{}' is already in use", binding.current_binding);
|
|
eprintln!("_register_shortcut duplicate error: {}", error_msg);
|
|
return Err(error_msg);
|
|
}
|
|
|
|
// Clone binding.id for use in the closure
|
|
let binding_id_for_closure = binding.id.clone();
|
|
|
|
app.global_shortcut()
|
|
.on_shortcut(shortcut, move |ah, scut, event| {
|
|
if scut == &shortcut {
|
|
let shortcut_string = scut.into_string();
|
|
let settings = get_settings(ah);
|
|
|
|
if let Some(action) = ACTION_MAP.get(&binding_id_for_closure) {
|
|
if settings.push_to_talk {
|
|
if event.state == ShortcutState::Pressed {
|
|
action.start(ah, &binding_id_for_closure, &shortcut_string);
|
|
} else if event.state == ShortcutState::Released {
|
|
action.stop(ah, &binding_id_for_closure, &shortcut_string);
|
|
}
|
|
} else {
|
|
if event.state == ShortcutState::Pressed {
|
|
let toggle_state_manager = ah.state::<ManagedToggleState>();
|
|
|
|
let mut states = toggle_state_manager.lock().expect("Failed to lock toggle state manager");
|
|
|
|
let is_currently_active = states.active_toggles
|
|
.entry(binding_id_for_closure.clone())
|
|
.or_insert(false);
|
|
|
|
if *is_currently_active {
|
|
action.stop(
|
|
ah,
|
|
&binding_id_for_closure,
|
|
&shortcut_string,
|
|
);
|
|
*is_currently_active = false; // Update state to inactive
|
|
} else {
|
|
action.start(ah, &binding_id_for_closure, &shortcut_string);
|
|
*is_currently_active = true; // Update state to active
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
println!(
|
|
"Warning: No action defined in ACTION_MAP for shortcut ID '{}'. Shortcut: '{}', State: {:?}",
|
|
binding_id_for_closure, shortcut_string, event.state
|
|
);
|
|
}
|
|
}
|
|
})
|
|
.map_err(|e| {
|
|
let error_msg = format!("Couldn't register shortcut '{}': {}", binding.current_binding, e);
|
|
eprintln!("_register_shortcut registration error: {}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn _unregister_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> {
|
|
let shortcut = match binding.current_binding.parse::<Shortcut>() {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
let error_msg = format!(
|
|
"Failed to parse shortcut '{}' for unregistration: {}",
|
|
binding.current_binding, e
|
|
);
|
|
eprintln!("_unregister_shortcut parse error: {}", error_msg);
|
|
return Err(error_msg);
|
|
}
|
|
};
|
|
|
|
app.global_shortcut().unregister(shortcut).map_err(|e| {
|
|
let error_msg = format!(
|
|
"Failed to unregister shortcut '{}': {}",
|
|
binding.current_binding, e
|
|
);
|
|
eprintln!("_unregister_shortcut error: {}", error_msg);
|
|
error_msg
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|