# tui/app.py Textual TUI frontend. Entry point: `run_tui()`. ## Entry point ```python from tui.app import run_tui run_tui() # called by main.py ``` --- ## Screen hierarchy ``` MonitorApp (App) ├── [default screen] │ ├── Header │ ├── #top-row (Horizontal) │ │ ├── DownloadPanel #dl-panel │ │ └── HitsPanel #hits-panel │ ├── StatsPanel #stats-panel │ ├── ChannelPanel #ch-panel │ └── Footer ├── SearchScreen (push/pop via 's') ├── HitsDBScreen (push/pop via 'h') └── KeywordsScreen (push/pop via 'k') ``` --- ## MonitorApp ### Threading model - **Bot backend** → `threading.Thread(daemon=True)` with its own `asyncio.new_event_loop()` Runs `_bot_main()` - Telethon is completely isolated from Textual's loop. - **TUI drain** → `set_interval(0.1, _drain_bus)` - polls `queue.Queue` every 100ms on Textual's loop. ### Key methods | Method | Description | |--------|-------------| | `on_mount()` | Calls `bus.init_bus()`, starts bot thread, sets drain interval | | `_drain_bus()` | Drains all pending events from `queue.Queue`, dispatches to widgets | | `_run_bot_thread()` | Thread entry: creates event loop, runs `_bot_main()` | | `_bot_main()` | Async bot backend: connect, auth, backfill, live handler loop | | `_signal_channel_changed()` | Thread-safely sets the bot loop's `asyncio.Event` via `call_soon_threadsafe` | ### Keybindings | Key | Action | |-----|--------| | `s` | Push `SearchScreen` | | `h` | Push `HitsDBScreen` | | `k` | Push `KeywordsScreen` | | `c` | Clear download + hits logs | | `r` | Force-refresh stats bar | | `q` / `ctrl+c` | Quit | --- ## Widgets ### DownloadPanel Left panel. Two `RichLog` widgets separated by a dashed line: - **top** (`#tdl-out`): raw tdl output lines (ANSI stripped) - **bottom** (`#dl-log`): structured download status entries Methods: `tdl_line(line)`, `queued(filename, size_mb, source, password)`, `status(filename, state, via)`, `clear_logs()` States for `status()`: `queued` · `downloading` · `done_tdl` · `done_tel` · `failed` ### HitsPanel Right panel. Single `RichLog` with color-coded hit entries. Reactive `hit_count` updates the panel title badge automatically. Methods: `add_hit(severity, raw, source, filename, reasons)`, `clear_log()` ### StatsPanel Slim horizontal bar. Polls `utils.database.stats()` every 10s via `set_interval`. Also refreshed immediately on each `EvHit` event. ### ChannelPanel Bottom panel. `ListView` + `Input` + buttons. Add/remove posts `EvChannelAdded` / `EvChannelRemoved` onto the bus. Changes apply immediately (handler re-registered). Not persisted to `config.py` automatically. --- ## Screens ### SearchScreen (`s`) - Text input → queries `utils.database.search(keyword)` - Results in a `DataTable` with columns: Sev, Time, URL, Username, Password, Source, File - Submit with `↵` or Search button; `Escape` to dismiss ### HitsDBScreen (`h`) - Toolbar buttons + number keys filter by severity - `r` → recent 50, `1`→CRITICAL, `2`→HIGH, `3`→MEDIUM, `4`→LOW - Calls `utils.database.recent()` / `by_severity()` ### KeywordsScreen (`k`) - Live-edit `config.TARGET_KEYWORDS` - Validates regex before adding - On change: rebuilds `utils.scorer.EMPLOYEE_DOMAINS` and `ORG_DOMAINS` - Bot handler recompiles patterns on the next incoming message automatically - **Changes are in-memory only** - copy to `config.py` to persist --- ## Bot auth flow (`_bot_main`) ``` await bot_client.connect() await bot_client.is_user_authorized()? → sign_in(bot_token=...) await user_client.connect() await user_client.is_user_authorized()? → log error + return (must run --no-tui first) warm_entity_cache() _make_handler(channels) ← registers NewMessage handler backfill_all() run_until_disconnected() ┐ _watch_channels() ┘ gathered ``` Channel-change signal path: ``` ChannelPanel button → EvChannel* on bus → _drain_bus → _signal_channel_changed() → call_soon_threadsafe(asyncio.Event.set) → _watch_channels() wakes → _make_handler() ```