tui-snapper
btrfs snapshots, managed by snapper, from the terminal.
Part of the tui-tools family.
Your machine has been taking snapshots before every package upgrade for
months. snapper list prints them; answering "what did that upgrade actually
change, and can I put back just this one file" from there is a different
matter. That is what this is for.


make demo # the whole UI, on sample data, with no btrfs and no root
Install
Arch Linux
Needs the tui-tools repository, which is a one-time
setup.
The one-liner detects the distribution and adds the repository and its signing
key:
curl -fsSL https://pkgs.tui.tools/install.sh | sh
Piping a script into a shell is not this family's style, so here is the same
setup by hand — read it, or read the script first with curl -fsSL https://pkgs.tui.tools/install.sh -o install.sh:
curl -fsSL -o /tmp/tui-tools.asc https://pkgs.tui.tools/pubkey.asc
sudo pacman-key --add /tmp/tui-tools.asc
sudo pacman-key --lsign-key \
"$(gpg --show-keys --with-colons /tmp/tui-tools.asc | awk -F: '/^fpr:/{print $10; exit}')"
printf '[tui-tools]\nServer = https://pkgs.tui.tools/arch/$arch\n' \
| sudo tee -a /etc/pacman.conf
sudo pacman -Sy
Then, and for every other tool in the family:
sudo pacman -S tui-snapper
Upgrades then arrive with the rest of your system updates.
Debian and Ubuntu
Needs the tui-tools repository, which is a one-time
setup.
The one-liner detects the distribution and adds the repository and its signing
key:
curl -fsSL https://pkgs.tui.tools/install.sh | sh
Piping a script into a shell is not this family's style, so here is the same
setup by hand — read it, or read the script first with curl -fsSL https://pkgs.tui.tools/install.sh -o install.sh:
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://pkgs.tui.tools/pubkey.asc \
| sudo gpg --dearmor -o /etc/apt/keyrings/tui-tools.gpg
echo "deb [signed-by=/etc/apt/keyrings/tui-tools.gpg] https://pkgs.tui.tools/deb stable main" \
| sudo tee /etc/apt/sources.list.d/tui-tools.list
sudo apt update
Then, and for every other tool in the family:
sudo apt install tui-snapper
Upgrades then arrive with the rest of your system updates.
Fedora and RHEL
Needs the tui-tools repository, which is a one-time
setup.
The one-liner detects the distribution and adds the repository and its signing
key:
curl -fsSL https://pkgs.tui.tools/install.sh | sh
Piping a script into a shell is not this family's style, so here is the same
setup by hand — read it, or read the script first with curl -fsSL https://pkgs.tui.tools/install.sh -o install.sh:
sudo rpm --import https://pkgs.tui.tools/pubkey.asc
sudo curl -fsSL -o /etc/yum.repos.d/tui-tools.repo https://pkgs.tui.tools/rpm/tui-tools.repo
sudo dnf makecache
Then, and for every other tool in the family:
sudo dnf install tui-snapper
Upgrades then arrive with the rest of your system updates.
Any distribution, static binary
curl -fsSL https://github.com/tui-tools/tui-snapper/releases/download/v0.1.1/tui-snapper_0.1.1_linux_amd64.tar.gz | tar -xz tui-snapper
sudo install -m0755 tui-snapper /usr/local/bin/tui-snapper
Static, no runtime dependencies beyond the snapper binary it drives.
From source
git clone https://github.com/tui-tools/tui-snapper
cd tui-snapper && make demo
make demo needs no btrfs filesystem and no root.
The distribution packages are not published yet. The commands below are here so
you know what they will be; a package repository at pkgs.tui.tools is what
turns them on.
openSUSE — coming soon
Needs the tui-tools repository, which is a one-time
setup.
sudo zypper install tui-snapper
The rpm repository is shared with dnf; zypper support is not tested yet.
Verify a download
Every release of tui-snapper ships a checksums.txt. Check an archive
against it before installing:
sha256sum -c checksums.txt --ignore-missing
What it does
The history. Every config's snapshots, newest first: the hourly timeline
ones, the pre/post pairs your package manager left behind — a post snapshot
shows the pre it belongs to, 47←46 — and the ones you took by hand. A
snapshot no cleanup algorithm will ever remove is colored as kept, because
that is the difference between a state you can rely on and one a timer will
quietly take away. The header carries the config, its subvolume, how far back
the history goes, and the space the snapshots hold when btrfs quotas make that
figure real.
What changed. d compares two snapshots. A post snapshot pairs with its
own pre without being asked, which is the comparison anyone opening a package
upgrade wants; mark two snapshots with space to compare any other pair.
enter on a path opens its unified diff, read-only and scrollable.

