terminal-ui.md 6.2 KB


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-6bddea306b 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 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-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 (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   |  <name> ── AI: ON                     |
    |   Cerca: mar                    |  10:02 Alice: ciao                    |
    | > Mario ●AI                     |  10:03 Tu: ciao!                      |
    |   Marta                         |  10:03 [AI] Tu: ...                   |
    |   (30%)                         +--------------------------------------+
    |                                 | Messaggio                             |
    |                                 | > typed text_                         |
    +---------------------------------+--------------------------------------+
     <status>   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: <filter> 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 <name> ── 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> (<git hash>) (version_label, from BUILD_VERSION, see Runtime and event loop). 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 as HH:MM <sender>: <text> 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;
    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.