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.
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.
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 type | You get |
|---|---|
call mom 6pm | today 18:00, with a reminder |
pay rent fri | due Friday |
by the 12th | the 12th (next month's if it's gone) |
day after tomorrow at 4 | in two days, 16:00 |
end of next week | next Friday |
a week from friday | the Friday after this one |
first monday of november | 2 Nov |
#home · ! | a tag · important |
shopping: eggs | into Shopping, from anywhere |
buy 2 eggs | just 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.
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
:todos what you owe a chat
:catchup a busy chat, in a few lines
:plan my day
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.
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.
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.
Costs nothing while it waits
With no window attached it stops drawing altogether. Idle, it does no work you'd notice.
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.
closed
moved away
attached
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.
| Badge | New message |
|---|---|
| 🔔 all | popup in the notification drawer, and a sound |
| 💬 popup | a silent popup |
| 🔊 sound | only a sound |
| 🔕 off | nothing |
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?").
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.
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.
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.
Hide a chat until it matters
:snooze fri archives the chat; on Friday it comes back, unread, with a notification.
"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".
See what's pending
The status bar counts them; click it or type :scheduled to see the list and cancel with x.
| Kind | Examples |
|---|---|
| Clock | 9am · 9:30pm · 21:30 · 00:35am · noon · tonight · eod |
| Day | today 5pm · tomorrow · fri 5pm · next friday · next week · weekend |
| Date | 12 oct · oct 12 3pm · 12/10 (day/month) · 2026-10-12 9:00 |
| From now | in 2h · 90m · in an hour · in half an hour · in 3 days · in 2 weeks |
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
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
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.
Search that finds what you meant.
/ searches the chat you're in (its whole history), S searches every chat. Every word you type, in any order; accents ignored; capitals make it exact. A full-text index answers in 0.3 ms over 50,000 messages. With a local model installed, it also finds messages by meaning.
Every word, any order
flat addr finds "the address of the flat". Word beginnings count; nothing has to be next to anything.
Accents and case
cafe finds "Café". Lowercase matches any case; type a capital and it's exact (vim's smartcase).
By meaning
With Ollama and EmbeddingGemma on your machine, S adds up to 20 messages close in meaning: flat address also finds "send me the location of the apartment". Indexed in the background; nothing leaves your computer. Gentle on a gaming PC: two CPU threads, the model leaves the GPU seconds after use, and indexing waits while a game has the GPU busy.
Then act on it
enter selects the match, ready for r f y…; n N go to older and newer matches.
Leave a chat out
A noisy group you never search? :noindex in it (or F1 → "don't index"): its messages leave the index and searching all chats skips it; / inside it still works. Every chat is indexed until you say so; :noindex list shows the ones you left out.
Keyboard first. Mouse welcome.
| Do | Get |
|---|---|
| click a chat | open it |
| click a link | open it in your browser (long links too, wherever they wrap) |
| click a photo, sticker or GIF | full-screen view |
| click a voice note | play it |
| click a quoted message | jump to the original |
| double-click a message | reply to it |
| right-click a message | select it, ready for r f y d i… |
| right-click it again | the reaction bar; click an emoji, and you're back to writing |
| middle-click (or ctrl+click) a chat | open it beside the one you're in |
| click ✕ on the chat beside | close the split |
| click reactions under a bubble | who reacted |
| click 🔔, ⏰ or 👁/🙈 in the status bar | change notifications, see what's scheduled, turn private reading on or off |
| click the 📌 bar under a chat's name | jump to the pinned message (again: the next one) |
| click a result in the palette | open it (click outside to close) |
| wheel | scroll whatever's under the pointer |
| shift + drag | select text the terminal's way |
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.
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.
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"
| Method | What |
|---|---|
| status | connected? sending allowed? |
| chats | the chat list, pinned first |
| messages | a chat's messages, page by page |
| search | words, then meaning ("similar": true) |
| send · reply | needs api_allow_send; 20 a minute |
| mark_read · mark_unread | everywhere, like on the phone |
| schedule | send, snooze or nudge at "tomorrow 9am" |
| subscribe | message · 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.
Every key, searchable.
Press ? in the app for this list. Here you can filter it.
| Key | Does | Where |
|---|
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.
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
| Package | For |
|---|---|
| kitty | sharp pictures, animations, profile pictures |
| mpv | voice notes and videos |
| ffmpeg | GIFs, making stickers |
| yazi | picking files to attach |
| wl-clipboard | clipboard on Wayland |
| Ollama | search 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.