--- type: architecture title: Runtime and event loop description: Process startup in src/main.rs, the tokio runtime, the two event channels, the input thread, the WhatsApp forwarder and the draw/recv TUI loop. tags: [runtime, tokio, event-loop, ratatui, crossterm] sources: - id: openwiki-source-b55a21a31ede1b56cd31a6a6 resource: repo://src/main.rs generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" } verified: - by: openwiki/0.7.2 at: 2026-10-19T19:53:56.480Z --- # Runtime and event loop The binary (`wh`) is a single-process terminal app. `src/main.rs` wires the modules together and runs one serialised event loop: every state change goes through [`App::handle_event`](app-state.md), so `App` needs no locking. ## Startup sequence `main` (`src/main.rs:28-51`) runs on the multi-threaded tokio runtime (`#[tokio::main]`) and: 1. Loads the configuration with `Config::load` and creates the data directory (`ensure_data_dir`). See [Configuration and data](../operations/configuration-and-data.md). 2. Opens the SQLite [history store](../concepts/history-store.md) at `history_db_path()`. 3. Creates two unbounded `tokio::sync::mpsc` channels: one for `AppEvent` and one for `WaEvent`. 4. Starts the [WhatsApp client](../integrations/whatsapp-client.md) with `whatsapp::start(session_db_path, wa_tx)`. 5. Spawns the WhatsApp forwarder. 6. Builds `App` from `AppDeps { config, history, wa, events }`. 7. Spawns the input thread. 8. Runs the TUI until quit. Any failure before the TUI starts aborts with an `anyhow` context such as `cannot load configuration`, `cannot open history database` or `cannot start WhatsApp client`. ## Event sources ``` crossterm (blocking) whatsapp-rust bot tokio tasks in App "wh-input" thread Translator task (AI, vision, send) | | | | AppEvent::Key/Redraw | WaEvent | AiReply / Sent v v | app_tx <--------- WhatsApp forwarder (Wa(..)) | ^ | +-----------------------------------------------------+ | app_rx --> event_loop: draw ui::render -> recv -> app.handle_event ``` - **WhatsApp forwarder** (`src/main.rs:57-65`): a tokio task that wraps each `WaEvent` in `AppEvent::Wa` and stops when either channel side closes. - **Input thread** (`src/main.rs:71-102`): a named OS thread (`wh-input`) because `crossterm::event::read` blocks. On a read error it sends `WaEvent::Error("terminal input error: …")` and stops instead of spinning. - **Event mapping** (`src/main.rs:109-115`): only `KeyEventKind::Press` keys become `AppEvent::Key` (Windows also emits release and repeat events); `Resize` becomes `AppEvent::Redraw`; everything else is dropped. - **Background tasks** spawned by `App` (AI completions, image download, sends) hold a clone of the sender and report with `AiReply` / `Sent`. ## TUI loop `run_tui` (`src/main.rs:121-126`) calls `ratatui::init`, runs the loop and always calls `ratatui::restore`, so raw mode and the alternate screen are left even on errors. `event_loop` (`src/main.rs:133-152`) repeats while `!app.should_quit`: draw a frame with [`ui::render`](terminal-ui.md), await the next event, and pass it to `app.handle_event`. The loop also ends cleanly when the event channel closes. Draw errors and fatal `handle_event` errors end the process. Before dispatching, the loop records `list_mode_before = app.list_mode`; if the event changed the list mode (F2 switches chats/contacts), it calls `terminal.clear()` to force a full repaint, so wide glyphs from the previous list cannot leave stale cells behind. Because rendering happens only after an event, the screen is redrawn on key presses, resizes, WhatsApp events and background task results — there is no timer tick.