# tui/events.py Thread-safe event bus between the bot backend thread and the Textual TUI. The bot thread calls `post()`. The TUI drains the queue every 100ms via `_drain_bus()`. ## Public API ```python from tui import events as bus # from core/ and tui/app.py from tui.events import post, init_bus, get_bus, tui_active ``` ### `init_bus() -> queue.Queue` Creates the `queue.Queue`. Called inside `MonitorApp.on_mount()` — **must run on Textual's event loop**, not before `App.run()`. ### `post(event: Any) -> None` Fire-and-forget from any thread. Silently drops if bus not initialised. Uses `queue.Queue.put_nowait()` — never blocks. ### `get_bus() -> queue.Queue | None` Returns the queue for the TUI consumer to drain. ### `tui_active: bool` Set to `True` by `init_bus()`. Checked by `core/tdl_downloader.py` to decide whether to pipe tdl output or inherit the terminal. --- ## Event types | Class | Fields | Posted by | Consumed by | |-------|--------|-----------|-------------| | `EvDownloadQueued` | `batch_id, filename, size_mb, source, password` | `tdl_downloader`, `scraper` | `DownloadPanel.queued()` | | `EvDownloadStarted` | `batch_id, filename` | `tdl_downloader`, `scraper` | `DownloadPanel.status("downloading")` | | `EvDownloadDone` | `batch_id, filename, via` | `tdl_downloader`, `scraper` | `DownloadPanel.status("done_tdl"\|"done_tel")` | | `EvDownloadFailed` | `batch_id, filename, reason` | `tdl_downloader`, `scraper` | `DownloadPanel.status("failed")` | | `EvTdlOutput` | `line` | `tdl_downloader._relay()` | `DownloadPanel.tdl_line()` | | `EvHit` | `severity, raw, source, filename, reasons` | `notifier.notify()` | `HitsPanel.add_hit()` + `StatsPanel.refresh_stats()` | | `EvChannelAdded` | `channel` | `ChannelPanel.on_button_pressed()` | `_drain_bus` → `_signal_channel_changed()` | | `EvChannelRemoved` | `channel` | `ChannelPanel.on_button_pressed()` | `_drain_bus` → `_signal_channel_changed()` | | `EvStatus` | `text, level` | everywhere | `MonitorApp.notify()` toast | `level` on `EvStatus`: `"info"` (default) · `"warning"` · `"error"` --- ## Threading model ``` Bot thread (own asyncio loop) └─ bus.post(event) ← queue.Queue.put_nowait() [thread-safe] ↓ queue.Queue ↓ Textual thread (Textual's loop) └─ _drain_bus() [set_interval 100ms] └─ q.get_nowait() loop └─ dispatch to widgets [safe, same thread as Textual] ``` Channel changes flow the other way: ``` _drain_bus sees EvChannelAdded/Removed → _signal_channel_changed() → loop.call_soon_threadsafe(asyncio.Event.set) → bot thread's _watch_channels() wakes ```