""" tui_events.py — Thread-safe event bus between the bot backend and the TUI. The bot backend runs in a dedicated thread with its own asyncio event loop (completely isolated from Textual's loop). Events are posted via a standard queue.Queue (thread-safe), and the TUI consumer polls it from Textual's loop using asyncio.get_event_loop().run_in_executor() bridging. post() is safe to call from any thread or any asyncio loop. """ import queue import threading from dataclasses import dataclass, field from typing import Any # Thread-safe queue — works across the bot thread and Textual's thread. _queue: queue.Queue | None = None _queue_lock = threading.Lock() # Set to True when the TUI is running so tdl pipes output instead of # writing directly to the terminal. tui_active: bool = False def init_bus() -> queue.Queue: """Call once from MonitorApp.on_mount() to create the queue.""" global _queue, tui_active _queue = queue.Queue() tui_active = True return _queue def get_bus() -> queue.Queue | None: return _queue def post(event: Any) -> None: """Fire-and-forget from any thread. Silently drops if bus not up.""" if _queue is not None: try: _queue.put_nowait(event) except queue.Full: pass # ─── Event types ────────────────────────────────────────────────────────────── @dataclass class EvDownloadQueued: """A file has been accepted and is waiting for tdl.""" batch_id: str filename: str size_mb: float source: str password: str | None @dataclass class EvDownloadStarted: """tdl has begun transferring this file.""" batch_id: str filename: str @dataclass class EvDownloadDone: """File fully downloaded (tdl or Telethon fallback).""" batch_id: str filename: str via: str # "tdl" | "telethon" @dataclass class EvDownloadFailed: """All download attempts failed.""" batch_id: str filename: str reason: str @dataclass class EvTdlOutput: """A line of output from tdl's stdout/stderr (TUI mode only).""" line: str @dataclass class EvHit: """A scored credential hit to display in the hits panel.""" severity: str raw: str source: str filename: str reasons: list[str] = field(default_factory=list) @dataclass class EvChannelAdded: """A channel was added to the live watch list.""" channel: str | int @dataclass class EvChannelRemoved: """A channel was removed from the live watch list.""" channel: str | int @dataclass class EvStatus: """Generic one-line status message (startup, errors, etc.).""" text: str level: str = "info" # "info" | "warning" | "error"