91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
161 lines
4.3 KiB
Python
161 lines
4.3 KiB
Python
"""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)
|