diff --git a/arch-updater/CHANGELOG.md b/arch-updater/CHANGELOG.md new file mode 100644 index 00000000..c512341e --- /dev/null +++ b/arch-updater/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +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.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). +- Fixed: hitting Update in terminal mode now closes the panel right away, instead of leaving it open with nothing left to show. +- Fixed: closing the terminal window before an update finished left the panel stuck showing "Updating in a terminal window…" forever. The engine now notices the terminal is gone and reports the run as failed, with the usual retry option. + +## 2.0.0 - 2026-08-19 + +- Added: an update mode setting. Run updates in a terminal window like before, or fully in the background with a live log, progress bar and one polkit password for the whole run. +- Added: an Ignored section in the panel to see and manage packages held back by pacman.conf's IgnorePkg or the plugin's own ignore list. +- Added: update history with per-package and whole-run rollback, resolved against the pacman/AUR cache. +- Added: an opt-in activity graph tracking pending-update counts across recent checks. +- Fixed: the Arch news check re-firing every few seconds after the first run, which could get the whole plugin auto-disabled shortly after login. +- Fixed: pacman's translated "[ignored]"/progress lines breaking the pending count and progress bar on non-English systems. + +## 1.1.0 - 2026-08-08 + +- Added: per-source icons in the package list header. +- Added: an activity graph showing pending-update counts over time, with per-point hover detail. +- Fixed: Dismiss now actually clears the pending list, and hitting Update closes the panel. +- Fixed: tightened package list spacing and icon sizing. + +## 1.0.1 - 2026-08-04 + +- Fixed: reduced CPU work in the Arch news HTTP callback. + +## 1.0.0 - 2026-07-28 + +- Initial release: check pacman, AUR and Flatpak for updates from the panel and the bar widget. diff --git a/arch-updater/README.md b/arch-updater/README.md index 2b268853..d1ee9882 100644 --- a/arch-updater/README.md +++ b/arch-updater/README.md @@ -21,10 +21,11 @@ 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` 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, and install the optional polkit rule. + `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. - `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. @@ -53,7 +54,11 @@ noctalia msg panel-toggle yuuto/arch-updater:panel The panel groups pending packages by source (Pacman, AUR, Flatpak). Click a source row to expand it into its packages. Each package row has an ignore button (see **Ignored packages**), a copy button (name and versions) and an -open button (its page on archlinux.org, the AUR, or Flathub). +open button (its page on archlinux.org, the AUR, or Flathub). The history +button next to **Check** opens the plugin's changelog, so you can see what +changed in each release without leaving the panel. It also opens on its own +once an update finishes, unless you turn that off with the **Show changelog +after updating** setting. **Update** follows the **Update mode** setting: diff --git a/arch-updater/panel.luau b/arch-updater/panel.luau index d7472cd4..4789d3e7 100644 --- a/arch-updater/panel.luau +++ b/arch-updater/panel.luau @@ -23,6 +23,8 @@ local armedKey = nil -- rollback button waiting for its confirming second click local armedAt = 0 -- when it was armed; the confirm auto-disarms after a while local ARM_TIMEOUT_S = 8 local activityHoverIndex = nil -- activity graph point currently under the pointer +local changelogOpen = false -- changelog view replaces the sources +local changelogAutoOpenPending = false -- set with changelogOpen when a just-finished update opened it, so onOpen() doesn't reset it away unseen local render @@ -30,6 +32,39 @@ local function tr(key, args) return noctalia.tr(key, args) end +-- CHANGELOG.md, parsed once at load: "## version — date" headers, "- " bullet +-- lines under each. readFile resolves relative to the plugin directory. +local function parseChangelog(text) + local releases = {} + local current = nil + for line in (text .. "\n"):gmatch("([^\n]*)\n") do + local version, date = line:match("^##%s+(%S+)%s*(.-)%s*$") + if version ~= nil then + -- Strip the "— " (or "- ") separator before the date. Done as a + -- literal gsub rather than folded into the match pattern above, + -- since Lua patterns treat a multi-byte UTF-8 dash inside a + -- character class as a set of individual bytes, not one glyph. + date = date:gsub("^—%s*", ""):gsub("^%-%s*", "") + current = { version = version, date = date, lines = {} } + table.insert(releases, current) + elseif current ~= nil then + local bullet = line:match("^%-%s+(.*)$") + if bullet ~= nil then + table.insert(current.lines, bullet) + end + end + end + return releases +end + +local changelog = (function() + local text = noctalia.readFile("CHANGELOG.md") + if type(text) ~= "string" then + return {} + end + return parseChangelog(text) +end)() + -- extra: a package name string, or a table merged into the request payload -- (pkg/version/at for the rollback family). local function request(action, extra) @@ -768,6 +803,45 @@ local function runViewRows() return rows end +-- The changelog view: a back button, then one release per entry in +-- CHANGELOG.md with its bullet points underneath. +local function changelogRows() + local rows = {} + table.insert(rows, ui.row({ key = "changelog-head", gap = 6, align = "center" }, { + ui.button({ + glyph = "chevron-left", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12, + tooltip = tr("action_back"), + onClick = function() + changelogOpen = false + render() + end, + }), + ui.label({ text = tr("changelog_title"), fontSize = 12, fontWeight = "bold", color = "on_surface", flexGrow = 1, maxLines = 1 }), + })) + if #changelog == 0 then + table.insert(rows, ui.row({ key = "changelog-empty", paddingH = 18 }, { + ui.label({ text = tr("changelog_empty"), fontSize = 11, color = "on_surface_variant" }), + })) + return rows + end + for ri, release in ipairs(changelog) do + local heading = "v" .. release.version + if release.date ~= nil and release.date ~= "" then + heading = heading .. " · " .. release.date + end + table.insert(rows, ui.row({ key = "changelog-v" .. ri, gap = 6, align = "center" }, { + ui.label({ text = heading, fontSize = 12, fontWeight = "bold", color = "on_surface" }), + })) + for li, bullet in ipairs(release.lines) do + table.insert(rows, ui.row({ key = "changelog-" .. ri .. "-" .. li, paddingH = 10, gap = 6 }, { + ui.label({ text = "•", fontSize = 11, color = "on_surface_variant" }), + ui.label({ text = bullet, fontSize = 11, color = "on_surface_variant", flexGrow = 1, maxLines = 3 }), + })) + end + end + return rows +end + -- Live tail of the update log, with a progress bar while packages are being -- processed. Shown during a run and kept on screen after a failed one. local function logSection() @@ -906,9 +980,14 @@ local function body() local children = {} -- The middle of the panel: the live log while updating (and after a - -- failure), an opened history run, or the package list. + -- failure), the changelog, an opened history run, or the package list. + -- The live log always wins over the changelog: a run in progress is more + -- urgent than release notes. if phaseOf() == "running" or (runFailed() and #logLines() > 0) then table.insert(children, logSection()) + elseif changelogOpen then + table.insert(children, ui.scroll({ key = "changelog-view", flexGrow = 1, gap = 4 }, changelogRows())) + return children else local runRows = openedRunAt ~= nil and runViewRows() or nil if runRows ~= nil then @@ -987,6 +1066,17 @@ render = function() request("check") end, }), + ui.button({ + key = "header-changelog" .. (changelogOpen and "-on" or ""), + glyph = "history", + variant = changelogOpen and "primary" or "ghost", + tooltip = tr("tip_changelog"), + onClick = function() + changelogOpen = not changelogOpen + armedKey = nil + render() + end, + }), ui.button({ glyph = "close", variant = "ghost", tooltip = tr("tip_close"), onClick = function() @@ -1002,8 +1092,10 @@ render = function() end -- Both footers hide while a check or run is on screen, so neither ever - -- competes with the live log for space. - local activity = not busy() and activitySection() or nil + -- competes with the live log for space. The activity graph also hides + -- while the changelog is open: it plots pending-update counts, which + -- has nothing to do with release notes. + local activity = not busy() and not changelogOpen and activitySection() or nil local history = not busy() and historySection() or nil if activity ~= nil or history ~= nil then table.insert(children, ui.separator({})) @@ -1050,10 +1142,14 @@ render = function() text = tr("action_update"), variant = "primary", enabled = hasUpdates, tooltip = backgroundMode and tr("tip_update") or tr("tip_update_terminal"), onClick = function() - -- The panel stays open: the log section takes over so the - -- run can be watched live (the log is tee'd from the - -- terminal too). request("update") + if not backgroundMode then + -- Terminal mode hands the run off to its own window, so + -- the panel has nothing left to show; background mode + -- keeps it open since the live log is the only place + -- progress is visible. + panel.close() + end end, })) table.insert(children, ui.row({ gap = 8, align = "center", justify = "end" }, footer)) @@ -1070,6 +1166,13 @@ function onOpen(_context) openedRunAt = nil armedKey = nil activityHoverIndex = nil + -- A fresh auto-open from a just-finished update (see the state watcher + -- below) survives this reset once, so it's not discarded before it's + -- ever seen; any other reason for opening still defaults to the sources. + if not changelogAutoOpenPending then + changelogOpen = false + end + changelogAutoOpenPending = false render() end @@ -1094,6 +1197,23 @@ noctalia.state.watch(STATE_KEY, function(value) if value.phase == "running" and (snapshot == nil or snapshot.phase ~= "running") then openedRunAt = nil armedKey = nil + changelogOpen = false + end + -- lastUpdateAt only moves for a real, successful update run (never a + -- rollback or a plain check, see recordUpdateRun in service.luau), and it + -- is bumped the moment the run succeeds, before the auto re-check even + -- starts - so a change here is an exact, one-shot "an update just + -- finished" signal. + local prevUpdateAt = snapshot ~= nil and tonumber(snapshot.lastUpdateAt) or nil + local newUpdateAt = tonumber(value.lastUpdateAt) + if + prevUpdateAt ~= nil + and newUpdateAt ~= nil + and newUpdateAt ~= prevUpdateAt + and noctalia.getConfig("show_changelog_after_update") ~= false + then + changelogOpen = true + changelogAutoOpenPending = true end snapshot = value render() diff --git a/arch-updater/plugin.toml b/arch-updater/plugin.toml index bb5667c1..bf93293e 100644 --- a/arch-updater/plugin.toml +++ b/arch-updater/plugin.toml @@ -1,12 +1,12 @@ id = "yuuto/arch-updater" name = "Arch Updater" -version = "2.0.0" +version = "2.0.1" 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", "date", "flatpak", "grep", "head", "install", "less", "pacman", "paru", "pkexec", "rm", "sed", "sh", "sudo", "tail", "tee", "test", "uname", "wc", "xdg-open", "yay"] +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"] tags = ["arch", "bar", "panel", "launcher", "system", "utility"] # ── General ────────────────────────────────────────────────────────────────── @@ -116,6 +116,13 @@ options = [ { value = "background", label_key = "settings.update_mode.options.background" }, ] +[[setting]] +key = "show_changelog_after_update" +type = "bool" +label_key = "settings.show_changelog_after_update.label" +description_key = "settings.show_changelog_after_update.description" +default = true + [[setting]] key = "rollback_auto_ignore" type = "bool" diff --git a/arch-updater/service.luau b/arch-updater/service.luau index bd5cbf22..e11d9bc2 100644 --- a/arch-updater/service.luau +++ b/arch-updater/service.luau @@ -36,6 +36,7 @@ local NEWS_FILE = "news_state.json" local IGNORE_FILE = "ignore.json" local RUNS_FILE = "runs.json" local RUN_META_FILE = "run_meta.json" +local RUN_PID_FILE = "run.pid" local ACTIVITY_FILE = "history_state.json" -- upstream's activity-graph data, name kept for compatibility local MAX_RUNS = 15 -- update runs kept for the history strip / rollback local MAX_RUN_PACKAGES = 100 -- per-run package list cap (storage and state) @@ -51,6 +52,8 @@ local FAST_TIMEOUT_MS = 5000 -- log tail / reboot check: local filesystem only local NEWS_RECHECK_HOURS = 6 local RUN_POLL_SECONDS = 2 local RUN_STALE_LIMIT_S = 1800 -- no log growth for this long = the background run is stuck +local RUN_PID_GRACE_S = 20 -- a terminal run's pidfile has this long to appear before it's given up on +local TERMINAL_CLOSED_CODE = -2 -- finishRun sentinel: the terminal's process died with no ::EXIT marker local RUN_RESUME_MAX_AGE_S = 6 * 3600 -- older unfinished logs are not resumed local AUTO_CHECK_DELAY = 10 -- ticks before an enabled auto-check's first run local MAX_LISTED = 300 -- packages kept per source for the panel's expandable list @@ -75,6 +78,8 @@ local runExit = nil -- exit code of the last background run, nil while unknown local runDone = 0 -- progress: package lines seen in the log so far local runTotal = 0 -- progress: pending count when the run started local runStaleS = 0 -- seconds without log growth during a run +local runStartedAt = 0 -- os.time() the current run began, for the terminal pidfile grace window +local pidConfirmedAlive = false -- true once a terminal run's pidfile has ever shown a live process local logTail = {} -- last log lines for the panel local lastTailText = "" local sinceCheck = 0 @@ -1108,6 +1113,15 @@ local function runMetaPath() return dir ~= nil and (dir .. "/" .. RUN_META_FILE) or nil end +-- Written by the terminal run's own shell (echo $$) as its first action, so +-- pollRunLog can tell a closed terminal window from one legitimately sitting +-- at a PKGBUILD prompt: the log alone cannot, since closing the window kills +-- the pipeline before it ever gets to write an ::EXIT marker. +local function runPidPath() + local dir = noctalia.pluginDataDir() + return dir ~= nil and (dir .. "/" .. RUN_PID_FILE) or nil +end + local function saveRunMeta() local path = runMetaPath() if path == nil then @@ -1138,6 +1152,10 @@ local function clearRunMeta() if path ~= nil then noctalia.runAsync("rm -f -- " .. shellQuote(path)) end + local pidPath = runPidPath() + if pidPath ~= nil then + noctalia.runAsync("rm -f -- " .. shellQuote(pidPath)) + end end local function beginRun(kind, expectTotal, mode) @@ -1150,6 +1168,8 @@ local function beginRun(kind, expectTotal, mode) runDone = 0 runTotal = expectTotal or total runStaleS = 0 + runStartedAt = os.time() + pidConfirmedAlive = false runPollTicks = 0 logTail = {} lastTailText = "" @@ -1214,7 +1234,12 @@ local function runUpdateTerminal() return end local quoted = shellQuote(path) - local wrapped = "printf '::START %s\\n' \"$(date +%s)\" > " .. quoted + -- echo $$ first thing: pollRunLog's pidfile check is how a closed + -- terminal window gets noticed at all, since closing it SIGHUPs the + -- pipeline before the trailing ::EXIT printf ever runs. + local pidPath = runPidPath() + local pidWrite = pidPath ~= nil and ("echo $$ > " .. shellQuote(pidPath) .. "; ") or "" + local wrapped = pidWrite .. "printf '::START %s\\n' \"$(date +%s)\" > " .. quoted .. "; { " .. buildTerminalCommand() .. "; printf '::EXIT %s\\n' \"$?\" ; } 2>&1 | tee -a " .. quoted .. "; echo; echo " .. shellQuote(tr("run.press_key")) .. "; read -n 1" if not launchTerminal(wrapped) then @@ -1305,6 +1330,13 @@ finishRun = function(code) phase = "clean" publish() startCheck() -- verify: phase becomes checking, then clean/ready + elseif code == TERMINAL_CLOSED_CODE then + runPackages = nil + clearRunMeta() + phase = "error" + errMsg = tr("err_terminal_closed") + publish() + noctalia.notifyError(tr("title"), errMsg) else runPackages = nil clearRunMeta() @@ -1333,6 +1365,20 @@ local function pollRunLog() local quoted = shellQuote(path) local cmd = "tail -n " .. tostring(keep + 8) .. " " .. quoted .. " 2>/dev/null" .. "; printf '::COUNT %s\\n' \"$(grep -cE '^(upgrading|installing|reinstalling|downgrading) |^(Updating|Installing) (app|runtime)/' " .. quoted .. " 2>/dev/null)\"" + -- A terminal run's pidfile: prints nothing while it hasn't appeared yet + -- (still launching), "::ALIVE 1" while the process it names is running, + -- "::ALIVE 0" once that process is gone - the only way to notice the + -- window was closed, since that kills the pipeline before ::EXIT. + if runMode == "terminal" then + local pidPath = runPidPath() + if pidPath ~= nil then + local pidQuoted = shellQuote(pidPath) + cmd = cmd + .. "; p=$(cat " .. pidQuoted .. " 2>/dev/null)" + .. "; if [ -n \"$p\" ] && kill -0 \"$p\" 2>/dev/null; then printf '::ALIVE 1\\n'" + .. "; elif [ -n \"$p\" ]; then printf '::ALIVE 0\\n'; fi" + end + end local started = noctalia.runAsync(cmd, function(result) if phase ~= "running" then return @@ -1348,6 +1394,13 @@ local function pollRunLog() if runStaleS >= RUN_STALE_LIMIT_S then finishRun(-1) end + elseif not pidConfirmedAlive and os.time() - runStartedAt > RUN_PID_GRACE_S then + -- Nothing has changed (no tail growth, no pidfile) since the + -- grace window opened and the pidfile has never once shown a + -- live process: the terminal never actually started. A run + -- already confirmed alive skips this - it may legitimately + -- sit idle at a prompt for as long as the user needs. + finishRun(TERMINAL_CLOSED_CODE) end return end @@ -1356,13 +1409,17 @@ local function pollRunLog() local lines = {} local exitCode = nil + local aliveMarker = nil for line in text:gmatch("[^\n]+") do local exitMatch = line:match("^::EXIT (%-?%d+)") local countMatch = line:match("^::COUNT (%d+)") + local aliveMatch = line:match("^::ALIVE (%d)") if exitMatch ~= nil then exitCode = tonumber(exitMatch) elseif countMatch ~= nil then runDone = tonumber(countMatch) or runDone + elseif aliveMatch ~= nil then + aliveMarker = aliveMatch elseif line:match("^::START ") == nil then line = stripAnsi(line) if trim(line) ~= "" then @@ -1370,6 +1427,9 @@ local function pollRunLog() end end end + if aliveMarker == "1" then + pidConfirmedAlive = true + end local tail = {} for i = math.max(1, #lines - keep + 1), #lines do table.insert(tail, lines[i]) @@ -1378,6 +1438,10 @@ local function pollRunLog() if exitCode ~= nil then finishRun(exitCode) + elseif runMode == "terminal" and aliveMarker == "0" then + -- The pidfile's process is gone and no ::EXIT ever showed up: + -- the terminal window was closed mid-run. + finishRun(TERMINAL_CLOSED_CODE) else publish() end diff --git a/arch-updater/translations/de.json b/arch-updater/translations/de.json index 056aac66..074dbd25 100644 --- a/arch-updater/translations/de.json +++ b/arch-updater/translations/de.json @@ -35,6 +35,8 @@ "updated_today": "Heute aktualisiert" }, "caption_checked": "geprüft {time}", + "changelog_empty": "Kein Changelog verfügbar", + "changelog_title": "Was ist neu", "caption_ignored": { "one": "1 Paket ignoriert", "other": "{count} Pakete ignoriert" @@ -46,6 +48,7 @@ "err_no_xdg_open": "xdg-open nicht gefunden, Paketseite kann nicht geöffnet werden", "err_polkit_install": "Polkit regeln können nicht installiert werden, Details im Systemlog prüfen", "err_run_failed": "Update fehlgeschlagen (exit {code}), versuche es erneut im Terminal oder prüfe die Log-Datei", + "err_terminal_closed": "Das Terminal-Fenster wurde geschlossen, bevor das Update fertig war. Versuche es erneut im Terminal oder prüfe die Log-Datei", "err_spawn": "checkupdates konnte nicht gestartet werden", "history_rollback_tag": "Rollback", "history_title": "Verlauf aktualisieren", @@ -143,6 +146,10 @@ "description": "Zeichnet die Anzahl ausstehender Updates über die letzten Prüfungen sowie den letzten Update-Zeitpunkt auf und zeigt sie als kleines Diagramm im Panel. Deaktiviert stoppt auch die Aufzeichnung.", "label": "Aktivitätsdiagramm anzeigen" }, + "show_changelog_after_update": { + "description": "Öffnet den Changelog im Panel automatisch, sobald ein Update erfolgreich abgeschlossen wurde.", + "label": "Changelog nach dem Update anzeigen" + }, "show_count": { "description": "Zeigt die Anzahl ausstehender Updates neben dem Bar-Symbol.", "label": "Anzahl der Updates anzeigen" @@ -189,6 +196,7 @@ "status_rolling_back": "Rollback läuft…", "status_running": "Update läuft in einem Terminal…", "status_running_terminal": "Update läuft in einem Terminal-Fenster…", + "tip_changelog": "Zeigt, was sich in den einzelnen Versionen geändert hat", "tip_check": "Jetzt auf Updates prüfen", "tip_close": "Schließen", "tip_copy": "Name und Versionen kopieren", diff --git a/arch-updater/translations/en.json b/arch-updater/translations/en.json index 2d1543c3..3b9cea7a 100644 --- a/arch-updater/translations/en.json +++ b/arch-updater/translations/en.json @@ -35,6 +35,8 @@ "updated_today": "Updated today" }, "caption_checked": "checked {time}", + "changelog_empty": "No changelog available", + "changelog_title": "What's new", "caption_ignored": { "one": "1 package ignored", "other": "{count} packages ignored" @@ -49,6 +51,7 @@ "err_no_xdg_open": "xdg-open not found, cannot open the package page", "err_polkit_install": "Could not install the polkit rule, see the system log for details", "err_run_failed": "Update failed (exit {code}), check the log or retry in a terminal", + "err_terminal_closed": "The terminal window closed before the update finished, check the log or retry in a terminal", "err_spawn": "Could not run checkupdates", "history_rollback_tag": "rollback", "history_title": "Update history", @@ -160,6 +163,10 @@ "description": "After a successful rollback, add the rolled-back packages to the plugin's ignore list so the next check does not immediately offer them again.", "label": "Ignore packages after a rollback" }, + "show_changelog_after_update": { + "description": "Open the panel's changelog automatically once an update run finishes successfully.", + "label": "Show changelog after updating" + }, "show_activity_graph": { "description": "Track pending-update counts across recent checks and when you last updated, shown as a small graph above the update history strip. Off (default) also stops recording the history.", "label": "Show activity graph" @@ -210,6 +217,7 @@ "status_rolling_back": "Rolling back in the background…", "status_running": "Updating in the background…", "status_running_terminal": "Updating in a terminal window…", + "tip_changelog": "See what's new in each release", "tip_check": "Check for updates now", "tip_close": "Close", "tip_copy": "Copy name and versions",