Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions arch-updater/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
All notable changes to Arch Updater are documented here. The panel's changelog
icon (the history icon next to Check) shows this same file.

## 2.1.0 - 2026-08-29

- Added: a maintenance section under the package list — package cache size with one-click pruning (paccache, configurable versions to keep), orphaned packages with confirmed removal (pacman -Rns), and a firmware-updates shortcut when fwupd is installed.
- Added: the Arch keyring is refreshed right before each update run (toggleable), so packages signed with new keys don't fail after a long gap between updates.
- Added: a free-disk-space check before starting an update; the run is refused with a clear message when the root filesystem is too full.
- Added: background update runs hold a systemd sleep/idle inhibitor, so a closed laptop lid can't interrupt a pacman transaction halfway.
- Added: the update log is scanned after every run for known failure signatures — a failed initramfs generation (critical: warns before you reboot), failed transactions, and package signature errors.

## 2.0.1 - 2026-08-21

- Added: a changelog view in the panel. Click the history icon next to Check to see what changed in each release. It also opens automatically once an update finishes, unless turned off in settings (Show changelog after updating).
Expand Down
73 changes: 68 additions & 5 deletions arch-updater/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ work as normal (the default, same behavior as before), or fully in the
background — polkit asks for your password, the panel shows a live log tail
and a progress bar, and a notification reports the result. Either way the
run is logged and recorded in an update history with per-package rollback.
A maintenance section keeps an eye on the package cache, orphaned packages
and (with fwupd) firmware updates, and the update log is scanned for known
failure signatures — a failed initramfs generation warns you before you
reboot into it.

## Plugin

Expand All @@ -21,18 +25,23 @@ run is logged and recorded in an update history with per-package rollback.

- `pacman-contrib` on `PATH` (for `checkupdates` and `pactree`), required.
- `pacman`, `sh`, `awk`, `sed`, `grep`, `tail`, `head`, `tee`, `wc`, `date`,
`rm`, `install`, `test`, `cat`, `kill` and `uname`, required — base tools
from any standard Arch install (coreutils and friends), used to run and
parse the checks, build the download size estimate, check the running
kernel, follow and open the update log, install the optional polkit rule,
and detect whether a terminal update run's process is still alive.
`rm`, `install`, `test`, `cat`, `kill`, `cut`, `du`, `df` and `uname`,
required — base tools from any standard Arch install (coreutils and
friends), used to run and parse the checks, build the download size
estimate, check the running kernel, measure the package cache and free
disk space, follow and open the update log, install the optional polkit
rule, and detect whether a terminal update run's process is still alive.
- `systemd-inhibit` (systemd), used to block sleep during background update
runs; skipped when absent.
- `pkexec` (polkit) with an authentication agent, required for the
background update mode and for rollback. Noctalia's built-in polkit agent
works out of the box.
- `yay` or `paru` on `PATH`, optional, for the AUR check and update.
Auto-detected by default, see the **AUR helper** setting.
- `flatpak`, optional, for the Flatpak check and update.
- `xdg-open`, optional, to open a package page or the Arch news page.
- `fwupdmgr` (fwupd), optional — when installed, the maintenance section
offers firmware updates in a terminal window.
- `less`, plus `sudo` and a terminal emulator, for the terminal update mode
(the default), the **Retry in terminal** fallback and the **Open full
log** viewer: Noctalia's own terminal detection (`$TERMINAL`, then the
Expand Down Expand Up @@ -112,6 +121,44 @@ changed: anything declined during an interactive terminal run is left out
of the entry, so the history never offers to "undo" an update that did not
happen. Failed runs are not recorded.

### Maintenance

After each check, a small section under the package list reports the state
of the system around updates (all gathered without root):

