fix(hotkey): 250ms debounce on pressed-edge dispatch

避免微动开关回弹 / 用户手抖双击造成的空转写报错和 ASR session 抢资源:
相邻 < 250ms 的 press 边沿直接丢弃,记 log。

- coordinator.rs: Inner 新增 last_hotkey_dispatch_at: Mutex<Option<Instant>>
- coordinator/dictation.rs: HOTKEY_DEBOUNCE 常量 + handle_pressed_edge 入口检查

与 hotkey_trigger_held 互补:held 防 press-without-release,本检查防
press-release-press 三连过快。每个有效边沿都会更新时间戳;用户正常使用
(不会快于 4 次/秒)不会触发。
This commit is contained in:
baiqing committed 2026-05-11 23:39:43 +08:00
1 parent e3a6c44d4d
commit 60ab2418bd
2 files changed
+31

No files matched your search

@@ -119,6 +119,11 @@ struct Inner {
hotkey: Mutex<Option<HotkeyMonitor>>,
hotkey_status: Mutex<HotkeyStatus>,
hotkey_trigger_held: AtomicBool,
/// 防抖时间戳:handle_pressed_edge 入口检查与本字段的距离,< 250ms 的边沿直接
/// 丢弃(误触双击 / 微动开关回弹 / 用户连点过快造成的空转写报错)。
/// 与 `hotkey_trigger_held` 互补 —— held 防 press-without-release,本字段防
/// press-release-press 三连过快。
last_hotkey_dispatch_at: Mutex<Option<std::time::Instant>>,
shortcut_recording_active: AtomicBool,
/// 自定义组合键监听器(global-hotkey crate)。当 `prefs.hotkey.trigger == Custom` 时
/// 代替 modifier-only 的 hotkey monitor。`None` 表示不使用自定义组合键或还没成功安装。
@@ -199,6 +204,7 @@ impl Coordinator {
hotkey: Mutex::new(None),
hotkey_status: Mutex::new(HotkeyStatus::default()),
hotkey_trigger_held: AtomicBool::new(false),
last_hotkey_dispatch_at: Mutex::new(None),
shortcut_recording_active: AtomicBool::new(false),
combo_hotkey: Mutex::new(None),
translation_hotkey: Mutex::new(None),
@@ -245,6 +251,7 @@ impl Coordinator {
hotkey: Mutex::new(None),
hotkey_status: Mutex::new(HotkeyStatus::default()),
hotkey_trigger_held: AtomicBool::new(false),
last_hotkey_dispatch_at: Mutex::new(None),
shortcut_recording_active: AtomicBool::new(false),
combo_hotkey: Mutex::new(None),
translation_hotkey: Mutex::new(None),
@@ -9,9 +9,33 @@ use super::qa::handle_qa_option_edge;
use super::resources::*;
use super::*;
/// 同一个 hotkey 边沿之间的最小间隔。低于此阈值的连按整体作为误触丢弃 ——
/// 避免微动开关回弹 / 用户手抖双击造成的空转写报错和 ASR session 抢资源。
const HOTKEY_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(250);
pub(super) async fn handle_pressed_edge(inner: &Arc<Inner>) {
let was_held = inner.hotkey_trigger_held.swap(true, Ordering::SeqCst);
if !was_held {
// 防抖:相邻 < HOTKEY_DEBOUNCE 的边沿直接丢弃,记到 log 方便排查。
// 与 `hotkey_trigger_held` 互补:held 防 press-without-release,本检查防
// press-release-press 三连过快。每个有效边沿都会更新时间戳。
let now = std::time::Instant::now();
let too_soon = {
let mut last = inner.last_hotkey_dispatch_at.lock();
let drop = matches!(*last, Some(t) if now.duration_since(t) < HOTKEY_DEBOUNCE);
if !drop {
*last = Some(now);
}
drop
};
if too_soon {
log::info!(
"[coord] hotkey pressed edge debounced (< {} ms since last dispatch)",
HOTKEY_DEBOUNCE.as_millis()
);
return;
}
// 路由:QA 浮窗可见时,rightOption 边沿走 QA;否则走主听写。详见 issue #118 v2。
// 例外:dictation session 已经在跑(Starting / Listening / Processing / Inserting),
// 即使 QA 浮窗被打开了,这条边沿也必须先走 dictation。否则 begin_qa_session 会