From 3878d83c33953c36e9303e54fbda3adc02f70496 Mon Sep 17 00:00:00 2001 From: Yuuto <95591108+Reiling-Jeff@users.noreply.github.com> Date: Tue, 28 Jul 2026 05:23:15 +0200 Subject: [PATCH] feat: add arch-updater plugin (#121) * feat: add arch-updater plugin * fix(arch-updater): declare shelled-out commands, close Flatpak ignore gap, surface real check failures Address PR review: plugin.toml only listed pacman-contrib despite invoking awk, sed, test, uname, pacman, sh, sudo, flatpak, xdg-open, yay and paru; the Flatpak ignore list was enforced in the panel but not in the actual `flatpak update`; and checkupdates/AUR/Flatpak checks piped through awk/sed in a way that swallowed real command failures as an empty/clean result instead of an error. * fix(arch-updater): fix Luau long-string that broke service.luau parsing The Flatpak ignore-filter awk snippet used a [[ ]] long bracket string, but its own content ("skip[arr[i]] = 1") contains a literal ]] that closed the string early, corrupting everything after it and failing the whole file's parse (not just the Flatpak path). Switched to a [=[ ]=] long bracket, which isn't terminated by a bare ]]. --- arch-updater/README.md | 140 ++++++ arch-updater/launcher.luau | 103 ++++ arch-updater/panel.luau | 430 +++++++++++++++++ arch-updater/plugin.toml | 157 +++++++ arch-updater/service.luau | 747 ++++++++++++++++++++++++++++++ arch-updater/thumbnail.webp | Bin 0 -> 51044 bytes arch-updater/translations/de.json | 141 ++++++ arch-updater/translations/en.json | 144 ++++++ arch-updater/widget.luau | 153 ++++++ 9 files changed, 2015 insertions(+) create mode 100644 arch-updater/README.md create mode 100644 arch-updater/launcher.luau create mode 100644 arch-updater/panel.luau create mode 100644 arch-updater/plugin.toml create mode 100644 arch-updater/service.luau create mode 100644 arch-updater/thumbnail.webp create mode 100644 arch-updater/translations/de.json create mode 100644 arch-updater/translations/en.json create mode 100644 arch-updater/widget.luau diff --git a/arch-updater/README.md b/arch-updater/README.md new file mode 100644 index 0000000..e47453e --- /dev/null +++ b/arch-updater/README.md @@ -0,0 +1,140 @@ +# Arch Updater + +Check pacman, AUR and Flatpak for updates from the bar, with an estimated +download size and an Arch news heads-up before you upgrade. Runs the update +in a terminal window. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `yuuto/arch-updater` | +| Entries | Bar widget: `widget`; panel: `panel`; service: `service`; launcher: `launcher` | +| Launcher Prefix | `/arch` | + +## Requirements + +- `pacman-contrib` on `PATH` (for `checkupdates`), required. +- `pacman`, `sh`, `sudo`, `awk`, `sed`, `test` and `uname`, required — base + tools from any standard Arch install, used to run and parse the pacman + check, build the download size estimate, check the running kernel, and + run the plain-pacman upgrade. +- `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. +- A terminal emulator for the update run: Noctalia's own detection + (`$TERMINAL`, then `ghostty`, `kitty`, `alacritty`, `wezterm`, `foot`, + `konsole`, `gnome-terminal`, `ptyxis`, `xterm`), or the one named in the + **Terminal** setting. + +Everything except `pacman-contrib`, `pacman`, `sh`, `sudo`, `awk`, `sed`, +`test` and `uname` is optional. A missing optional tool is skipped, not +treated as an error. + +## Usage + +Add the `widget` bar widget from Noctalia's widget picker. Left click opens +the panel, right click checks for updates now, middle click opens the +widget's own settings. You can also open the panel directly or bind it in +your compositor: + +```sh +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 a copy +button (name and versions) and an open button (its page on archlinux.org, +the AUR, or Flathub). **Check Updates** queries all sources, **Update** opens +a terminal running the upgrade, **Dismiss** keeps the numbers but returns the +bar glyph to its resting colour. + +Type `/arch` in the launcher for quick actions (check, update, open news), or +`/arch ` to fuzzy-search the packages from the last check. Activating a +result opens that package's page. + +## Extras + +Not in the v4 plugin: + +- **Arch Linux news.** The news feed is checked periodically. An unread post + is flagged in the panel and the bar tooltip, with a button that opens the + news page and marks it read. Arch news is where manual-intervention steps + get announced, so it's worth seeing before a big upgrade. +- **Reboot recommendation.** Detected from whether the running kernel's + `/usr/lib/modules/` directory still exists, so it works for any + kernel flavour (`linux`, `linux-lts`, `linux-zen`, `linux-cachyos`, ...) + without naming one. Shown in the panel and the bar tooltip, and colours the + bar glyph independently of the pending count. +- **Estimated download size** for the pending pacman packages (`pacman -Si`), + shown before you commit to **Update**. +- **An ignore list that's actually enforced.** v4 only let you rewrite the + raw check/update commands. Here, packages in **Ignore packages** are + filtered out of both the count and the update run (`--ignore`), on top of + `pacman.conf`'s own `IgnorePkg`. +- **Auto-detected AUR helper**, with `yay`/`paru`/a custom command/off as + explicit overrides. +- **A generic package-page link** (`archlinux.org/packages`, + `aur.archlinux.org`, `flathub.org`) instead of hardcoded per-repo mirror + URLs, so it stays correct across Arch-based distros. + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `aur_helper` | `select` | `auto` | Which AUR helper to use: auto-detect (yay, then paru), `yay`, `paru`, a custom command, or off. | +| `aur_check_cmd` | `string` | *(empty)* | Custom AUR check command, only used when `aur_helper` is `custom`. Must print `name oldver -> newver` per line. | +| `flatpak_enabled` | `bool` | `true` | Also check and update Flatpak. Skipped automatically when `flatpak` isn't installed. | +| `ignore_packages` | `string_list` | *(empty)* | Package names excluded from the count and passed as `--ignore` on update. | +| `auto_check_hours` | `int` | `0` | Check automatically every N hours. `0` never checks on its own. | +| `notify_on_updates` | `bool` | `true` | Send a desktop notification when a check finds packages to upgrade. | +| `show_download_size` | `bool` | `true` | Show the estimated pacman download size (`pacman -Si`) in the panel. | +| `check_arch_news` | `bool` | `true` | Check the Arch Linux news feed and flag unread posts. | +| `check_reboot_needed` | `bool` | `true` | Flag when the running kernel is no longer installed on disk. | +| `terminal` | `string` | *(empty)* | Terminal command for the update run, e.g. `kitty`. Empty uses Noctalia's detection. | +| `assume_yes` | `bool` | `false` | Pass `--noconfirm` / `-y` so package managers do not ask for confirmation. | +| `update_cmd` | `string` | *(empty)* | Full override for the update command. Empty builds it from the settings above. | +| `glyph` | `glyph` | `package` | The glyph shown for the widget on the bar. | +| `show_count` | `bool` | `true` | Show the pending-update count next to the bar glyph. | +| `hide_on_empty` | `bool` | `false` | Hide the widget entirely when there is nothing to show. | + +## IPC + +```sh +noctalia msg plugin yuuto/arch-updater:service all check +noctalia msg plugin yuuto/arch-updater:service all update +noctalia msg plugin yuuto/arch-updater:service all dismiss +``` + +## Notes + +- **Commands spawned.** `checkupdates`; the configured AUR helper's `-Qua` + (or your custom command); `flatpak list` / `flatpak remote-ls --updates`; + `pacman -Si` for the download size (piped through `awk` to sum it); a local + `test -d` against `uname -r` for the reboot check; and, for the update, + your terminal running `sh` to launch `sudo pacman`/the AUR helper, + optionally `flatpak update` (filtered through `awk` when packages are + ignored, using `sed` to combine the Flatpak listings during the check). No + upgrade command runs outside the terminal. +- **Network.** `checkupdates`, the AUR helper and the Flatpak check contact + mirrors, the AUR RPC, or a Flatpak remote, same as the corresponding + upgrade would. The Arch news check fetches `archlinux.org/feeds/news/` on + its own schedule, independent of a manual check. +- **Privileges.** The plugin never elevates anything itself. Plain pacman + updates run via `sudo pacman -Syu` in the terminal, where `sudo` prompts + normally. An AUR helper handles its own privilege escalation as usual. +- **Files.** One small file in the plugin's data directory tracks the last + Arch news post you've read, so unread counts survive a restart. Nothing + else is written; `pacman.conf` is never modified. +- **Sizes are pacman-only.** AUR and Flatpak downloads aren't sized. Most AUR + packages build from source, where a download size wouldn't mean much. + +## Credits + +Ported from the v4 QML "Arch Updater" plugin (MIT), rebuilt for v5's Luau +plugin API with a different feature set. See **Extras** above. + +## License + +MIT. diff --git a/arch-updater/launcher.luau b/arch-updater/launcher.luau new file mode 100644 index 0000000..5867c96 --- /dev/null +++ b/arch-updater/launcher.luau @@ -0,0 +1,103 @@ +--!nonstrict +-- arch-updater launcher provider, under the `/arch` prefix. +-- +-- Empty query shows the three quick actions. Any other text is fuzzy-matched +-- against the pending packages from the last check, read straight from the +-- shared "arch_state" the engine publishes. Activating a package opens its +-- page, same as the panel's "open" button on a package row. + +local STATE_KEY = "arch_state" +local REQUEST_KEY = "arch_request" + +local function tr(key, args) + return noctalia.tr(key, args) +end + +local function request(action) + local prev = noctalia.state.get(REQUEST_KEY) + local nonce = (type(prev) == "table" and tonumber(prev.nonce) or 0) + 1 + noctalia.state.set(REQUEST_KEY, { nonce = nonce, action = action }) +end + +local function shellQuote(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function openUrl(url) + if noctalia.commandExists("xdg-open") then + noctalia.runAsync("xdg-open " .. shellQuote(url) .. " >/dev/null 2>&1") + end +end + +local SOURCE_GLYPH = { pacman = "package", aur = "box", flatpak = "app-window" } + +local function packageUrl(sourceKey, name) + if sourceKey == "pacman" then + return "https://archlinux.org/packages/?q=" .. noctalia.string.urlEncode(name) + elseif sourceKey == "aur" then + return "https://aur.archlinux.org/packages/" .. noctalia.string.urlEncode(name) + end + return "https://flathub.org/apps/" .. noctalia.string.urlEncode(name) +end + +local function commandResults() + return { + { id = "cmd-check", title = tr("action_check"), subtitle = tr("launcher.check_subtitle"), glyph = "refresh" }, + { id = "cmd-update", title = tr("action_update"), subtitle = tr("launcher.update_subtitle"), glyph = "download" }, + { id = "cmd-news", title = tr("action_open_news"), subtitle = tr("launcher.news_subtitle"), glyph = "news" }, + } +end + +local function packageResults(query) + local state = noctalia.state.get(STATE_KEY) + if type(state) ~= "table" then + return {} + end + + local results = {} + local sourceList = { { key = "pacman", entry = state.pacman }, { key = "aur", entry = state.aur }, { key = "flatpak", entry = state.flatpak } } + for _, source in ipairs(sourceList) do + local items = type(source.entry) == "table" and source.entry.items or nil + if type(items) == "table" then + for _, item in ipairs(items) do + local score = noctalia.fuzzyScore(query, item.name) + if score ~= nil then + local subtitle = (item.from ~= nil and item.from ~= "" and item.to ~= nil and item.to ~= "") + and (item.from .. " → " .. item.to) + or tr("source." .. source.key) + table.insert(results, { + id = "pkg-" .. source.key .. "-" .. item.name, + title = item.name, + subtitle = subtitle, + glyph = SOURCE_GLYPH[source.key] or "package", + score = score, + }) + end + end + end + end + return results +end + +function onQuery(query) + if query == "" then + launcher.setResults(query, commandResults()) + return + end + launcher.setResults(query, packageResults(query)) +end + +function onActivate(id) + if id == "cmd-check" then + request("check") + elseif id == "cmd-update" then + request("update") + elseif id == "cmd-news" then + request("open_news") + else + local sourceKey, name = id:match("^pkg%-([a-z]+)%-(.+)$") + if sourceKey ~= nil and name ~= nil then + openUrl(packageUrl(sourceKey, name)) + end + end +end diff --git a/arch-updater/panel.luau b/arch-updater/panel.luau new file mode 100644 index 0000000..bdf70d6 --- /dev/null +++ b/arch-updater/panel.luau @@ -0,0 +1,430 @@ +--!nonstrict +-- arch-updater update panel. Pure renderer over the shared state: the engine +-- (service.luau) publishes "arch_state" and performs the "arch_request" +-- actions this panel emits, so closing the panel never interrupts a check or +-- a run in progress. +-- +-- "Check Updates" only queries pacman, the AUR helper and (optionally) +-- Flatpak. Only then do "Update" (open a terminal and run the upgrade) and +-- "Dismiss" (keep the numbers, quiet the bar) light up. + +local STATE_KEY = "arch_state" +local REQUEST_KEY = "arch_request" + +local snapshot = nil +local expanded = {} -- source key -> the package list is open +local hoverKey = nil -- package row currently under the pointer +local hoverText = "" -- what the detail line shows +local listOpen = false -- at least one source is expanded this render + +local render + +local function tr(key, args) + return noctalia.tr(key, args) +end + +local function request(action) + local prev = noctalia.state.get(REQUEST_KEY) + local nonce = (type(prev) == "table" and tonumber(prev.nonce) or 0) + 1 + noctalia.state.set(REQUEST_KEY, { nonce = nonce, action = action }) +end + +local function shellQuote(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function openUrl(url) + if not noctalia.commandExists("xdg-open") then + noctalia.notifyError(tr("title"), tr("err_no_xdg_open")) + return + end + noctalia.runAsync("xdg-open " .. shellQuote(url) .. " >/dev/null 2>&1") +end + +-- Opens a generic Arch package search instead of a per-repo mirror URL, so +-- it stays correct across Arch-based distros. +local function openPackage(sourceKey, name) + if sourceKey == "pacman" then + openUrl("https://archlinux.org/packages/?q=" .. noctalia.string.urlEncode(name)) + elseif sourceKey == "aur" then + openUrl("https://aur.archlinux.org/packages/" .. noctalia.string.urlEncode(name)) + elseif sourceKey == "flatpak" then + openUrl("https://flathub.org/apps/" .. noctalia.string.urlEncode(name)) + end +end + +local function detailFor(item) + local from = item.from ~= nil and item.from or "" + local to = item.to ~= nil and item.to or "" + if to == "" then + return item.name + end + if from == "" then + return item.name .. " → " .. to + end + return item.name .. " " .. from .. " → " .. to +end + +local function phaseOf() + return snapshot ~= nil and snapshot.phase or "idle" +end + +local function totalOf() + return snapshot ~= nil and tonumber(snapshot.total) or 0 +end + +local function busy() + local phase = phaseOf() + return phase == "checking" or phase == "running" +end + +-- Only "checking" blocks a new check: "running" still allows one, since it +-- doubles as a manual unstick if the update's process-poll heuristic ever +-- misses the terminal finishing. +local function checking() + return phaseOf() == "checking" +end + +local function headline() + local phase = phaseOf() + if phase == "missing" then + return tr("status_missing"), "error" + elseif phase == "error" then + return (snapshot ~= nil and snapshot.err) or tr("status_error"), "error" + elseif phase == "checking" then + local step = snapshot.step + if step ~= nil and step ~= "" then + return tr("status_checking_step", { step = step }), "secondary" + end + return tr("status_checking"), "secondary" + elseif phase == "running" then + return tr("status_running"), "secondary" + elseif phase == "clean" then + return tr("status_clean"), "on_surface" + elseif phase == "ready" then + return noctalia.trp("status_ready", totalOf(), {}), "primary" + end + return tr("status_idle"), "on_surface_variant" +end + +local VERSION_WIDTH = 78 + +local function sourceLabel(key, entry) + if key == "aur" and type(entry.helper) == "string" and entry.helper ~= "" then + return tr("source.aur_named", { helper = entry.helper }) + end + return tr("source." .. key) +end + +-- pacman, then the AUR helper, then Flatpak. Only sources with pending +-- packages, biggest first. +local function orderedSources() + if snapshot == nil then + return {} + end + local candidates = { + { key = "pacman", entry = snapshot.pacman }, + { key = "aur", entry = snapshot.aur }, + { key = "flatpak", entry = snapshot.flatpak }, + } + local pending = {} + for _, candidate in ipairs(candidates) do + if type(candidate.entry) == "table" and (candidate.entry.n or 0) > 0 then + table.insert(pending, candidate) + end + end + table.sort(pending, function(a, b) + return a.entry.n > b.entry.n + end) + return pending +end + +local function cleanSources() + if snapshot == nil then + return {} + end + local names = {} + local candidates = { { key = "pacman", entry = snapshot.pacman }, { key = "flatpak", entry = snapshot.flatpak } } + if snapshot.aur ~= nil and (snapshot.aur.n or 0) == 0 and noctalia.getConfig("aur_helper") ~= "off" then + table.insert(candidates, { key = "aur", entry = snapshot.aur }) + end + for _, candidate in ipairs(candidates) do + if type(candidate.entry) == "table" and (candidate.entry.n or 0) == 0 then + table.insert(names, sourceLabel(candidate.key, candidate.entry)) + end + end + return names +end + +local function packageRow(sourceKey, index, item) + local key = "pkg-" .. sourceKey .. "-" .. index + local children = { + ui.label({ text = item.name, fontSize = 11, color = "on_surface", flexGrow = 1, maxLines = 1 }), + } + local from = item.from ~= nil and item.from or "" + local to = item.to ~= nil and item.to or "" + if to ~= "" then + if from ~= "" then + table.insert(children, ui.label({ + text = from, fontSize = 11, color = "on_surface_variant", maxWidth = VERSION_WIDTH, maxLines = 1, + })) + end + table.insert(children, ui.label({ text = "→", fontSize = 11, color = "on_surface_variant" })) + table.insert(children, ui.label({ + text = to, fontSize = 11, color = "primary", fontWeight = "semibold", maxWidth = VERSION_WIDTH, maxLines = 1, + })) + end + table.insert(children, ui.button({ + glyph = "copy", variant = "ghost", controlSize = "sm", tooltip = tr("tip_copy"), + onClick = function() + noctalia.copyToClipboard(detailFor(item), "text/plain") + end, + })) + table.insert(children, ui.button({ + glyph = "external-link", variant = "ghost", controlSize = "sm", tooltip = tr("tip_open_page"), + onClick = function() + openPackage(sourceKey, item.name) + end, + })) + return ui.row({ + key = key, + paddingH = 18, + gap = 4, + align = "center", + onHover = function(state) + if state == "true" then + hoverKey = key + hoverText = detailFor(item) + elseif hoverKey == key then + hoverKey = nil + hoverText = "" + else + return + end + render() + end, + }, children) +end + +local function sourceRows() + local rows = {} + for _, source in ipairs(orderedSources()) do + local names = type(source.entry.items) == "table" and source.entry.items or {} + local open = expanded[source.key] == true and #names > 0 + listOpen = listOpen or open + local header = { gap = 8, align = "center", key = "src-" .. source.key .. (open and "-open" or "") } + if #names > 0 then + local sourceKey = source.key + header.onClick = function() + expanded[sourceKey] = not expanded[sourceKey] + hoverKey = nil + hoverText = "" + render() + end + end + table.insert(rows, ui.row(header, { + ui.glyph({ name = #names == 0 and "point" or (open and "chevron-down" or "chevron-right"), size = 12, color = "on_surface_variant" }), + ui.label({ text = sourceLabel(source.key, source.entry), color = "on_surface", flexGrow = 1 }), + ui.label({ text = tostring(source.entry.n), color = "primary", fontWeight = "bold" }), + })) + + if open then + for index, item in ipairs(names) do + table.insert(rows, packageRow(source.key, index, item)) + end + if source.entry.n > #names then + table.insert(rows, ui.row({ key = "more-" .. source.key, paddingH = 18 }, { + ui.label({ text = tr("more_packages", { count = source.entry.n - #names }), fontSize = 11, color = "on_surface_variant" }), + })) + end + end + end + return rows +end + +-- Download size, reboot recommendation and Arch news. Each is its own line, +-- so turning one off in settings just removes that line. +local function extras() + local lines = {} + if snapshot == nil then + return lines + end + + if type(snapshot.downloadSizeMiB) == "number" then + local size = snapshot.downloadSizeMiB + local text = size >= 1024 and tr("size_gib", { value = string.format("%.2f", size / 1024) }) + or tr("size_mib", { value = string.format("%.1f", size) }) + table.insert(lines, ui.row({ key = "size", gap = 6, align = "center" }, { + ui.glyph({ name = "download", size = 13, color = "on_surface_variant" }), + ui.label({ text = text, fontSize = 12, color = "on_surface_variant" }), + })) + end + + if snapshot.rebootRecommended == true then + table.insert(lines, ui.row({ key = "reboot", gap = 6, align = "center" }, { + ui.glyph({ name = "alert-triangle", size = 13, color = "warning" }), + ui.label({ text = tr("reboot_recommended"), fontSize = 12, color = "warning", flexGrow = 1 }), + })) + end + + if (snapshot.newsUnread or 0) > 0 then + table.insert(lines, ui.row({ key = "news", gap = 6, align = "center" }, { + ui.glyph({ name = "news", size = 13, color = "on_surface" }), + ui.label({ + text = noctalia.trp("news_unread", snapshot.newsUnread, { title = snapshot.newsLatestTitle or "" }), + fontSize = 12, + color = "on_surface", + flexGrow = 1, + maxLines = 2, + }), + ui.button({ + text = tr("action_open_news"), variant = "ghost", controlSize = "sm", + onClick = function() + request("open_news") + end, + }), + })) + end + + return lines +end + +local function body() + local children = {} + + local rows = sourceRows() + if #rows > 0 then + table.insert(children, ui.scroll({ key = "sources", flexGrow = 1, gap = 6 }, rows)) + else + table.insert(children, ui.spacer({ key = "filler", flexGrow = 1 })) + end + + if listOpen then + table.insert(children, ui.label({ + key = "hover-detail", + text = hoverText ~= "" and hoverText or tr("hover_hint"), + fontSize = 11, + color = hoverText ~= "" and "on_surface" or "on_surface_variant", + maxLines = 1, + })) + end + + for _, line in ipairs(extras()) do + table.insert(children, line) + end + + local clean = cleanSources() + if #clean > 0 then + table.insert(children, ui.label({ + text = tr("up_to_date", { sources = table.concat(clean, ", ") }), + fontSize = 11, + color = "on_surface_variant", + maxLines = 2, + })) + end + + return children +end + +local function footerCaption() + if snapshot == nil or snapshot.checkedAt == nil or snapshot.checkedAt == "" then + return nil + end + local parts = { tr("caption_checked", { time = snapshot.checkedAt }) } + local ignored = tonumber(snapshot.ignoredCount) or 0 + if ignored > 0 then + table.insert(parts, noctalia.trp("caption_ignored", ignored, {})) + end + return table.concat(parts, " · ") +end + +render = function() + listOpen = false + + local text, color = headline() + local phase = phaseOf() + local hasUpdates = totalOf() > 0 and (phase == "ready" or phase == "running") + + local children = { + ui.row({ gap = 8, align = "center" }, { + ui.label({ text = tr("title"), fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }), + ui.button({ + key = "header-check" .. (checking() and "-off" or ""), + glyph = "refresh", + variant = "ghost", + enabled = not checking() and phase ~= "missing", + tooltip = tr("tip_check"), + onClick = function() + request("check") + end, + }), + ui.button({ + glyph = "close", variant = "ghost", tooltip = tr("tip_close"), + onClick = function() + panel.close() + end, + }), + }), + ui.label({ text = text, color = color, maxLines = 2 }), + } + + for _, node in ipairs(body()) do + table.insert(children, node) + end + + local caption = footerCaption() + if caption ~= nil then + table.insert(children, ui.separator({})) + table.insert(children, ui.label({ text = caption, fontSize = 11, color = "on_surface_variant", maxLines = 2 })) + end + + if phase ~= "missing" then + table.insert(children, ui.row({ gap = 8, align = "center" }, { + ui.button({ + key = "check" .. (checking() and "-off" or ""), + glyph = "refresh", text = tr("action_check"), variant = "ghost", enabled = not checking(), flexGrow = 1, + onClick = function() + request("check") + end, + }), + ui.button({ + key = "dismiss" .. (hasUpdates and "" or "-off"), + text = tr("action_dismiss"), variant = "ghost", enabled = hasUpdates and snapshot.dismissed ~= true, + onClick = function() + request("dismiss") + end, + }), + ui.button({ + key = "update" .. (hasUpdates and not busy() and "" or "-off"), + glyph = "download", text = tr("action_update"), variant = "primary", enabled = hasUpdates and not busy(), + tooltip = tr("tip_update"), + onClick = function() + request("update") + end, + }), + })) + end + + panel.render(ui.column({ flexGrow = 1, gap = 10, align = "stretch" }, children)) +end + +function onOpen(_context) + snapshot = noctalia.state.get(STATE_KEY) + expanded = {} + hoverKey = nil + hoverText = "" + render() +end + +noctalia.state.watch(STATE_KEY, function(value) + if type(value) ~= "table" then + return + end + if value.phase == "checking" and (snapshot == nil or snapshot.phase ~= "checking") then + expanded = {} + hoverKey = nil + hoverText = "" + end + snapshot = value + render() +end) diff --git a/arch-updater/plugin.toml b/arch-updater/plugin.toml new file mode 100644 index 0000000..c21e7d0 --- /dev/null +++ b/arch-updater/plugin.toml @@ -0,0 +1,157 @@ +id = "yuuto/arch-updater" +name = "Arch Updater" +version = "1.0.0" +plugin_api = 9 +author = "yuuto" +license = "MIT" +icon = "package" +description = "Check pacman, AUR and Flatpak updates, get an Arch news and reboot heads-up, then upgrade from a terminal." +dependencies = ["pacman-contrib", "awk", "flatpak", "pacman", "paru", "sed", "sh", "sudo", "test", "uname", "xdg-open", "yay"] +tags = ["arch", "bar", "panel", "launcher", "system", "utility"] + +# ── General ────────────────────────────────────────────────────────────────── + +[[setting]] +key = "aur_helper" +type = "select" +label_key = "settings.aur_helper.label" +description_key = "settings.aur_helper.description" +default = "auto" +options = [ + { value = "auto", label_key = "settings.aur_helper.options.auto" }, + { value = "yay", label_key = "settings.aur_helper.options.yay" }, + { value = "paru", label_key = "settings.aur_helper.options.paru" }, + { value = "custom", label_key = "settings.aur_helper.options.custom" }, + { value = "off", label_key = "settings.aur_helper.options.off" }, +] + +[[setting]] +key = "aur_check_cmd" +type = "string" +label_key = "settings.aur_check_cmd.label" +description_key = "settings.aur_check_cmd.description" +default = "" +visible_when = { key = "aur_helper", values = ["custom"] } + +[[setting]] +key = "flatpak_enabled" +type = "bool" +label_key = "settings.flatpak_enabled.label" +description_key = "settings.flatpak_enabled.description" +default = true + +[[setting]] +key = "ignore_packages" +type = "string_list" +label_key = "settings.ignore_packages.label" +description_key = "settings.ignore_packages.description" +default = [] + +[[setting]] +key = "auto_check_hours" +type = "int" +label_key = "settings.auto_check_hours.label" +description_key = "settings.auto_check_hours.description" +default = 0 +min = 0 +max = 168 + +[[setting]] +key = "notify_on_updates" +type = "bool" +label_key = "settings.notify_on_updates.label" +description_key = "settings.notify_on_updates.description" +default = true + +# ── Extras (not in the v4 plugin) ─────────────────────────────────────────── + +[[setting]] +key = "show_download_size" +type = "bool" +label_key = "settings.show_download_size.label" +description_key = "settings.show_download_size.description" +default = true + +[[setting]] +key = "check_arch_news" +type = "bool" +label_key = "settings.check_arch_news.label" +description_key = "settings.check_arch_news.description" +default = true + +[[setting]] +key = "check_reboot_needed" +type = "bool" +label_key = "settings.check_reboot_needed.label" +description_key = "settings.check_reboot_needed.description" +default = true + +# ── Update run ─────────────────────────────────────────────────────────────── + +[[setting]] +key = "terminal" +type = "string" +label_key = "settings.terminal.label" +description_key = "settings.terminal.description" +default = "" + +[[setting]] +key = "assume_yes" +type = "bool" +label_key = "settings.assume_yes.label" +description_key = "settings.assume_yes.description" +default = false +advanced = true + +[[setting]] +key = "update_cmd" +type = "string" +label_key = "settings.update_cmd.label" +description_key = "settings.update_cmd.description" +default = "" +advanced = true + +[[service]] +id = "service" +entry = "service.luau" + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 420 +height = 540 +placement = "attached" +position = "auto" +open_near_click = true + +[[widget]] +id = "widget" +entry = "widget.luau" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + description_key = "settings.glyph.description" + default = "package" + + [[widget.setting]] + key = "show_count" + type = "bool" + label_key = "settings.show_count.label" + description_key = "settings.show_count.description" + default = true + + [[widget.setting]] + key = "hide_on_empty" + type = "bool" + label_key = "settings.hide_on_empty.label" + description_key = "settings.hide_on_empty.description" + default = false + +[[launcher_provider]] +id = "launcher" +entry = "launcher.luau" +prefix = "arch" +glyph = "package" +include_in_global_search = false diff --git a/arch-updater/service.luau b/arch-updater/service.luau new file mode 100644 index 0000000..60e0a85 --- /dev/null +++ b/arch-updater/service.luau @@ -0,0 +1,747 @@ +--!nonstrict +-- arch-updater singleton engine. Checks pacman, the AUR helper and Flatpak, +-- publishes the result as shared state, and runs the update in a terminal. +-- +-- state "arch_state" = { nonce, phase, step, total, pacman, aur, +-- flatpak, downloadSizeMiB, rebootRecommended, +-- newsUnread, newsLatestTitle, dismissed, err, +-- checkedAt, ignoredCount } +-- requests "arch_request" = { nonce, action } -- check|update|dismiss +-- +-- Checking runs pacman, then the AUR helper, then Flatpak, then (if pacman +-- has pending packages) a `pacman -Si` pass for the download size. +-- +-- Update never runs in the background. runUpdate() opens a terminal so +-- pacman/the AUR helper can prompt and sudo can ask for a password, then +-- polls for the process to end and re-checks automatically. + +local STATE_KEY = "arch_state" +local REQUEST_KEY = "arch_request" +local NEWS_FILE = "news_state.json" +local NEWS_URL = "https://archlinux.org/feeds/news/" +local NEWS_PAGE = "https://archlinux.org/news/" + +local CHECK_TIMEOUT_MS = 45000 -- pacman/AUR/flatpak checks: each may sync a mirror +local SIZE_TIMEOUT_MS = 20000 -- pacman -Si: local db, no mirror sync +local FAST_TIMEOUT_MS = 5000 -- reboot check: local filesystem only +local NEWS_RECHECK_HOURS = 6 +local RUN_POLL_SECONDS = 2 +local RUN_GRACE_SECONDS = 12 +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 + +local phase = "idle" -- idle|checking|clean|ready|running|error|missing +local step = "" -- source label being checked (phase == "checking") +local total = 0 +local sources = { pacman = { n = 0, items = {} }, aur = { n = 0, items = {}, helper = "" }, flatpak = { n = 0, items = {} } } +local downloadSizeMiB = nil +local rebootRecommended = false +local newsUnread = 0 +local newsLatestTitle = nil +local newsItems = {} +local newsLastSeenGuid = nil +local dismissed = false +local errMsg = nil +local checkedAt = "" +local stateNonce = 0 +local lastRequestNonce = 0 + +local runTicks = 0 +local runSeen = false +local runPollTicks = 0 +local updateProcessName = "pacman" +local sinceCheck = 0 +local startupTicks = 0 +local sinceNewsCheck = 0 +local newsStateLoaded = false + +local startCheck +local checkNews + +local function cfg(key) + return noctalia.getConfig(key) +end + +local function tr(key, args) + return noctalia.tr(key, args) +end + +local function trim(value) + return noctalia.string.trim(value or "") +end + +local function shellQuote(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +-- Package names reach the command line (--ignore, pacman -Si), so only +-- pacman's own name grammar is accepted. Anything else is dropped with a log +-- line instead of being quoted. +local function ignoreList() + local raw = cfg("ignore_packages") + if type(raw) ~= "table" then + return {} + end + local names = {} + for _, entry in ipairs(raw) do + local name = trim(tostring(entry)) + if name:match("^[a-zA-Z0-9._+-]+$") ~= nil then + table.insert(names, name) + elseif name ~= "" then + noctalia.log("arch-updater: ignoring invalid package name '" .. name .. "'") + end + end + return names +end + +local function ignoreSet() + local set = {} + for _, name in ipairs(ignoreList()) do + set[name] = true + end + return set +end + +-- ── Parsing ────────────────────────────────────────────────────────────────── + +-- One "name oldver -> newver" line per package: checkupdates' format, which +-- yay -Qua and paru -Qua also use. +local function parseVersionLines(output, ignored) + local items = {} + local n = 0 + for line in (output or ""):gmatch("[^\n]+") do + local parts = {} + for token in line:gmatch("%S+") do + table.insert(parts, token) + end + if #parts >= 4 and not ignored[parts[1]] then + n += 1 + if #items < MAX_LISTED then + table.insert(items, { name = parts[1], from = parts[2], to = parts[4] }) + end + end + end + return n, items +end + +-- Flatpak has no "name oldver -> newver" line, so the query joins installed +-- and pending by application id (tab-separated name/from/to). +local function parseTabLines(output, ignored) + local items = {} + local n = 0 + for line in (output or ""):gmatch("[^\n]+") do + local fields = {} + for field in (line .. "\t"):gmatch("([^\t]*)\t") do + table.insert(fields, field) + end + local name = fields[1] or "" + if name ~= "" and not ignored[name] then + n += 1 + if #items < MAX_LISTED then + table.insert(items, { name = name, from = fields[2] or "", to = fields[3] or "" }) + end + end + end + return n, items +end + +-- ── AUR helper resolution ──────────────────────────────────────────────────── + +-- auto tries yay then paru. An explicit choice is trusted as-is and reported +-- missing instead of falling back to another helper. +local function resolveAurHelper() + local choice = cfg("aur_helper") + if choice == "off" then + return nil + end + if choice == "custom" then + return "custom" + end + if choice == "yay" or choice == "paru" then + return choice + end + if noctalia.commandExists("yay") then + return "yay" + end + if noctalia.commandExists("paru") then + return "paru" + end + return nil +end + +local function aurCheckCommand(helper) + if helper == "custom" then + local raw = trim(cfg("aur_check_cmd")) + return raw ~= "" and raw or nil + end + -- No 2>/dev/null and no formatting pipe here: stderr is inspected below + -- to tell a real failure from the "-Qua" family's usual no-updates exit + -- code, and the raw output already matches checkupdates' own format. + return helper .. " -Qua" +end + +-- ── Publishing ─────────────────────────────────────────────────────────────── + +local function publish() + stateNonce += 1 + noctalia.state.set(STATE_KEY, { + nonce = stateNonce, + phase = phase, + step = step, + total = total, + pacman = sources.pacman, + aur = sources.aur, + flatpak = sources.flatpak, + downloadSizeMiB = downloadSizeMiB, + rebootRecommended = rebootRecommended, + newsUnread = newsUnread, + newsLatestTitle = newsLatestTitle, + dismissed = dismissed, + err = errMsg, + checkedAt = checkedAt, + ignoredCount = #ignoreList(), + flatpakEnabled = cfg("flatpak_enabled") == true, + }) +end + +-- ── Checking pipeline: pacman → AUR → Flatpak → size → reboot → done ──────── + +local checkFlatpak +local checkSize +local checkReboot +local finishCheck + +local function failCheck(message) + phase = "error" + errMsg = message + publish() +end + +checkReboot = function() + if cfg("check_reboot_needed") ~= true then + rebootRecommended = false + finishCheck() + return + end + -- A kernel upgrade replaces the whole /usr/lib/modules/ tree. + -- Once the running kernel's own directory is gone, a reboot is what + -- switches to the new one. Works for any kernel flavour since it checks + -- against `uname -r` directly, without naming one. + local started = noctalia.runAsync( + [[test -d "/usr/lib/modules/$(uname -r)" && echo present || echo missing]], + function(result) + rebootRecommended = trim(result.stdout or "") == "missing" + finishCheck() + end, + FAST_TIMEOUT_MS + ) + if not started then + rebootRecommended = false + finishCheck() + end +end + +checkSize = function() + if cfg("show_download_size") ~= true or sources.pacman.n == 0 then + downloadSizeMiB = nil + checkReboot() + return + end + local names = {} + for _, item in ipairs(sources.pacman.items) do + table.insert(names, shellQuote(item.name)) + end + if #names == 0 then + -- More pending than MAX_LISTED kept a name for, so the size would + -- under-count. Left unknown instead of wrong. + downloadSizeMiB = nil + checkReboot() + return + end + local cmd = "LC_ALL=C pacman -Si " .. table.concat(names, " ") .. [[ 2>/dev/null | awk ' +/^Name/ { name=$3 } +/^Download Size/ && !(name in done) { + done[name]=1 + v=$4; u=$5 + gsub(",", ".", v) + if (u == "GiB") v = v * 1024 + else if (u == "KiB") v = v / 1024 + else if (u == "B") v = v / 1024 / 1024 + sum += v +} +END { printf "%.2f", sum }']] + local started = noctalia.runAsync(cmd, function(result) + local value = tonumber(trim(result.stdout or "")) + downloadSizeMiB = (not result.timedOut and result.exitCode == 0 and value ~= nil) and value or nil + checkReboot() + end, SIZE_TIMEOUT_MS) + if not started then + downloadSizeMiB = nil + checkReboot() + end +end + +checkFlatpak = function(ignored) + if cfg("flatpak_enabled") ~= true or not noctalia.commandExists("flatpak") then + sources.flatpak = { n = 0, items = {} } + checkSize() + return + end + step = tr("source.flatpak") + publish() + -- Flatpak tracks commits, so a version string often doesn't move across + -- an update. Short commits stand in when it doesn't, joined by + -- application id in one awk pass. + -- Each call's own output and exit code are captured before piping into + -- awk, so a real flatpak failure (e.g. no remote, network down) fails + -- the whole command instead of awk quietly succeeding on empty input. + local cmd = [[ +listOut=$(flatpak list --columns=application,version,active 2>/dev/null); listCode=$? +updOut=$(flatpak remote-ls --updates --columns=application,version,commit 2>/dev/null); updCode=$? +if [ "$listCode" -ne 0 ] || [ "$updCode" -ne 0 ]; then + exit 1 +fi +{ printf '%s\n' "$listOut" | sed 's/^/L /' + printf '%s\n' "$updOut" | sed 's/^/R /' +} | awk -F'\t' ' +{ tag=substr($1,1,1); app=substr($1,3) } +tag=="L" { v[app]=$2; c[app]=$3 } +tag=="R" { from=v[app]; to=$2 + if (from=="" || to=="" || from==to) { from=substr(c[app],1,7); to=substr($3,1,7) } + print app"\t"from"\t"to }']] + local started = noctalia.runAsync(cmd, function(result) + if result.timedOut then + sources.flatpak = { n = 0, items = {} } + checkSize() + return + end + if result.exitCode ~= 0 then + noctalia.log("arch-updater: flatpak check failed (exit " .. tostring(result.exitCode) .. ")") + failCheck(tr("err_flatpak_failed")) + return + end + local n, items = parseTabLines(result.stdout, ignored) + sources.flatpak = { n = n, items = items } + checkSize() + end, CHECK_TIMEOUT_MS) + if not started then + sources.flatpak = { n = 0, items = {} } + checkSize() + end +end + +local function checkAur(ignored) + local helper = resolveAurHelper() + if helper == nil then + sources.aur = { n = 0, items = {}, helper = "" } + checkFlatpak(ignored) + return + end + if helper ~= "custom" and not noctalia.commandExists(helper) then + sources.aur = { n = 0, items = {}, helper = "" } + checkFlatpak(ignored) + return + end + local cmd = aurCheckCommand(helper) + if cmd == nil then + sources.aur = { n = 0, items = {}, helper = "" } + checkFlatpak(ignored) + return + end + step = helper == "custom" and tr("source.aur") or tr("source.aur_named", { helper = helper }) + publish() + local started = noctalia.runAsync(cmd, function(result) + if result.timedOut then + sources.aur = { n = 0, items = {}, helper = "" } + checkFlatpak(ignored) + return + end + -- "-Qua" (like plain pacman -Qu) exits non-zero for "nothing to + -- upgrade" too, so exit code alone can't tell that apart from a real + -- failure. A real failure prints something to stderr; "no updates" + -- doesn't. + if result.exitCode ~= 0 and trim(result.stderr or "") ~= "" then + noctalia.log("arch-updater: " .. helper .. " -Qua failed: " .. trim(result.stderr)) + failCheck(tr("err_aur_failed")) + return + end + local n, items = parseVersionLines(result.stdout, ignored) + sources.aur = { n = n, items = items, helper = helper == "custom" and "" or helper } + checkFlatpak(ignored) + end, CHECK_TIMEOUT_MS) + if not started then + sources.aur = { n = 0, items = {}, helper = "" } + checkFlatpak(ignored) + end +end + +startCheck = function() + if phase == "checking" then + return + end + if not noctalia.commandExists("checkupdates") then + phase = "missing" + errMsg = tr("err_no_checkupdates") + publish() + return + end + + phase = "checking" + errMsg = nil + sources = { pacman = { n = 0, items = {} }, aur = { n = 0, items = {}, helper = "" }, flatpak = { n = 0, items = {} } } + downloadSizeMiB = nil + step = tr("source.pacman") + publish() + + local ignored = ignoreSet() + -- checkupdates exits 2 for "no updates" (not an error), 1 for a real + -- failure (mirror/db sync, etc). Normalize the former to 0 so only a + -- genuine failure reaches result.exitCode below. + local cmd = [[checkupdates 2>/dev/null; code=$?; if [ "$code" -eq 2 ]; then exit 0; fi; exit "$code"]] + local started = noctalia.runAsync(cmd, function(result) + if result.timedOut then + failCheck(tr("err_check_timeout")) + return + end + if result.exitCode ~= 0 then + noctalia.log("arch-updater: checkupdates failed (exit " .. tostring(result.exitCode) .. ")") + failCheck(tr("err_check_failed")) + return + end + local n, items = parseVersionLines(result.stdout, ignored) + sources.pacman = { n = n, items = items } + checkAur(ignored) + end, CHECK_TIMEOUT_MS) + + if not started then + failCheck(tr("err_spawn")) + end +end + +finishCheck = function() + total = sources.pacman.n + sources.aur.n + sources.flatpak.n + step = "" + phase = total > 0 and "ready" or "clean" + dismissed = false + checkedAt = noctalia.formatTime("%H:%M") + sinceCheck = 0 + publish() + if total > 0 and cfg("notify_on_updates") == true then + noctalia.notify(tr("title"), noctalia.trp("notify_updates", total, { count = total })) + end +end + +-- ── Arch Linux news ────────────────────────────────────────────────────────── + +local function newsStatePath() + local dir, err = noctalia.pluginDataDir() + if dir == nil then + noctalia.log("arch-updater: cannot resolve plugin data dir: " .. tostring(err)) + return nil + end + return dir .. "/" .. NEWS_FILE +end + +local function loadNewsState() + if newsStateLoaded then + return + end + newsStateLoaded = true + local path = newsStatePath() + local encoded = path ~= nil and noctalia.readFile(path) or nil + local ok, decoded = pcall(function() + return encoded ~= nil and noctalia.json.decode(encoded) or nil + end) + if ok and type(decoded) == "table" and type(decoded.lastSeenGuid) == "string" then + newsLastSeenGuid = decoded.lastSeenGuid + end +end + +local function saveNewsState() + local path = newsStatePath() + if path == nil then + return + end + local encoded = noctalia.json.encode({ lastSeenGuid = newsLastSeenGuid }) + if encoded ~= nil then + noctalia.writeFile(path, encoded) + end +end + +local HTML_ENTITIES = { + ["<"] = "<", + [">"] = ">", + ["""] = '"', + ["'"] = "'", + ["'"] = "'", + ["&"] = "&", +} + +local function unescapeHtml(text) + return (text:gsub("&#?%w+;", HTML_ENTITIES)) +end + +-- Plain RSS 2.0, so a few gmatch patterns are enough, no XML library needed. +-- Wrapped in pcall: a feed change degrades to no news data, not a crash. +local function parseNewsFeed(xml) + local items = {} + for block in xml:gmatch("(.-)") do + local title = block:match("(.-)") + local link = block:match("(.-)") + local guid = block:match("]*>(.-)") + if title ~= nil and link ~= nil then + table.insert(items, { + title = unescapeHtml(trim(title)), + link = trim(link), + guid = guid ~= nil and trim(guid) or trim(link), + }) + end + end + return items +end + +local function applyNewsItems(items) + newsItems = items + if #items == 0 then + newsUnread = 0 + newsLatestTitle = nil + return + end + newsLatestTitle = items[1].title + if newsLastSeenGuid == nil then + -- First run: today's news is the baseline, not a backlog to alert on. + newsLastSeenGuid = items[1].guid + saveNewsState() + newsUnread = 0 + return + end + local unread = 0 + for _, item in ipairs(items) do + if item.guid == newsLastSeenGuid then + break + end + unread += 1 + end + newsUnread = unread +end + +checkNews = function() + if cfg("check_arch_news") ~= true then + return + end + loadNewsState() + local ok = noctalia.http({ url = NEWS_URL }, function(res) + if not res.ok or res.body == nil or res.body == "" then + return + end + local parsed, items = pcall(parseNewsFeed, res.body) + if parsed and type(items) == "table" then + applyNewsItems(items) + publish() + end + end) + if not ok then + noctalia.log("arch-updater: could not start the Arch news request") + end +end + +-- Opens the news page and marks everything fetched so far as read. +local function openNews() + if #newsItems > 0 then + newsLastSeenGuid = newsItems[1].guid + saveNewsState() + newsUnread = 0 + publish() + end + noctalia.runAsync("xdg-open " .. shellQuote(NEWS_PAGE) .. " >/dev/null 2>&1") +end + +-- ── Updating ───────────────────────────────────────────────────────────────── + +-- Uses Noctalia's terminal discovery ($TERMINAL, then the usual emulators) +-- unless a terminal is configured. +local function launchTerminal(cmd) + local term = trim(cfg("terminal")) + if term == "" then + return noctalia.runInTerminal(cmd) + end + local first = term:match("^%S+") or term + local bin = first:match("([^/]+)$") or first + local separator = (bin == "gnome-terminal" or bin == "kgx" or bin == "ptyxis") and "--" or "-e" + return noctalia.runAsync(term .. " " .. separator .. " sh -lc " .. shellQuote(cmd)) +end + +-- Builds the update command from the AUR helper, ignore list and Flatpak +-- settings. "update_cmd" overrides it outright, for cases the built default +-- doesn't cover. +local function buildUpdateCommand() + local override = trim(cfg("update_cmd")) + if override ~= "" then + updateProcessName = "pacman" + return override + end + + local ignored = ignoreList() + local ignoreFlag = #ignored > 0 and (" --ignore " .. table.concat(ignored, ",")) or "" + local yesFlag = cfg("assume_yes") == true and " --noconfirm" or "" + + local helper = resolveAurHelper() + local parts = {} + if helper ~= nil and helper ~= "custom" and noctalia.commandExists(helper) then + table.insert(parts, helper .. " -Syu" .. yesFlag .. ignoreFlag) + updateProcessName = helper + else + table.insert(parts, "sudo pacman -Syu" .. yesFlag .. ignoreFlag) + updateProcessName = "pacman" + end + + if cfg("flatpak_enabled") == true and noctalia.commandExists("flatpak") then + local flatpakYes = cfg("assume_yes") == true and " -y" or "" + if #ignored > 0 then + -- Same ignore list as pacman/AUR: filter ignored refs out of the + -- pending Flatpak list before updating, so an app hidden from the + -- panel's count can't still slip in through a bare `flatpak update`. + local skipList = shellQuote(table.concat(ignored, "\n")) + table.insert( + parts, + "flatpak_refs=$(flatpak remote-ls --updates --columns=application 2>/dev/null | awk -v ignore=" + .. skipList + .. [=[ 'BEGIN { n = split(ignore, arr, "\n"); for (i = 1; i <= n; i++) skip[arr[i]] = 1 } !($0 in skip)'); ]=] + .. [[if [ -n "$flatpak_refs" ]; then flatpak update]] + .. flatpakYes + .. [[ $flatpak_refs; fi]] + ) + else + table.insert(parts, "flatpak update" .. flatpakYes) + end + end + + table.insert(parts, "echo; echo " .. shellQuote(tr("run.press_key")) .. "; read -n 1") + return table.concat(parts, "; ") +end + +local function runUpdate() + if phase == "running" or phase == "checking" then + return + end + if not noctalia.commandExists("checkupdates") then + phase = "missing" + errMsg = tr("err_no_checkupdates") + publish() + return + end + if not launchTerminal(buildUpdateCommand()) then + phase = "error" + errMsg = tr("err_no_terminal") + publish() + noctalia.notifyError(tr("title"), tr("err_no_terminal")) + return + end + phase = "running" + errMsg = nil + step = "" + runTicks = 0 + runPollTicks = 0 + runSeen = false + publish() +end + +-- Polls for the update process. Seeing it then losing it means the run +-- ended, and triggers a re-check. Never seeing it within the grace period +-- re-checks anyway instead of sitting in "running" forever. +local function pollRun() + runPollTicks += 1 + if runPollTicks < RUN_POLL_SECONDS then + return + end + runPollTicks = 0 + noctalia.processMatches(function(matched) + if phase ~= "running" then + return + end + if matched then + runSeen = true + return + end + if runSeen or runTicks >= RUN_GRACE_SECONDS then + startCheck() + end + end, updateProcessName) +end + +-- ── Requests, lifecycle ────────────────────────────────────────────────────── + +local function handle(action) + if action == "check" then + -- Also works as a manual unstick: if the process-poll heuristic in + -- pollRun() ever misses the update finishing, this forces a re-check + -- instead of leaving the UI stuck on "running". + startCheck() + elseif action == "update" then + runUpdate() + elseif action == "dismiss" then + if not dismissed then + dismissed = true + publish() + end + elseif action == "open_news" then + openNews() + end +end + +noctalia.state.watch(REQUEST_KEY, function(value) + if type(value) ~= "table" then + return + end + local nonce = tonumber(value.nonce) or 0 + if nonce <= lastRequestNonce then + return + end + lastRequestNonce = nonce + handle(value.action) +end) + +-- Scriptable control: +-- noctalia msg plugin yuuto/arch-updater:service all check +-- noctalia msg plugin yuuto/arch-updater:service all update +-- noctalia msg plugin yuuto/arch-updater:service all dismiss +function onIpc(event, _payload) + handle(event) +end + +function update() + if phase == "running" then + runTicks += 1 + pollRun() + elseif phase ~= "checking" then + local hours = tonumber(cfg("auto_check_hours")) or 0 + if hours > 0 then + if phase == "idle" then + startupTicks += 1 + if startupTicks >= AUTO_CHECK_DELAY then + startCheck() + end + else + sinceCheck += 1 + if sinceCheck >= hours * 3600 then + startCheck() + end + end + end + end + + if cfg("check_arch_news") == true then + sinceNewsCheck += 1 + if sinceNewsCheck >= NEWS_RECHECK_HOURS * 3600 or sinceNewsCheck == AUTO_CHECK_DELAY then + sinceNewsCheck = 0 + checkNews() + end + end +end + +noctalia.setUpdateInterval(1000) +if not noctalia.commandExists("checkupdates") then + phase = "missing" + errMsg = tr("err_no_checkupdates") +end +publish() diff --git a/arch-updater/thumbnail.webp b/arch-updater/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..1444c67f3ffefe0c85720fd5925ae3ca23aeb7ef GIT binary patch literal 51044 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

&@3$bj(BN(fc{q1phg% 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 literal 0 HcmV?d00001 diff --git a/arch-updater/translations/de.json b/arch-updater/translations/de.json new file mode 100644 index 0000000..4376e21 --- /dev/null +++ b/arch-updater/translations/de.json @@ -0,0 +1,141 @@ +{ + "title": "Arch Updater", + "action_check": "Updates prüfen", + "action_dismiss": "Verwerfen", + "action_update": "Aktualisieren", + "action_open_news": "News öffnen", + "status_idle": "Noch nicht geprüft", + "status_checking": "Prüfe auf Updates…", + "status_checking_step": "Prüfe {step}…", + "status_running": "Update läuft in einem Terminal…", + "status_clean": "System ist aktuell", + "status_error": "Prüfung fehlgeschlagen", + "status_missing": "checkupdates fehlt, pacman-contrib installieren", + "status_ready": { + "one": "1 Paket zu aktualisieren", + "other": "{count} Pakete zu aktualisieren" + }, + "err_no_checkupdates": "checkupdates nicht gefunden, pacman-contrib installieren und PATH prüfen", + "err_check_timeout": "Zeitüberschreitung beim Prüfen auf Updates", + "err_spawn": "checkupdates konnte nicht gestartet werden", + "err_no_terminal": "Kein Terminal-Emulator gefunden, in den Plugin-Einstellungen festlegen", + "err_no_xdg_open": "xdg-open nicht gefunden, Paketseite kann nicht geöffnet werden", + "source": { + "pacman": "Pacman", + "aur": "AUR", + "aur_named": "AUR ({helper})", + "flatpak": "Flatpak" + }, + "notify_updates": { + "one": "1 Paket zu aktualisieren", + "other": "{count} Pakete zu aktualisieren" + }, + "run": { + "press_key": "Beliebige Taste zum Schließen drücken" + }, + "more_packages": "+{count} weitere", + "hover_hint": "Zeigt beim Überfahren eines Pakets die vollständigen Versionen", + "up_to_date": "Aktuell: {sources}", + "caption_checked": "geprüft {time}", + "caption_ignored": { + "one": "1 Paket ignoriert", + "other": "{count} Pakete ignoriert" + }, + "size_mib": "≈ {value} MiB Downloadgröße", + "size_gib": "≈ {value} GiB Downloadgröße", + "reboot_recommended": "Neustart empfohlen, der laufende Kernel ist nicht mehr installiert", + "news_unread": { + "one": "1 ungelesener Arch-News-Beitrag: „{title}“", + "other": "{count} ungelesene Arch-News-Beiträge, zuletzt: „{title}“" + }, + "tip_check": "Jetzt auf Updates prüfen", + "tip_close": "Schließen", + "tip_update": "Update in einem Terminal-Fenster ausführen", + "tip_copy": "Name und Versionen kopieren", + "tip_open_page": "Paketseite öffnen", + "tooltip_status": "Status", + "tooltip_pending": "Ausstehend", + "tooltip_checked": "Geprüft", + "tooltip_reboot_key": "Neustart", + "tooltip_reboot_value": "empfohlen", + "tooltip_news": "Arch News", + "tooltip_news_value": { + "one": "1 ungelesen", + "other": "{count} ungelesen" + }, + "tooltip_hints": "Klick: Panel · rechts: jetzt prüfen", + "launcher": { + "check_subtitle": "Pacman, AUR und Flatpak auf Updates prüfen", + "update_subtitle": "Terminal öffnen und Update ausführen", + "news_subtitle": "Arch-Linux-News-Seite öffnen" + }, + "settings": { + "aur_helper": { + "label": "AUR-Helfer", + "description": "Welcher AUR-Helfer AUR-Pakete prüft und aktualisiert. „Automatisch erkennen“ versucht zuerst yay, dann paru. „Eigener Befehl“ verwendet den unten angegebenen Prüfbefehl.", + "options": { + "auto": "Automatisch erkennen (yay, dann paru)", + "yay": "yay", + "paru": "paru", + "custom": "Eigener Befehl", + "off": "Aus, nur Pacman" + } + }, + "aur_check_cmd": { + "label": "Eigener AUR-Prüfbefehl", + "description": "Wird nur verwendet, wenn oben „Eigener Befehl“ gewählt ist. Muss pro Paket eine Zeile im Format 'name oldver -> newver' ausgeben, wie 'yay -Qua' es tut." + }, + "flatpak_enabled": { + "label": "Flatpak einbeziehen", + "description": "Prüft zusätzlich (und führt beim Aktualisieren) 'flatpak update' aus. Wird automatisch übersprungen, wenn Flatpak nicht installiert ist." + }, + "ignore_packages": { + "label": "Pakete ignorieren", + "description": "Paketnamen, die aus der Zählung ausgeschlossen und beim Update zusätzlich als --ignore übergeben werden, ergänzend zu pacman.confs eigenem IgnorePkg." + }, + "auto_check_hours": { + "label": "Intervall für automatische Prüfung (Stunden)", + "description": "Prüft automatisch alle N Stunden auf Updates. 0 (Standard) prüft nie von selbst." + }, + "notify_on_updates": { + "label": "Bei gefundenen Updates benachrichtigen", + "description": "Sendet eine Desktop-Benachrichtigung, wenn eine Prüfung aktualisierbare Pakete findet." + }, + "show_download_size": { + "label": "Downloadgröße anzeigen", + "description": "Schätzt die gesamte Downloadgröße der ausstehenden Pacman-Pakete mit 'pacman -Si'. AUR- und Flatpak-Größen sind nicht enthalten." + }, + "check_arch_news": { + "label": "Arch-Linux-News prüfen", + "description": "Ruft den Arch-Linux-News-Feed ab und markiert ungelesene Beiträge im Panel." + }, + "check_reboot_needed": { + "label": "Auf nötigen Neustart prüfen", + "description": "Prüft, ob die Dateien des aktuell laufenden Kernels noch vorhanden sind. Fehlen sie, wurde ein neuerer Kernel installiert, und erst ein Neustart wechselt darauf." + }, + "terminal": { + "label": "Terminal", + "description": "Terminal-Befehl für den Update-Lauf, z. B. kitty oder ghostty. Leer (Standard) verwendet Noctalias eigene Terminal-Erkennung ($TERMINAL, dann die gängigen Emulatoren)." + }, + "assume_yes": { + "label": "Automatisch mit Ja bestätigen", + "description": "Übergibt --noconfirm / -y, damit Paketmanager nicht nach Bestätigung fragen. Aus bedeutet, du bestätigst jedes Paket im Terminal-Fenster selbst." + }, + "update_cmd": { + "label": "Eigener Update-Befehl", + "description": "Vollständige Überschreibung des im Terminal ausgeführten Befehls beim Aktualisieren. Leer (Standard) baut ihn aus AUR-Helfer, Ignorierliste, Flatpak und „Automatisch mit Ja bestätigen“ oben zusammen." + }, + "glyph": { + "label": "Bar-Symbol", + "description": "Das für das Widget in der Bar angezeigte Symbol." + }, + "show_count": { + "label": "Anzahl der Updates anzeigen", + "description": "Zeigt die Anzahl ausstehender Updates neben dem Bar-Symbol." + }, + "hide_on_empty": { + "label": "Ausblenden, wenn nichts anliegt", + "description": "Blendet das Widget vollständig aus, wenn keine Updates ausstehen und kein Neustart empfohlen wird. Aus (Standard) zeigt das Symbol immer." + } + } +} diff --git a/arch-updater/translations/en.json b/arch-updater/translations/en.json new file mode 100644 index 0000000..29a592b --- /dev/null +++ b/arch-updater/translations/en.json @@ -0,0 +1,144 @@ +{ + "title": "Arch Updater", + "action_check": "Check Updates", + "action_dismiss": "Dismiss", + "action_update": "Update", + "action_open_news": "Open news", + "status_idle": "Not checked yet", + "status_checking": "Checking for updates…", + "status_checking_step": "Checking {step}…", + "status_running": "Update running in a terminal…", + "status_clean": "System is up to date", + "status_error": "Update check failed", + "status_missing": "checkupdates not found, install pacman-contrib", + "status_ready": { + "one": "1 package to upgrade", + "other": "{count} packages to upgrade" + }, + "err_no_checkupdates": "checkupdates not found, install pacman-contrib and check your PATH", + "err_check_timeout": "Timed out while checking for updates", + "err_check_failed": "checkupdates failed, see the system log for details", + "err_aur_failed": "AUR check failed, see the system log for details", + "err_flatpak_failed": "Flatpak check failed, see the system log for details", + "err_spawn": "Could not run checkupdates", + "err_no_terminal": "No terminal emulator found, set one in the plugin settings", + "err_no_xdg_open": "xdg-open not found, cannot open the package page", + "source": { + "pacman": "Pacman", + "aur": "AUR", + "aur_named": "AUR ({helper})", + "flatpak": "Flatpak" + }, + "notify_updates": { + "one": "1 package to upgrade", + "other": "{count} packages to upgrade" + }, + "run": { + "press_key": "Press any key to close" + }, + "more_packages": "+{count} more", + "hover_hint": "Hover a package for its full versions", + "up_to_date": "Up to date: {sources}", + "caption_checked": "checked {time}", + "caption_ignored": { + "one": "1 package ignored", + "other": "{count} packages ignored" + }, + "size_mib": "≈ {value} MiB to download", + "size_gib": "≈ {value} GiB to download", + "reboot_recommended": "Reboot recommended, the running kernel is no longer installed", + "news_unread": { + "one": "1 unread Arch news post: \"{title}\"", + "other": "{count} unread Arch news posts, latest: \"{title}\"" + }, + "tip_check": "Check for updates now", + "tip_close": "Close", + "tip_update": "Run the update in a terminal window", + "tip_copy": "Copy name and versions", + "tip_open_page": "Open package page", + "tooltip_status": "Status", + "tooltip_pending": "Pending", + "tooltip_checked": "Checked", + "tooltip_reboot_key": "Reboot", + "tooltip_reboot_value": "recommended", + "tooltip_news": "Arch news", + "tooltip_news_value": { + "one": "1 unread", + "other": "{count} unread" + }, + "tooltip_hints": "click: panel · right: check now", + "launcher": { + "check_subtitle": "Check pacman, AUR and Flatpak for updates", + "update_subtitle": "Open a terminal and run the update", + "news_subtitle": "Open the Arch Linux news page" + }, + "settings": { + "aur_helper": { + "label": "AUR helper", + "description": "Which AUR helper checks and upgrades AUR packages. 'Auto-detect' tries yay, then paru. 'Custom command' lets you supply your own check command below.", + "options": { + "auto": "Auto-detect (yay, then paru)", + "yay": "yay", + "paru": "paru", + "custom": "Custom command", + "off": "Off, pacman only" + } + }, + "aur_check_cmd": { + "label": "Custom AUR check command", + "description": "Only used when the AUR helper above is 'Custom command'. Must print one 'name oldver -> newver' line per package, like 'yay -Qua' does." + }, + "flatpak_enabled": { + "label": "Include Flatpak", + "description": "Also check (and, on Update, run) 'flatpak update'. Ignored automatically when flatpak is not installed." + }, + "ignore_packages": { + "label": "Ignore packages", + "description": "Package names to leave out of the count and pass as --ignore to the update itself, in addition to pacman.conf's own IgnorePkg." + }, + "auto_check_hours": { + "label": "Auto-check interval (hours)", + "description": "Check for updates automatically every N hours. 0 (default) never checks on its own." + }, + "notify_on_updates": { + "label": "Notify when updates are found", + "description": "Send a desktop notification when a check finds packages to upgrade." + }, + "show_download_size": { + "label": "Show download size", + "description": "Estimate the total download size for the pending pacman packages with 'pacman -Si'. AUR and Flatpak sizes are not included." + }, + "check_arch_news": { + "label": "Check Arch Linux news", + "description": "Fetch the Arch Linux news feed and flag unread posts in the panel." + }, + "check_reboot_needed": { + "label": "Check if a reboot is needed", + "description": "Detect whether the currently running kernel's files are still on disk. When they are gone, a newer kernel was installed and a reboot is what switches you to it." + }, + "terminal": { + "label": "Terminal", + "description": "Terminal command used for the update run, e.g. kitty or ghostty. Empty (default) uses Noctalia's terminal detection ($TERMINAL, then the common emulators)." + }, + "assume_yes": { + "label": "Answer yes automatically", + "description": "Pass --noconfirm / -y so package managers do not ask for confirmation. Off means you confirm each one in the terminal window." + }, + "update_cmd": { + "label": "Custom update command", + "description": "Full override for the command run in the terminal on Update. Empty (default) builds it from the AUR helper, ignore list, Flatpak and 'Answer yes' settings above." + }, + "glyph": { + "label": "Bar glyph", + "description": "The glyph shown for the widget on the bar." + }, + "show_count": { + "label": "Show the update count", + "description": "Show the number of pending updates next to the bar glyph." + }, + "hide_on_empty": { + "label": "Hide when there is nothing to show", + "description": "Hide the widget entirely when there are no pending updates and no reboot recommendation. Off (default) always shows the glyph." + } + } +} diff --git a/arch-updater/widget.luau b/arch-updater/widget.luau new file mode 100644 index 0000000..a1a0302 --- /dev/null +++ b/arch-updater/widget.luau @@ -0,0 +1,153 @@ +--!nonstrict +-- arch-updater bar widget: pending-update badge and panel toggle. +-- +-- Pure renderer over the "arch_state" the engine (service.luau) publishes. +-- Every bar showing the widget agrees on the count without running any +-- command itself. Actions go back as "arch_request" entries, so one engine +-- owns the checks and the update run no matter how many widgets exist. +-- +-- Click mapping: +-- Left click: open/close the panel +-- Right click: check for updates now +-- +-- No middle-click handler: the host already binds it to the widget's own +-- settings, so a callback here would be dead code. + +local PANEL_ID = "yuuto/arch-updater:panel" +local REQUEST_KEY = "arch_request" +local STATE_KEY = "arch_state" + +local snapshot = nil + +local function tr(key, args) + return noctalia.tr(key, args) +end + +local function request(action) + local prev = noctalia.state.get(REQUEST_KEY) + local nonce = (type(prev) == "table" and tonumber(prev.nonce) or 0) + 1 + noctalia.state.set(REQUEST_KEY, { nonce = nonce, action = action }) +end + +local function pending() + return snapshot ~= nil + and snapshot.phase == "ready" + and snapshot.dismissed ~= true + and (snapshot.total or 0) > 0 +end + +-- "Pacman 12 · AUR (yay) 3 · Flatpak 1": non-zero sources only. +local function breakdown() + if snapshot == nil then + return "" + end + local parts = {} + local pacman = snapshot.pacman + if type(pacman) == "table" and (pacman.n or 0) > 0 then + table.insert(parts, tr("source.pacman") .. " " .. pacman.n) + end + local aur = snapshot.aur + if type(aur) == "table" and (aur.n or 0) > 0 then + local label = (aur.helper ~= nil and aur.helper ~= "") and tr("source.aur_named", { helper = aur.helper }) or tr("source.aur") + table.insert(parts, label .. " " .. aur.n) + end + local flatpak = snapshot.flatpak + if type(flatpak) == "table" and (flatpak.n or 0) > 0 then + table.insert(parts, tr("source.flatpak") .. " " .. flatpak.n) + end + return table.concat(parts, " · ") +end + +local function statusLabel() + if snapshot == nil then + return tr("status_idle") + end + local phase = snapshot.phase + if phase == "missing" then + return tr("status_missing") + elseif phase == "checking" then + local current = snapshot.step + if current ~= nil and current ~= "" then + return tr("status_checking_step", { step = current }) + end + return tr("status_checking") + elseif phase == "running" then + return tr("status_running") + elseif phase == "error" then + return snapshot.err or tr("status_error") + elseif phase == "clean" then + return tr("status_clean") + elseif phase == "ready" then + return noctalia.trp("status_ready", snapshot.total or 0, {}) + end + return tr("status_idle") +end + +local function render() + barWidget.setGlyph(noctalia.getConfig("glyph")) + + local phase = snapshot ~= nil and snapshot.phase or "idle" + local reboot = snapshot ~= nil and snapshot.rebootRecommended == true + if phase == "missing" or phase == "error" then + barWidget.setGlyphColor("error") + elseif phase == "checking" or phase == "running" then + barWidget.setGlyphColor("secondary") + elseif reboot then + barWidget.setGlyphColor("warning") + elseif pending() then + barWidget.setGlyphColor("primary") + else + barWidget.setGlyphColor("on_surface") + end + + if pending() and noctalia.getConfig("show_count") == true then + barWidget.setText(tostring(snapshot.total)) + else + barWidget.setText("") + end + + local empty = snapshot == nil or (snapshot.total or 0) == 0 + barWidget.setVisible(not (noctalia.getConfig("hide_on_empty") == true and empty and not reboot)) + + local rows = { { key = tr("tooltip_status"), value = statusLabel() } } + local detail = breakdown() + if detail ~= "" then + table.insert(rows, { key = tr("tooltip_pending"), value = detail }) + end + if reboot then + table.insert(rows, { key = tr("tooltip_reboot_key"), value = tr("tooltip_reboot_value") }) + end + if snapshot ~= nil and (snapshot.newsUnread or 0) > 0 then + table.insert(rows, { key = tr("tooltip_news"), value = noctalia.trp("tooltip_news_value", snapshot.newsUnread, {}) }) + end + if snapshot ~= nil and snapshot.checkedAt ~= nil and snapshot.checkedAt ~= "" then + table.insert(rows, { key = tr("tooltip_checked"), value = snapshot.checkedAt }) + end + table.insert(rows, { key = "", value = tr("tooltip_hints") }) + barWidget.setTooltip(rows) +end + +noctalia.state.watch(STATE_KEY, function(value) + if type(value) == "table" then + snapshot = value + render() + end +end) + +-- Periodic re-render keeps the glyph/visibility in sync with widget-setting +-- edits, which do not move the engine's state. +function update() + render() +end + +function onClick() + noctalia.togglePanel(PANEL_ID) +end + +function onRightClick() + request("check") +end + +noctalia.setUpdateInterval(1000) +snapshot = noctalia.state.get(STATE_KEY) +render()