v0.2.1: Fix cancel key (Escape) being swallowed globally
The cancel key was consumed by rdev::grab at all times, not just during recording/transcribing. This made the Escape key unusable system-wide while Mouth was running. Now the cancel key only gets swallowed when Mouth is actively recording or transcribing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -2224,7 +2224,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mouth"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+2
-1
@@ -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")?;
|
||||
|
||||
|
||||
+15
-7
@@ -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<HotkeyEvent>,
|
||||
shared_state: Arc<SharedState>,
|
||||
) {
|
||||
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
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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 {
|
||||
|
||||
Reference in New Issue
Block a user