Add desktop adapters and extras

This commit is contained in:
Thales Maciel 2026-02-24 12:59:19 -03:00
parent a83a843e1a
commit fb1d0c07f9
No known key found for this signature in database
GPG key ID: 33112E6833C34679
10 changed files with 383 additions and 276 deletions

View file

@ -7,7 +7,6 @@ import signal
import sys
import threading
import time
import warnings
from pathlib import Path
import gi
@ -16,17 +15,7 @@ from faster_whisper import WhisperModel
from config import Config, load, redacted_dict
from recorder import start_recording, stop_recording
from aiprocess import build_processor
from inject import inject
from x11_hotkey import listen
gi.require_version("Gtk", "3.0")
try:
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3 # type: ignore[import-not-found]
except ValueError:
AppIndicator3 = None
from gi.repository import GLib, Gtk # type: ignore[import-not-found]
from desktop import get_desktop_adapter
class State:
@ -37,10 +26,8 @@ class State:
OUTPUTTING = "outputting"
ASSETS_DIR = Path(__file__).parent / "assets"
RECORD_TIMEOUT_SEC = 300
STT_LANGUAGE = "en"
TRAY_UPDATE_MS = 250
def _compute_type(device: str) -> str:
@ -51,8 +38,9 @@ def _compute_type(device: str) -> str:
class Daemon:
def __init__(self, cfg: Config, *, llama_verbose: bool = False):
def __init__(self, cfg: Config, desktop, *, llama_verbose: bool = False):
self.cfg = cfg
self.desktop = desktop
self.lock = threading.Lock()
self.state = State.IDLE
self.proc = None
@ -64,32 +52,6 @@ class Daemon:
compute_type=_compute_type(cfg.stt.get("device", "cpu")),
)
self.ai_processor = build_processor(verbose=llama_verbose)
self.indicator = None
self.status_icon = None
if AppIndicator3 is not None:
self.indicator = AppIndicator3.Indicator.new(
"lel",
self._icon_path(State.IDLE),
AppIndicator3.IndicatorCategory.APPLICATION_STATUS,
)
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
else:
logging.warning("AppIndicator3 unavailable; falling back to deprecated Gtk.StatusIcon")
warnings.filterwarnings(
"ignore",
message=".*Gtk.StatusIcon.*",
category=DeprecationWarning,
)
self.status_icon = Gtk.StatusIcon()
self.status_icon.set_visible(True)
self.status_icon.connect("popup-menu", self._on_tray_menu)
self.menu = Gtk.Menu()
quit_item = Gtk.MenuItem(label="Quit")
quit_item.connect("activate", lambda *_: self._quit())
self.menu.append(quit_item)
self.menu.show_all()
if self.indicator is not None:
self.indicator.set_menu(self.menu)
def set_state(self, state: str):
with self.lock:
@ -105,9 +67,6 @@ class Daemon:
def _quit(self):
os._exit(0)
def _on_tray_menu(self, _icon, _button, _time):
self.menu.popup(None, None, None, None, 0, _time)
def toggle(self):
with self.lock:
if self.state == State.IDLE:
@ -198,7 +157,7 @@ class Daemon:
self.set_state(State.OUTPUTTING)
logging.info("outputting started")
backend = self.cfg.injection.get("backend", "clipboard")
inject(text, backend)
self.desktop.inject_text(text, backend)
except Exception as exc:
logging.error("output failed: %s", exc)
finally:
@ -221,41 +180,6 @@ class Daemon:
parts.append(text)
return " ".join(parts).strip()
def _icon_path(self, state: str) -> str:
if state == State.RECORDING:
return str(ASSETS_DIR / "recording.png")
if state == State.STT:
return str(ASSETS_DIR / "stt.png")
if state == State.PROCESSING:
return str(ASSETS_DIR / "processing.png")
return str(ASSETS_DIR / "idle.png")
def _title(self, state: str) -> str:
if state == State.RECORDING:
return "Recording"
if state == State.STT:
return "STT"
if state == State.PROCESSING:
return "AI Processing"
return "Idle"
def _update_tray(self):
state = self.get_state()
icon_path = self._icon_path(state)
if self.indicator is not None:
self.indicator.set_icon_full(icon_path, self._title(state))
self.indicator.set_label(self._title(state), "")
elif self.status_icon is not None:
self.status_icon.set_from_file(icon_path)
self.status_icon.set_tooltip_text(self._title(state))
return True
def run_tray(self):
self._update_tray()
GLib.timeout_add(TRAY_UPDATE_MS, self._update_tray)
Gtk.main()
def _lock_single_instance():
runtime_dir = Path(os.getenv("XDG_RUNTIME_DIR", "/tmp")) / "lel"
runtime_dir.mkdir(parents=True, exist_ok=True)
@ -291,8 +215,9 @@ def main():
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
desktop = get_desktop_adapter()
try:
daemon = Daemon(cfg, llama_verbose=args.verbose)
daemon = Daemon(cfg, desktop, llama_verbose=args.verbose)
except Exception as exc:
logging.error("startup failed: %s", exc)
raise SystemExit(1)
@ -308,14 +233,11 @@ def main():
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)
threading.Thread(
target=lambda: listen(
cfg.daemon.get("hotkey", ""),
lambda: logging.info("hotkey pressed (dry-run)") if args.dry_run else daemon.toggle(),
),
daemon=True,
).start()
daemon.run_tray()
desktop.start_hotkey_listener(
cfg.daemon.get("hotkey", ""),
lambda: logging.info("hotkey pressed (dry-run)") if args.dry_run else daemon.toggle(),
)
desktop.run_tray(daemon.get_state, daemon._quit)
if __name__ == "__main__":