79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
from pynput.keyboard import Controller as KeyboardController, Listener as KeyboardListener, Key, KeyCode
|
|
|
|
_MODIFIER_MAP = {
|
|
"ctrl": {Key.ctrl_l, Key.ctrl_r},
|
|
"ctrl_l": {Key.ctrl_l}, "ctrl_r": {Key.ctrl_r},
|
|
"shift": {Key.shift_l, Key.shift_r},
|
|
"shift_l": {Key.shift_l}, "shift_r": {Key.shift_r},
|
|
"alt": {Key.alt_l, Key.alt_r},
|
|
"alt_l": {Key.alt_l}, "alt_r": {Key.alt_r},
|
|
}
|
|
|
|
_KEY_MAP = {
|
|
"space": Key.space, "tab": Key.tab, "enter": Key.enter,
|
|
"esc": Key.esc, "escape": Key.esc,
|
|
"up": Key.up, "down": Key.down, "left": Key.left, "right": Key.right,
|
|
"home": Key.home, "end": Key.end, "page_up": Key.page_up, "page_down": Key.page_down,
|
|
"insert": Key.insert, "delete": Key.delete, "backspace": Key.backspace,
|
|
}
|
|
for i in range(1, 13):
|
|
_KEY_MAP[f"f{i}"] = getattr(Key, f"f{i}")
|
|
|
|
|
|
def _parse_hotkey(hotkey_str):
|
|
"""Parse hotkey string into (modifier_sets, trigger_key).
|
|
Returns: (list of sets-of-pynput-keys for each modifier, pynput key for trigger)
|
|
"""
|
|
parts = [p.strip().lower() for p in hotkey_str.split("+")]
|
|
modifiers = []
|
|
for p in parts[:-1]:
|
|
if p in _MODIFIER_MAP:
|
|
modifiers.append(_MODIFIER_MAP[p])
|
|
elif p in _KEY_MAP:
|
|
modifiers.append({_KEY_MAP[p]})
|
|
else:
|
|
modifiers.append({KeyCode.from_char(p)})
|
|
trigger_part = parts[-1]
|
|
if trigger_part in _KEY_MAP:
|
|
trigger = _KEY_MAP[trigger_part]
|
|
elif trigger_part in _MODIFIER_MAP:
|
|
trigger = next(iter(_MODIFIER_MAP[trigger_part]))
|
|
else:
|
|
trigger = KeyCode.from_char(trigger_part)
|
|
return modifiers, trigger
|
|
|
|
|
|
class HotkeyListener:
|
|
"""Hold-to-record hotkey using pynput. No root required on X11."""
|
|
|
|
def __init__(self, hotkey_str, on_press, on_release):
|
|
self._modifiers, self._trigger = _parse_hotkey(hotkey_str)
|
|
self._on_press = on_press
|
|
self._on_release = on_release
|
|
self._pressed = set()
|
|
self._active = False
|
|
self._listener = KeyboardListener(on_press=self._key_down, on_release=self._key_up)
|
|
self._listener.daemon = True
|
|
self._listener.start()
|
|
|
|
def _matches_trigger(self, key):
|
|
return key == self._trigger
|
|
|
|
def _modifiers_held(self):
|
|
return all(any(k in self._pressed for k in mod_set) for mod_set in self._modifiers)
|
|
|
|
def _key_down(self, key):
|
|
self._pressed.add(key)
|
|
if not self._active and self._matches_trigger(key) and self._modifiers_held():
|
|
self._active = True
|
|
self._on_press()
|
|
|
|
def _key_up(self, key):
|
|
self._pressed.discard(key)
|
|
if self._active and self._matches_trigger(key):
|
|
self._active = False
|
|
self._on_release()
|
|
|
|
def stop(self):
|
|
self._listener.stop()
|