diff --git a/Cargo.lock b/Cargo.lock index 099f3cf..ab95aad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2224,7 +2224,7 @@ dependencies = [ [[package]] name = "mouth" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 9bc4970..b7b3f61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mouth" -version = "0.2.0" +version = "0.2.1" edition = "2024" description = "Offline speech-to-text with global hotkey and paste" license-file = "LICENSE" diff --git a/src/cli/run_cmd.rs b/src/cli/run_cmd.rs index 9ccc1ad..4ff480f 100644 --- a/src/cli/run_cmd.rs +++ b/src/cli/run_cmd.rs @@ -103,10 +103,11 @@ pub fn run() -> Result<()> { }) .context("Failed to spawn recorder thread")?; + let hotkey_state = Arc::clone(&shared_state); thread::Builder::new() .name("mouth-hotkey".into()) .spawn(move || { - hotkey::listen(hotkey_combo, cancel_combo, hotkey_tx); + hotkey::listen(hotkey_combo, cancel_combo, hotkey_tx, hotkey_state); }) .context("Failed to spawn hotkey thread")?; diff --git a/src/hotkey.rs b/src/hotkey.rs index 7a0342f..3cc9a96 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -1,10 +1,14 @@ use anyhow::{bail, Result}; use rdev::{self, Event, EventType, Key}; use std::cell::RefCell; +use std::sync::atomic::Ordering; use std::sync::mpsc; +use std::sync::Arc; use std::time::{Duration, Instant}; use tracing::{debug, error, info}; +use crate::shared_state::SharedState; + /// Events sent from the hotkey listener to the coordinator. #[derive(Debug, Clone, Copy)] pub enum HotkeyEvent { @@ -380,6 +384,7 @@ pub fn listen( hotkey: HotkeyCombination, cancel_key: HotkeyCombination, tx: mpsc::Sender, + shared_state: Arc, ) { let debounce_duration = Duration::from_millis(30); @@ -406,16 +411,19 @@ pub fn listen( EventType::KeyPress(key) => { s.modifier_state.update(&key, true); - // Check cancel key — swallow it + // Check cancel key — only swallow it when actively recording/transcribing if key == cancel_key.key && s.modifier_state.all_held(&cancel_key.modifiers) { - if now.duration_since(s.last_event_time) >= debounce_duration { - s.last_event_time = now; - debug!("Cancel key pressed"); - if tx.send(HotkeyEvent::Cancel).is_err() { - error!("Failed to send cancel event"); + if shared_state.is_active.load(Ordering::Acquire) { + if now.duration_since(s.last_event_time) >= debounce_duration { + s.last_event_time = now; + debug!("Cancel key pressed"); + if tx.send(HotkeyEvent::Cancel).is_err() { + error!("Failed to send cancel event"); + } } + return None; } - return None; + // Not active — let the key pass through } // Check hotkey — swallow it diff --git a/src/shared_state.rs b/src/shared_state.rs index 57b6ae2..618b008 100644 --- a/src/shared_state.rs +++ b/src/shared_state.rs @@ -1,9 +1,13 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::RwLock; use std::time::Instant; /// Thread-safe shared state accessible by the coordinator, IPC listener, and tray icon. pub struct SharedState { pub state: RwLock, + /// True when recording or transcribing — the hotkey listener uses this to + /// decide whether to swallow the cancel key. + pub is_active: AtomicBool, pub model: String, pub accelerator: String, pub started_at: Instant, @@ -13,6 +17,7 @@ impl SharedState { pub fn new(model: String, accelerator: String) -> Self { Self { state: RwLock::new("idle".to_string()), + is_active: AtomicBool::new(false), model, accelerator, started_at: Instant::now(), @@ -23,6 +28,7 @@ impl SharedState { if let Ok(mut s) = self.state.write() { *s = state.to_string(); } + self.is_active.store(state != "idle", Ordering::Release); } pub fn get_state(&self) -> String {