Files
stealergram/tui/events.md
anti 48f486ac97 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
2026-04-02 01:58:49 -03:00

2.6 KiB

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

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