Putting things back. u in the diff view runs undochange for the
selected paths, or for every path you marked. One configuration file goes back
without the rest of the filesystem moving.
Managing the history. Create a snapshot with a description, a type and a
cleanup algorithm; delete one, or every snapshot you marked; change a
description; pin a snapshot by clearing its cleanup algorithm; run number,
timeline or empty-pre-post cleanup. Every one of them is shown as the
exact snapper command line first.

Rollback, honestly. This is the one thing that genuinely differs between
machines, so the tool works out which mechanism is in front of you instead of
assuming:
- where
snapper rollback is the real mechanism — the config is the root
filesystem and snapper carries the rollback flag — it is offered, with the
warning it deserves;
- on an Omarchy or limine-snapper-sync layout, the snapshots are in
the boot menu and booting one is the rollback. tui-snapper reads
/boot/limine.conf and shows those entries read-only. It never edits that
file, and it never swaps subvolumes itself.

The timers, read-only. T shows whether snapper-timeline.timer and
snapper-cleanup.timer are running and enabled, which is the answer to "why
did nothing get cleaned up". Turning them on and off is systemd's job, and
tui-systemd already does it.
It needs root, including to read
Most tools in this family read as an ordinary user and escalate only to change
something. snapper does not work that way: it opens its subvolumes directly,
so even snapper list fails for a non-root user with
IO error (open failed path:/ errno:1 (Operation not permitted)).
So either run the whole thing with sudo tui-snapper, or leave
sudo = "sudo -n" configured and keep a sudo timestamp warm — every snapper
call is then escalated individually. The tool says which of the two it is
doing in its header, and explains this failure rather than showing you an
empty list. --demo needs neither.
Keys
| Key |
What it does |
↑ ↓ j k |
move the selection; g / G first and last, pgup / pgdn a page |
space |
mark a snapshot: two marks are a range, many are one delete |
d / enter |
compare two snapshots; a post snapshot pairs with its own pre |
enter |
in the diff: that file's changes, read-only |
u |
in the diff: put the selected or marked files back |
c |
create a snapshot: description, type, cleanup algorithm |
D |
delete the selected snapshot, or every marked one |
e / a |
change the description / the cleanup algorithm |
C |
run a cleanup: number, timeline or empty-pre-post |
R |
rollback, or how this machine actually rolls back |
s / T |
switch config / snapper's timers, read-only |
/ |
filter the list |
r |
re-read the current view |
? / q |
help / quit |

