Add pipeline engine and remove legacy compatibility paths

This commit is contained in:
Thales Maciel 2026-02-25 22:40:03 -03:00
parent 3bc473262d
commit e221d49020
18 changed files with 1523 additions and 399 deletions

View file

@ -4,7 +4,7 @@ import logging
import threading
import time
import warnings
from typing import Callable, Iterable
from typing import Any, Callable, Iterable
import gi
from Xlib import X, XK, display
@ -43,6 +43,8 @@ class X11Adapter:
self.indicator = None
self.status_icon = None
self.menu = None
self._hotkey_listener_lock = threading.Lock()
self._hotkey_listeners: dict[str, dict[str, Any]] = {}
if AppIndicator3 is not None:
self.indicator = AppIndicator3.Indicator.new(
"aman",
@ -65,15 +67,29 @@ class X11Adapter:
if self.menu:
self.menu.popup(None, None, None, None, 0, _time)
def start_hotkey_listener(self, hotkey: str, callback: Callable[[], None]) -> None:
mods, keysym = self._parse_hotkey(hotkey)
self._validate_hotkey_registration(mods, keysym)
thread = threading.Thread(target=self._listen, args=(mods, keysym, callback), daemon=True)
thread.start()
def set_hotkeys(self, bindings: dict[str, Callable[[], None]]) -> None:
if not isinstance(bindings, dict):
raise ValueError("bindings must be a dictionary")
next_listeners: dict[str, dict[str, Any]] = {}
try:
for hotkey, callback in bindings.items():
if not callable(callback):
raise ValueError(f"callback for hotkey {hotkey} must be callable")
next_listeners[hotkey] = self._spawn_hotkey_listener(hotkey, callback)
except Exception:
for listener in next_listeners.values():
self._stop_hotkey_listener(listener)
raise
with self._hotkey_listener_lock:
previous = self._hotkey_listeners
self._hotkey_listeners = next_listeners
for listener in previous.values():
self._stop_hotkey_listener(listener)
def start_cancel_listener(self, callback: Callable[[], None]) -> None:
mods, keysym = self._parse_hotkey("Escape")
thread = threading.Thread(target=self._listen, args=(mods, keysym, callback), daemon=True)
thread = threading.Thread(target=self._listen, args=(mods, keysym, callback, threading.Event()), daemon=True)
thread.start()
def inject_text(
@ -127,7 +143,14 @@ class X11Adapter:
finally:
self.request_quit()
def _listen(self, mods: int, keysym: int, callback: Callable[[], None]) -> None:
def _listen(
self,
mods: int,
keysym: int,
callback: Callable[[], None],
stop_event: threading.Event,
listener_meta: dict[str, Any] | None = None,
) -> None:
disp = None
root = None
keycode = None
@ -135,14 +158,26 @@ class X11Adapter:
disp = display.Display()
root = disp.screen().root
keycode = self._grab_hotkey(disp, root, mods, keysym)
while True:
if listener_meta is not None:
listener_meta["display"] = disp
listener_meta["root"] = root
listener_meta["keycode"] = keycode
listener_meta["ready"].set()
while not stop_event.is_set():
if disp.pending_events() == 0:
time.sleep(0.05)
continue
ev = disp.next_event()
if ev.type == X.KeyPress and ev.detail == keycode:
state = ev.state & ~(X.LockMask | X.Mod2Mask)
if state == mods:
callback()
except Exception as exc:
logging.error("hotkey listener stopped: %s", exc)
if listener_meta is not None:
listener_meta["error"] = exc
listener_meta["ready"].set()
if not stop_event.is_set():
logging.error("hotkey listener stopped: %s", exc)
finally:
if root is not None and keycode is not None and disp is not None:
try:
@ -150,6 +185,13 @@ class X11Adapter:
disp.sync()
except Exception:
pass
if disp is not None:
try:
disp.close()
except Exception:
pass
if listener_meta is not None:
listener_meta["ready"].set()
def _parse_hotkey(self, hotkey: str):
mods = 0
@ -185,6 +227,51 @@ class X11Adapter:
except Exception:
pass
def _spawn_hotkey_listener(self, hotkey: str, callback: Callable[[], None]) -> dict[str, Any]:
mods, keysym = self._parse_hotkey(hotkey)
self._validate_hotkey_registration(mods, keysym)
stop_event = threading.Event()
listener_meta: dict[str, Any] = {
"hotkey": hotkey,
"mods": mods,
"keysym": keysym,
"stop_event": stop_event,
"display": None,
"root": None,
"keycode": None,
"error": None,
"ready": threading.Event(),
}
thread = threading.Thread(
target=self._listen,
args=(mods, keysym, callback, stop_event, listener_meta),
daemon=True,
)
listener_meta["thread"] = thread
thread.start()
if not listener_meta["ready"].wait(timeout=2.0):
stop_event.set()
raise RuntimeError("hotkey listener setup timed out")
if listener_meta["error"] is not None:
stop_event.set()
raise listener_meta["error"]
if listener_meta["keycode"] is None:
stop_event.set()
raise RuntimeError("hotkey listener failed to initialize")
return listener_meta
def _stop_hotkey_listener(self, listener: dict[str, Any]) -> None:
listener["stop_event"].set()
disp = listener.get("display")
if disp is not None:
try:
disp.close()
except Exception:
pass
thread = listener.get("thread")
if thread is not None:
thread.join(timeout=1.0)
def _grab_hotkey(self, disp, root, mods, keysym):
keycode = disp.keysym_to_keycode(keysym)
if keycode == 0: