app-state.md 6.5 KB


type: architecture title: Application state and event handling description: How the App struct in src/app.rs holds UI state, routes AppEvent values, handles keys, ingests messages and runs background tasks. tags: [app, state, events, keybindings, tokio] verified:

  • by: openwiki/0.7.2 at: 2026-10-19T18:58:46.219Z sources:
  • id: openwiki-source-9fec9d1af4 resource: repo://src/app.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } ---

Application state and event handling

src/app.rs owns the whole application state. The event loop described in Runtime and event loop feeds every input into App::handle_event, and the terminal UI renders the public fields of App after each event. App is the only component that mutates the history store in response to user and WhatsApp activity.

Events

AppEvent (src/app.rs:49-70) is the single message type of the main channel:

Variant Producer Meaning
Key(KeyEvent) input thread A key press (release/repeat events are filtered out upstream).
Redraw input thread Terminal resized; nothing to change, just redraw.
Wa(WaEvent) WhatsApp forwarder An event from the WhatsApp client.
AiReply { chat_jid, result } AI background task Generated reply text, or an error string.
Sent { chat_jid, text, is_ai, result } send background task Outcome of a text send.

handle_event (src/app.rs:189-202) dispatches each variant. Only fatal history (SQLite) errors propagate as Err; every other failure is turned into a status text shown in the footer.

State

App (src/app.rs:119-147) exposes UI fields read by the renderer: screen, qr_code, focus, list_mode, chats, contacts, selected, open_chat, open_chat_name, open_chat_autoreply, messages, input, status and should_quit. Private fields hold the History, the Config, an optional AiClient, the autoreply ReplyGate, the WaHandle and a clone of the event sender used by background tasks.

  • Screen (src/app.rs:72-78): Connecting → Pairing (QR shown) → Main.
  • Focus (src/app.rs:80-85): List or Input.
  • ListMode (src/app.rs:87-92): Chats or Contacts.

App::new (src/app.rs:154-183) builds the AI client with build_ai, loads chats and contacts from history, starts on Connecting with list focus in chat mode, and creates the ReplyGate with the current time so that backlog messages are never auto-answered.

build_ai (src/app.rs:729-737) returns no client when system_prompt is missing (status STATUS_AI_NOT_CONFIGURED) and reports a client build error with the AI non disponibile prefix.

WhatsApp events

handle_wa_event (src/app.rs:208-230):

  • Qr → switch to Pairing and store the QR payload.
  • Connected → switch to Main, status Connesso, refresh own JIDs.
  • Disconnected / Error → update status only.
  • LoggedOut → back to Connecting.
  • Contact → on_contact: upsert the contact, reload contacts and chats and refresh the open chat name (src/app.rs:236-244).
  • Message → on_message (src/app.rs:251-260).

on_message saves the message and stops immediately when History::save_message reports a duplicate, so history sync replays never trigger UI reloads or autoreplies twice. New messages reload the chat list, the open conversation and then go through maybe_autoreply (see AI autoreply).

Own JIDs are fetched by refresh_own_jids (src/app.rs:323-330); the list is empty while the device is not paired, so the refresh is retried later.

Key bindings

handle_key (src/app.rs:395-417):

Key Where Action
Ctrl+C any screen Quit (should_quit = true).
Ctrl+A main screen Toggle autoreply for the open chat.
Tab main screen Switch focus between list and input.
F2 main screen Switch chat list / contact list; selection resets to 0.
Up / Down list focus Move selection, clamped to the list.
Enter list focus Open the selected chat or contact.
characters / Backspace input focus Edit the input line.
Esc input focus Return focus to the list.
Enter input focus Send the trimmed input to the open chat.

Non-main screens ignore everything except Ctrl+C. is_command (src/app.rs:758-760) treats Ctrl only as a command when Alt is not held, because Windows reports AltGr as Ctrl+Alt (needed for @, #, € on the Italian layout).

open_selected (src/app.rs:497-511) loads display name, autoreply flag and the last MESSAGES_SHOWN (200) messages, then moves focus to the input. The reload_* helpers (src/app.rs:528-553) keep selected inside the active list.

toggle_autoreply (src/app.rs:477-491) always allows disabling. Enabling goes through can_enable_autoreply (src/app.rs:743-751), which first requires an AI client and then a private chat. The flag is persisted with History::set_autoreply.

Sending and background tasks

All network work runs in tokio tasks that report back through the event channel; send_event ignores a closed channel because that only happens on shutdown (src/app.rs:606-609).

  • submit_input (src/app.rs:453-461) ignores blank input or no open chat, clears the input and calls spawn_send with is_ai: false.
  • spawn_send (src/app.rs:713-722) calls WaHandle::send_text and emits Sent.
  • handle_sent (src/app.rs:365-389) stores a successful send as a from_me message (with is_ai preserved) and reloads the views; a failure sets Invio fallito: ….
  • spawn_ai_request (src/app.rs:616-626) runs a text completion and emits AiReply.
  • spawn_vision_request (src/app.rs:635-645) runs one task that downloads the image and calls the vision model, falling back to the text courtesy turns on failure (complete_with_fallback, src/app.rs:653-666). When both fail, combine_errors joins the two messages. Image bytes stay in memory.
  • send_ai_reply_if_enabled (src/app.rs:350-359) re-checks the persisted autoreply flag before sending an AI reply, discarding it with Risposta AI scartata: autoreply disattivato if the user turned it off meanwhile.

Tests

Unit tests at src/app.rs:762 onward cover can_enable_autoreply, vision_applies, combine_errors, selection clamping/stepping, the toggle helpers and is_command. See Unit tests.