NORMAL whatsapp-tui

whatsapp-tui

WhatsApp in your terminal, driven by the keyboard like vim. It redraws in a third of a millisecond, gets you to any chat with ctrl+p like VS Code, keeps running when you close the window, and does what the official apps can't: send later, snooze chats, read without blue ticks, search by meaning, and talk to your own scripts. Next to your chats sits a space of your own: today's tasks, lists, notes and saved messages, mirrored to Obsidian, with a small AI on your own machine.

$ yay -S whatsapp-tui-git
0.35 msto redraw a 400-message chat
~0%CPU with the window closed
1app, however many windows you open
0bytes leave your machine for search
kitty — whatsapp-tuiclick here, then press keys
Chats 6 · 2 unread
NORMALchats⏰ 1🔔 all● onlinectrl+p chats · F1 commands · ? help
try: or double-click a bubble to reply, right-click to select (again to react)
ctrl+pthe palette, like VS Code

Any chat, any feature, three keystrokes away.

ctrl+p jumps to any chat or contact by a few letters of the name, in order: hrsh finds Hari Shankar. F1 (or ctrl+shift+p) runs any feature by name. The chat you land on is already loaded, so it opens the moment you press enter.

›

ctrl+p go to chat

Fuzzy, like fzf: letters that start words or run together rank higher; matched letters light up. Recent chats come first, then the rest; archived chats, contacts you've never written to and phone numbers are found too. ctrl+p enter flips back to the previous chat, like alt+tab. alt+enter opens beside.

F1 every command

