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.
voiceio/health.py ADDED
@@ -0,0 +1,194 @@
1
+ """Health check / diagnostic report for all backends."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+
7
+ from voiceio import platform as plat
8
+ from voiceio.hotkeys import chain as hotkey_chain
9
+ from voiceio.typers import chain as typer_chain
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class BackendStatus:
16
+ name: str
17
+ ok: bool
18
+ reason: str = ""
19
+ fix_hint: str = ""
20
+ fix_cmd: list[str] = field(default_factory=list)
21
+
22
+
23
+ @dataclass
24
+ class IBusStatus:
25
+ component_installed: bool = False
26
+ daemon_running: bool = False
27
+ socket_reachable: bool = False
28
+ gnome_source_configured: bool = False
29
+ env_persisted: bool = False
30
+
31
+ @dataclass
32
+ class HealthReport:
33
+ platform: plat.Platform
34
+ hotkey_backends: list[BackendStatus] = field(default_factory=list)
35
+ typer_backends: list[BackendStatus] = field(default_factory=list)
36
+ audio_ok: bool = False
37
+ audio_reason: str = ""
38
+ cli_in_path: bool = False
39
+ ibus_checks: IBusStatus | None = None
40
+
41
+ @property
42
+ def all_ok(self) -> bool:
43
+ has_hotkey = any(b.ok for b in self.hotkey_backends)
44
+ has_typer = any(b.ok for b in self.typer_backends)
45
+ return has_hotkey and has_typer and self.audio_ok
46
+
47
+
48
+ def check_health(p: plat.Platform | None = None) -> HealthReport:
49
+ """Run all probes and return a health report."""
50
+ if p is None:
51
+ p = plat.detect()
52
+
53
+ report = HealthReport(platform=p)
54
+
55
+ # Probe hotkey backends
56
+ for name, backend, probe in hotkey_chain.resolve(p):
57
+ report.hotkey_backends.append(BackendStatus(
58
+ name=name, ok=probe.ok, reason=probe.reason,
59
+ fix_hint=probe.fix_hint, fix_cmd=probe.fix_cmd,
60
+ ))
61
+
62
+ # Probe typer backends
63
+ for name, backend, probe in typer_chain.resolve(p):
64
+ report.typer_backends.append(BackendStatus(
65
+ name=name, ok=probe.ok, reason=probe.reason,
66
+ fix_hint=probe.fix_hint, fix_cmd=probe.fix_cmd,
67
+ ))
68
+
69
+ # Check audio
70
+ try:
71
+ import sounddevice as _sd
72
+ _sd.query_devices(kind="input")
73
+ report.audio_ok = True
74
+ except Exception as e:
75
+ report.audio_reason = str(e)
76
+
77
+ # Check CLI in PATH
78
+ from voiceio.service import symlinks_installed
79
+ report.cli_in_path = symlinks_installed()
80
+
81
+ # IBus-specific checks (only if IBus is in the typer chain)
82
+ ibus_backends = [b for b in report.typer_backends if b.name == "ibus"]
83
+ if ibus_backends and ibus_backends[0].ok:
84
+ report.ibus_checks = _check_ibus()
85
+
86
+ return report
87
+
88
+
89
+ def _check_ibus() -> IBusStatus:
90
+ """Run detailed IBus health checks."""
91
+ from pathlib import Path
92
+ from voiceio.ibus import SOCKET_PATH
93
+ from voiceio.typers.ibus import (
94
+ _ibus_daemon_running, _component_installed,
95
+ )
96
+
97
+ status = IBusStatus()
98
+ status.component_installed = _component_installed()
99
+ status.daemon_running = _ibus_daemon_running()
100
+ status.socket_reachable = SOCKET_PATH.exists()
101
+
102
+ # Check GNOME input source
103
+ try:
104
+ from voiceio.platform import detect
105
+ if detect().is_gnome:
106
+ import subprocess
107
+ result = subprocess.run(
108
+ ["gsettings", "get", "org.gnome.desktop.input-sources", "sources"],
109
+ capture_output=True, text=True, timeout=3,
110
+ )
111
+ status.gnome_source_configured = (
112
+ result.returncode == 0 and "('ibus', 'voiceio')" in result.stdout
113
+ )
114
+ except Exception:
115
+ pass
116
+
117
+ # Check env persistence
118
+ env_file = Path.home() / ".config" / "environment.d" / "voiceio.conf"
119
+ status.env_persisted = env_file.exists()
120
+
121
+ return status
122
+
123
+
124
+ def _icon(ok: bool, warn: bool = False) -> str:
125
+ if ok:
126
+ return "\u2714" # ✔
127
+ if warn:
128
+ return "\u26A0" # ⚠
129
+ return "\u2718" # ✘
130
+
131
+
132
+ def _backend_line(b: BackendStatus, active: bool = False) -> list[str]:
133
+ lines = []
134
+ icon = _icon(b.ok)
135
+ suffix = " \u25C0 active" if active else ""
136
+ line = f" {icon} {b.name}{suffix}"
137
+ if not b.ok:
138
+ line += f" \u2014 {b.reason}"
139
+ lines.append(line)
140
+ if b.fix_hint and not b.ok:
141
+ lines.append(f" \u2192 {b.fix_hint}")
142
+ return lines
143
+
144
+
145
+ def format_report(report: HealthReport) -> str:
146
+ """Format a health report as a human-readable string."""
147
+ lines = []
148
+ lines.append(f"Platform: {report.platform.os} / {report.platform.display_server} / {report.platform.desktop}")
149
+ lines.append("")
150
+
151
+ # Find which backends are active (first OK in each list)
152
+ active_hotkey = next((b.name for b in report.hotkey_backends if b.ok), None)
153
+ active_typer = next((b.name for b in report.typer_backends if b.ok), None)
154
+
155
+ lines.append("Hotkey backends:")
156
+ for b in report.hotkey_backends:
157
+ lines.extend(_backend_line(b, active=(b.name == active_hotkey)))
158
+
159
+ lines.append("")
160
+ lines.append("Typer backends:")
161
+ for b in report.typer_backends:
162
+ lines.extend(_backend_line(b, active=(b.name == active_typer)))
163
+
164
+ lines.append("")
165
+ icon = _icon(report.audio_ok)
166
+ line = f"Audio: {icon}"
167
+ if not report.audio_ok:
168
+ line += f" \u2014 {report.audio_reason}"
169
+ lines.append(line)
170
+
171
+ icon = _icon(report.cli_in_path)
172
+ line = f"CLI in PATH: {icon}"
173
+ if not report.cli_in_path:
174
+ line += " \u2014 run 'voiceio setup' or 'voiceio doctor --fix'"
175
+ lines.append(line)
176
+
177
+ if report.ibus_checks is not None:
178
+ ibus = report.ibus_checks
179
+ lines.append("")
180
+ lines.append("IBus pipeline:")
181
+ for label, ok, hint in [
182
+ ("Component installed", ibus.component_installed, "voiceio setup"),
183
+ ("Daemon running", ibus.daemon_running, "ibus-daemon -drxR"),
184
+ ("Engine socket", ibus.socket_reachable, "start voiceio to activate"),
185
+ ("GNOME input source", ibus.gnome_source_configured, "voiceio setup"),
186
+ ("Env persisted (reboot-safe)", ibus.env_persisted, "voiceio setup"),
187
+ ]:
188
+ icon = _icon(ok, warn=True)
189
+ line = f" {icon} {label}"
190
+ if not ok:
191
+ line += f" \u2014 {hint}"
192
+ lines.append(line)
193
+
194
+ return "\n".join(lines)
@@ -0,0 +1,22 @@
1
+ """Hotkey detection backends."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from voiceio.hotkeys.base import HotkeyBackend
8
+ from voiceio.platform import Platform
9
+
10
+
11
+ def create_hotkey_backend(name: str, platform: Platform) -> HotkeyBackend:
12
+ """Create a hotkey backend by name."""
13
+ if name == "evdev":
14
+ from voiceio.hotkeys.evdev import EvdevHotkey
15
+ return EvdevHotkey()
16
+ if name == "pynput":
17
+ from voiceio.hotkeys.pynput_backend import PynputHotkey
18
+ return PynputHotkey()
19
+ if name == "socket":
20
+ from voiceio.hotkeys.socket_backend import SocketHotkey
21
+ return SocketHotkey()
22
+ raise ValueError(f"Unknown hotkey backend: {name}")
@@ -0,0 +1,27 @@
1
+ """Base protocol and types for hotkey backends."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Callable, Protocol, runtime_checkable
5
+
6
+ from voiceio.backends import ProbeResult
7
+
8
+ __all__ = ["ProbeResult", "HotkeyBackend"]
9
+
10
+
11
+ @runtime_checkable
12
+ class HotkeyBackend(Protocol):
13
+ """Protocol for hotkey detection backends."""
14
+
15
+ name: str
16
+
17
+ def probe(self) -> ProbeResult:
18
+ """Check if this backend can work on the current system."""
19
+ ...
20
+
21
+ def start(self, combo: str, on_trigger: Callable[[], None]) -> None:
22
+ """Start listening for the hotkey combo. Non-blocking."""
23
+ ...
24
+
25
+ def stop(self) -> None:
26
+ """Stop listening. Idempotent."""
27
+ ...
@@ -0,0 +1,83 @@
1
+ """Fallback chain for hotkey backends."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import TYPE_CHECKING
6
+
7
+ from voiceio.backends import ProbeResult
8
+ from voiceio.hotkeys.base import HotkeyBackend
9
+
10
+ if TYPE_CHECKING:
11
+ from voiceio.platform import Platform
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+ # Preference order by platform
16
+ _CHAINS: dict[tuple[str, str], list[str]] = {
17
+ # (display_server, desktop) -> backend list
18
+ ("x11", "*"): ["pynput", "evdev", "socket"],
19
+ ("wayland", "*"): ["evdev", "socket"],
20
+ ("quartz", "*"): ["pynput"],
21
+ }
22
+
23
+
24
+ def _get_chain(platform: Platform) -> list[str]:
25
+ """Get the preference chain for this platform."""
26
+ # Try exact match first
27
+ key = (platform.display_server, platform.desktop)
28
+ if key in _CHAINS:
29
+ return _CHAINS[key]
30
+ # Try wildcard desktop
31
+ key = (platform.display_server, "*")
32
+ if key in _CHAINS:
33
+ return _CHAINS[key]
34
+ # Fallback
35
+ return ["socket"]
36
+
37
+
38
+ def resolve(platform: Platform, override: str | None = None) -> list[tuple[str, HotkeyBackend, ProbeResult]]:
39
+ """Probe backends in preference order, return list of (name, backend, probe_result).
40
+
41
+ If override is set (not "auto"/None), only probe that one backend.
42
+ """
43
+ from voiceio.hotkeys import create_hotkey_backend
44
+
45
+ if override and override != "auto":
46
+ backend = create_hotkey_backend(override, platform)
47
+ result = backend.probe()
48
+ return [(override, backend, result)]
49
+
50
+ chain = _get_chain(platform)
51
+ results = []
52
+ for name in chain:
53
+ try:
54
+ backend = create_hotkey_backend(name, platform)
55
+ result = backend.probe()
56
+ results.append((name, backend, result))
57
+ except Exception as e:
58
+ log.debug("Failed to create backend '%s': %s", name, e)
59
+ results.append((name, None, ProbeResult(ok=False, reason=str(e))))
60
+
61
+ return results
62
+
63
+
64
+ def select(platform: Platform, override: str | None = None) -> HotkeyBackend:
65
+ """Select the first working hotkey backend.
66
+
67
+ Raises RuntimeError if none work.
68
+ """
69
+ results = resolve(platform, override)
70
+
71
+ for name, backend, probe in results:
72
+ if probe.ok and backend is not None:
73
+ log.info("Selected hotkey backend: %s", name)
74
+ return backend
75
+ log.debug("Hotkey backend '%s' unavailable: %s", name, probe.reason)
76
+
77
+ # Build error message
78
+ reasons = [f" {name}: {probe.reason}" for name, _, probe in results if not probe.ok]
79
+ hints = [probe.fix_hint for _, _, probe in results if probe.fix_hint]
80
+ msg = "No working hotkey backend found:\n" + "\n".join(reasons)
81
+ if hints:
82
+ msg += "\n\nTo fix:\n" + "\n".join(f" - {h}" for h in hints)
83
+ raise RuntimeError(msg)
@@ -0,0 +1,134 @@
1
+ """Evdev hotkey backend: reads /dev/input directly."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Callable
9
+
10
+ from voiceio.backends import ProbeResult
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+ DEBOUNCE_SECS = 0.8
15
+
16
+
17
+ class EvdevHotkey:
18
+ """Hotkey detection via Linux evdev (needs input group)."""
19
+
20
+ name = "evdev"
21
+
22
+ def probe(self) -> ProbeResult:
23
+ try:
24
+ import evdev # noqa: F401
25
+ except ImportError:
26
+ return ProbeResult(ok=False, reason="evdev package not installed",
27
+ fix_hint="pip install evdev")
28
+
29
+ # Check if we can open any keyboard device
30
+ for path in sorted(Path("/dev/input/").glob("event*")):
31
+ try:
32
+ with open(path, "rb"):
33
+ return ProbeResult(ok=True)
34
+ except PermissionError:
35
+ import getpass
36
+ return ProbeResult(
37
+ ok=False, reason="No permission to read /dev/input",
38
+ fix_hint="sudo usermod -aG input $USER && newgrp input",
39
+ fix_cmd=["sudo", "usermod", "-aG", "input", getpass.getuser()],
40
+ )
41
+ except OSError:
42
+ continue
43
+
44
+ return ProbeResult(ok=False, reason="No input devices found")
45
+
46
+ def start(self, combo: str, on_trigger: Callable[[], None]) -> None:
47
+ import evdev
48
+ from evdev import ecodes
49
+
50
+ MODIFIER_MAP = {
51
+ "super": {ecodes.KEY_LEFTMETA, ecodes.KEY_RIGHTMETA},
52
+ "ctrl": {ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL},
53
+ "alt": {ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTALT},
54
+ "shift": {ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT},
55
+ }
56
+
57
+ KEY_MAP = {
58
+ **{chr(c): getattr(ecodes, f"KEY_{chr(c).upper()}") for c in range(ord("a"), ord("z") + 1)},
59
+ **{str(i): getattr(ecodes, f"KEY_{i}") for i in range(10)},
60
+ **{f"f{i}": getattr(ecodes, f"KEY_F{i}") for i in range(1, 13)},
61
+ "space": ecodes.KEY_SPACE,
62
+ "pause": ecodes.KEY_PAUSE,
63
+ "insert": ecodes.KEY_INSERT,
64
+ "scroll_lock": ecodes.KEY_SCROLLLOCK,
65
+ "print_screen": ecodes.KEY_SYSRQ,
66
+ }
67
+
68
+ parts = [p.strip().lower() for p in combo.split("+")]
69
+ required_mods: set[int] = set()
70
+ for mod in parts[:-1]:
71
+ required_mods.update(MODIFIER_MAP.get(mod, set()))
72
+ key_code = KEY_MAP.get(parts[-1])
73
+ if key_code is None:
74
+ raise ValueError(f"Unknown key: {parts[-1]}")
75
+
76
+ keyboards = []
77
+ for path in sorted(Path("/dev/input/").glob("event*")):
78
+ try:
79
+ dev = evdev.InputDevice(str(path))
80
+ caps = dev.capabilities(verbose=False)
81
+ if ecodes.EV_KEY in caps:
82
+ keys = caps[ecodes.EV_KEY]
83
+ if ecodes.KEY_A in keys and ecodes.KEY_Z in keys:
84
+ keyboards.append(dev)
85
+ except (PermissionError, OSError):
86
+ continue
87
+
88
+ if not keyboards:
89
+ raise RuntimeError("No keyboard devices accessible")
90
+
91
+ self._running = threading.Event()
92
+ self._running.set()
93
+ pressed: set[int] = set()
94
+ pressed_lock = threading.Lock()
95
+ last_trigger = [0.0]
96
+
97
+ def check_mods() -> bool:
98
+ for codes in MODIFIER_MAP.values():
99
+ if codes & required_mods and not (codes & pressed):
100
+ return False
101
+ return True
102
+
103
+ def read_device(dev: evdev.InputDevice) -> None:
104
+ try:
105
+ for event in dev.read_loop():
106
+ if not self._running.is_set():
107
+ break
108
+ if event.type != ecodes.EV_KEY:
109
+ continue
110
+ key_event = evdev.categorize(event)
111
+ with pressed_lock:
112
+ if key_event.keystate == evdev.KeyEvent.key_down:
113
+ pressed.add(event.code)
114
+ if event.code == key_code and check_mods():
115
+ now = time.monotonic()
116
+ if now - last_trigger[0] >= DEBOUNCE_SECS:
117
+ last_trigger[0] = now
118
+ on_trigger()
119
+ elif key_event.keystate == evdev.KeyEvent.key_up:
120
+ pressed.discard(event.code)
121
+ except OSError:
122
+ pass
123
+
124
+ self._threads = []
125
+ for dev in keyboards:
126
+ t = threading.Thread(target=read_device, args=(dev,), daemon=True)
127
+ t.start()
128
+ self._threads.append(t)
129
+
130
+ log.info("Evdev hotkey listener started on %d keyboards", len(keyboards))
131
+
132
+ def stop(self) -> None:
133
+ if hasattr(self, "_running"):
134
+ self._running.clear()
@@ -0,0 +1,80 @@
1
+ """Pynput hotkey backend for X11 and macOS."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import time
6
+ from typing import Callable
7
+
8
+ from voiceio.backends import ProbeResult
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ DEBOUNCE_SECS = 0.8
13
+
14
+
15
+ class PynputHotkey:
16
+ """Hotkey detection via pynput (X11 + macOS)."""
17
+
18
+ name = "pynput"
19
+
20
+ def probe(self) -> ProbeResult:
21
+ try:
22
+ import pynput # noqa: F401
23
+ except ImportError:
24
+ return ProbeResult(ok=False, reason="pynput package not installed",
25
+ fix_hint="pip install pynput")
26
+
27
+ import os
28
+ session = os.environ.get("XDG_SESSION_TYPE", "")
29
+ if session == "wayland":
30
+ return ProbeResult(ok=False, reason="pynput does not work on Wayland",
31
+ fix_hint="Use evdev or socket backend instead.")
32
+
33
+ return ProbeResult(ok=True)
34
+
35
+ def start(self, combo: str, on_trigger: Callable[[], None]) -> None:
36
+ from pynput import keyboard
37
+
38
+ MOD_MAP = {
39
+ "super": {keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r},
40
+ "ctrl": {keyboard.Key.ctrl, keyboard.Key.ctrl_l, keyboard.Key.ctrl_r},
41
+ "alt": {keyboard.Key.alt, keyboard.Key.alt_l, keyboard.Key.alt_r},
42
+ "shift": {keyboard.Key.shift, keyboard.Key.shift_l, keyboard.Key.shift_r},
43
+ }
44
+
45
+ parts = [p.strip().lower() for p in combo.split("+")]
46
+ required_mods = [parts[i] for i in range(len(parts) - 1)]
47
+ key_name = parts[-1]
48
+ if len(key_name) == 1:
49
+ target_key = keyboard.KeyCode.from_char(key_name)
50
+ else:
51
+ target_key = getattr(keyboard.Key, key_name)
52
+
53
+ pressed_mods: set = set()
54
+ last_trigger = [0.0]
55
+
56
+ def on_press(key):
57
+ for mod_keys in MOD_MAP.values():
58
+ if key in mod_keys:
59
+ pressed_mods.add(key)
60
+ if key == target_key:
61
+ for mod_name in required_mods:
62
+ if not (MOD_MAP[mod_name] & pressed_mods):
63
+ return
64
+ now = time.monotonic()
65
+ if now - last_trigger[0] >= DEBOUNCE_SECS:
66
+ last_trigger[0] = now
67
+ on_trigger()
68
+
69
+ def on_release(key):
70
+ for mod_keys in MOD_MAP.values():
71
+ if key in mod_keys:
72
+ pressed_mods.discard(key)
73
+
74
+ self._listener = keyboard.Listener(on_press=on_press, on_release=on_release)
75
+ self._listener.start()
76
+ log.info("Pynput hotkey listener started")
77
+
78
+ def stop(self) -> None:
79
+ if hasattr(self, "_listener"):
80
+ self._listener.stop()
@@ -0,0 +1,77 @@
1
+ """Socket-based hotkey backend: DE shortcut fires voiceio-toggle."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import os
6
+ import socket
7
+ import threading
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from voiceio.backends import ProbeResult
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+ SOCKET_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio.sock"
17
+ DEBOUNCE_SECS = 0.8
18
+
19
+
20
+ class SocketHotkey:
21
+ """Listens on a Unix DGRAM socket for 'toggle' commands."""
22
+
23
+ name = "socket"
24
+
25
+ def probe(self) -> ProbeResult:
26
+ runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
27
+ if not runtime_dir:
28
+ return ProbeResult(ok=False, reason="XDG_RUNTIME_DIR not set",
29
+ fix_hint="Running under a normal user session should set this.")
30
+ return ProbeResult(ok=True)
31
+
32
+ def start(self, combo: str, on_trigger: Callable[[], None]) -> None:
33
+ SOCKET_PATH.unlink(missing_ok=True)
34
+
35
+ self._on_trigger = on_trigger
36
+ self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
37
+ self._sock.bind(str(SOCKET_PATH))
38
+ self._sock.settimeout(1.0)
39
+ self._running = True
40
+ self._last_trigger: float = 0
41
+ self._thread = threading.Thread(target=self._loop, daemon=True)
42
+ self._thread.start()
43
+ log.debug("Socket listener started at %s", SOCKET_PATH)
44
+
45
+ def _loop(self) -> None:
46
+ while self._running:
47
+ try:
48
+ data = self._sock.recv(64)
49
+ if data != b"toggle":
50
+ continue
51
+ now = time.monotonic()
52
+ if now - self._last_trigger < DEBOUNCE_SECS:
53
+ continue
54
+ self._last_trigger = now
55
+ self._on_trigger()
56
+ except socket.timeout:
57
+ continue
58
+ except OSError:
59
+ break
60
+
61
+ def stop(self) -> None:
62
+ self._running = False
63
+ if hasattr(self, "_sock") and self._sock:
64
+ self._sock.close()
65
+ SOCKET_PATH.unlink(missing_ok=True)
66
+
67
+
68
+ def send_toggle() -> bool:
69
+ """Send a toggle command to the running daemon."""
70
+ try:
71
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
72
+ sock.sendto(b"toggle", str(SOCKET_PATH))
73
+ sock.close()
74
+ return True
75
+ except (ConnectionRefusedError, FileNotFoundError, OSError) as e:
76
+ log.error("Could not reach voiceio daemon: %s", e)
77
+ return False
@@ -0,0 +1,8 @@
1
+ """IBus input method engine for VoiceIO."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+ SOCKET_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio-ibus.sock"
8
+ READY_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio-ibus.ready"