- **Package cache** — its size, and how many superseded versions a prune
would remove (from `paccache`'s dry run) with the space they'd free. The
trash button runs the prune through one `pkexec` call: it keeps the
configured number of versions of each installed package (**Cached
versions to keep**, default 2) and drops cached files of uninstalled
packages entirely.
- **Orphaned packages** — the `pacman -Qtdq` list. The trash button (a
second click confirms, like rollback) removes them with their unneeded
dependencies via `pkexec pacman -Rns`; the tooltip lists what would go.
The scan repeats afterwards, since removing orphans can orphan the next
layer.
- **Firmware updates** — shown only when `fwupdmgr` is installed; the
button opens a terminal running `fwupdmgr refresh; fwupdmgr update`.

Around the update run itself:

- **Keyring first** (on by default): each run starts with
`pacman -Sy --needed archlinux-keyring`, so packages signed with new keys
don't fail after a long gap between updates.
- **Free-space check**: an update is refused up front when the root
filesystem has less than **Minimum free space** (default 1 GiB) plus
twice the estimated download size free — better than pacman dying on a
full disk halfway through a transaction.
- **Sleep inhibitor**: background runs hold a systemd sleep/idle inhibitor
so a closed lid can't interrupt a transaction.
- **Log analysis**: when a run finishes, its log is scanned for known
failure signatures — a failed initramfs generation (shown in red and
raised as a critical notification: check it before rebooting), a failed
pacman transaction, and package signature errors.

The whole section can be turned off with the **Maintenance section**
setting.

### Activity graph

The optional activity graph (off by default, **Show activity graph**)
Expand Down Expand Up @@ -186,6 +233,11 @@ setting.
| `show_activity_graph` | `bool` | `false` | Track pending-update counts across checks and show them as a small graph. Off also stops recording. |
| `activity_history_length` | `int` | `10` | How many recent checks the activity graph keeps (3–30). |
| `update_mode` | `select` | `terminal` | How **Update** runs the upgrade: in a terminal window (interactive, like before) or in the background (non-interactive). |
| `keyring_first` | `bool` | `true` | Refresh `archlinux-keyring` (`pacman -Sy --needed`) right before each update run. |
| `inhibit_sleep` | `bool` | `true` | Hold a systemd sleep/idle inhibitor for the duration of a background update run. |
| `min_free_mib` | `int` | `1024` | Refuse to start an update when root has less than this (plus twice the download estimate) free. `0` disables. |
| `maintenance_enabled` | `bool` | `true` | Scan and show the maintenance section (cache, orphans, firmware) after each check. |
| `cache_keep` | `int` | `2` | How many versions of each installed package the cache prune keeps (1–5). |
| `rollback_auto_ignore` | `bool` | `false` | After a successful rollback, add the rolled-back packages to the plugin's ignore list. |
| `hide_polkit_hint` | `bool` | `false` | Hide the panel line offering to install the polkit keep-authorization rule. |
| `log_lines` | `int` | `14` | How many of the latest update-log lines the panel shows during a run (6–30). |
Expand All @@ -205,6 +257,10 @@ noctalia msg plugin yuuto/arch-updater:service all update_terminal
noctalia msg plugin yuuto/arch-updater:service all dismiss
noctalia msg plugin yuuto/arch-updater:service all ignore:NAME
noctalia msg plugin yuuto/arch-updater:service all unignore:NAME
noctalia msg plugin yuuto/arch-updater:service all prune_cache
noctalia msg plugin yuuto/arch-updater:service all remove_orphans
noctalia msg plugin yuuto/arch-updater:service all fwupd_update
noctalia msg plugin yuuto/arch-updater:service all maintenance
```

`update` follows the **Update mode** setting; `update_background` and
Expand All @@ -228,6 +284,13 @@ panel-managed ignore list.
reverse-dependency warning. After a successful run, `pacman -Q` verifies
which packages actually changed before the history entry is written.
**Open full log** shows the log with `less` in your terminal.
Maintenance scan: `du -sb` and `df -Pk` for sizes, `pacman -Qtdq` for
orphans, `paccache -dk<n>` (dry run) for prunable versions; the finished
run's log is checked with `grep` for known failure signatures. Prune:
one `pkexec sh -c 'paccache -rk<n>; paccache -ruk0'`; orphan removal:
`pkexec pacman -Rns` on the scanned list; firmware:
`fwupdmgr refresh; fwupdmgr update` in your terminal. Background runs
are wrapped in `systemd-inhibit --what=sleep:idle`.
- **Privileges.** Escalation happens only through polkit: `pkexec pacman`
for repo packages and rollback, and the AUR helper escalates its install
steps through `pkexec` itself (`--sudo pkexec`). AUR builds run
Expand Down
112 changes: 110 additions & 2 deletions arch-updater/panel.luau
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ end
-- Two-click rollback: the first click arms the button (it turns into a
-- confirm label), the second sends the request. Anything else re-renders
-- the armed state away.
local function rollbackButton(key, tooltip, onConfirm)
local function rollbackButton(key, tooltip, onConfirm, glyph)
if armedKey == key then
return ui.button({
key = key .. "-armed",
Expand All @@ -715,7 +715,7 @@ local function rollbackButton(key, tooltip, onConfirm)
end
return ui.button({
key = key,
glyph = "undo", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12,
glyph = glyph or "undo", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12,
tooltip = tooltip,
onClick = function()
armedKey = key
Expand Down Expand Up @@ -976,6 +976,90 @@ local function extras()
return lines
end

-- ── Maintenance: cache, orphans, firmware ────────────────────────────────────

local function formatGib(bytes)
return string.format("%.1f", (tonumber(bytes) or 0) / 1073741824)
end

local function maintenanceSection()
if snapshot == nil or type(snapshot.maintenance) ~= "table" or busy() or changelogOpen or openedRunAt ~= nil then
return nil
end
local m = snapshot.maintenance
local busyM = snapshot.maintenanceBusy == true
local rows = {}

-- Cache size plus what a prune would free (paccache dry run).
if tonumber(m.cacheBytes) ~= nil then
local text = tr("maint_cache", { size = formatGib(m.cacheBytes) })
if (tonumber(m.pruneCount) or 0) > 0 then
text = text .. " · " .. tr("maint_cache_prunable", { count = tostring(m.pruneCount), saved = tostring(m.pruneSaved or "") })
end
local row = {
ui.glyph({ name = "database", size = 13, color = "on_surface_variant" }),
ui.label({ text = text, fontSize = 12, color = "on_surface_variant", flexGrow = 1, maxLines = 2 }),
}
if (tonumber(m.pruneCount) or 0) > 0 then
table.insert(row, ui.button({
key = "maint-prune" .. (busyM and "-off" or ""),
glyph = "trash", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12,
enabled = not busyM,
tooltip = tr("tip_prune"),
onClick = function()
request("prune_cache")
end,
}))
end
table.insert(rows, ui.row({ key = "maint-cache", gap = 6, align = "center" }, row))
end

-- Orphans go through the same armed confirm as rollback: their removal
-- is a root pacman -Rns.
local orphans = type(m.orphans) == "table" and m.orphans or {}
if #orphans > 0 and not busyM then
local names = table.concat(orphans, ", ")
if #names > 200 then
names = names:sub(1, 200) .. "…"
end
table.insert(rows, ui.row({ key = "maint-orphans", gap = 6, align = "center" }, {
ui.glyph({ name = "package-off", size = 13, color = "on_surface_variant" }),
ui.label({
text = noctalia.trp("maint_orphans", #orphans, { count = #orphans }),
fontSize = 12, color = "on_surface_variant", flexGrow = 1,
}),
rollbackButton("maint-orphans-rm", tr("tip_orphans", { names = names }), function()
request("remove_orphans")
end, "trash"),
}))
end

-- Firmware (fwupd): shown only when the tool is on the system; the
-- update itself is interactive, so it opens a terminal.
if m.fwupd == true then
table.insert(rows, ui.row({ key = "maint-fwupd", gap = 6, align = "center" }, {
ui.glyph({ name = "cpu", size = 13, color = "on_surface_variant" }),
ui.label({ text = tr("maint_fwupd"), fontSize = 12, color = "on_surface_variant", flexGrow = 1 }),
ui.button({
glyph = "external-link", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12,
tooltip = tr("tip_fwupd"),
onClick = function()
request("fwupd_update")
end,
}),
}))
end

if busyM then
table.insert(rows, ui.label({ key = "maint-busy", text = tr("maint_busy"), fontSize = 11, color = "secondary" }))
end

if #rows == 0 then
return nil
end
return ui.column({ key = "maintenance", gap = 4 }, rows)
end

local function body()
local children = {}

Expand Down Expand Up @@ -1019,6 +1103,11 @@ local function body()
table.insert(children, line)
end

local maint = maintenanceSection()
if maint ~= nil then
table.insert(children, maint)
end

if not busy() then
local clean = cleanSources()
if #clean > 0 then
Expand Down Expand Up @@ -1087,6 +1176,25 @@ render = function()
ui.label({ text = text, color = color, maxLines = 2 }),
}

-- Known failure signatures found in the last run's log: a broken
-- initramfs must be seen before the next reboot, not after.
if not busy() and snapshot ~= nil and type(snapshot.logWarnings) == "table" then
for _, tag in ipairs(snapshot.logWarnings) do
local key = tag == "INITRAMFS" and "warn_initramfs"
or tag == "COMMIT" and "warn_commit"
or tag == "SIGNATURE" and "warn_signature"
or nil
if key ~= nil then
table.insert(children, ui.label({
text = tr(key),
fontSize = 11,
color = tag == "SIGNATURE" and "warning" or "error",
maxLines = 2,
}))
end
end
end

for _, node in ipairs(body()) do
table.insert(children, node)
end
Expand Down
48 changes: 45 additions & 3 deletions arch-updater/plugin.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
id = "yuuto/arch-updater"
name = "Arch Updater"
version = "2.0.1"
version = "2.1.0"
plugin_api = 9
author = "yuuto"
license = "MIT"
icon = "package"
description = "Check pacman, AUR and Flatpak updates, then upgrade in a terminal or in the background, with run history and rollback."
dependencies = ["pacman-contrib", "awk", "cat", "date", "flatpak", "grep", "head", "install", "kill", "less", "pacman", "paru", "pkexec", "rm", "sed", "sh", "sudo", "tail", "tee", "test", "uname", "wc", "xdg-open", "yay"]
description = "Check pacman, AUR and Flatpak updates, upgrade in a terminal or the background, with history, rollback and maintenance."
dependencies = ["pacman-contrib", "awk", "cat", "cut", "date", "df", "du", "flatpak", "grep", "head", "install", "kill", "less", "pacman", "paru", "pkexec", "rm", "sed", "sh", "sudo", "systemd-inhibit", "tail", "tee", "test", "uname", "wc", "xdg-open", "yay"]
tags = ["arch", "bar", "panel", "launcher", "system", "utility"]

# ── General ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -123,6 +123,48 @@ label_key = "settings.show_changelog_after_update.label"
description_key = "settings.show_changelog_after_update.description"
default = true

[[setting]]
key = "keyring_first"
type = "bool"
label_key = "settings.keyring_first.label"
description_key = "settings.keyring_first.description"
default = true

[[setting]]
key = "inhibit_sleep"
type = "bool"
label_key = "settings.inhibit_sleep.label"
description_key = "settings.inhibit_sleep.description"
default = true

[[setting]]
key = "min_free_mib"
type = "int"
label_key = "settings.min_free_mib.label"
description_key = "settings.min_free_mib.description"
default = 1024
min = 0
max = 16384

# ── Maintenance ──────────────────────────────────────────────────────────────

[[setting]]
key = "maintenance_enabled"
type = "bool"
label_key = "settings.maintenance_enabled.label"
description_key = "settings.maintenance_enabled.description"
default = true

[[setting]]
key = "cache_keep"
type = "int"
label_key = "settings.cache_keep.label"
description_key = "settings.cache_keep.description"
default = 2
min = 1
max = 5
visible_when = { key = "maintenance_enabled", values = ["true"] }

[[setting]]
key = "rollback_auto_ignore"
type = "bool"
Expand Down
Loading
Loading