- Go 100%
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> |
||
|---|---|---|
| cmd/plient | ||
| internal | ||
| .gitignore | ||
| CHANGELOG.md | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
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.