Configuration
/etc/tui-snapper/config.toml, then ~/.config/tui-snapper/config.toml, then
TUI_SNAPPER_* in the environment, then the flags. See
examples/config.toml.
config = "root" # the snapper config to open on
sudo = "sudo -n" # "" runs snapper directly
boot-config = "/boot/limine.conf" # where the rollback screen reads the boot menu
no-dbus = false # true where no snapperd is running
theme = "" # empty follows the active Omarchy theme
no-dbus is worth knowing about. snapper normally talks to snapperd over
D-Bus; in a container, a chroot or a rescue shell there is no snapperd, and
every command comes back as
Failure (org.freedesktop.DBus.Error.FileNotFound). --no-dbus makes snapper
do the work itself. The flag becomes part of the previewed command line, so
the dialog still shows exactly what will run.
Compatibility
tui-snapper probes its backend once at startup and shows the version in the
header. A version nobody has tested is marked (untested) there rather than
hidden; one below the minimum is marked as such and the tool still runs.
snapper
|
|
| Binary |
snapper |
| Version read with |
snapper --version |
| Minimum |
0.8.6 |
| Tested |
0.10.6, 0.13.0, 0.13.1 |
| Version-gated features |
undochange-root-paths (since 0.10.6) |
| Versions |
What changes |
<0.8.6 |
snapper has no --jsonout, and both the config list and the snapshot list are read as JSON, so nothing can be listed at all |
<0.10.6 |
snapper undochange cannot recreate a path that sits directly under the subvolume root, so putting back a deleted top-level file or directory fails with "failed to create" |
<0.10.7 |
snapper diff is broken for LVM-based configs, so the file view comes back empty there even when the compare screen lists the change |
The tested versions are generated from compat/results.jsonl, which the tool's
own smoke test appends to when it runs against a real machine in
tui-lab.
The rules it keeps
These are the family's, and they are what the tests are about:
- Preview, then confirm. Nothing changes without the exact
snapper
command line being shown first. The dialog is the only path to a mutation,
and internal/snapper/command.go is the only place a command line is built.
- Read-only by default. Starting the tool only reads.
- No daemon, no state of its own. The filesystem is the source of truth,
and it is re-read after every change.
--demo always works. It builds and previews every command for real and
touches nothing.
- Backend behind an interface. The UI never names a binary.
- Responsive. It works in a 40-column pane and on a full screen.
Development
make check # gofmt, go vet, tests
make demo # the UI on sample data
make screenshots # re-render the README frames from --demo
make manifest # validate tool.json against the family schema
make compat # harvest tui-lab results into compat/results.jsonl
make readme # regenerate the Install and Compatibility sections
The parsers are table-tested against output captured from a real snapper
0.13.1 on a real btrfs filesystem — internal/snapper/testdata holds those
captures verbatim, including
testdata/limine-omarchy-server.conf: a /boot/limine.conf taken from an
Omarchy Server 4.0.1 VM after four snapper create calls and one
limine-snapper-sync run. The three layouts in limine_test.go beside it are
reconstructions rather than captures, and say so: they were rebuilt from the
writer that produces them and from limine's own CONFIG.md. The capture
corrected one of them — the titles that machine generates are
4 │ 2026-08-29 21:27:27, the snapshot number first and then the timestamp,
and not the timestamp alone.
Three things worth knowing if you extend a parser:
snapper status is plain text even under --jsonout in 0.13.1, so it
is parsed line by line while list and list-configs are read as JSON;
snapper --jsonout list keys its array by the config's own name, not by
a fixed field, so {"root": [...]} is what comes back;
/boot is mode 0700 on an Omarchy install, so reading limine.conf
needs the same escalation the snapper calls use. A tool that opens it
directly finds nothing and then reports the wrong rollback mechanism, which
is what tui-lab caught.
--check
--check is the non-interactive read path, the same one tui-firewall and
tui-systemd have. It drives the real backend, prints the parsed model as
JSON and exits 0 or 1. It never builds and never runs a mutation, so it is
safe anywhere.
$ sudo tui-snapper --check | head -9
{
"tool": "tui-snapper",
"version": "dev",
"backend": "snapper",
"describe": "snapper via /usr/bin/sudo -n",
"config": "root",
"subvolume": "/",
"configs": 1,
"config_list": [
That is what makes assertions like "the tool's snapshot count equals
snapper list's" possible, and it is what test/smoke.sh runs on each of the
lab's three machines.
License
MIT — see LICENSE. Part of the
tui-tools family.