All projects
06OPEN SOURCE2026
wa-tui brand mark: a white monospace tilde on a rounded teal square

WA-TUI

WhatsApp Web as a keyboard-driven terminal UI — QR pairing, fuzzy search, typing indicators and read receipts, with no Electron window in sight.

Visit wa-tui.gtchakama.commacOS · Linux · Windows (terminal)
wa-tui — zsh
$

Overview

What is WA-TUI

WA-TUI is WhatsApp Web as a keyboard-driven terminal UI. You pair once by QR, then live in a paged chat list with fuzzy search, quoted replies, media downloads, typing indicators in both directions, read receipts and native desktop notifications — with no Electron window anywhere. It installs through a Homebrew tap, npm or npx.

Role
Creator & maintainer — designed and built the TUI, the WhatsApp bridge and the release pipeline.
Platform
macOS · Linux · Windows (terminal)
Stack
Node.js · neo-blessed · whatsapp-web.js · Puppeteer
Install
brew · npm · npx
Open source
GitHub · CI on Node 18 / 20
Web
wa-tui.gtchakama.com

Contribution

What I built

  1. 01Built a full chat client in the terminal: paged chat list with all / direct / group filters, unread-only toggle, three sort orders, quoted replies, media downloads and five colour palettes persisted to ~/.wa-tui.
  2. 02Wrote the fuzzy finder (Ctrl+K or /): every query term must match as a substring, with higher scores at the start of a field, on word boundaries and for longer terms — across chats and the open thread at once.
  3. 03Made presence real in both directions: outgoing typing is debounced to one pulse per 480 ms burst, peer typing and voice-recording states are read through a bridge injected into the WhatsApp Web page, and rows show ✓ / ✓✓ / read.
  4. 04Handled first run properly: WA-TUI installs its own Chromium with a progress bar on the boot screen, prunes stale cache folders first, and keeps the session so you pair once.
  5. 05Shipped three ways — a Homebrew tap, `npm i -g @gtchakama/wa-tui` and `npx` — with GitHub Actions running the Node 18 / 20 test matrix and cutting a release on every version bump.

How it works

How the data moves

The core paths through the system — a dot travels each hop, and the caption says what happens there.

Sending a message, with typing pulses

Keystrokes become one quiet typing pulse; ⏎ sends through the linked session and the row shows the ticks.

NOYESStart
Type in the composer
Pressed ⏎?
Debounced typing pulse — one per 480 ms burst
sendMessage via whatsapp-web.js (quoted reply if any)
Linked WhatsApp Web session in headless Chromium
Row redraws ✓ sent → ✓✓ delivered → read
End

Swipe sideways to see the whole chart.

01 / 08keys

You type in the composer at the bottom of the TUI.

From the code

A few lines that matter

The fuzzy finder scores every chatsrc/ui/renderer.js
function fuzzyMatchScore(query, text) {
  const parts = searchQueryParts(query);
  if (!parts.length) return 0;
  const haystack = String(text || '').toLowerCase();
  let score = 0;
  for (const part of parts) {
    const idx = haystack.indexOf(part);
    if (idx === -1) return -1; // all parts must be substrings
    // Prefer matches at the start or at word boundaries
    if (idx === 0) score += 20;
    else if (' -_./:@'.includes(haystack[idx - 1] || '')) score += 10;
    score += Math.max(0, 10 - idx); // earlier matches score higher
    score += part.length;           // longer matching parts score higher
  }
  return score;
}

Every term has to match; position, word boundaries and term length decide the order.

One typing pulse per burst, not per keystrokesrc/ui/renderer.js
const OUTGOING_TYPING_DEBOUNCE_MS = 480;

function scheduleOutgoingTypingPulse() {
  if (state.screen !== 'chatDetail' || !state.currentChatId) return;
  clearTimeout(outgoingTypingTimer);
  outgoingTypingTimer = setTimeout(() => {
    outgoingTypingTimer = null;
    void waService.pulseOutgoingTyping(state.currentChatId, state.currentRawChat);
  }, OUTGOING_TYPING_DEBOUNCE_MS);
}

Keystrokes reset the timer; the other side sees a calm "typing…" instead of a flood.

Decisions

Calls that shaped it

A terminal UI instead of another desktop window

The people who want this already live in a terminal; neo-blessed gives panes, colour and keyboard focus without shipping a browser shell.

whatsapp-web.js over a hand-rolled protocol client

Driving the real WhatsApp Web session in headless Chromium keeps multi-device, media and read receipts correct without reverse-engineering the protocol.

Debounce outgoing typing, don't pulse per keystroke

One typing pulse per 480 ms burst is what the other side expects and keeps the session quiet.

Read peer typing through an injected in-page bridge

Typing and recording states are not exposed by the library, so a small bridge in the page reports them back to the TUI.

Stack

  • Node.js
  • neo-blessed
  • whatsapp-web.js
  • Puppeteer
  • GitHub Actions
  • Homebrew