lazyntfy

A lazygit-style terminal client for ntfy. Follow your topics, read and search messages, and publish — without leaving the terminal.

Go License: GPL-3.0-or-later Built with Bubble Tea PRs welcome

╭ topics ─────╮ ╭ messages ─────────────────╮ ╭ detail ───────────────╮
│ ● alerts 12 │ │ 06-14 09:21  CRIT DiskFull │ │ CRITICAL              │
│ ○ backups   │ │ 06-14 09:03  high Backup…  │ │ DiskFull              │
│ ○ deploy    │ │ 06-13 22:47  def  Deploy…  │ │ 2026-06-14 09:21:07   │
│             │ │                            │ │ mK3n9Qx…              │
│             │ │                            │ │                       │
╰─────────────╯ ╰────────────────────────────╯ ╰───────────────────────╯
 lazyntfy alerts  12/12                    ● connected        ? help  q quit

lazyntfy is a keyboard-driven TUI for ntfy: the left pane lists topics, the middle pane lists messages, and the right pane shows the selected message in full. It talks to the ntfy HTTP API directly, follows several topics over a single stream, and keeps a local cache so your history is there the moment you launch it.

Contents

Features

  • Three-pane, lazygit-style layout — topics, messages, and full detail side by side.
  • Follow many topics at once — topics that share a server and credentials are multiplexed onto a single stream.
  • Account discovery — start with no topics and lazyntfy lists your subscriptions, reserved topics, and ACL grants.
  • Alert-aware — Grafana and Alertmanager webhook payloads are summarized into status, alert name, severity, and summary; v reveals the raw JSON.
  • Persistent — topics and messages are cached in a local SQLite database and restored on the next start, so the list is populated as soon as the app opens.
  • 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.

Install

lazyntfy needs a Go 1.26+ toolchain only to build. Prebuilt installs are available for Arch Linux and Debian/Ubuntu.

Arch Linux

A PKGBUILD lives in packaging/archlinux. Build and install the package with makepkg:

git clone https://src.panahifar.ir/ahp/lazyntfy.git
cd lazyntfy/packaging/archlinux
makepkg -si

This installs /usr/bin/lazyntfy and the man page under /usr/share/man, and pulls in ca-certificates so TLS to your ntfy server works.

Debian and Ubuntu

A debhelper-free source package lives in debian. Build a .deb and install it system-wide:

git clone https://src.panahifar.ir/ahp/lazyntfy.git
cd lazyntfy
make deb                       # writes dist/lazyntfy_<version>_<arch>.deb
sudo apt install ./dist/lazyntfy_*.deb

apt install ./file.deb (or dpkg -i followed by apt -f install) resolves the ca-certificates dependency. The packaging needs only the Go toolchain and dpkg-dev, and make deb passes -d so it also builds where golang-go is not installed as a distribution package.

From source

git clone https://src.panahifar.ir/ahp/lazyntfy.git
cd lazyntfy
make install                   # into ~/.local, no root needed

make install honors PREFIX, BINDIR, MANDIR, DOCDIR, and DESTDIR. Choosing a system prefix installs for every user:

sudo make install PREFIX=/usr/local   # /usr/local/bin/lazyntfy, /usr/local/share/man/man1
sudo make install PREFIX=/usr         # /usr/bin/lazyntfy, /usr/share/man/man1

Stage a filesystem tree into a temporary root instead of the live system (handy for building your own package):

make install DESTDIR="$PWD/stage" PREFIX=/usr

With go install

From a checkout:

go install ./cmd/lazyntfy      # binary in $(go env GOPATH)/bin

Quick start

lazyntfy alerts          # follow one topic on the default host
lazyntfy                 # or start with no arguments to load every topic on your account

Then j/k to move, enter to open a message, / to filter, p to publish, and ? for help.

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

Keys act on the focused pane. Use tab / shift-tab or 1 / 2 / 3 to change panes.

Key Action
j / k, ↓ / ↑ move in the focused pane
g / G, home / end first / last
ctrl-u / ctrl-d, pgup / pgdn page up / down
1 / 2 / 3 focus topics / messages / detail
tab / shift-tab next / previous pane
enter descend: topics → messages → detail
esc leave detail, or clear the filter
/ filter by title, body, tags, and message id
r reconnect all streams
p publish to the focused topic
a subscribe to another topic for this session
v toggle the raw JSON view
o open the message's click URL
? in-app help
q / ctrl-c quit

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

lazyntfy reads the official ntfy client configuration at ~/.config/ntfy/client.yml:

default-host: https://ntfy.example
default-token: tk_xxxxxxxxxxxxxxxx
# or, instead of a token:
# default-user: alice
# default-password: s3cret

subscribe:
  - topic: alerts
  - topic: https://other.example/backups      # explicit origin
    user: bob
    password: hunter2
  - topic: private                            # per-topic token
    token: tk_yyyyyyyyyyyyyyyy

Precedence. --host, --token, and --user override the file. A token beats a user: passing --user clears the token, and passing --token clears the user. 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

Topics and messages are kept in a small SQLite database so the last session is restored on the next start.

  • Path — $XDG_DATA_HOME/lazyntfy/history.db, defaulting to ~/.local/share/lazyntfy/history.db.
  • Contents — remembered topics, the newest 1000 messages per topic, and a little 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:

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

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:

go test ./internal/history/...
go test -race ./...

Packaging

Both package formats are defined in the repository. make deb builds a Debian package from the working tree; the Arch PKGBUILD builds the v$pkgver release archive from the Gitea instance.

Format Build Definition
Debian / Ubuntu make deb → dist/*.deb debian/
Arch Linux make arch → makepkg packaging/archlinux/PKGBUILD

When cutting a release:

  1. Bump the version in debian/changelog and pkgver in the PKGBUILD.
  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 makepkg --printsrcinfo > .SRCINFO.

Project layout

Path Purpose
cmd/lazyntfy flags and program startup
internal/config client.yml, topic resolution, CLI overrides
internal/history on-disk topics and messages (SQLite)
internal/ntfy HTTP subscribe, publish, grouping, reconnect
internal/store newest-first messages, delete, clear, filter
internal/alert Grafana / Alertmanager summary
internal/theme Catppuccin adaptive palette
internal/textutil width-aware text wrapping and clipping
internal/tui Bubble Tea interface
man lazyntfy(1) man page
debian Debian / Ubuntu package definition
packaging/archlinux Arch Linux PKGBUILD

Contributing

  1. Fork the repository and create a topic branch.
  2. Keep commits focused and conventional (feat:, fix:, docs:, …).
  3. Run make fmt vet test before opening a pull request.

License

GNU General Public License, version 3 or later. See LICENSE.

S
Description
Lazy terminal client for ntfy
Readme GPL-3.0
123 KiB
Languages
Go 97.5%
Makefile 1.6%
Shell 0.9%