Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 226 additions & 18 deletions src-tauri/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ rusqlite = { version = "0.37", features = ["bundled"] }
tar = "0.4.44"
flate2 = "1.0"
transcribe-rs = { version = "0.2.2", features = ["whisper", "parakeet", "moonshine"] }
handy-keys = "0.1.4"
ferrous-opencc = "0.2.3"
specta = "=2.0.0-rc.22"
specta-typescript = "0.0.9"
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ pub fn run() {
shortcut::change_append_trailing_space_setting,
shortcut::change_app_language_setting,
shortcut::change_update_checks_setting,
shortcut::change_keyboard_implementation_setting,
shortcut::get_keyboard_implementation,
shortcut::handy_keys::start_handy_keys_recording,
shortcut::handy_keys::stop_handy_keys_recording,
trigger_update_check,
commands::cancel_operation,
commands::get_app_dir_path,
Expand Down
21 changes: 21 additions & 0 deletions src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,24 @@ pub enum RecordingRetentionPeriod {
Months3,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
#[serde(rename_all = "snake_case")]
pub enum KeyboardImplementation {
Tauri,
HandyKeys,
}

impl Default for KeyboardImplementation {
fn default() -> Self {
// Default to HandyKeys only on macOS where it's well-tested.
// Windows and Linux use Tauri by default (handy-keys not sufficiently tested yet).
#[cfg(target_os = "macos")]
return KeyboardImplementation::HandyKeys;
#[cfg(not(target_os = "macos"))]
return KeyboardImplementation::Tauri;
}
}

impl Default for ModelUnloadTimeout {
fn default() -> Self {
ModelUnloadTimeout::Never
Expand Down Expand Up @@ -295,6 +313,8 @@ pub struct AppSettings {
pub app_language: String,
#[serde(default)]
pub experimental_enabled: bool,
#[serde(default)]
pub keyboard_implementation: KeyboardImplementation,
}

fn default_model() -> String {
Expand Down Expand Up @@ -584,6 +604,7 @@ pub fn get_default_settings() -> AppSettings {
append_trailing_space: false,
app_language: default_app_language(),
experimental_enabled: false,
keyboard_implementation: KeyboardImplementation::default(),
}
}

Expand Down
91 changes: 91 additions & 0 deletions src-tauri/src/shortcut/handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Shared shortcut event handling logic
//!
//! This module contains the common logic for handling shortcut events,
//! used by both the Tauri and handy-keys implementations.

use log::warn;
use std::sync::Arc;
use tauri::{AppHandle, Manager};

use crate::actions::ACTION_MAP;
use crate::managers::audio::AudioRecordingManager;
use crate::settings::get_settings;
use crate::ManagedToggleState;

/// Handle a shortcut event from either implementation.
///
/// This function contains the shared logic for:
/// - Looking up the action in ACTION_MAP
/// - Handling the cancel binding (only fires when recording)
/// - Handling push-to-talk mode (start on press, stop on release)
/// - Handling toggle mode (toggle state on press only)
///
/// # Arguments
/// * `app` - The Tauri app handle
/// * `binding_id` - The ID of the binding (e.g., "transcribe", "cancel")
/// * `hotkey_string` - The string representation of the hotkey
/// * `is_pressed` - Whether this is a key press (true) or release (false)
pub fn handle_shortcut_event(
app: &AppHandle,
binding_id: &str,
hotkey_string: &str,
is_pressed: bool,
) {
let settings = get_settings(app);

let Some(action) = ACTION_MAP.get(binding_id) else {
warn!(
"No action defined in ACTION_MAP for shortcut ID '{}'. Shortcut: '{}', Pressed: {}",
binding_id, hotkey_string, is_pressed
);
return;
};

// Cancel binding: only fires when recording and key is pressed
if binding_id == "cancel" {
let audio_manager = app.state::<Arc<AudioRecordingManager>>();
if audio_manager.is_recording() && is_pressed {
action.start(app, binding_id, hotkey_string);
}
return;
}

// Push-to-talk mode: start on press, stop on release
if settings.push_to_talk {
if is_pressed {
action.start(app, binding_id, hotkey_string);
} else {
action.stop(app, binding_id, hotkey_string);
}
return;
}

// Toggle mode: toggle state on press only
if is_pressed {
// Determine action and update state while holding the lock,
// but RELEASE the lock before calling the action to avoid deadlocks.
// (Actions may need to acquire the lock themselves, e.g., cancel_current_operation)
let should_start: bool;
{
let toggle_state_manager = app.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.to_string())
.or_insert(false);

should_start = !*is_currently_active;
*is_currently_active = should_start;
} // Lock released here

// Now call the action without holding the lock
if should_start {
action.start(app, binding_id, hotkey_string);
} else {
action.stop(app, binding_id, hotkey_string);
}
}
}
Loading