Initial commit: ULPgrammer
- Core Telegram monitoring pipeline (scraper, processor, notifier, downloaders) - Textual TUI frontend with thread-safe event bus - SQLite persistence, severity scoring, dedup cache - Fixed ULP parser: handles https:// truncation, port+path URLs, semicolon separator - Test suite: 88 tests across scorer, cache, database, processor
This commit is contained in:
66
tui/events.md
Normal file
66
tui/events.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user