--- type: architecture title: Terminal UI rendering description: How src/ui.rs draws the connecting, QR pairing and main screens with ratatui, including message formatting, wrapping, the input box and the footer. tags: [ui, ratatui, tui, qr-code, rendering] sources: - id: openwiki-source-6bddea306b9b87f180f1fc02 resource: repo://src/ui.rs generated: { by: "claude-code", at: "2026-10-19T18:58:46.219Z" } verified: - by: openwiki/0.7.2 at: 2026-10-19T19:53:56.480Z --- # 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-91`). ## Screens `render` (`src/ui.rs:97-104`) 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:142-155`) 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:182-191`) 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:161-175`) 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:197-212`): ``` +-----------------+--------------------------------------+ | Chat [F2: ...] | ── AI: ON | | > Alice ●AI | 10:02 Alice: ciao | | Bob | 10:03 Tu: ciao! | | | 10:03 [AI] Tu: ... | | (30%) +--------------------------------------+ | | Messaggio | | | > typed text_ | +-----------------+--------------------------------------+ Tab:focus Enter:apri/invia F2:chat/contatti ... ``` - **List pane** (30% width, `src/ui.rs:232-274`): chats or contacts depending on `list_mode`, title "Chat [F2: Contatti]" / "Contatti [F2: Chat]". The selection is reversed with a `"> "` marker and clamped by `selected_index`. Chats with autoreply enabled show a green ` ●AI` marker. - **Messages pane** (`src/ui.rs:280-315`): title is ` ── AI: ON|OFF ` or " Nessuna chat aperta ". 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:461-491`): 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:497-506`): bold status text followed by the dimmed key help `Tab:focus Enter:apri/invia F2:chat/contatti Ctrl+A:AI chat Ctrl+C:esci`. The focused pane (list or input) has a yellow border (`pane_block`, `src/ui.rs:218-226`). ## Message formatting `format_message_line` (`src/ui.rs:346-362`) renders a [stored message](../concepts/history-store.md) as `HH:MM : ` in the local time zone; out-of-range timestamps show `--:--` (`src/ui.rs:368-376`). `sender_label` (`src/ui.rs:383-396`) 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:334-340`): 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:410-422`). `wrap_text` (`src/ui.rs:428-455`) 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:508-636` cover 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 and selection clamping. See [Unit tests](../testing/unit-tests.md).