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, the optional web panel thread and the draw/recv TUI loop. tags: [runtime, tokio, event-loop, ratatui, crossterm, web-panel] sources:
b55a21a31e
resource: repo://src/main.rs
generated: { by: "claude-code", at: "2026-10-20T20:27:06.515Z" }
verified: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, so App needs no locking.
main (src/main.rs:30-54) runs on the multi-threaded tokio runtime
(#[tokio::main]) and:
Config::load and creates the data directory
(ensure_data_dir). See Configuration and data.history_db_path().tokio::sync::mpsc channels: one for AppEvent
and one for WaEvent.whatsapp::start(session_db_path, wa_tx).start_web_panel (src/main.rs:74-79): nothing happens when
web_port is unset, otherwise web::start(port, app_tx) binds the port.App from AppDeps { config, history, wa, events }.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 <port>
(for example when the port is already in use).
crossterm (blocking) whatsapp-rust bot tiny_http (blocking) tokio tasks in App
"wh-input" thread Translator task "wh-web" thread (AI, vision, send,
| | | cooldown timers)
| Key/Redraw | WaEvent | Web(WebRequest) |
v v v | AiReply / Sent /
app_tx <------ WhatsApp forwarder (Wa(..)) <-------+ | CooldownElapsed
^ |
+-----------------------------------------------------------------------+
|
app_rx --> event_loop: draw ui::render -> recv -> app.handle_event
src/main.rs:60-68): a tokio task that wraps each
WaEvent in AppEvent::Wa and stops when either channel side closes.wh-web, see Web control panel):
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.src/main.rs:85-116): 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.src/main.rs:123-129): only KeyEventKind::Press keys
become AppEvent::Key (Windows also emits release and repeat events);
Resize becomes AppEvent::Redraw; everything else is dropped.App (AI completions, image download,
sends, cooldown timers) hold a clone of the sender and report with
AiReply / Sent / CooldownElapsed.run_tui (src/main.rs:135-140) 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:147-166) repeats while !app.should_quit:
draw a frame with ui::render, 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.