Beta. The family is days old and still changing. Package names, flags and keys may move without notice until 1.0. Pin versions, and report what breaks.
A terminal UI for the updates waiting on your machine. It reads them from
whichever package manager the machine actually runs — pacman, apt or dnf —
and answers the question a list of versions never does: what will applying
this cost, and what has to restart afterwards?
Every distribution ships a one-line upgrade command. None of them tells you, up
front, that the kernel in the list means a reboot, that sshd will keep the old
openssl mapped until it is restarted, or that a snapshot could have been taken
first. tui-update puts all of that on one screen, in the
Omarchy visual style, and previews the exact command
sequence before running it.
Status: early, under validation. An independent tool that follows the Omarchy visual style; it is not part of the Omarchy project and not endorsed by its maintainers. Expect rough edges.
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 | shPiping 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 -SyThen, and for every other tool in the family:
sudo pacman -S tui-updateUpgrades then arrive with the rest of your system updates.
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 | shPiping 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 updateThen, and for every other tool in the family:
sudo apt install tui-updateUpgrades then arrive with the rest of your system updates.
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 | shPiping 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 makecacheThen, and for every other tool in the family:
sudo dnf install tui-updateUpgrades then arrive with the rest of your system updates.
curl -fsSL https://github.com/tui-tools/tui-update/releases/download/v0.3.1/tui-update_0.3.1_linux_amd64.tar.gz | tar -xz tui-update
sudo install -m0755 tui-update /usr/local/bin/tui-updateOne static binary. Verify it against checksums.txt from the same release.
git clone https://github.com/tui-tools/tui-update
cd tui-update && make build
sudo install -m0755 bin/tui-update /usr/local/bin/tui-updateNeeds Go 1.27 or newer.
Not packaged for these yet; the static binary works everywhere in the meantime.
paru -S tui-update-binThe -bin package installs the released static binary.
Needs the tui-tools repository, which is a one-time setup.
sudo zypper install tui-updateThe rpm repository is shared with dnf; zypper support is not tested yet.
Every release of tui-update ships a checksums.txt. Check an archive against
it before installing:
sha256sum -c checksums.txt --ignore-missingWebsite: https://tui.tools/tools/tui-update/
One static binary, no daemon, no state of its own. Nothing keeps running after you quit.
tui-update --demo--demo runs against a sample machine: fourteen pending updates including a
kernel and two openssl security fixes, a reboot already required, sshd and
nginx holding old code open, a snapper root configuration to snapshot into,
and nginx already held so both halves of the hold key have something to do.
Every key works, every command is built and previewed for real, and nothing
touches your system.
--demo-no-versionlock is the same machine with dnf's versionlock plugin
missing, which is how the hold key's refusal — and the package name it hands
you — can be seen without uninstalling a plugin on a real host.
--demo-machine picks another sample machine: ubuntu drives apt, with the
non-interactive upgrade it runs, and omarchy drives Omarchy Server, where U
hands the terminal over to the machine's own updater and comes back (see
Nothing asks behind the screen). The default
is fedora, the machine described above.
enter on the pending list gives you the plan, and it is four answers:
- The restart classification.
none,serviceswith the list of units still holding replaced code open, orrebootwith the reason. It comes from whatever on this machine can actually read the process table —needrestart -bon apt,needs-restartingon dnf,omarchy-server-update-restart --dry-runon Omarchy Server — and falls back to the package names when none of them answered. The screen always says which it was. - The snapshot.
snapshot before: yeswith the exactsnapper -c root create -t pre …and-t post --pre-number <pre-number> …that would run, ornowith the reason: snapper is not installed, or it has no configuration covering/. - The manager's own dry run, quoted:
apt-get -s upgrade,dnf upgrade --assumeno. On pacman there is none that does not first synchronise the databases, which needs root, so the pending list stands in and the screen says so. - The whole command sequence, in order, each line with what it does.
Two keys change the plan, and both re-render it before anything is confirmed:
mcycles the upgrades the manager really has:upgrade, apt'sdist-upgrade, and on dnf a security-only one. The cycle is built from the backend's capabilities, so a manager that cannot narrow an upgrade to the advisories never lands on a mode it would have to fake — see Security-only upgrades.sturns the snapshot pair off and on. The snapperpreandpostcommands appear and disappear on the plan you are reading, so what the confirm dialog carries is what you last looked at. A machine with no snapper root configuration says so instead of moving a toggle that means nothing.
m reaches a security mode on dnf only, and that is a deliberate,
uncomfortable asymmetry rather than an omission.
dnf can. dnf upgrade --security narrows the very same transaction to the
advisories the distribution published. It is the same --security word the
tool already uses to fill the SEC column, written once in the code, so the
column and the mode cannot come to mean different things. Packages carrying no
advisory of their own can still be pulled in as dependencies — the plan says so
in its notes, because that is dnf resolving a transaction, not the tool
widening one.
apt cannot, and nothing here pretends otherwise. There is no
apt-get upgrade --security. The two things people reach for instead are both
rejected here:
apt-get -o Dir::Etc::SourceList=/dev/null upgradewith only the security list. It works by lying to apt about which sources exist, which changes how the solver sees the world; the resulting transaction is not the one apt would compute on the real machine. It is not shipped.unattended-upgrade --dry-run, thenunattended-upgrade. This is the honest apt mechanism and it was evaluated seriously — it applies the distribution's ownUnattended-Upgrade::Allowed-Originspolicy, and its dry run really does print what it would do. It is still not what a key called "security-only" may run, for three reasons:- It is not security-only.
Allowed-Originsis whatever the admin configured, and Debian's shipped default includes the plain stable archive alongside the security one. Labelling that "security-only" would be false on a default Debian. - It can reboot the machine. With
Automatic-Reboot "true"set, a command confirmed as an upgrade reboots the host.tui-updatenever reboots by itself, and it will not hand that decision to a config file it did not write. - It is a separate package (
unattended-upgrades) that a minimal Debian does not carry, and it applies its own blacklist and its own logging — so the previewed sequence would stop being the sequence that ran.
- It is not security-only.
So on apt the mode is gated off: m cycles upgrade and dist-upgrade
and stops there. apt still shows the security column — a package's pocket says
whether it is a security update, and that claim is true — which is why the
capability is split in two in the code: SecurityMetadata (apt: yes) and
SecurityUpgrade (apt: no). On pacman there is neither.
The honest workaround on apt is the manager's own: hold the packages you do not
want moved, with H, and run the plain upgrade.
H on a package row holds it at its installed version, or lifts a hold it
already carries. Held rows are marked ·held and greyed out, because a package
nothing is going to upgrade should not look like one that is.
| Manager | Hold | Lift | Read from |
|---|---|---|---|
| apt | apt-mark hold <pkg> |
apt-mark unhold <pkg> |
apt-mark showhold |
| dnf | dnf versionlock add <pkg> |
dnf versionlock delete <pkg> |
dnf versionlock list |
| pacman | — | — | — |
Both are previewed and confirmed like every other change. Placing a hold is marked destructive and lifting one is not, which is the right way round: a held package stops receiving updates, security fixes included, until somebody lifts it.
Two honest caveats:
- dnf's versionlock is a plugin, and a minimal install does not carry it.
Without it,
dnf versionlock addfails withNo such command: versionlock, which tells a reader nothing. So the plugin is detected when the pending list is read, andHrefuses up front, naming the package to install —python3-dnf-plugin-versionlock, ordnf-plugins-extras-versionlockon RHEL and its rebuilds.tui-update --demo --demo-no-versionlockdrives that exact machine, so the refusal can be seen without breaking a real one. - A versionlocked package disappears from
dnf check-update, because the lock excludes it from the available set. So on dnf a lock you placed will usually not be on the list any more — which isdnf versionlock list's job to answer, and lifting it is adnf versionlock deleteon the command line. On apt a held package stays on the upgradable list, soHtoggles it both ways from the screen. - pacman holds packages in
/etc/pacman.conf(IgnorePkg), which is a file to edit rather than a command to run. This tool does not edit configuration files, soHsays so there rather than inventing a command.
U opens a confirm dialog carrying every command of the sequence. y runs
them, n does not. There is no other path to a change: the UI hands the same
values to the preview and to the runner, so what you read is what executes.
The sequence is run one command at a time, and the output streams into a pane
as each one answers, so a long download is visible rather than a frozen screen.
The pane reads that output the way a terminal would: a line a program redraws
in place with a carriage return, like dpkg's (Reading database ... counter,
shows only its last state, and escape sequences are stripped the same way the
status line strips them.
The snapshot pair is paired by number. snapper links a post snapshot to its
pre one with --pre-number, and that number only exists once the pre snapshot
has been taken. So the dialog shows the post step with <pre-number> in its
place, the number snapper create --print-number prints for the pre snapshot
fills it in, and the apply pane shows the post command exactly as it then runs.
If the pre snapshot fails, or prints no number, the sequence stops there and
the upgrade does not run. If a step fails after the pre snapshot was taken, the
pane and the status line name that snapshot and the
snapper -c root delete <n> that removes it, so it is not left behind unseen.
While tui-update is on screen, a command it runs has no terminal: the
kit runner
starts every step in a session of its own, so a program that wants to ask a
question fails at once instead of waiting, invisible, behind the UI. The two
upgrades that can ask are handled so that they do not have to.
apt asks nothing. The upgrade runs as
sudo -n env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade
and the confirm dialog says what each part means. debconf takes its defaults.
needrestart, which Ubuntu server runs after every transaction, restarts the
services left on replaced libraries instead of asking which ones. And when a
package ships a new version of a config file this machine has changed, the
local file is kept: dpkg leaves the package's version beside it as
<file>.dpkg-dist, to compare and merge by hand. A file nobody changed is
replaced as usual. The variables go through env after sudo, because sudo
resets the environment, and they are part of the command line you confirm.
Omarchy Server's updater gets the terminal. omarchy-server-update run --no-reboot is interactive by design: it can ask through sudo, and through its
own prompts. So it is not run as a background step. After the same preview and
confirm, tui-update hands the terminal over to it, the updater runs exactly as
if you had typed the command, and when it exits the screen comes back, the
sequence goes on (the post snapshot, when there is one) and the machine is read
again. Its step is previewed with sudo rather than sudo -n, because with
the terminal in hand sudo may ask for your password, as it would by hand.
When the upgrade needed a reboot, the apply screen offers R afterwards — with
its own confirm dialog, naming the reason and warning that every session on the
machine ends there. Nothing in this tool starts a reboot without that.
t shows the machine's unattended-update mechanism and its real state:
omarchy-server-update.timer, apt-daily-upgrade.timer with
unattended-upgrades.service, or dnf-automatic.timer. e and d enable or
disable it, previewed like everything else — because a machine that upgrades
itself while nobody is watching is precisely the thing this tool exists to make
legible.
tui-update # drive the real package manager
tui-update --demo # sample machine, no privileges needed
tui-update --demo --demo-no-versionlock # the same machine without dnf's
# versionlock plugin, so h refuses
tui-update --demo --demo-machine ubuntu # an apt machine
tui-update --demo --demo-machine omarchy # Omarchy Server: U hands the
# terminal to its updater
tui-update --check # read the updates, print JSON, exit
tui-update --report # print what a bug report needs, exit
tui-update --theme ~/mytheme/colors.toml
tui-update --sudo "" # run the commands directly (as root)
tui-update --version--check is the non-interactive read path: it loads the state through the same
backend the UI would use, prints it as JSON and exits 0, or exits 1 with the
reason if the manager cannot be read. No UI, and it never builds or runs a
mutation.
$ tui-update --check | head -16
{
"tool": "tui-update",
"version": "0.1.0",
"manager": "dnf",
"distro": "fedora",
"describe": "dnf via /usr/bin/sudo -n on fedora",
"pending": 10,
"security": 0,
"rebootRequired": false,
"restart": "none",
"services": [],
"snapshot": false,
"canHold": true,
"holds": 0,
"timers": [It exists so a test can assert on what the tool parsed rather than on what it
painted — that the count matches dnf check-update, that the reboot verdict
matches needs-restarting -r, that the timer state matches
systemctl is-enabled.
It never refreshes the manager's metadata. A refresh writes to a root-owned cache, and the whole read path is built to work as an ordinary user against what is already on disk. The smoke test asserts it on the cache's own mtime.
tui-lab uses it to test this tool
against real machines on Arch, Ubuntu and Fedora; the assertions live in
test/smoke.sh.
--report prints, in one block, everything a maintainer has to ask for
otherwise: the tool and kit versions, the package manager and the version
probed off it, which of the binaries around it are installed, the
distribution, the kernel, the terminal, the theme, the escalation prefix, and
whether the running binary came from a package. It needs no privileges and
reads no updates, so it answers instantly and it works on the machine where
the bug is — including one where no supported manager can be detected at all,
which is itself a thing worth reporting.
$ tui-update --report
tui-update 0.1.0 (kit v0.2.9)
backend: dnf 5.2.18
mode: live
distro: fedora 42 (Fedora Linux 42 (Workstation Edition))
kernel: 6.19.14-108.fc42.x86_64
arch: x86_64
locale: en_US.UTF-8
term: xterm-256color
theme: tokyo-night
sudo: sudo -n
root: no
binary: /usr/bin/tui-update (packaged)
helpers: dnf present, rpm present, needs-restarting present, snapper absent, systemctl presentThe helpers line is the one worth reading twice: most of what looks like a
bug here is a binary that is not installed. A pending list that looks stale is
checkupdates or fakeroot missing, an empty restart list is needrestart or
needs-restarting missing, a plan that takes no snapshot is snapper missing.
The block is written to be published as it is: it carries no hostname, user
name, home path or address, and no environment variable beyond LANG,
LC_ALL, TERM and TERM_PROGRAM. A binary living under your home directory
is reported as being there without naming the path. --report works with
--demo too, where it says so on the mode line and names the manager the
sample machine imitates.
The bug form asks for this block first — see
.github/ISSUE_TEMPLATE/bug_report.yml.
Every one of these is previewed and confirmed first.
| What runs | |
|---|---|
| snapshot before | snapper -c root create -t pre -d "…" --print-number |
| refresh (apt) | apt-get update |
| refresh (dnf) | dnf makecache --refresh -q |
| upgrade (pacman) | pacman -Syu --noconfirm |
| upgrade (Omarchy Server) | omarchy-server-update run --no-reboot, with the terminal handed over |
| upgrade (apt) | env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade, or dist-upgrade |
| upgrade (dnf) | dnf -y upgrade, or dnf -y upgrade --security |
| snapshot after | snapper -c root create -t post --pre-number <n> -d "…" --print-number, <n> being the number the pre snapshot printed |
| hold (apt) | apt-mark hold <pkg> / apt-mark unhold <pkg> |
| hold (dnf) | dnf versionlock add <pkg> / dnf versionlock delete <pkg> |
| timers | systemctl enable --now <unit> / systemctl disable --now <unit> |
| reboot | systemctl reboot |
Nothing else. No package is ever installed or removed by this tool: it applies the upgrade the manager itself would apply, whole — or, on dnf, the security half of it, which is the manager's own subset and not one this tool computes. The only per-package change it can make is a hold, and a hold installs nothing.
On Omarchy Server the upgrade goes through the machine's own wrapper rather than
straight to pacman, because that wrapper runs the same pacman -Syu and then
classifies and restarts what changed — driving pacman directly there would skip
half the job the machine was set up to do. --no-reboot is passed, since taking
the reboot is this tool's user's decision. The wrapper can ask questions, so
it runs with the terminal handed over to it rather than as a background step.
| Key | Action |
|---|---|
↑/k, ↓/j |
Move the selection, or scroll a text screen |
g / G |
First / last |
pgup / pgdn |
Scroll a page |
enter / p |
The plan: what applying the updates would do |
U |
Apply it, after confirming the whole sequence |
m |
On the plan: cycle upgrade, dist-upgrade (apt) and security (dnf) |
s |
On the plan: take the pre/post snapshot, or do not |
h |
The package manager's own transaction history |
H |
Hold the selected package at its version, or lift the hold |
t |
The unattended-update timers |
e / d |
On the timers screen: enable / disable the unit |
R |
Re-read; on a finished upgrade, offer the reboot |
/ |
Filter the packages (matches any column; esc clears) |
esc |
Leave the screen |
? |
Help |
q |
Quit |
- Read the pending updates from
checkupdatesorpacman -Qu,apt list --upgradable, ordnf check-update— always from the metadata already on disk, never refreshing it. - Show current → new, the repository, the download size where the manager publishes one, and the security flag where it publishes that, with kernel and firmware grouped to the top.
- Classify what an upgrade would leave behind: nothing, a list of services, or a
reboot with its reason — from
needrestart,needs-restarting,/var/run/reboot-required,omarchy-server-update-restart, or the package names when none of those could be read. - Decide whether a
snapperpre-upgrade snapshot is possible, and build the pre/post pair that makessnapper status <pre>..<post>list what changed. - Preview and run the whole sequence — snapshot, refresh, upgrade, snapshot — one command at a time, with the output streaming into a pane.
- Cycle the upgrade modes the manager really has, including a security-only one on dnf, and turn the snapshot pair off and on, both re-rendering the plan before the confirm dialog.
- Hold a package at its installed version and lift the hold, on apt and on dnf, refusing up front where the machine has no way to do it.
- Offer the reboot afterwards, behind its own confirm dialog. Never take one.
- Show the manager's last 20 transactions from
/var/log/pacman.log,/var/log/apt/history.logordnf history list, read-only. - Show and toggle the machine's unattended-update unit.
- Follow the active Omarchy theme, and respect
NO_COLOR.
- No per-package upgrade. The list is not a selection: it is what the
manager would do, and the tool applies that whole or not at all. The way to
leave a package behind is to hold it, with
H, and then upgrade. - No security-only upgrade on apt or pacman. apt has no command that applies only the security updates, and the approximations are either dishonest or capable of rebooting the machine — the reasoning is above. pacman publishes no security metadata at all.
- No hold on pacman.
IgnorePkglives in/etc/pacman.conf, and this tool does not edit configuration files. - No rollback. The snapshot is taken so that
snapper undochangeor a boot into the pre snapshot is possible; performing it is not a key here. - No per-package size on pacman or apt. Neither
apt list --upgradablenorcheckupdatesreports one, and asking for it would mean a second privileged read per package. The column is hidden there rather than filled with guesses. - No security flag on pacman. Arch publishes no per-package security
metadata, so the column reads
n/a— which is a different claim fromno. - No zypper, no apk, no nix, no flatpak, no snap. The
updates.Backendinterface is where one would go; nothing in the UI names a package manager. - No AUR, no third-party helpers.
paruandyayare not driven. - No download-only, no partial upgrade, no
--exclude.
tui-update 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.
| Binary | pacman |
| Version read with | pacman --version |
| Minimum | 6.0 |
| Tested | 7.1.0 |
| Versions | What changes |
|---|---|
>=6.0 |
pacman publishes no security metadata, so no update is ever marked as a security fix here; the column reads n/a rather than no |
>=6.0 |
there is no dry run that does not first synchronise the databases, which needs root, so the plan quotes the pending list instead of a simulated transaction |
>=6.0 |
checkupdates needs pacman-contrib and fakeroot, since it builds its private copy of the sync database under it; without either the pending list falls back to pacman -Qu, which is whatever the last pacman -Sy left on disk, and the screen says so |
>=6.0 |
on Omarchy Server the upgrade runs through omarchy-server-update run --no-reboot, and omarchy-server-update-restart --dry-run is what classifies the restarts; on plain Arch there is no classifier and the package names decide |
| Binary | apt |
| Version read with | apt --version |
| Minimum | 2.0 |
| Tested | 2.8.3 |
| Version-gated features | solver3 (since 2.9.3) |
| Versions | What changes |
|---|---|
>=2.0 |
a security update is one whose pocket ends in -security; apt publishes no advisory id, so that pocket name is the whole reference |
>=2.0 |
neither apt list --upgradable nor apt-get -s upgrade reports a per-package size, so the size column is not shown; the plan carries apt's own download and disk totals instead |
>=2.0 |
the services to restart come from needrestart -b, which reads other processes' memory maps and therefore needs root; without it the package names decide |
>=2.0 |
there is no upgrade that applies only the security updates; unattended-upgrade honours Unattended-Upgrade::Allowed-Origins, which on Debian includes the plain archive too and can reboot on its own, so the security-only mode is not offered here |
>=2.0 |
a package is held with apt-mark hold, the dpkg selection every apt front end honours; apt-mark showhold is the unprivileged read the pending list marks its rows from |
| Binary | dnf |
| Version read with | dnf --version |
| Minimum | 4.0 |
| Tested | 5.2.18, 5.4.1 |
| Version-gated features | dnf5 (since 5.0) |
| Versions | What changes |
|---|---|
>=5.0 |
dnf --version prints dnf5 version 5.2.18.0, where dnf4 prints a bare 4.24.0 on its first line; both are read by the same pattern, which keeps three components because a four-part version is not one the family schema records |
>=5.0 |
dnf needs-restarting refreshes the repository metadata before answering, which a read path must not do, so the standalone needs-restarting binary from dnf-plugins-core is used instead and its absence falls back to the package names |
>=4.0 |
dnf check-update exits 100 when there are updates, so its exit code is interpreted rather than treated as a failure; it also prints only the new version, and the installed one is read from rpm |
>=4.0 |
dnf upgrade --security narrows the same transaction to the advisories, so the security-only mode is the plain upgrade with one flag rather than a second code path; dependencies with no advisory of their own still come along |
>=4.0 |
holding a package needs the versionlock plugin, which a minimal install does not carry; it is detected before the key is offered, and the refusal names python3-dnf-plugin-versionlock, or dnf-plugins-extras-versionlock on RHEL and its rebuilds |
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.
/etc/tui-update/config.toml, then ~/.config/tui-update/config.toml (the user
file overrides the machine-wide one), then TUI_UPDATE_* in the environment.
Flags override everything. See examples/config.toml.
# Privilege escalation prefix; "" runs the command directly.
sudo = "sudo -n"
# Path to an Omarchy-style colors.toml; empty follows the active theme.
theme = ""The default palette is Tokyo Night. On Omarchy, the tool reads the active
desktop theme from ~/.config/omarchy/current/theme/colors.toml and follows it.
TUI_THEME or --theme override; NO_COLOR drops color and keeps layout. The
rules live in tui-kit.
The UI never builds a pacman, apt or dnf command line. It talks to
internal/updates.Backend, which returns a manager-neutral model:
Model{Manager, Distro, Pending, SecurityCount, Restart, Snapshot, Timers, Hold, Notes}
Package{Name, Arch, Current, New, Repo, Size, Security, SecurityRef, Group, Held}
Restart{Class, Services, Reason, RebootRequired, Source, Detail}
Snapshot{Available, Config, Reason, Pre, Post}
HoldSupport{Available, Reason, Hint}
PlanOptions{Mode, Snapshot}
Plan{Title, Mode, DryRun, Restart, Snapshot, TakeSnapshot, Commands, HandOff, Explain, Notes}
What each manager can do is a Capabilities value the backend returns, and the
UI is built from it rather than from a list of manager names: the mode cycle,
the SEC column and the hold key all read the capabilities. That is why
SecurityMetadata and SecurityUpgrade are two fields — apt answers yes to the
first and no to the second, and one boolean could not say that.
internal/pkgmgr is the only package that starts a process. Which manager it
drives is decided by the binary that is installed, cross-checked against
/etc/os-release, so a Debian container on an Arch host does not end up driving
the host's pacman. Around the manager it drives rpm, systemctl, snapper,
the restart classifiers and, on Omarchy Server, that machine's own update
wrapper —
check-exec.sh
fails the build if any other package imports os/exec.
Mutations are runner.Command values produced by the backend. The UI shows them
and, on confirmation, hands the same ones back to the
kit runner,
which resolves the binary and the privilege prefix. That is the whole trust
boundary, and it is why the preview is guaranteed to match what executes.
A step the plan marks in HandOff is the one exception to the runner: it is
interactive by design, so Backend.HandOff prepares it as a process that
Bubble Tea's tea.Exec starts with the terminal handed over. It is built in
internal/pkgmgr from the same resolved binary and prefix (without -n), and
its preview is built the same way, so the rule holds for it too.
make check # gofmt, go vet, the exec boundary and the tests: what CI runs
make test
make build
make demo
make screenshots # re-render the frames above from --demoEvery argv the tool can build is pinned by string equality in
internal/pkgmgr/command_test.go: a change to any of them is a change to what a
user reads before agreeing to it, and it fails the build until it is
deliberate. The parser tests run against captured command output in
internal/pkgmgr/testdata — the dnf fixtures from a real Fedora 42 host running
dnf5, the apt and pacman ones written against the documented line shapes.
Dependencies are deliberately small: Bubble Tea, Bubbles and tui-kit, which carries the palette, the widgets, the config loader and the command runner shared by the whole family.
- An upgrade can drop the session you are working over — a
sshdrestart, anopensslchange, a reboot. The plan says which of those apply before you agree to anything. - The snapshot is taken before the refresh and the upgrade, so it is worth something. If snapper is missing or has no root configuration, the plan says so in the same place rather than silently skipping it.
- The restart classifiers read other processes' memory maps, which needs root.
Without
sudo -nthey see only your own processes, and the plan says the scan was partial rather than presenting it as the whole answer. - Reading never refreshes the manager's metadata. That means the pending list is as fresh as the last refresh someone made — which the plan makes explicit by putting the refresh in the sequence, before the upgrade.
dnf check-updateexits 100 when there are updates andneeds-restarting -rexits 1 when a reboot is needed. Both are answers, not failures, and they are read as such.
Contributions arrive as pull requests, and the guide the whole family follows is CONTRIBUTING.md in tui-kit. A vulnerability goes to SECURITY.md instead, never into a public issue.
MIT. See LICENSE.








