mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 06:10:43 -07:00
Dep swap: - Drop the git-pinned jamiepine/rdev fork we were carrying since the upstream crate is abandoned. - Depend on keytap 0.2 from crates.io — our own cross-platform global keyboard tap crate. Clean shutdown via Drop, Sonoma-safe by design (no TSMGetInputSourceProperty calls off the main thread, so `set_is_main_thread(false)` is gone), and properly versioned. Chord engine rewrite: - Delete hotkey_monitor.rs's internal Chord state machine (Match enum, KeyEvent enum, step()/classify() methods, associated unit tests). keytap's ChordMatcher subsumes it: Momentary chord for PTT, add_toggle() for Toggle-to-talk, longest-match resolution, sticky-end for Toggle. Net: -80 LOC in hotkey_monitor.rs; the remaining module is the dispatcher loop + Effect→Tauri translation. - Preserve the PTT→Toggle "RestartRecording" upgrade signal. keytap emits End(PTT)+Start(Toggle) atomically (same Instant) when the held set upgrades from a shorter chord to a longer superset. The dispatcher peeks at the matcher with a 5 ms recv_timeout after any End and coalesces the pair into Effect::RestartRecording so the frontend still gets the "discard the transition-moment audio" signal instead of an unrelated Stop+Start pair. - HotkeyMonitor::update_bindings now actually tears down the tap on empty bindings instead of leaving an idle CGEventTap around. New bindings rebuild the matcher and the dispatcher thread from scratch. key_codes.rs: - Rewrite the browser-code → Key table against keytap's cleaner Key variant names (`A`..`Z` not `KeyA`..`KeyZ`, `Digit0`..`Digit9` not `Num0`..`Num9`, `ArrowUp` not `UpArrow`, `AltLeft`/`AltRight` instead of `Alt`/`AltGr`, `Period` not `Dot`, …). On-disk chord string format (W3C `KeyboardEvent.code` identifiers) is unchanged, so capture_settings rows written before the swap round-trip identically. Legacy aliases (`Alt`, `AltGr`, `Num0`, `UpArrow`, `Dot`, …) kept for forward-compat on old rows. main.rs / input_monitoring.rs: - Update the few doc comments that referenced `rdev::listen` to describe keytap's Tap; no behavioural change. - build_chord_bindings now imports from keytap::Key. - enable_hotkey / disable_hotkey / update_chord_bindings reach into HotkeyMonitor via &mut since apply()/update_bindings() now mutate. Tests live in keytap now (22 chord-related tests in keytap 0.2, including the PTT→Toggle upgrade scenario that used to be tested in hotkey_monitor.rs). Voicebox's hotkey_monitor.rs is thin enough that local testing would be trivia. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
83 lines
3.6 KiB
Rust
83 lines
3.6 KiB
Rust
//! Platform permission gate for the global keyboard tap.
|
|
//!
|
|
//! On macOS 10.15+, creating a CGEventTap that observes keyboard events
|
|
//! requires the host process to be listed under System Settings → Privacy &
|
|
//! Security → Input Monitoring. Without that trust, keytap's `Tap` returns
|
|
//! a permission error and no key events ever flow through the chord engine.
|
|
//!
|
|
//! The relevant TCC pair lives in IOKit, mirroring `AXIsProcessTrusted` /
|
|
//! `AXIsProcessTrustedWithOptions` on the Accessibility side:
|
|
//!
|
|
//! - `IOHIDCheckAccess(kIOHIDRequestTypeListenEvent)` — read the current
|
|
//! grant without prompting. We call this from the Captures settings UI
|
|
//! so the row can show "granted" / "missing" without surprising the user.
|
|
//! - `IOHIDRequestAccess(kIOHIDRequestTypeListenEvent)` — fire the
|
|
//! "Voicebox would like to receive keystrokes from any application"
|
|
//! dialog and add Voicebox to the Input Monitoring pane (toggle off).
|
|
//! Returns true when access is already granted; otherwise returns false
|
|
//! and queues the prompt. The user still has to flip the toggle on; this
|
|
//! just gets us into the list.
|
|
//!
|
|
//! `enable_hotkey` calls `request` on first invocation so the prompt fires
|
|
//! from a deterministic, user-initiated point (the Captures toggle) instead
|
|
//! of as a side-effect of keytap's `Tap` creating its CGEventTap.
|
|
//!
|
|
//! Windows / Linux don't gate keyboard taps behind a TCC-style permission,
|
|
//! so those branches return `true`.
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod ffi {
|
|
use std::os::raw::c_uint;
|
|
|
|
/// `kIOHIDRequestTypeListenEvent` from `<IOKit/hidsystem/IOHIDLib.h>` —
|
|
/// the request-type discriminator for "I want to read keyboard / mouse
|
|
/// events created by other processes."
|
|
pub const REQUEST_TYPE_LISTEN_EVENT: c_uint = 1;
|
|
|
|
/// `kIOHIDAccessTypeGranted` from `IOHIDLib.h`. The other values are
|
|
/// `Denied = 1` and `Unknown = 2`; we only ever care about the granted
|
|
/// case so they don't get their own constants.
|
|
pub const ACCESS_TYPE_GRANTED: c_uint = 0;
|
|
|
|
#[link(name = "IOKit", kind = "framework")]
|
|
extern "C" {
|
|
/// Returns the current access state as an `IOHIDAccessType` enum
|
|
/// (Granted=0, Denied=1, Unknown=2). No prompt side-effect.
|
|
///
|
|
/// Declared as `c_uint` rather than `bool`: the C signature returns
|
|
/// the full enum, and reading a 3-valued enum into Rust's 1-bit
|
|
/// `bool` is undefined behaviour that silently inverts our gate.
|
|
pub fn IOHIDCheckAccess(request_type: c_uint) -> c_uint;
|
|
|
|
/// Returns true when access is already granted; otherwise queues
|
|
/// the system prompt and returns false synchronously. Safe to call
|
|
/// repeatedly — once the entry exists in the Input Monitoring pane
|
|
/// macOS won't re-prompt. Real `Boolean` (UInt8) return on the C
|
|
/// side, so `bool` here is correct.
|
|
pub fn IOHIDRequestAccess(request_type: c_uint) -> bool;
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
pub fn is_trusted() -> bool {
|
|
unsafe { ffi::IOHIDCheckAccess(ffi::REQUEST_TYPE_LISTEN_EVENT) == ffi::ACCESS_TYPE_GRANTED }
|
|
}
|
|
|
|
/// Fire the Input Monitoring prompt if not already granted. Returns the
|
|
/// current grant state; a `false` here means the prompt was queued and the
|
|
/// user needs to flip the toggle in System Settings before key events flow.
|
|
#[cfg(target_os = "macos")]
|
|
pub fn request() -> bool {
|
|
unsafe { ffi::IOHIDRequestAccess(ffi::REQUEST_TYPE_LISTEN_EVENT) }
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub fn is_trusted() -> bool {
|
|
true
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
pub fn request() -> bool {
|
|
true
|
|
}
|