"""Cross-platform keyboard control via pynput.""" from typing import List, Optional, Union from pynput.keyboard import Controller as _KBController from pynput.keyboard import Key as _Key _kb = _KBController() # Unified key name → pynput Key map (sourced from computer-server/handlers/windows.py) _SPECIAL: dict = { "enter": _Key.enter, "return": _Key.enter, "esc": _Key.esc, "escape": _Key.esc, "space": _Key.space, "tab": _Key.tab, "backspace": _Key.backspace, "delete": _Key.delete, "home": _Key.home, "end": _Key.end, "pageup": _Key.page_up, "page_up": _Key.page_up, "pagedown": _Key.page_down, "page_down": _Key.page_down, "up": _Key.up, "down": _Key.down, "left": _Key.left, "right": _Key.right, "shift": _Key.shift, "shift_l": _Key.shift_l, "shift_r": _Key.shift_r, "ctrl": _Key.ctrl, "ctrl_l": _Key.ctrl_l, "ctrl_r": _Key.ctrl_r, "control": _Key.ctrl, "alt": _Key.alt, "alt_l": _Key.alt_l, "alt_r": _Key.alt_r, "cmd": _Key.cmd, "command": _Key.cmd, "win": _Key.cmd, "super": _Key.cmd, "meta": _Key.cmd, "option": _Key.alt, "option_l": _Key.alt_l, "option_r": _Key.alt_r, "capslock": _Key.caps_lock, "caps_lock": _Key.caps_lock, "f1": _Key.f1, "f2": _Key.f2, "f3": _Key.f3, "f4": _Key.f4, "f5": _Key.f5, "f6": _Key.f6, "f7": _Key.f7, "f8": _Key.f8, "f9": _Key.f9, "f10": _Key.f10, "f11": _Key.f11, "f12": _Key.f12, "f13": _Key.f13, "f14": _Key.f14, "f15": _Key.f15, "f16": _Key.f16, "f17": _Key.f17, "f18": _Key.f18, "f19": _Key.f19, "f20": _Key.f20, } # Keys that may not exist on all platforms (e.g. macOS lacks insert, print_screen) for _name, _attr in [ ("insert", "insert"), ("print_screen", "print_screen"), ("pause", "pause"), ("num_lock", "num_lock"), ("scroll_lock", "scroll_lock"), ]: _val = getattr(_Key, _attr, None) if _val is not None: _SPECIAL[_name] = _val def _resolve(key: str) -> Optional[Union[str, _Key]]: """Resolve a key string to a pynput Key or single character.""" if not key: return None lk = key.lower() if lk in _SPECIAL: return _SPECIAL[lk] if len(key) == 1: return key return None # ── Key press / release ─────────────────────────────────────────────────────── def key_down(key: str) -> None: """Press and hold a key.""" k = _resolve(key) if k is None: raise ValueError(f"Unknown key: {key!r}") _kb.press(k) def key_up(key: str) -> None: """Release a key.""" k = _resolve(key) if k is None: raise ValueError(f"Unknown key: {key!r}") _kb.release(k) def press_key(key: str) -> None: """Press and immediately release a key.""" k = _resolve(key) if k is None: raise ValueError(f"Unknown key: {key!r}") _kb.press(k) _kb.release(k) # ── Text typing ─────────────────────────────────────────────────────────────── def type_text(text: str) -> None: """Type a string of text (supports Unicode).""" _kb.type(text) # ── Hotkeys ─────────────────────────────────────────────────────────────────── def hotkey(keys: List[str]) -> None: """Press a combination of keys, e.g. ['ctrl', 'c']. Modifiers are held while the last key is tapped, then released in reverse order (standard hotkey behaviour). """ resolved = [_resolve(k) for k in keys] if any(k is None for k in resolved): bad = [k for k, r in zip(keys, resolved) if r is None] raise ValueError(f"Unknown keys in hotkey: {bad}") seq: List[Union[str, _Key]] = [k for k in resolved if k is not None] if not seq: return # Hold all modifiers except the last key for k in seq[:-1]: _kb.press(k) # Tap the action key last = seq[-1] _kb.press(last) _kb.release(last) # Release modifiers in reverse order for k in reversed(seq[:-1]): _kb.release(k)