Skip to content

Repository files navigation

tui-update

OpenSSF Scorecard OpenSSF Best Practices

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.

The pending list

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.

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-update

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-update

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-update

Upgrades then arrive with the rest of your system updates.

Any distribution, static binary

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-update

One static binary. Verify it against checksums.txt from the same release.

From source

git clone https://github.com/tui-tools/tui-update
cd tui-update && make build
sudo install -m0755 bin/tui-update /usr/local/bin/tui-update

Needs Go 1.27 or newer.

Not packaged for these yet; the static binary works everywhere in the meantime.

Arch Linux (AUR) — coming soon

paru -S tui-update-bin

The -bin package installs the released static binary.

openSUSE — coming soon

Needs the tui-tools repository, which is a one-time setup.

sudo zypper install tui-update

The rpm repository is shared with dnf; zypper support is not tested yet.

Verify a download

Every release of tui-update ships a checksums.txt. Check an archive against it before installing:

sha256sum -c checksums.txt --ignore-missing

Website: https://tui.tools/tools/tui-update/

One static binary, no daemon, no state of its own. Nothing keeps running after you quit.

Try it without root

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.

The plan is the point

The plan

enter on the pending list gives you the plan, and it is four answers:

  • The restart classification. none, services with the list of units still holding replaced code open, or reboot with the reason. It comes from whatever on this machine can actually read the process table — needrestart -b on apt, needs-restarting on dnf, omarchy-server-update-restart --dry-run on 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: yes with the exact snapper -c root create -t pre … and -t post --pre-number <pre-number> … that would run, or no with 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:

  • m cycles the upgrades the manager really has: upgrade, apt's dist-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.
  • s turns the snapshot pair off and on. The snapper pre and post commands 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.

Security-only upgrades

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 upgrade with 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, then unattended-upgrade. This is the honest apt mechanism and it was evaluated seriously — it applies the distribution's own Unattended-Upgrade::Allowed-Origins policy, 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:
    1. It is not security-only. Allowed-Origins is 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.
    2. It can reboot the machine. With Automatic-Reboot "true" set, a command confirmed as an upgrade reboots the host. tui-update never reboots by itself, and it will not hand that decision to a config file it did not write.
    3. 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.

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.

Holding a package back

The pending list

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 add fails with No such command: versionlock, which tells a reader nothing. So the plugin is detected when the pending list is read, and H refuses up front, naming the package to install — python3-dnf-plugin-versionlock, or dnf-plugins-extras-versionlock on RHEL and its rebuilds. tui-update --demo --demo-no-versionlock drives 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 is dnf versionlock list's job to answer, and lifting it is a dnf versionlock delete on the command line. On apt a held package stays on the upgradable list, so H toggles 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, so H says so there rather than inventing a command.

Every change is previewed

The confirm dialog

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.

Nothing asks behind the screen

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.

The apt upgrade, previewed

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.

The Omarchy updater, handed the terminal

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.

It never reboots by itself

The apply screen

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.

Unattended updates, made visible

The timers

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.

Usage

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, for scripts and tests

--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, for bug reports

--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 present

The 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.

What it can do to your machine

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.

Keys

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

Help

What v0.1 can do

  • Read the pending updates from checkupdates or pacman -Qu, apt list --upgradable, or dnf 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 snapper pre-upgrade snapshot is possible, and build the pre/post pair that makes snapper 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.log or dnf history list, read-only.
  • Show and toggle the machine's unattended-update unit.
  • Follow the active Omarchy theme, and respect NO_COLOR.

What v0.1 cannot do

  • 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. IgnorePkg lives in /etc/pacman.conf, and this tool does not edit configuration files.
  • No rollback. The snapshot is taken so that snapper undochange or 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 --upgradable nor checkupdates reports 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 from no.
  • No zypper, no apk, no nix, no flatpak, no snap. The updates.Backend interface is where one would go; nothing in the UI names a package manager.
  • No AUR, no third-party helpers. paru and yay are not driven.
  • No download-only, no partial upgrade, no --exclude.

Compatibility

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.

pacman

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

apt

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

dnf

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.

Configuration

/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 = ""

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.

Architecture

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.

Development

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 --demo

Every 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.

Safety notes

  • An upgrade can drop the session you are working over — a sshd restart, an openssl change, 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 -n they 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-update exits 100 when there are updates and needs-restarting -r exits 1 when a reboot is needed. Both are answers, not failures, and they are read as such.

Contributing

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.

License

MIT. See LICENSE.

About

Pending package updates, what they restart, and a snapshot first (Linux TUI, previews every change)

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

Generated from tui-tools/tui-template