--- type: architecture title: Runtime and event loop description: Process startup in src/main.rs (including the --version flag and the build version embedded by build.rs), the tokio runtime, the two event channels, the input thread, the WhatsApp forwarder, the optional web panel thread and the draw/recv TUI loop. tags: [runtime, tokio, event-loop, ratatui, crossterm, web-panel, version] verified: - by: openwiki/0.7.2 at: 2026-10-21T21:49:12.291Z sources: - id: openwiki-source-36f054f1f38f2197a6af5bca resource: repo://build.rs - id: openwiki-source-b55a21a31ede1b56cd31a6a6 resource: repo://src/main.rs generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" } --- # 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. ## Build version `build.rs` runs `git rev-parse --short HEAD` at compile time and exports the result as the `WH_GIT_HASH` environment variable (`unknown` when git is missing, fails or prints nothing); it asks cargo to rebuild when `.git/HEAD`, `.git/refs` or `.git/packed-refs` change (`build.rs:14-33`). `main.rs` combines it into `BUILD_VERSION` = ` ()` (`src/main.rs:24-25`), which the [terminal UI](terminal-ui.md) shows at the bottom right. ## Startup sequence `main` (`src/main.rs:36-64`) runs on the multi-threaded tokio runtime (`#[tokio::main]`). If any command-line argument is `--version` or `-V` (`wants_version`, `src/main.rs:66-72`) it only prints `wh ` and exits, without reading the configuration or touching the terminal. Otherwise it: 1. Loads the configuration with `Config::load` (`config.toml`, `SOUL.md`, `vision.md`) 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. Starts the optional [web control panel](../integrations/web-control-panel.md) with `start_web_panel` (`src/main.rs:88-97`): nothing happens when `web_port` is unset, otherwise `web::start(port, app_tx)` binds the port. 7. Builds `App` from `AppDeps { config, history, wa, events }`. 8. Spawns the input thread. 9. 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`, `cannot start WhatsApp client` or `cannot start web server on port ` (for example when the port is already in use). ## Event sources ``` crossterm (blocking) whatsapp-rust bot tiny_http (blocking) tokio tasks in App "wh-input" thread Translator task "wh-web" thread (AI, image description, | | | send, reply timers) | Key/Redraw | WaEvent | Web(WebRequest) | v v v | AiReply / Sent / app_tx <------ WhatsApp forwarder (Wa(..)) <-------+ | ImagesDescribed / ^ | ReplyDue +-----------------------------------------------------------------------+ | app_rx --> event_loop: draw ui::render -> recv -> app.handle_event ``` - **WhatsApp forwarder** (`src/main.rs:74-86`): a tokio task that wraps each `WaEvent` in `AppEvent::Wa` and stops when either channel side closes. - **Web thread** (`wh-web`, see [Web control panel](../integrations/web-control-panel.md)): a blocking HTTP server on its own OS thread; each API request is sent as `AppEvent::Web` and the thread waits on a oneshot channel for the state snapshot that `App` sends back after handling it. - **Input thread** (`src/main.rs:99-134`): 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:136-147`): 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 downloads and descriptions, sends, reply-delay timers) hold a clone of the sender and report with `AiReply` / `ImagesDescribed` / `Sent` / `ReplyDue`. ## TUI loop `run_tui` (`src/main.rs:149-158`) 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:160-184`) 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. `should_quit` is set by F10 or by a quit request from the web panel. 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, web panel requests and background task results — there is no timer tick. ## Tests `src/main.rs` tests check that only `--version` / `-V` request the version and that `BUILD_VERSION` starts with the package version and names a commit in parentheses. See [Unit tests](../testing/unit-tests.md).