terminal-ui.md 4.9 KB


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-6bddea306b 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 and fills a ratatui frame. It never mutates App; the event loop 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 (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: ...]  |  <name> ── AI: ON                     |
    | > Alice ●AI     |  10:02 Alice: ciao                    |
    |   Bob           |  10:03 Tu: ciao!                      |
    |                 |  10:03 [AI] Tu: ...                   |
    |   (30%)         +--------------------------------------+
    |                 | Messaggio                             |
    |                 | > typed text_                         |
    +-----------------+--------------------------------------+
     <status>   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 <name> ── 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 as HH:MM <sender>: <text> 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;
    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.