"snooze", "mute", "pin", "send later", "split", "rename group"… Only what works where you are is listed. With a message selected, its actions come first, only the ones that fit it: reply, react, pin, edit (if it's yours), delete… Select several with V and you get what works on all of them. Commands that need a time, a folder or a number start the : line for you.

VS Code's search keys

ctrl+f finds in the chat, F3 / shift+F3 next and previous, ctrl+shift+f searches every chat, and # in the palette does too.

Instant open

The chat under the highlight loads in the background, and the last 24 you looked at stay loaded. Filtering 8,000 chats and contacts takes under a millisecond and a half per key.

kitty.conf (optional)

map --when-focus-on var:whatsapp_tui ctrl+shift+p send_text all \e[25~
map --when-focus-on var:whatsapp_tui ctrl+shift+f send_text all \e[26~
map --when-focus-on var:whatsapp_tui ctrl+comma send_text all \e[28~

Terminals send ctrl+shift+p and ctrl+shift+f exactly like ctrl+p and ctrl+f, and ctrl+, not at all, so these lines send them as F13, F14 and F15, only inside whatsapp-tui. F1, S and :settings work without them.

📋 TODAYyour own space, next to your chats

Your to-dos live where you already are.

📋 Today is pinned above your chats, with a count of what's due. Every list is a chat of its own: 📥 Inbox, 📝 Notes, 🔖 Saved, and as many as you make. Type a task the way you'd say it; x ticks it off. It's yours alone: kept in its own local database, never sent to WhatsApp.

❯

Each line ends with a block id (^t12) that ties it to its task, hidden in Obsidian's reading view. Marks are the Obsidian Tasks plugin's: 📅 due, ⏫ important, ✅ done. Edit, tick, add, reorder or delete lines in Obsidian and the app follows within two seconds.

Typed like you'd say it

You typeYou get
call mom 6pmtoday 18:00, with a reminder
pay rent fridue Friday
by the 12ththe 12th (next month's if it's gone)
day after tomorrow at 4in two days, 16:00
end of next weeknext Friday
a week from fridaythe Friday after this one
first monday of november2 Nov
#home · !a tag · important
shopping: eggsinto Shopping, from anywhere
buy 2 eggsjust that: a bare number isn't a time

Every key you know

x done · e edit · t when (t fri 5pm) · ! important · d delete · u undo (anything, even a deleted list) · J/K reorder · >/< into or out of a checklist · m move to a list · f send to a chat · Y copy · c clear done.

OVERDUE  1
 ☐ pay electricity bill  yesterday
TODAY  2
▌☐ call mom ! #family  ⏰ 18:00
 ☐ Packing  ▾ 1/3
     ☐ passport
     ☑ charger
▸ DONE  4

Checklists

Several lines make one: the first is its title. enter folds it, the count shows how far along it is, and > tucks a task under the one above.

Trip packing:
- passport
- [x] charger
- sunscreen #beach

Organised like chats

P pins a list under Today, e archives it, d deletes it (and :undo brings it back). :list Shopping makes one, :rename, :icon 🛒. ctrl+p finds lists by name; ctrl+p then @ finds what's in them.

📝 Notes

A notebook holds pages: a title, then whatever you write, in the input box that grows to the whole pane (enter is a new line, esc saves). Headings, lists, - [ ] boxes and code show as such. Each page is a file in your vault.

🔖 Saved · ☐ from messages

In a chat, v then b keeps a message in 🔖 Saved; T makes it a task that remembers where it came from (↩ Arjun), and enter on it takes you back to the message.

Lists you share

:share sends a list to a chat and keeps them linked: when someone there writes done: eggs, eggs is ticked off here.

Priya: done: eggs
→ Priya ticked off “eggs” in Shopping

From the shell, or a key

$ whatsapp-tui todo call mom 6pm '#family'
Added: ☐ call mom #family  (today 18:00 · Inbox · #14)
$ whatsapp-tui todo
☐ call mom #family  (today 18:00 · Inbox · #14)

Super+Shift+T opens a small floating prompt that shows what it reads as you type: → Shopping 📅 Fri 18:00 #weekend. Scripts get the same through the local API.

Reminders

A task with a time pings you when it's due, through the same notifications as your messages, even with the window closed.

AIqwen3:4b, on your own machine

A small model that knows when to stay quiet.

With Ollama and qwen3:4b (2.5 GB), a local model helps where rules can't. About a second per answer on a mid-range GPU, loaded only when you ask, gone from GPU memory two minutes later. Nothing leaves your machine. It never does the calendar sums small models get wrong: it copies the time words out, and the app's own parser turns them into dates. Every example below is its real answer, word for word (small models have quirks, which is why everything it suggests waits for your enter).

T a message, as a task

Arjun: can you send me the slides before friday evening?
↓ the :task line, filled in for you to check
:task send slides to Arjun before friday evening → ☐ send slides to Arjun ⏰ Fri 18:00 ↩ Arjun

:todos what you owe a chat

Priya: who's booking the cab for saturday? You: I'll do it tonight Ravi: also someone send me the wifi password Priya: thanks for the cake yesterday! Arjun: @You can you bring the projector to the lab on monday morning?
↓ enter adds each to 📥 Inbox
☐ Do cab booking for Saturday tonight · You ☐ Send wifi password Ravi ☐ Bring projector to lab monday morning · Arjun

:catchup a busy chat, in a few lines

Priya: dinner at 8 at Paradise? Ravi: I'm in Arjun: can't, exam tomorrow Priya: ok 7 of us then, booking Priya: @You you're coming right?
↓
• Priya confirms dinner at Paradise at 8 for 7 people • Ravi agrees to attend • Arjun can't attend due to exam • Priya asks if @You is coming ☐ Reply to Priya about coming to dinner enter: a task

:plan my day

10:00 · call mom at 18:00 · finish slides ! · gym · pay electricity bill
↓ "use this plan" sets the times (u undoes)
11:00 finish slides 12:30 gym 14:00 pay electricity bill 18:00 call mom

What it doesn't do

Calendar sums. Asked which day "a week from friday" is, a 4B model is wrong most of the time (we tried, with and without examples). So it only copies the time words out of a message, and the app's own parser, which reads a week from friday, first monday of november, two days before the 12th and the rest, works out the date. Instantly, and right.

BACKGROUNDone app, any number of windows

Close the window. It keeps working.

The first launch starts whatsapp-tui in the background, like tmux or herdr. Every launch after that is just a window onto it. Close the window and it stays connected: it still gets your messages, still notifies you, still sends what you scheduled. Open it again and you're exactly where you were.

q

Closing only detaches

q, :q, ctrl+c or the window's close button leave the app running. :q! or whatsapp-tui --stop quit it for real.

↺

Reopen and resume

Same chat, same scroll position, same half-written message, the same selection. Pictures redraw in the new window.

⇄

A second window takes over

Open it from another workspace and it moves there; the old window closes. It's never open twice, never logged in twice.

↑

Updates itself on the next launch

Install a new build and the next launch notices, restarts the background app on it and attaches. No stale version left behind.

0%

Costs nothing while it waits

With no window attached it stops drawing altogether. Idle, it does no work you'd notice.

1

Or just one terminal

whatsapp-tui --foreground (or background = false in the config) runs it the classic way, in that terminal only. --debug writes a log for troubleshooting.

kitty #1
closed
kitty #2
moved away
kitty #3
attached
↓ one socket in /run/user/1000 ↓
whatsapp-tui --server
connected · notifying · sending what's scheduled

Notifications, your way

The bell in the status bar says what a new message does. Click it or press M to cycle; the choice is saved.

BadgeNew message
🔔 allpopup in the notification drawer, and a sound
💬 popupa silent popup
🔊 soundonly a sound
🔕 offnothing

No notification for the chat you're looking at, or for chats you muted on your phone (they show 🔕) unless someone mentions you. Reactions to your messages notify too ("Priya: Reacted ❤️ to: dinner at 8?").

NORMALmoving through your chats

Treat chats like an inbox.

Every chat has a next step: read it and archive it, come back later, or jump to the next one. Three keys do most of it, and they sync to your phone.

J next unread

Opens the next chat with unread messages, wherever you are.

e done

Marks the chat read, archives it, and opens the next unread one. When there's nothing left you're back at the list: inbox zero. In the archive, e brings a chat back.

U mark unread

For "deal with this later". Inside a chat it also leaves it, so opening doesn't mark it read again.

I activity

One list, across all chats: messages that @mention you, replies to your messages, and reactions to them. enter opens the message in its chat, even years back.

Mon 6 Oct 14:05 ❤️ Priya reacted in Hostel to: dinner at 8?
Mon 6 Oct 13:40 @ Ravi mentioned you in Lab: @you push the fix
Sun 5 Oct 22:12 ↩ Arjun replied in Arjun: yes, 8 works

v in the list: side by side

Opens a second chat beside the one you're in; it stays live as messages arrive. Or ctrl+p, a name, alt+enter; or middle-click (or ctrl+click) a chat, or type :split priya. W swaps which one you write in; X or the ✕ on it closes it.

│ Arjun                │ Hostel  beside · W swap  ✕
│ ╭────────────────╮  │ ╭──────────────╮
│ │ coming at 8?   │  │ │ Priya        │
│ ╰────────────────╯  │ │ yes!         │

Drafts that stay

Switch chats, close the window, restart: what you were writing is still in the box. The list shows it like WhatsApp does: Draft: see you at 8.

Find, filter, archive

Pinned chats stay on top and muted ones are marked 🔕, synced from your phone, as are archives, renamed groups and chats you read there. Status updates stay out of your chat list. / filters chats and contacts by name or number (archived ones too; enter opens), u shows only unread, A the archive, K a chat's info and members, V a profile picture full screen.

Mentions stand out

A message that mentions you gets a red border and "@ mentioned you"; the chat gets a red @ badge. @ jumps between them.

COMMANDthings WhatsApp can't do

Send later. Snooze. Get nudged.

Because the app is always running, it can act when you're not there. Write naturally; the time is read the way people say it.

:later

Send at the right time

Write the message, esc, then :later tomorrow 9am. It goes out then, window open or not. Due while you're offline? It's sent as soon as you reconnect.

:snooze

Hide a chat until it matters

:snooze fri archives the chat; on Friday it comes back, unread, with a notification.

:nudge

"Remind me if they don't reply"

:nudge 3h. If they've answered by then, nothing happens. If not: "⏰ No reply from Priya since Mon 14:05".

⏰ 3

See what's pending

The status bar counts them; click it or type :scheduled to see the list and cancel with x.

:later

KindExamples
Clock9am · 9:30pm · 21:30 · 00:35am · noon · tonight · eod
Daytoday 5pm · tomorrow · fri 5pm · next friday · next week · weekend
Date12 oct · oct 12 3pm · 12/10 (day/month) · 2026-10-12 9:00
From nowin 2h · 90m · in an hour · in half an hour · in 3 days · in 2 weeks
VISUALselect a message, act on it

Every action is one key away.

v selects the message you're looking at; j k move. Then:

Reply, three ways

enter replies, p replies privately to someone in a group, or just double-click the message. Click a quote to jump to the message it answers, even if it's months back.

▎↩ Replying to Priya   ctrl+x cancel
▎yes!

React with anything

r then 1–6 for 👍 ❤️ 😂 😮 😢 🙏, x to take yours back, or type a name: fire opens a grid of all 1,898 emoji, searched by name. Or right-click a message twice.

1 👍  2 ❤️  3 😂  4 😮  5 😢  6 🙏  x remove  + more

Who reacted

w, or click the reactions under a bubble: 👍 You · 👍 Arjun · ❤️ Priya. Click yours to remove it.

Edit, forward, copy, save, delete

e edits your message (first 15 minutes), f forwards to one or several chats, y copies text or the picture, s saves media to Downloads, d deletes for you or (e) for everyone. R retries a message that didn't send.

Several at once

V starts a range, j/k grow it. Then y copies them as a transcript, f forwards them in order, d deletes them, s saves all their media.

[Mon 6 Oct 14:02] Priya: dinner at 8?
[Mon 6 Oct 14:03] Arjun: yes!
[Mon 6 Oct 14:05] You: count me in

P pin it

Pins the selected message for everyone for 7 days (24 hours or 30 days from F1); P again unpins. The newest pin sits under the chat's name: click it to jump there, again for the next one. Pins made on phones show up too.

Hostel 4th floor  group
📌 Ravi: Wi-Fi password is on the fridge

Pin and mute chats

P in the list pins a chat to the top (three at most, like the phone). :mute 8h, :mute 1w or just :mute; :unmute. It all syncs with your phone, and a muted chat still tells you when you're @mentioned.

Read without blue ticks

:private opens chats without sending read receipts; they stay unread until you mark them with U or :read. Its switch sits in the status bar: 👁 read while it's off, 🙈 private while it's on; a click flips it. A chat you opened privately stays unread after you turn it off. And a chat only counts as read once you click or type in the window: focus from the mouse passing over it doesn't.

Deleted, but still readable

When someone deletes a message for everyone after it reached you, you can still read what it said, marked and faded. Only delete for me removes it.

╭──────────────────────────╮
│ 🚫 deleted by Priya      │
│ the wifi password is 4421│
╰──────────────────────────╯

Everything shows

Polls with their options, events with time and place, live location and places as a clickable map link, contact cards, view-once messages (marked, since only your phone may open them), forwarded marks, edits, and WhatsApp-style formatting: *bold*, _italic_, ~strike~, `code`, quotes and lists.

📊 Poll: Dinner where?
○ Paradise
○ Bawarchi
(pick up to 2)

Your messages, at a glance

□□sending ■□sent: it reached WhatsApp ■■delivered to their phone ■■read, in blue like WhatsApp's ticks (purple when a voice note or video was played); in a group, only once everyone has read it ✕not sent: v, select it, R retries

In groups, each person has their own colour (their name and their bubble's border), and up to eight people in a chat never share one, so you see who's talking at a glance. i on your message shows who read it and when, who has it, and who's still waiting:

Message info  ·  sent 14:02
✓✓ Read by  2
  Priya                 14:03
  Ravi                  14:10
✓✓ Delivered to  1
  Arjun                 14:02
✓ Not delivered yet  1
  Amma
INSERTwriting, pictures, voice

Pictures in the terminal. Voice notes too.

In kitty, photos, stickers and GIFs draw sharp, right in the chat; animated ones play. Other terminals get pictures made of block characters.

Photos, stickers, GIFs

Inline, at the size they fit. Click one (or space) for full screen; o opens it in its app.

Voice notes

Shown as ▶ 🎤 Voice note 0:12. Click it or press space: it plays in the terminal with a progress bar; space pauses, ← → seek, q stops.

Videos

Open in their own mpv window (or your video player), so the chat stays usable.

Downloads you can see

Nothing looks frozen: a live line shows Downloading video ▓▓▓▓▓▓░░░░ 58% 4.1 / 7.1 MB.

Paste, drop, pick

ctrl+v pastes a screenshot to send with a caption. Drag files onto the window. a opens yazi to pick several.

Stickers and GIFs

s opens a tray of the ones you've sent and received. n makes a new one from any file, p turns the clipboard image into a sticker.

Writing like a text box

ctrl+a selects all, ctrl+← ctrl+→ jump words, ctrl+backspace deletes one, shift+enter adds a line.

Mentions

Type @ in a group: a member list pops up, tab picks. @all mentions everyone, as on the phone.

MOUSEfor when your hand's on it anyway

Keyboard first. Mouse welcome.

DoGet
click a chatopen it
click a linkopen it in your browser (long links too, wherever they wrap)
click a photo, sticker or GIFfull-screen view
click a voice noteplay it
click a quoted messagejump to the original
double-click a messagereply to it
right-click a messageselect it, ready for r f y d i…
right-click it againthe reaction bar; click an emoji, and you're back to writing
middle-click (or ctrl+click) a chatopen it beside the one you're in
click ✕ on the chat besideclose the split
click reactions under a bubblewho reacted
click 🔔, ⏰ or 👁/🙈 in the status barchange notifications, see what's scheduled, turn private reading on or off
click the 📌 bar under a chat's namejump to the pinned message (again: the next one)
click a result in the paletteopen it (click outside to close)
wheelscroll whatever's under the pointer
shift + dragselect text the terminal's way
BENCHmeasured, not promised

Faster than the frame you're looking at.

WhatsApp's desktop app is a browser wrapper that users report at 1–2 GB of memory. whatsapp-tui's own working memory is about 5 MB. Each message bubble is drawn once and reused until it changes, so a new message, a reaction or a key press redraws in well under a millisecond.

before the speed worknowlog scale · 400-message group chat · i7-10750H

Only what changed

Rendered bubbles are cached by everything that affects how they look. Moving the selection rebuilds two bubbles, not four hundred.

Pictures as raw pixels

Images go to kitty at exactly the size they're shown, as raw pixels: no PNG encoding. At most 300 are kept, and kitty is told to forget the rest.

A tidy disk

Downloaded media is capped (1 GB by default, least recently viewed goes first); history syncs write in one transaction; search has its own index.

Quiet when idle

The cursor stops blinking when the window isn't focused, and with no window attached nothing is drawn at all.

APIyour chats, your scripts

Hook it up to anything.

The running app answers your scripts on a private local socket, and runs your hooks when something happens. Ask for chats and messages, search, schedule, subscribe to a live stream of events. Sending is off unless you allow it, and rate-limited when you do.

from the shell

# the 5 newest chats, as JSON
whatsapp-tui api chats '{"limit": 5}'

# everything about the flat, across all chats
whatsapp-tui api search '{"query": "flat address"}' | jq '.[].text'

# a live stream of mentions, one JSON line each
whatsapp-tui api subscribe '{"events": ["mention"]}'

~/.config/whatsapp-tui/hooks/on-mention (chmod +x)

#!/bin/sh
# a loud notification when someone mentions you
jq -r '"\(.message.sender_name) in \(.message.chat_name): \(.message.text)"' |
  xargs -0 notify-send -u critical "You were mentioned"
MethodWhat
statusconnected? sending allowed?
chatsthe chat list, pinned first
messagesa chat's messages, page by page
searchwords, then meaning ("similar": true)
send · replyneeds api_allow_send; 20 a minute
mark_read · mark_unreadeverywhere, like on the phone
schedulesend, snooze or nudge at "tomorrow 9am"
subscribemessage · mention · reaction · reminder

python: print every mention as it happens

import json, os, socket
s = socket.socket(socket.AF_UNIX)
s.connect(os.environ["XDG_RUNTIME_DIR"] + "/whatsapp-tui-api.sock")
f = s.makefile("rw")
f.write(json.dumps({"id": 1, "method": "subscribe"}) + "\n"); f.flush()
for line in f:
    e = json.loads(line)
    if e.get("event") == "mention":
        print(e["message"]["chat_name"], e["message"]["text"])

The full reference (every method, every field, hooks, examples) is docs/API.md in the repository.

?the whole keyboard

Every key, searchable.

Press ? in the app for this list. Here you can filter it.

/
KeyDoesWhere
CONFIG~/.config/whatsapp-tui/config.ini

Make it yours.

ctrl+, (or :settings) opens every setting on one screen: j/k to move, ←/→ to change. It's saved at once and most of it applies while you watch. Try two of them:


    
  Settings   j/k move · ←/→ change · esc close

  Appearance
    Theme                        rose-pine
  ▌ Space between messages       ‹ normal ›
    Bubble borders               rounded
    Chat list width              38
    Mouse                        on

  Notifications & privacy
    Notifications                all
    Private reading              off

  Search & AI
    Chats left out of search     none  ›
    AI help (local model)        on

  Your lists
    Mirror to a folder           ~/vault/WhatsApp TUI  ✎
[ui]
theme             = rose-pine   ; rose-pine, rose-pine-moon, rose-pine-dawn
images            = auto        ; auto, kitty, blocks, off
avatars           = true
mouse             = true
highlight_opacity = 0.8         ; see-through highlights in kitty
message_spacing   = normal      ; compact, normal, roomy
bubble_style      = rounded     ; rounded, square, thick, double, none

[general]
notifications   = all       ; all, popup, sound, off
background      = true      ; keep running when the window closes
private_reading = false     ; no read receipts until you mark read
media_cache_mb  = 1024
api_allow_send  = false
semantic_search = true      ; when Ollama runs
download_path   = ~/Downloads
◐

Three Rosé Pine themes

Main, Moon and Dawn, the palette this page is in. In kitty, highlights can be see-through over a transparent window.

⌥

kitty niceties

Two lines in kitty.conf let shift+enter make a new line and ctrl+v paste images, only inside whatsapp-tui.

⌂

Omarchy and app launchers

make install adds a launcher entry, so it opens from rofi, Omarchy's menu or any app menu.

:qget it running

Two minutes to your chats.

install (Arch: from the AUR; elsewhere: Go and a C compiler)

# Arch Linux
yay -S whatsapp-tui-git

# anywhere else
git clone git@github.com:sambuaneesh/whatsapp-tui.git
cd whatsapp-tui
make install         # to ~/.local/bin, plus a launcher entry
whatsapp-tui         # scan the QR: WhatsApp → Settings → Linked devices

# optional, for search by meaning (all local)
sudo pacman -S ollama-cuda && sudo systemctl enable --now ollama
ollama pull embeddinggemma

Works best with

PackageFor
kittysharp pictures, animations, profile pictures
mpvvoice notes and videos
ffmpegGIFs, making stickers
yazipicking files to attach
wl-clipboardclipboard on Wayland
Ollamasearch by meaning (optional)

Linux and macOS; Windows through WSL. whatsapp-tui is an unofficial client built on whatsmeow. Some people report WhatsApp banning accounts for unofficial clients, so anything that sends on its own is opt-in, rate-limited and visible.