Files
omarchy-beeper/README.md
T

205 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# omarchy-beeper
An Omarchy bar widget for [Beeper](https://www.beeper.com/): every unread
conversation across every network Beeper bridges — WhatsApp, Signal, Slack,
Telegram, Discord, Instagram, iMessage — in one pile. Click a row to open that
chat in Beeper Desktop and mark it read.
It is read and mark-read only. The plugin **never sends** a message, a
reaction, a draft, or an attachment.
![Preview](preview.png)
## Requirements
- Omarchy 4.0 or newer (the plugin uses the `bar-widget` plugin kind).
- Beeper Desktop, running, with the local API enabled.
- `python3` (Omarchy ships it; no extra packages, no `sudo`, no `pkexec`).
## Install
```bash
omarchy plugin add https://github.com/prdlk/omarchy-beeper.git --enable
```
Then set up the token once, in a terminal:
```bash
~/.config/omarchy/plugins/prdlk.beeper/bin/omarchy-beeper auth
```
The prompt tells you where to get the token: **Beeper Desktop → Settings →
Integrations → create an access token** (read access is enough). Paste it at
the hidden prompt. It is stored in
`~/.config/omarchy-beeper/secrets/token.json`, mode `600`, in a `700`
directory, and is never passed on a command line.
Full walkthrough, including troubleshooting: [docs/SETUP.md](docs/SETUP.md).
Update and remove:
```bash
omarchy plugin update prdlk.beeper
omarchy plugin update prdlk.beeper --yes # skip the prompt
omarchy plugin remove prdlk.beeper
```
Removing the plugin does **not** delete `~/.config/omarchy-beeper/`. Delete it
yourself, and revoke the token in Beeper Desktop → Settings → Integrations:
```bash
rm -rf ~/.config/omarchy-beeper
```
## Using it
| Action | What happens |
| --- | --- |
| Click the bar icon | Open or close the panel |
| Right-click the bar icon | Focus Beeper Desktop |
| Middle-click the bar icon | Refresh now |
| Click a row | Open that chat in Beeper Desktop, mark it read, close the panel |
### Keys
| Key | Action |
| --- | --- |
| `j` / `k`, `Down` / `Up` | Move the cursor |
| `Enter`, `Space`, `o` | Open the chat under the cursor |
| `a` | Mark the chat under the cursor as read, without opening it |
| `A` | Mark every unread chat as read — press twice to confirm |
| `n` / `p` | Next / previous page |
| `i` | Focus Beeper Desktop |
| `Tab` | Switch to the next bar panel |
| `Esc` | Cancel a pending mark-all, or close the panel |
### Settings
Configure these in the Omarchy bar widget settings (they live in `shell.json`):
| Key | Range | Default | Meaning |
| --- | --- | --- | --- |
| `max` | 1–50 | 25 | Rows per page |
| `refreshIntervalSec` | 15–3600 | 60 | Poll interval |
`~/.config/omarchy-beeper/config` can also carry `max=25` for CLI runs, and
`OMARCHY_BEEPER_MAX` overrides both for a single invocation.
## What counts as unread
One row is one unread **chat**, not one message, sorted newest activity first.
Not in the pile:
- **muted** chats (`includeMuted=false`),
- **archived** chats,
- **low-priority** chats.
The last two come free with `inbox=primary` on `GET /v1/chats/search`, which
Beeper defines as non-archived and non-low-priority. If you want a muted chat
to nag you, unmute it in Beeper.
The badge counts unread chats, capped at 200 — a deeper pile is a Beeper
problem, not a bar problem. When the cap is hit the panel says so. `A`
(mark-all) is not capped at 200: it walks up to 1000 chats with a 100-second
budget and reports a partial count if it runs out.
## CLI
The panel is a thin drawing layer; everything else is `bin/omarchy-beeper`.
Every command prints exactly one JSON object and exits 0, even on failure.
```
omarchy-beeper list [--limit N] [--page OFFSET] # the pile, newest first
omarchy-beeper read <id> # mark one chat read
omarchy-beeper read-all # mark every unread chat read
omarchy-beeper open [<id>] # focus Beeper, optionally a chat
omarchy-beeper auth # store the token (terminal only)
```
`list` emits:
```json
{
"ok": true,
"unread": 7,
"accountCount": 3,
"nextPage": "25",
"thisPage": "0",
"inboxes": [{ "account": "WhatsApp", "unread": 3, "searchUrl": "" }],
"messages": [
{
"id": "beeper:<base64url chat id>",
"threadId": "<chat id>",
"subject": "<chat title>",
"from": "<last sender>",
"snippet": "<last message>",
"ts": 1770804000,
"labels": ["WhatsApp"],
"url": ""
}
]
}
```
Failures look like `{"ok":false,"error":"…"}`. The two you will actually see:
- `Beeper Desktop is not running` — start Beeper.
- `token missing or invalid; run: omarchy-beeper auth` — the token was never
set up, or you revoked it.
## Design decisions
- **The QML layer never talks to the network.** `Panel.qml` runs the CLI and
parses one JSON object from stdout. It never sees the token, never opens a
socket, and renders every API string with `textFormat: Text.PlainText`.
- **Everything is bound to `http://localhost:23373`.** `lib/beeper.py` refuses
any path outside `/v1/`, refuses any URL that would leave that origin, and
does not follow redirects — a 3xx could point off localhost.
- **`url` is always empty.** Beeper has no web address for a chat, so opening
one means `POST /v1/focus` with the chat id: Beeper Desktop comes forward
with that conversation selected. There is no browser hand-off and no deep
link to validate.
- **Snippets cost one request per visible row.** The chat objects Beeper
returns carry no message preview, so the last message is fetched with
`GET /v1/chats/{chatID}/messages` — but only for the rows on screen, never
for the whole pile. A snippet that fails to load costs the snippet, not the
row.
- **Paging is a numeric offset.** The API is cursor-based; the CLI walks the
cursors once, caps at 200 chats, sorts, and slices. `nextPage` is the next
offset, so `p` can step back without replaying cursors.
- **`accountCount` comes from `GET /v1/accounts`**, not from the pile, so the
network chip appears whenever you actually have several networks connected.
- **No WebSocket.** Beeper's `ws://localhost:23373/v1/ws` is experimental and
the panel is on a 15–3600 s poll anyway; a second event path would only add
a way to disagree with the list. Polling only, deliberately.
- **`read-all` snapshots first.** It collects the unread chat ids before it
marks anything, because the query it walks is defined by what is unread.
## Known Beeper limitations
- Message history can be incomplete until Beeper finishes indexing after a
fresh install or a new bridge. Titles and unread counts are right; a snippet
may be blank for a while.
- On-device (local) connections are the most reliable; cloud bridges can lag.
- iMessage is macOS-only. On Linux it simply never appears; there is no
special case for it.
- Beeper Desktop must be running. The API is local and dies with the app; the
bar icon dims to half opacity and the panel keeps the last list.
## Development
```bash
python3 -m unittest discover -s tests -t tests -v # mocked, offline
python3 -m compileall -q lib tests
bash -n bin/omarchy-beeper
qmllint -I <shell-qml-root> Panel.qml BeeperIcon.qml
```
The tests never touch a live Beeper: `tests/support.py` fakes the HTTP opener
and serves payloads shaped like the documented API.
## License
MIT — see [LICENSE](LICENSE).