--- type: architecture title: Terminal UI rendering description: How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including the filtered list with global autoreply marker, message formatting, wrapping, the input box and the footer with key help and build version. tags: [ui, ratatui, tui, qr-code, rendering] sources: - id: openwiki-source-6bddea306b9b87f180f1fc02 resource: repo://src/ui.rs generated: { by: "claude-code", at: "2026-10-21T21:49:12.291Z" } verified: - by: openwiki/0.7.2 at: 2026-10-21T21:49:12.291Z --- # Terminal UI rendering `src/ui.rs` is a pure view: `render(frame, &App)` reads the [application state](app-state.md) and fills a ratatui frame. It never mutates `App`; the [event loop](runtime-and-event-loop.md) calls it once per event. All user-facing strings are Italian constants at the top of the file (`src/ui.rs:21-99`). ## Screens `render` (`src/ui.rs:107-114`) dispatches on `app.screen`: | Screen | Function | Content | |---|---|---| | `Connecting` | `render_connecting` | "Connessione a WhatsApp in corso..." plus the status line, centered | | `Pairing` | `render_pairing` | Title, QR code, pairing steps ("WhatsApp > Impostazioni > Dispositivi collegati > Collega un dispositivo"), status | | `Main` | `render_main` | List pane, messages pane, input pane, footer | `render_centered` (`src/ui.rs:152-165`) centers a block of lines vertically and horizontally. ### QR code The QR payload comes from the [WhatsApp client](../integrations/whatsapp-client.md) (`WaEvent::Qr`). `qr_to_text` (`src/ui.rs:192-201`) encodes it with the `qrcode` crate using `Dense1x2` half-block characters and a quiet zone; colors are inverted (dark modules drawn as empty cells) so the code scans on a dark terminal background. `qr_block_lines` (`src/ui.rs:171-185`) replaces the QR with a single line when: - no code has arrived yet: "In attesa del codice QR..."; - encoding fails: "Errore generazione QR: "; - the code does not fit the terminal (rows minus `PAIRING_TEXT_ROWS = 5`, or columns): "Terminale troppo piccolo per il QR: ingrandisci la finestra". ## Main screen layout `render_main` (`src/ui.rs:207-222`): ``` +---------------------------------+--------------------------------------+ | Chat [F2: Contatti] ●AI tutte | ── AI: ON | | Cerca: mar | 10:02 Alice: ciao | | > Mario ●AI | 10:03 Tu: ciao! | | Marta | 10:03 [AI] Tu: ... | | (30%) +--------------------------------------+ | | Messaggio | | | > typed text_ | +---------------------------------+--------------------------------------+ Tab:focus Enter:apri/invia F2:chat/contatti Canc:elimina F3:AI chat F4:AI tutte F10:esci v0.1.0 (e147fe6) ``` (The list title is a single line: base title, then the global marker, then the search text.) - **List pane** (30% width, `src/ui.rs:242-261`): the filtered chats or contacts (`App::visible_chats` / `visible_contacts`) depending on `list_mode`. The title (`list_title`, `src/ui.rs:268-277`) is "Chat [F2: Contatti]" / "Contatti [F2: Chat]", followed — in chat mode only — by `●AI tutte` when the global autoreply is on, and by `Cerca: ` when a filter is typed. The selection is reversed with a `"> "` marker and clamped by `selected_index`. Chats whose per-chat autoreply is enabled show a green ` ●AI` marker (`chat_item`, `src/ui.rs:291-298`). - **Messages pane** (`src/ui.rs:312-348`): title is ` ── AI: ON|OFF ` or " Nessuna chat aperta "; `AI: ON` is shown when the open chat's flag **or** the global autoreply is on. All messages are wrapped to the inner width and only the last rows that fit are kept, so the newest messages are always visible (no scrolling). - **Input pane** (3 rows, `src/ui.rs:494-506`): shows `"> " + input`; when the text is wider than the box only the tail is shown (`tail_fitting`), and the terminal cursor is placed after it while the input has focus. - **Footer** (1 row, `src/ui.rs:530-573`): split into a left area with the bold status text followed by the key help `Tab:focus Enter:apri/invia F2:chat/contatti Canc:elimina F3:AI chat F4:AI tutte F10:esci`, and a right area, exactly as wide as needed, with the dimmed build version `v () ` (`version_label`, from `BUILD_VERSION`, see [Runtime and event loop](runtime-and-event-loop.md)). The help is dimmed, except `F4:AI tutte`, which is drawn green while the global autoreply is on (`key_help_spans`). The focused pane (list or input) has a yellow border (`pane_block`, `src/ui.rs:228-236`). ## Message formatting `format_message_line` (`src/ui.rs:379-399`) renders a [stored message](../concepts/history-store.md) as `HH:MM : ` in the local time zone; out-of-range timestamps show `--:--` (`format_clock_time`, `src/ui.rs:401-410`). `sender_label` (`src/ui.rs:416-433`) picks: 1. `Tu` for own messages, `[AI] Tu` when the message was generated by the [AI autoreply](../workflows/ai-autoreply.md); 2. otherwise the trimmed, non-blank sender name; 3. otherwise the user part of the sender JID (text before `@`). Colors (`message_style`, `src/ui.rs:367-373`): AI messages cyan, own messages green, incoming messages default. ## Wrapping and widths Widths are measured in terminal cells through ratatui's `Line::width`, so wide characters (CJK, emoji) count as two cells (`src/ui.rs:443-455`). `wrap_text` (`src/ui.rs:461-488`) splits on `\n`, wraps each paragraph by characters (not words), drops control characters and always emits at least one row per paragraph. ## Tests Unit tests in `src/ui.rs:575` onward cover the footer key help (F4 highlighted only when the global autoreply is on), the version label, sender labels and fallbacks, clock formatting in a fixed time zone, QR text generation and its error/missing cases, wrapping, keeping the latest visible lines, `tail_fitting`, the messages title, the list title (global marker and filter) and selection clamping. See [Unit tests](../testing/unit-tests.md).