Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 06:25:41 -06:00
commit 80bef6f524
63 changed files with 5674 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
"""Global push-to-talk hotkey.
The wake word is the primary trigger, but it misfires in a noisy room and
won't fire at all if you're on a call — so there's a keyboard fallback that
works even when the pet has no focus (it's a frameless Qt.Tool window with no
taskbar entry, so an ordinary QShortcut would never see the key).
Needs `pynput`, which needs an X11/Win32/macOS input hook: on most Wayland
sessions it can't grab global keys, and on macOS it needs Accessibility
permission. All of that is a soft failure — start() reports the reason and
the wake word / tray keep working.
"""
from __future__ import annotations
from typing import Callable, Optional
# Aliases for the names people actually type in a .env file.
_ALIASES = {
"control": "ctrl",
"ctl": "ctrl",
"option": "alt",
"opt": "alt",
"win": "cmd",
"windows": "cmd",
"super": "cmd",
"meta": "cmd",
"command": "cmd",
"return": "enter",
"escape": "esc",
"del": "delete",
"ins": "insert",
"pgup": "page_up",
"pgdn": "page_down",
}
class HotkeyError(Exception):
pass
def to_pynput_spec(spec: str) -> str:
""""ctrl+alt+space" -> "<ctrl>+<alt>+<space>" (pynput's GlobalHotKeys
syntax: named keys in angle brackets, literal characters bare)."""
tokens = [t.strip().lower() for t in (spec or "").split("+")]
tokens = [t for t in tokens if t]
if not tokens:
raise HotkeyError("empty hotkey")
parts = []
for token in tokens:
token = _ALIASES.get(token, token)
parts.append(token if len(token) == 1 else f"<{token}>")
return "+".join(parts)
class GlobalHotkey:
"""Fires *callback* whenever the hotkey is pressed, anywhere. Safe to
construct unconditionally — nothing happens until start(), and start()
reports failure instead of raising into the UI thread."""
def __init__(self, spec: str, callback: Callable[[], None]):
self.spec = spec
self._callback = callback
self._listener = None
@property
def running(self) -> bool:
return self._listener is not None
def start(self) -> Optional[str]:
"""None on success, otherwise a human-readable reason it's off."""
if not (self.spec or "").strip():
return None # explicitly disabled — not an error worth reporting
try:
pynput_spec = to_pynput_spec(self.spec)
except HotkeyError as exc:
return f"push-to-talk hotkey {self.spec!r} is invalid: {exc}"
try:
from pynput import keyboard
except Exception as exc: # ImportError, or a backend that won't load
return f"push-to-talk needs pynput ({exc}) — wake word still works"
try:
listener = keyboard.GlobalHotKeys({pynput_spec: self._safe_callback})
listener.daemon = True
listener.start()
except Exception as exc:
return f"push-to-talk unavailable on this session ({exc}) — wake word still works"
self._listener = listener
return None
def _safe_callback(self) -> None:
# This runs on pynput's listener thread; an exception there would
# silently kill the listener for the rest of the session.
try:
self._callback()
except Exception:
pass
def stop(self) -> None:
if self._listener is not None:
try:
self._listener.stop()
except Exception:
pass
self._listener = None