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.