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:
b55a21a31e
resource: repo://src/main.rs
generated: { by: "claude-code", at: "2026-10-19T19:53:56.480Z" }
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:28-51) 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).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 or
cannot start WhatsApp client.
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
src/main.rs:57-65): a tokio task that wraps each
WaEvent in AppEvent::Wa and stops when either channel side closes.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.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.App (AI completions, image download,
sends) hold a clone of the sender and report with AiReply / Sent.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, 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.