- Rename project to stealergram throughout - Add pyproject.toml (replaces requirements.txt split, folds pytest.ini) - Replace all em-dashes with hyphens across all source files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
88 lines
3.6 KiB
Markdown
88 lines
3.6 KiB
Markdown
# 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()`.
|
|
Web SSE consumers call `subscribe()` / `unsubscribe()` to get their own broadcast queue.
|
|
|
|
## 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
|
|
from tui.events import subscribe, unsubscribe
|
|
from tui.events import set_bot_context, signal_channel_changed
|
|
```
|
|
|
|
### `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. Delivers to the TUI queue **and** all subscriber queues.
|
|
Uses `queue.Queue.put_nowait()` - never blocks.
|
|
|
|
### `get_bus() -> queue.Queue | None`
|
|
Returns the TUI queue for `_drain_bus()` to consume.
|
|
|
|
### `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.
|
|
|
|
### `subscribe() -> queue.Queue`
|
|
Register a new subscriber. Returns a private `queue.Queue` that receives every future `post()`. Thread-safe. Call once per SSE connection.
|
|
|
|
### `unsubscribe(q: queue.Queue) -> None`
|
|
Remove a subscriber queue. Safe to call if already removed. Call on SSE disconnect.
|
|
|
|
### `set_bot_context(loop, event) -> None`
|
|
Called by `_bot_main()` once the bot asyncio loop and `_ch_changed` event exist. Enables `signal_channel_changed()`.
|
|
|
|
### `signal_channel_changed() -> None`
|
|
Wake the bot's `_watch_channels()` coroutine from any thread. Used by web config routes after channel list is updated.
|
|
|
|
---
|
|
|
|
## 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 (TUI) + _subscribers[0..n] (web SSE)
|
|
↓ ↓
|
|
Textual thread Web thread (uvicorn)
|
|
_drain_bus() SSE generator: q.get_nowait()
|
|
```
|
|
|
|
Channel changes from TUI:
|
|
```
|
|
_drain_bus sees EvChannelAdded/Removed
|
|
→ _signal_channel_changed()
|
|
→ loop.call_soon_threadsafe(asyncio.Event.set)
|
|
→ bot thread's _watch_channels() wakes
|
|
```
|
|
|
|
Channel changes from web:
|
|
```
|
|
PUT /config/channels
|
|
→ config.save_runtime_config()
|
|
→ bus.signal_channel_changed() ← uses stored loop + event
|
|
→ bot thread's _watch_channels() wakes
|
|
```
|