python-voiceio 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ """Wtype text injection backend for wlroots compositors (Sway, Hyprland)."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import shutil
6
+ import subprocess
7
+
8
+ from voiceio.backends import ProbeResult
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class WtypeTyper:
14
+ """Type text via wtype (wlroots-based Wayland compositors)."""
15
+
16
+ name = "wtype"
17
+
18
+ def probe(self) -> ProbeResult:
19
+ if not shutil.which("wtype"):
20
+ return ProbeResult(ok=False, reason="wtype not installed",
21
+ fix_hint="sudo apt install wtype")
22
+
23
+ import os
24
+ session = os.environ.get("XDG_SESSION_TYPE", "")
25
+ if session != "wayland":
26
+ return ProbeResult(ok=False, reason="wtype requires Wayland")
27
+
28
+ # wtype doesn't work on GNOME/Mutter, only wlroots compositors
29
+ desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
30
+ if "gnome" in desktop:
31
+ return ProbeResult(
32
+ ok=False,
33
+ reason="wtype does not work on GNOME (Mutter doesn't support virtual keyboard protocol)",
34
+ fix_hint="Use ydotool instead.",
35
+ )
36
+
37
+ return ProbeResult(ok=True)
38
+
39
+ def type_text(self, text: str) -> None:
40
+ if not text:
41
+ return
42
+ subprocess.run(
43
+ ["wtype", "--", text],
44
+ check=True, capture_output=True,
45
+ )
46
+
47
+ def delete_chars(self, n: int) -> None:
48
+ if n <= 0:
49
+ return
50
+ # Batch all backspaces into one call: wtype -k BackSpace -k BackSpace ...
51
+ args = []
52
+ for _ in range(n):
53
+ args.extend(["-k", "BackSpace"])
54
+ subprocess.run(
55
+ ["wtype", *args],
56
+ check=True, capture_output=True,
57
+ )
@@ -0,0 +1,45 @@
1
+ """Xdotool text injection backend for X11."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import shutil
6
+ import subprocess
7
+
8
+ from voiceio.backends import ProbeResult
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class XdotoolTyper:
14
+ """Type text via xdotool (X11)."""
15
+
16
+ name = "xdotool"
17
+
18
+ def probe(self) -> ProbeResult:
19
+ if not shutil.which("xdotool"):
20
+ return ProbeResult(ok=False, reason="xdotool not installed",
21
+ fix_hint="sudo apt install xdotool")
22
+
23
+ import os
24
+ session = os.environ.get("XDG_SESSION_TYPE", "")
25
+ if session == "wayland":
26
+ return ProbeResult(ok=False, reason="xdotool does not work on Wayland",
27
+ fix_hint="Use ydotool or wtype instead.")
28
+
29
+ return ProbeResult(ok=True)
30
+
31
+ def type_text(self, text: str) -> None:
32
+ if not text:
33
+ return
34
+ subprocess.run(
35
+ ["xdotool", "type", "--clearmodifiers", "--delay", "12", "--", text],
36
+ check=True, capture_output=True,
37
+ )
38
+
39
+ def delete_chars(self, n: int) -> None:
40
+ if n <= 0:
41
+ return
42
+ subprocess.run(
43
+ ["xdotool", "key", "--clearmodifiers", "--delay", "12"] + ["BackSpace"] * n,
44
+ check=True, capture_output=True,
45
+ )
@@ -0,0 +1,115 @@
1
+ """Ydotool text injection backend for Wayland via uinput."""
2
+ from __future__ import annotations
3
+
4
+ import functools
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+
10
+ from voiceio.backends import ProbeResult
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ @functools.lru_cache(maxsize=1)
16
+ def _get_ydotool_version() -> tuple[int, ...]:
17
+ """Get ydotool major version. Returns (0,) on failure. Cached."""
18
+ try:
19
+ # v1.x prints version, v0.x doesn't support --version
20
+ result = subprocess.run(
21
+ ["ydotool", "--version"], capture_output=True, text=True, timeout=2,
22
+ )
23
+ if result.returncode == 0:
24
+ # e.g. "ydotool 1.0.4"
25
+ parts = result.stdout.strip().split()[-1].split(".")
26
+ return tuple(int(p) for p in parts)
27
+ except (FileNotFoundError, subprocess.TimeoutExpired, ValueError):
28
+ pass
29
+ return (0,)
30
+
31
+
32
+ def _needs_daemon() -> bool:
33
+ """v1.x needs ydotoold, v0.x talks to /dev/uinput directly."""
34
+ return _get_ydotool_version() >= (1,)
35
+
36
+
37
+ def _ydotoold_running() -> bool:
38
+ """Check if the ydotoold daemon is running."""
39
+ try:
40
+ result = subprocess.run(["pgrep", "-x", "ydotoold"], capture_output=True)
41
+ return result.returncode == 0
42
+ except FileNotFoundError:
43
+ return True # can't check, assume ok
44
+
45
+
46
+ def _has_uinput_access() -> bool:
47
+ """Check if current user can write to /dev/uinput."""
48
+ try:
49
+ return os.access("/dev/uinput", os.W_OK)
50
+ except OSError:
51
+ return False
52
+
53
+
54
+ class YdotoolTyper:
55
+ """Type text via ydotool (Wayland, needs uinput access)."""
56
+
57
+ name = "ydotool"
58
+
59
+ def probe(self) -> ProbeResult:
60
+ if not shutil.which("ydotool"):
61
+ return ProbeResult(ok=False, reason="ydotool not installed",
62
+ fix_hint="sudo apt install ydotool")
63
+
64
+ if _needs_daemon():
65
+ # v1.x: needs ydotoold running
66
+ if not _ydotoold_running():
67
+ ydotoold_path = shutil.which("ydotoold") or "ydotoold"
68
+ return ProbeResult(
69
+ ok=False,
70
+ reason="ydotoold daemon not running",
71
+ fix_hint=f"sudo {ydotoold_path} &",
72
+ fix_cmd=["sudo", ydotoold_path],
73
+ )
74
+ else:
75
+ # v0.x: needs /dev/uinput write access
76
+ if not _has_uinput_access():
77
+ return ProbeResult(
78
+ ok=False,
79
+ reason="No write access to /dev/uinput",
80
+ fix_hint="sudo chmod 0666 /dev/uinput (or add udev rule)",
81
+ fix_cmd=["sudo", "chmod", "0666", "/dev/uinput"],
82
+ )
83
+
84
+ return ProbeResult(ok=True)
85
+
86
+ def __init__(self) -> None:
87
+ self._v1 = _get_ydotool_version() >= (1,)
88
+
89
+ def type_text(self, text: str) -> None:
90
+ if not text:
91
+ return
92
+ subprocess.run(
93
+ ["ydotool", "type", "--delay", "10", "--key-delay", "2", "--", text],
94
+ check=True, capture_output=True,
95
+ )
96
+
97
+ def delete_chars(self, n: int) -> None:
98
+ if n <= 0:
99
+ return
100
+ if self._v1:
101
+ # v1.x: raw keycode:state syntax
102
+ args = []
103
+ for _ in range(n):
104
+ args.extend(["14:1", "14:0"])
105
+ subprocess.run(
106
+ ["ydotool", "key", *args],
107
+ check=True, capture_output=True,
108
+ )
109
+ else:
110
+ # v0.x: key names, batch all backspaces in one call
111
+ keys = ["Backspace"] * n
112
+ subprocess.run(
113
+ ["ydotool", "key", "--key-delay", "2", *keys],
114
+ check=True, capture_output=True,
115
+ )