Compare commits

...
2 Commits
Author SHA1 Message Date
ahp 998732e98b docs(man): split the movement keys from the next entry
A missing line break merged the .TP macro into the previous sentence, so the g/G entry rendered literally as ".TP".
2026-10-11 12:35:00 +03:30
ahp 54aa87676d docs(readme): rewrite for a clearer, complete overview
Add badges, a contents list, and a screenshot caption; document every flag in a table, topic-resolution rules, the --since replay window, the publish form, and the adaptive pane layout. Add How it works, History and storage, and Troubleshooting sections, and correct the go install instructions.
2026-10-11 12:34:53 +03:30
2 changed files with 223 additions and 92 deletions
+221 -91
View File
@@ -1,8 +1,12 @@
# lazyntfy # lazyntfy
A [lazygit](https://github.com/jesseduffield/lazygit)-style terminal client for **A [lazygit](https://github.com/jesseduffield/lazygit)-style terminal client for [ntfy](https://ntfy.sh).**
[ntfy](https://ntfy.sh): topics in the left pane, messages in the middle pane, Follow your topics, read and search messages, and publish — without leaving the terminal.
and the selected message in the right pane.
[![Go](https://img.shields.io/badge/Go-1.26-00ADD8?logo=go&logoColor=white)](https://go.dev)
[![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)](LICENSE)
[![Built with Bubble Tea](https://img.shields.io/badge/built%20with-Bubble%20Tea-ff69b4)](https://github.com/charmbracelet/bubbletea)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](#contributing)
``` ```
╭ topics ─────╮ ╭ messages ─────────────────╮ ╭ detail ───────────────╮ ╭ topics ─────╮ ╭ messages ─────────────────╮ ╭ detail ───────────────╮
@@ -15,31 +19,49 @@ and the selected message in the right pane.
lazyntfy alerts 12/12 ● connected ? help q quit lazyntfy alerts 12/12 ● connected ? help q quit
``` ```
lazyntfy talks to the ntfy HTTP API directly (`GET /topic/json` to subscribe, lazyntfy is a keyboard-driven TUI for [ntfy](https://ntfy.sh): the left pane lists
`POST /topic` to publish). It reads `~/.config/ntfy/client.yml` for credentials topics, the middle pane lists messages, and the right pane shows the selected
and subscriptions, groups topics that share a server and credentials onto one message in full. It talks to the ntfy HTTP API directly, follows several topics
stream, and reconnects automatically — resuming from the newest event it has over a single stream, and keeps a local cache so your history is there the moment
already seen so nothing is delivered twice. you launch it.
## Contents
- [Features](#features)
- [Install](#install)
- [Quick start](#quick-start)
- [Usage](#usage)
- [Key bindings](#key-bindings)
- [Configuration](#configuration)
- [How it works](#how-it-works)
- [History and storage](#history-and-storage)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
- [Packaging](#packaging)
- [Project layout](#project-layout)
- [Contributing](#contributing)
- [License](#license)
## Features ## Features
- **lazygit-style layout** — topics, messages, and message detail in three panes. - **Three-pane, lazygit-style layout** — topics, messages, and full detail side by side.
- **Account discovery** — with no topic on the command line it lists your - **Follow many topics at once** — topics that share a server and credentials are
subscriptions, reservations, and ACL grants. multiplexed onto a single stream.
- **Alert-aware** — Grafana and Alertmanager webhook bodies are summarized - **Account discovery** — start with no topics and lazyntfy lists your
(status, alert name, severity, summary); `v` shows the raw JSON. subscriptions, reserved topics, and ACL grants.
- **Persistent** — topics you add and the messages that arrive are stored in a - **Alert-aware** — Grafana and Alertmanager webhook payloads are summarized into
small SQLite file and restored on the next start. status, alert name, severity, and summary; `v` reveals the raw JSON.
- **Resilient** — reconnects with exponential backoff, honoring `message_delete` - **Persistent** — topics and messages are cached in a local SQLite database and
and `message_clear` events. restored on the next start, so the list is populated as soon as the app opens.
- **Single binary, no CGo** — pure Go, including the SQLite driver. - **Resilient** — reconnects with exponential backoff and resumes from the newest
event it has already seen, so nothing is delivered twice.
- **Publish from the terminal** — press `p` to send a title, priority, tags, and body.
- **Single static binary** — pure Go, no CGo, including the SQLite driver.
## Requirements ## Install
- Go 1.26 or newer (to build) lazyntfy needs a **Go 1.26+ toolchain only to build**. Prebuilt installs are
- A reachable ntfy server available for Arch Linux and Debian/Ubuntu.
## Installation
### Arch Linux ### Arch Linux
@@ -52,9 +74,8 @@ cd lazyntfy/packaging/archlinux
makepkg -si makepkg -si
``` ```
This installs `/usr/bin/lazyntfy`, the man page under `/usr/share/man`, the This installs `/usr/bin/lazyntfy` and the man page under `/usr/share/man`, and
license, and the README, and pulls in `ca-certificates` so TLS to your ntfy pulls in `ca-certificates` so TLS to your ntfy server works.
server works.
### Debian and Ubuntu ### Debian and Ubuntu
@@ -68,9 +89,9 @@ make deb # writes dist/lazyntfy_<version>_<arch>.deb
sudo apt install ./dist/lazyntfy_*.deb sudo apt install ./dist/lazyntfy_*.deb
``` ```
`apt install ./file.deb` (or `dpkg -i` followed by `apt -f install`) resolves `apt install ./file.deb` (or `dpkg -i` followed by `apt -f install`) resolves the
the `ca-certificates` dependency. The packaging needs only the Go toolchain and `ca-certificates` dependency. The packaging needs only the Go toolchain and
`dpkg-dev`; `make deb` passes `-d` so it also builds where `golang-go` is not `dpkg-dev`, and `make deb` passes `-d` so it also builds where `golang-go` is not
installed as a distribution package. installed as a distribution package.
### From source ### From source
@@ -82,7 +103,7 @@ make install # into ~/.local, no root needed
``` ```
`make install` honors `PREFIX`, `BINDIR`, `MANDIR`, `DOCDIR`, and `DESTDIR`. `make install` honors `PREFIX`, `BINDIR`, `MANDIR`, `DOCDIR`, and `DESTDIR`.
Picking a system prefix installs for every user: Choosing a system prefix installs for every user:
```sh ```sh
sudo make install PREFIX=/usr/local # /usr/local/bin/lazyntfy, /usr/local/share/man/man1 sudo make install PREFIX=/usr/local # /usr/local/bin/lazyntfy, /usr/local/share/man/man1
@@ -104,46 +125,101 @@ From a checkout:
go install ./cmd/lazyntfy # binary in $(go env GOPATH)/bin go install ./cmd/lazyntfy # binary in $(go env GOPATH)/bin
``` ```
## Usage ## Quick start
```sh ```sh
lazyntfy # topics from the account, their history, and client.yml lazyntfy alerts # follow one topic on the default host
lazyntfy alerts # one topic, expanded with default-host lazyntfy # or start with no arguments to load every topic on your account
lazyntfy --since 1h alerts # replay the last hour
lazyntfy --host https://ntfy.example alerts
lazyntfy --token tk_xxx alerts
lazyntfy --user name:pass alerts
``` ```
With no topic on the command line, lazyntfy asks the server for your account Then `j`/`k` to move, `enter` to open a message, `/` to filter, `p` to publish,
subscriptions, reserved topics, and ACL grants (`GET /v1/account` and, when and `?` for help.
allowed, `GET /v1/users`), then adds the topics remembered from previous
sessions. You can always add a topic for the current session with `a`. ## Usage
```
lazyntfy [flags] [topic...]
```
### Flags
| Flag | Default | Description |
| --- | --- | --- |
| `--config path` | `~/.config/ntfy/client.yml` | Read the ntfy client config from `path`. |
| `--host url` | from the config file | Override `default-host`. |
| `--token token` | from the config file | Send `token` as a Bearer credential. |
| `--user user[:password]` | from the config file | Send HTTP basic credentials. |
| `--since value` | `all` | Replay window: `all`, a duration, a Unix timestamp, or a message id. |
| `--version` | | Print the version and exit. |
| `--help` | | Print a short usage summary and exit. |
### Specifying topics
Topics can be passed as positional arguments or listed in `client.yml`:
- A **short name** is expanded with `default-host`: `lazyntfy alerts`.
- A **`host/topic`** or a **full URL** carries its own origin:
`lazyntfy ntfy.example:8443/alerts` or `lazyntfy https://other.example/backups`.
- Short names need a `default-host`; pass `--host` or use a `host/topic` form.
With **no positional topics**, lazyntfy assembles the subscription list from, in
order:
1. the `subscribe:` list in `client.yml`;
2. your account — subscriptions and reservations from `GET /v1/account`, and ACL
grants from `GET /v1/users` when the credential may read it;
3. topics remembered from previous sessions.
Press `a` at any time to add a topic for the current session.
### Replaying history
`--since` sets the `since` query parameter for each stream: `all` (the default),
a duration such as `1h` or `30m`, a Unix timestamp, or a message id.
When history was restored from disk and `--since` is left at its default,
lazyntfy sets `since` to the newest stored event instead, so the first connection
picks up where the last session stopped instead of replaying the server's whole
cache. An explicit `--since` always wins.
### Publishing
Press `p` to open a small form with four fields — **title**, **priority**,
**tags**, and **message**:
- **priority** accepts `1`–`5` or the ntfy names `min`, `low`, `default`, `high`, `max`
(empty means `default`).
- **tags** is a comma-separated list.
- **message** is required; it is published as plain text with `Title`, `Priority`,
and `Tags` headers.
## Key bindings ## Key bindings
Keys act on the focused pane. Use `tab` / `shift-tab` or `1` / `2` / `3` to Keys act on the focused pane. Use `tab` / `shift-tab` or `1` / `2` / `3` to change
change panes. panes.
| Key | Action | | Key | Action |
| --- | --- | | --- | --- |
| `j` / `k`, arrows | move in the focused pane | | `j` / `k`, `↓` / `↑` | move in the focused pane |
| `g` / `G` | first / last | | `g` / `G`, `home` / `end` | first / last |
| `ctrl-u` / `ctrl-d` | page up / down | | `ctrl-u` / `ctrl-d`, `pgup` / `pgdn` | page up / down |
| `1` / `2` / `3` | focus topics, messages, detail | | `1` / `2` / `3` | focus topics / messages / detail |
| `tab` / `shift-tab` | next / previous pane | | `tab` / `shift-tab` | next / previous pane |
| `enter` | topics → messages → detail | | `enter` | descend: topics → messages → detail |
| `esc` | back from detail, or clear the filter | | `esc` | leave detail, or clear the filter |
| `/` | filter title, body, tags, and id | | `/` | filter by title, body, tags, and message id |
| `r` | reconnect | | `r` | reconnect all streams |
| `p` | publish to the focused topic | | `p` | publish to the focused topic |
| `a` | subscribe to another topic for this session | | `a` | subscribe to another topic for this session |
| `v` | toggle raw JSON | | `v` | toggle the raw JSON view |
| `o` | open the message click URL | | `o` | open the message's click URL |
| `?` | help | | `?` | in-app help |
| `q` | quit | | `q` / `ctrl-c` | quit |
On a narrow terminal the detail pane is hidden until it is focused. The layout adapts to the terminal width: at 100 columns and wider all three panes
are shown; from 72–99 columns the topics pane is paired with the focused pane;
and below 72 columns only the focused pane is drawn. Terminals smaller than 40×8
show `terminal too small`.
## Configuration ## Configuration
@@ -153,26 +229,97 @@ lazyntfy reads the official ntfy client configuration at
```yaml ```yaml
default-host: https://ntfy.example default-host: https://ntfy.example
default-token: tk_xxxxxxxxxxxxxxxx default-token: tk_xxxxxxxxxxxxxxxx
# or, instead of a token:
# default-user: alice
# default-password: s3cret
subscribe: subscribe:
- topic: alerts - topic: alerts
- topic: https://other.example/backups - topic: https://other.example/backups # explicit origin
user: alice user: bob
password: secret password: hunter2
- topic: private # per-topic token
token: tk_yyyyyyyyyyyyyyyy
``` ```
Positional topics on the command line replace the file's `subscribe` list. **Precedence.** `--host`, `--token`, and `--user` override the file. A token beats
Credentials are sent in the `Authorization` header and are never printed or a user: passing `--user` clears the token, and passing `--token` clears the user.
written back. See `man lazyntfy` for every flag. A per-topic `token` or `user` in `subscribe` applies to that topic only. Credentials
are sent in the `Authorization` header (Bearer or Basic) and are never printed or
written to disk.
If the default file is missing and you pass topics on the command line, lazyntfy
still runs. Point `--config` at a different file to load another one.
## How it works
- **Subscribe** — `GET /<topic>[,<topic>...]/json` returning NDJSON, with a `since`
query parameter per stream.
- **Publish** — `POST /<topic>` with a plain-text body and `Title` / `Priority` /
`Tags` headers.
- **Grouping** — topics that share an origin and credentials share one connection;
each group is supervised independently.
- **Reconnect** — on a dropped stream the group retries with exponential backoff
(1s, doubling up to 30s) and sets `since` to the newest event time it has already
seen, so messages are not delivered twice.
- **Events** — `message` adds a message, while `message_delete` and `message_clear`
remove messages locally as well. Malformed lines are skipped and reported in the
status bar.
- **Truncated cache** — if the server caps its replay it sends
`X-Messages-Truncated`, and lazyntfy notes that the cache was truncated to the
newest messages.
## History and storage ## History and storage
Topics you add and the messages that arrive are kept in a small SQLite database Topics and messages are kept in a small SQLite database so the last session is
at `$XDG_DATA_HOME/lazyntfy/history.db` restored on the next start.
(`~/.local/share/lazyntfy/history.db` by default), so the last session is
restored on the next start. Each topic keeps its newest 1000 messages; expired - **Path** — `$XDG_DATA_HOME/lazyntfy/history.db`, defaulting to
messages are pruned on startup. Credentials are never written to the database — `~/.local/share/lazyntfy/history.db`.
the current run's authentication is always used. History is best effort: if the - **Contents** — remembered topics, the newest 1000 messages per topic, and a little
file cannot be opened, the client still runs. UI state such as the last selected topic.
- **Maintenance** — messages with an `expires` time are pruned on startup, and the
1000-message cap is enforced as messages arrive.
- **Privacy** — credentials are never written to the database; the current run's
authentication is always used.
- **Best effort** — if the database cannot be opened, lazyntfy runs without history.
To start clean, delete the file:
```sh
rm -f "${XDG_DATA_HOME:-$HOME/.local/share}/lazyntfy/history.db"
```
## Troubleshooting
| Symptom | What to do |
| --- | --- |
| `no topics yet — press a to add one` | Pass a topic, add `subscribe:` entries, or use a token so account discovery can list topics. |
| `HTTP 401/403: not authorized to list topics` | The credential cannot read `/v1/account`; pass topics explicitly instead. |
| `no default-host` | Short topic names need a host. Pass `--host`, or use `host/topic`. |
| `x509: certificate signed by unknown authority` | Install CA certificates (`ca-certificates`). |
| `terminal too small` | Resize to at least 40×8. |
| `o` does nothing | Opening the click URL uses `xdg-open`; install it or a desktop opener. |
| First launch shows a flood of messages | Narrow the window with `--since 1h`. |
## Development
```sh
make build # build ./lazyntfy
make test # go test ./...
make vet # go vet ./...
make fmt # gofmt -w .
make run TOPIC=alerts
make man # install just the man page
make clean # remove the binary and build output
```
Run one package's tests, or the full suite with the race detector:
```sh
go test ./internal/history/...
go test -race ./...
```
## Packaging ## Packaging
@@ -187,38 +334,21 @@ archive from the Gitea instance.
When cutting a release: When cutting a release:
1. Bump the version in [`debian/changelog`](debian/changelog) and `pkgver` in 1. Bump the version in [`debian/changelog`](debian/changelog) and `pkgver` in the
the `PKGBUILD`. `PKGBUILD`.
2. Tag the commit (`git tag -s vX.Y.Z`) and push it. 2. Tag the commit (`git tag -s vX.Y.Z`) and push it.
3. In `packaging/archlinux`, run `updpkgsums` to refresh the tarball checksum and 3. In `packaging/archlinux`, run `updpkgsums` to refresh the tarball checksum and
`makepkg --printsrcinfo > .SRCINFO`. `makepkg --printsrcinfo > .SRCINFO`.
## Development
```sh
make build # build ./lazyntfy
make test # go test ./...
make vet # go vet ./...
make fmt # gofmt -w .
make run TOPIC=alerts
make man # install just the man page
```
Run a single package's tests, for example:
```sh
go test ./internal/history/...
```
## Project layout ## Project layout
| Path | Purpose | | Path | Purpose |
| --- | --- | | --- | --- |
| `cmd/lazyntfy` | flags and program startup | | `cmd/lazyntfy` | flags and program startup |
| `internal/config` | `client.yml` plus CLI overrides | | `internal/config` | `client.yml`, topic resolution, CLI overrides |
| `internal/history` | on-disk topics and messages (SQLite) | | `internal/history` | on-disk topics and messages (SQLite) |
| `internal/ntfy` | HTTP subscribe, publish, reconnect | | `internal/ntfy` | HTTP subscribe, publish, grouping, reconnect |
| `internal/store` | newest-first messages, delete, clear | | `internal/store` | newest-first messages, delete, clear, filter |
| `internal/alert` | Grafana / Alertmanager summary | | `internal/alert` | Grafana / Alertmanager summary |
| `internal/theme` | Catppuccin adaptive palette | | `internal/theme` | Catppuccin adaptive palette |
| `internal/textutil` | width-aware text wrapping and clipping | | `internal/textutil` | width-aware text wrapping and clipping |
+2 -1
View File
@@ -86,7 +86,8 @@ or
to change panes. to change panes.
.TP .TP
.B j ", " k ", " \(dqup\(dq ", " \(dqdown\(dq .B j ", " k ", " \(dqup\(dq ", " \(dqdown\(dq
Move within the focused pane..TP Move within the focused pane.
.TP
.B g ", " G .B g ", " G
Jump to the first or last item. Jump to the first or last item.
.TP .TP