No description
Find a file
Arne Skaar Fismen 204b5307ae feat(tui): desktop notifications on new inbound messages
Emit an OSC 9 escape on InboundEvent so terminals at the far end of
the pty (Ghostty, iTerm2, WezTerm, Konsole, ...) post a native
desktop notification. Title is the sender's contact name (falling
back to the URL display form); body is the payload preview, truncated
to 140 runes.

Stays quiet in the cases where notifications would be wrong:

- Selected peer: that thread is already on screen; the user doesn't
  need to be told.
- Pre-Ready: the initial state sync replays the inbox as a flurry of
  InboundEvents; without this gate, startup would fire dozens of
  notifications. Suppressed until the first ReadyEvent flips
  readySeen.

Through tmux the sequence is wrapped in DCS passthrough
(`ESC P tmux ; <ESC-doubled payload> ESC \`) when $TMUX is set, so it
survives tmux's escape stripping. The user must also enable
`allow-passthrough on` in their tmux config — documented in README.

Pure-helper split: notificationSequence(title, body, inTmux) returns
the bytes so the passthrough wrapping is unit-tested rather than
asserted by terminal behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:41:41 +02:00
cmd/plient plient v0.1.0 2026-05-14 18:04:48 +02:00
internal feat(tui): desktop notifications on new inbound messages 2026-05-14 18:41:41 +02:00
.gitignore plient v0.1.0 2026-05-14 18:04:48 +02:00
CHANGELOG.md feat(tui): desktop notifications on new inbound messages 2026-05-14 18:41:41 +02:00
go.mod plient v0.1.0 2026-05-14 18:04:48 +02:00
go.sum plient v0.1.0 2026-05-14 18:04:48 +02:00
LICENSE plient v0.1.0 2026-05-14 18:04:48 +02:00
README.md feat(tui): desktop notifications on new inbound messages 2026-05-14 18:41:41 +02:00

plient

Terminal client for posta-server, the inbox daemon for the posta protocol.

plient talks to a server's authenticated v1 API: REST for state mutations (/api/v1/identity, /contacts, /messages, /send, /upload) and SSE for live updates (/api/v1/events). Every UI mutation flows through a single tea.Msg switch so a port to another framework — the in-flight iOS client — has one canonical list of events to mirror.

Build

go build ./cmd/plient

Stamp the version into the binary (handy for builds from source):

go build -ldflags "-X main.version=$(git rev-parse --short HEAD)" \
    -o plient ./cmd/plient
./plient -version    # → plient a1b2c3d

go run ./cmd/plient works for development; the version reports as dev. Once the project is tag-installable, releases will stamp the binary with the tag (v0.1.0, etc.) instead of a short SHA.

First run

Run plient with no config and it walks through interactive setup:

plient first-run setup
config will be written to /home/you/.config/plient/config.toml (mode 0600)

server URL (e.g. https://arne.posta.no): arne.posta.no
bearer token (mst_…): ****************
config saved.

The token is probed against GET /api/v1/identity before the file is written; a 401 reprompts for the token, a network error reprompts for the URL.

The config file is plain TOML at $XDG_CONFIG_HOME/plient/config.toml (or ~/.config/plient/config.toml), mode 0600, parent directory 0700:

server_url = "https://arne.posta.no"
token      = "mst_..."

Keybindings

Press ? from the contacts or thread pane for the full in-app reference. Summary:

Pane Keys
Global tab / shift-tab cycle focus · ? help · ctrl+c quit
Contacts j/k select · J/K jump · g/G ends · n new · s settings · enter compose · q quit
Thread j/k half-page · J/K full-page · enter compose
Compose enter send · alt+enter newline · ctrl+u/ctrl+d scroll thread

Mouse: wheel scrolls the active pane; click selects a contact. Holding shift bypasses the mouse capture so the terminal's native text selection works again. Inside tmux you usually need set -g mouse on.

Desktop notifications

On each new inbound message — unless the peer is the one you're currently looking at — plient emits an OSC 9 escape (the iTerm2-originated notification sequence). Modern terminals turn this into a native desktop notification at the far end of the pty: Ghostty, iTerm2, WezTerm, Konsole, and others honor it.

Through tmux you need passthrough enabled in ~/.tmux.conf:

set -g allow-passthrough on

…and the notification lands on whatever terminal is currently attached. A detached tmux session silently drops it — there's no notification queue.

Through SSH there's nothing extra to configure; the escape is just bytes on the pty, which is what SSH already forwards.

Architecture

cmd/plient/         entry point, first-run flow, version flag
internal/api/       HTTP client + SSE parser + actor-doc verifier
internal/config/    XDG config (load, save, mode 0600)
internal/state/     pure state machine — no I/O, no UI
internal/tui/       bubbletea Model/View — owns one state.State

internal/state is the canonical reference for the iOS port: every server event has one Apply* method and every test exercises it through that surface, so the iOS client can mirror the same set without re-deriving them from the API spec.

Compatibility

Built against posta-server v1.1 of the client API (CLIENT_API.md). Specifically requires the kind discriminator on messages, the /contacts/{url}/unread + /mark-read endpoints, and the read_watermark_changed SSE event.

License

MIT — see LICENSE.