Global push-to-talk hotkey via a CGEventTap.
tauri-plugin-global-shortcut cannot listen for a bare held modifier —
it only accepts a full accelerator (Cmd+Shift+X). Diapason solved this with a
compiled Objective-C++ CGEventTap; here the same tap is driven from Python
through PyObjC, so it needs no separate native module and works whether or not
the Tauri GUI is running.
The event classification lives in keycodes.py and is fully tested. This
module is the thin live layer: it creates the tap, pumps a run loop on a
background thread, and calls on_down / on_up when the bound key
transitions. Creating the tap requires Accessibility permission
(AXIsProcessTrusted); without it macOS returns a null tap, which we surface as
a clear error rather than silent dead keys.
Classes
AccessibilityError
Bases: RuntimeError
Raised when the event tap cannot be created (permission not granted).
HotkeyListener
HotkeyListener(
*,
hotkey: str = "control",
on_down: Callable[[], None],
on_up: Callable[[], None],
)
Fire on_down/on_up when the bound modifier is pressed/released.
Source code in src/diapason/desktop/hotkey.py
| def __init__(
self,
*,
hotkey: str = "control",
on_down: Callable[[], None],
on_up: Callable[[], None],
) -> None:
self.hotkey = normalize_hotkey(hotkey)
self._on_down = on_down
self._on_up = on_up
self._thread: Optional[threading.Thread] = None
self._runloop = None
self._down = False
# PyObjC does NOT keep the Python callback / tap / source alive just
# because the C layer holds them. If they are only locals in start(),
# the garbage collector reclaims them and the tap goes silent — the
# exact "nothing happens when I press the key" failure. Hold them.
self._tap = None
self._source = None
self._callback = None
|
Methods:
start
Create the tap and pump its run loop on a daemon thread.
Source code in src/diapason/desktop/hotkey.py
| def start(self) -> None:
"""Create the tap and pump its run loop on a daemon thread."""
import Quartz # type: ignore
# A listen-only keyboard tap is starved of events without INPUT
# MONITORING — not Accessibility. The tap can still be *created*, so
# checking up front gives a real error instead of silent dead keys.
from diapason.desktop.permissions import input_monitoring_ok
if not input_monitoring_ok():
raise AccessibilityError(
"This process lacks Input Monitoring, so the key tap receives "
"no events. Grant it in System Settings › Privacy & Security › "
"Input Monitoring (add your terminal app), fully quit and "
"reopen the terminal, then retry."
)
def _tap_callback(proxy, type_, event, refcon):
# A tap disabled by timeout/user-input must be re-enabled or it
# stays dead for the rest of the session.
if type_ in (
Quartz.kCGEventTapDisabledByTimeout,
Quartz.kCGEventTapDisabledByUserInput,
):
if self._tap is not None:
Quartz.CGEventTapEnable(self._tap, True)
return event
keycode = Quartz.CGEventGetIntegerValueField(
event, Quartz.kCGKeyboardEventKeycode
)
flags = Quartz.CGEventGetFlags(event)
self._handle(int(keycode), int(flags))
return event # never swallow — we only observe
self._callback = _tap_callback
self._tap = Quartz.CGEventTapCreate(
Quartz.kCGSessionEventTap,
Quartz.kCGHeadInsertEventTap,
Quartz.kCGEventTapOptionListenOnly,
Quartz.CGEventMaskBit(Quartz.kCGEventFlagsChanged),
_tap_callback,
None,
)
if self._tap is None:
raise AccessibilityError(
"Could not create the keyboard event tap. Grant Accessibility "
"to your terminal app in System Settings › Privacy & Security › "
"Accessibility, then retry."
)
self._source = Quartz.CFMachPortCreateRunLoopSource(None, self._tap, 0)
def _run() -> None:
self._runloop = Quartz.CFRunLoopGetCurrent()
Quartz.CFRunLoopAddSource(
self._runloop, self._source, Quartz.kCFRunLoopCommonModes
)
Quartz.CGEventTapEnable(self._tap, True)
Quartz.CFRunLoopRun()
self._thread = threading.Thread(target=_run, daemon=True, name="hotkey-tap")
self._thread.start()
|
Functions: