fix(arch-updater): tighten package list UI and fix Dismiss/Update panel behavior (#289)
* fix(arch-updater): tighten package list spacing and icon size * fix(arch-updater): make Dismiss actually clear the pending list * fix(arch-updater): make hitting update close the panel * feat(arch-updater): add per-source icons to the package list header * feat(arch-updater): add an activity graph to the panel Tracks the pending-update count across recent checks and when the last update ran, persisted to disk so it survives restarts. Off also stops recording history, not just hiding it. * feat(arch-updater): show per-point detail on activity graph hover ui.graph has no pointer props of its own, so a row of ghost buttons sits under the line as per-point hit targets, each with a native tooltip. History entries now carry a timestamp and whether the check followed an update run, so hovering a point says when it happened and either its pending count or "Updated". * fix(arch-updater): load activity history at startup, not on first check loadHistoryState() only ran lazily inside recordCheck/recordUpdateRun, so a freshly started service published an empty history until a check completed, hiding the graph even when prior sessions had data on disk. * fix(arch-updater): render activity graph with sharp points, not curves
This commit is contained in:
@@ -47,8 +47,8 @@ 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
|
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,
|
button (name and versions) and an open button (its page on archlinux.org,
|
||||||
the AUR, or Flathub). **Check Updates** queries all sources, **Update** opens
|
the AUR, or Flathub). **Check Updates** queries all sources, **Update** opens
|
||||||
a terminal running the upgrade, **Dismiss** keeps the numbers but returns the
|
a terminal running the upgrade, **Dismiss** clears the pending list and
|
||||||
bar glyph to its resting colour.
|
closes the panel, returning the bar glyph to its resting colour.
|
||||||
|
|
||||||
Type `/arch` in the launcher for quick actions (check, update, open news), or
|
Type `/arch` in the launcher for quick actions (check, update, open news), or
|
||||||
`/arch <text>` to fuzzy-search the packages from the last check. Activating a
|
`/arch <text>` to fuzzy-search the packages from the last check. Activating a
|
||||||
@@ -78,6 +78,10 @@ Not in the v4 plugin:
|
|||||||
- **A generic package-page link** (`archlinux.org/packages`,
|
- **A generic package-page link** (`archlinux.org/packages`,
|
||||||
`aur.archlinux.org`, `flathub.org`) instead of hardcoded per-repo mirror
|
`aur.archlinux.org`, `flathub.org`) instead of hardcoded per-repo mirror
|
||||||
URLs, so it stays correct across Arch-based distros.
|
URLs, so it stays correct across Arch-based distros.
|
||||||
|
- **An activity graph.** A small trend line of the pending-update count
|
||||||
|
across the most recent checks, plus when you last ran an update. Persisted
|
||||||
|
to disk so it survives restarts. On by default, and turning it off also
|
||||||
|
stops recording the history, not just hiding it.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
@@ -92,6 +96,8 @@ Not in the v4 plugin:
|
|||||||
| `show_download_size` | `bool` | `true` | Show the estimated pacman download size (`pacman -Si`) in the panel. |
|
| `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_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. |
|
| `check_reboot_needed` | `bool` | `true` | Flag when the running kernel is no longer installed on disk. |
|
||||||
|
| `show_activity_graph` | `bool` | `true` | Track and show the pending-update trend and last-update time in the panel. Off also stops recording history. |
|
||||||
|
| `activity_history_length` | `int` | `10` | How many of the most recent checks to keep for the activity graph. |
|
||||||
| `terminal` | `string` | *(empty)* | Terminal command for the update run, e.g. `kitty`. Empty uses Noctalia's detection. |
|
| `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. |
|
| `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. |
|
| `update_cmd` | `string` | *(empty)* | Full override for the update command. Empty builds it from the settings above. |
|
||||||
|
|||||||
+194
-5
@@ -6,7 +6,7 @@
|
|||||||
--
|
--
|
||||||
-- "Check Updates" only queries pacman, the AUR helper and (optionally)
|
-- "Check Updates" only queries pacman, the AUR helper and (optionally)
|
||||||
-- Flatpak. Only then do "Update" (open a terminal and run the upgrade) and
|
-- Flatpak. Only then do "Update" (open a terminal and run the upgrade) and
|
||||||
-- "Dismiss" (keep the numbers, quiet the bar) light up.
|
-- "Dismiss" (clear the pending list and close the panel) light up.
|
||||||
|
|
||||||
local STATE_KEY = "arch_state"
|
local STATE_KEY = "arch_state"
|
||||||
local REQUEST_KEY = "arch_request"
|
local REQUEST_KEY = "arch_request"
|
||||||
@@ -15,6 +15,7 @@ local snapshot = nil
|
|||||||
local expanded = {} -- source key -> the package list is open
|
local expanded = {} -- source key -> the package list is open
|
||||||
local hoverKey = nil -- package row currently under the pointer
|
local hoverKey = nil -- package row currently under the pointer
|
||||||
local hoverText = "" -- what the detail line shows
|
local hoverText = "" -- what the detail line shows
|
||||||
|
local activityHoverIndex = nil -- activity graph point currently under the pointer
|
||||||
local listOpen = false -- at least one source is expanded this render
|
local listOpen = false -- at least one source is expanded this render
|
||||||
|
|
||||||
local render
|
local render
|
||||||
@@ -116,6 +117,12 @@ local function sourceLabel(key, entry)
|
|||||||
return tr("source." .. key)
|
return tr("source." .. key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local SOURCE_GLYPHS = { pacman = "package", aur = "cloud", flatpak = "app-window" }
|
||||||
|
|
||||||
|
local function sourceGlyph(key)
|
||||||
|
return SOURCE_GLYPHS[key] or "package"
|
||||||
|
end
|
||||||
|
|
||||||
-- pacman, then the AUR helper, then Flatpak. Only sources with pending
|
-- pacman, then the AUR helper, then Flatpak. Only sources with pending
|
||||||
-- packages, biggest first.
|
-- packages, biggest first.
|
||||||
local function orderedSources()
|
local function orderedSources()
|
||||||
@@ -175,13 +182,15 @@ local function packageRow(sourceKey, index, item)
|
|||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
table.insert(children, ui.button({
|
table.insert(children, ui.button({
|
||||||
glyph = "copy", variant = "ghost", controlSize = "sm", tooltip = tr("tip_copy"),
|
glyph = "copy", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12,
|
||||||
|
tooltip = tr("tip_copy"),
|
||||||
onClick = function()
|
onClick = function()
|
||||||
noctalia.copyToClipboard(detailFor(item), "text/plain")
|
noctalia.copyToClipboard(detailFor(item), "text/plain")
|
||||||
end,
|
end,
|
||||||
}))
|
}))
|
||||||
table.insert(children, ui.button({
|
table.insert(children, ui.button({
|
||||||
glyph = "external-link", variant = "ghost", controlSize = "sm", tooltip = tr("tip_open_page"),
|
glyph = "external-link", variant = "ghost", controlSize = "sm", width = 22, height = 22, glyphSize = 12,
|
||||||
|
tooltip = tr("tip_open_page"),
|
||||||
onClick = function()
|
onClick = function()
|
||||||
openPackage(sourceKey, item.name)
|
openPackage(sourceKey, item.name)
|
||||||
end,
|
end,
|
||||||
@@ -224,6 +233,7 @@ local function sourceRows()
|
|||||||
end
|
end
|
||||||
table.insert(rows, ui.row(header, {
|
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.glyph({ name = #names == 0 and "point" or (open and "chevron-down" or "chevron-right"), size = 12, color = "on_surface_variant" }),
|
||||||
|
ui.glyph({ name = sourceGlyph(source.key), size = 13, color = "on_surface_variant" }),
|
||||||
ui.label({ text = sourceLabel(source.key, source.entry), color = "on_surface", flexGrow = 1 }),
|
ui.label({ text = sourceLabel(source.key, source.entry), color = "on_surface", flexGrow = 1 }),
|
||||||
ui.label({ text = tostring(source.entry.n), color = "primary", fontWeight = "bold" }),
|
ui.label({ text = tostring(source.entry.n), color = "primary", fontWeight = "bold" }),
|
||||||
}))
|
}))
|
||||||
@@ -289,12 +299,182 @@ local function extras()
|
|||||||
return lines
|
return lines
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Normalizes the pending-count history to 0..1 for ui.graph, relative to the
|
||||||
|
-- min/max in the window (not a fixed scale, since pending counts vary wildly
|
||||||
|
-- between systems). A flat window (min == max, e.g. every check so far found
|
||||||
|
-- the same count) centers the line at 0.5 instead of pinning it to the top
|
||||||
|
-- edge, where it would be indistinguishable from the box border.
|
||||||
|
local function activityValues(history)
|
||||||
|
local minN, maxN = tonumber(history[1].n) or 0, tonumber(history[1].n) or 0
|
||||||
|
for _, entry in ipairs(history) do
|
||||||
|
local n = tonumber(entry.n) or 0
|
||||||
|
if n < minN then
|
||||||
|
minN = n
|
||||||
|
end
|
||||||
|
if n > maxN then
|
||||||
|
maxN = n
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local range = maxN - minN
|
||||||
|
local values = {}
|
||||||
|
for _, entry in ipairs(history) do
|
||||||
|
local n = tonumber(entry.n) or 0
|
||||||
|
table.insert(values, range > 0 and (n - minN) / range or 0.5)
|
||||||
|
end
|
||||||
|
return values
|
||||||
|
end
|
||||||
|
|
||||||
|
local ACTIVITY_GRAPH_SUBDIVISIONS = 8
|
||||||
|
|
||||||
|
local function upsampleLinear(values, subdivisions)
|
||||||
|
if #values < 2 or subdivisions <= 1 then
|
||||||
|
return values
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
for i = 1, #values - 1 do
|
||||||
|
local a, b = values[i], values[i + 1]
|
||||||
|
for s = 0, subdivisions - 1 do
|
||||||
|
table.insert(out, a + (b - a) * (s / subdivisions))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(out, values[#values])
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function padGraphLookbehind(values)
|
||||||
|
if #values == 0 then
|
||||||
|
return values
|
||||||
|
end
|
||||||
|
local out = { values[1], values[1] }
|
||||||
|
for _, v in ipairs(values) do
|
||||||
|
table.insert(out, v)
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "2 hours ago", "3 days ago", etc. nil for entries migrated from the old
|
||||||
|
-- history format, which never recorded a timestamp.
|
||||||
|
local function relativeTime(at)
|
||||||
|
local t = tonumber(at)
|
||||||
|
if t == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local diff = os.time() - t
|
||||||
|
if diff < 60 then
|
||||||
|
return tr("activity.just_now")
|
||||||
|
elseif diff < 3600 then
|
||||||
|
local m = math.floor(diff / 60)
|
||||||
|
return noctalia.trp("activity.minutes_ago", m, { count = m })
|
||||||
|
elseif diff < 86400 then
|
||||||
|
local h = math.floor(diff / 3600)
|
||||||
|
return noctalia.trp("activity.hours_ago", h, { count = h })
|
||||||
|
else
|
||||||
|
local d = math.floor(diff / 86400)
|
||||||
|
return noctalia.trp("activity.days_ago", d, { count = d })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "Updated · 2 hours ago" for the check that verified an update run, else
|
||||||
|
-- "3 pending updates · 2 hours ago". Shared by the hover tooltip and the
|
||||||
|
-- top-right caption so both describe a point the same way.
|
||||||
|
local function describeEntry(entry)
|
||||||
|
local n = tonumber(entry.n) or 0
|
||||||
|
local text = entry.afterUpdate == true and tr("activity.updated")
|
||||||
|
or noctalia.trp("activity.pending_at", n, { count = n })
|
||||||
|
local when = relativeTime(entry.at)
|
||||||
|
if when ~= nil then
|
||||||
|
text = text .. " · " .. when
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ui.graph takes no pointer props of its own (only row/column/box/image/button
|
||||||
|
-- do), so per-point hover is a row of hit targets placed right under the
|
||||||
|
-- line. Each is a ghost button so it can carry a native tooltip at the
|
||||||
|
-- pointer, not just a box for the hit test. It cannot highlight the point on
|
||||||
|
-- the line itself, only drive the tooltip and the caption above it.
|
||||||
|
--
|
||||||
|
-- Real points sit at (k-1)/(#history-1) of the width (see
|
||||||
|
-- padGraphLookbehind), i.e. #history-1 equal gaps, not #history equal slots
|
||||||
|
-- - so this builds one equal-width segment per gap rather than per entry,
|
||||||
|
-- ending each segment exactly on the point at its right edge. That leaves
|
||||||
|
-- entry 1 (at the left edge, with no gap before it) without its own hover
|
||||||
|
-- zone, but keeps every other spike landing right at the end of its segment
|
||||||
|
-- instead of drifting toward the start of an oversized one.
|
||||||
|
local function activityHoverRow(history)
|
||||||
|
local segments = {}
|
||||||
|
for i = 2, #history do
|
||||||
|
segments[i - 1] = ui.button({
|
||||||
|
key = "activity-hit-" .. i,
|
||||||
|
variant = "ghost",
|
||||||
|
flexGrow = 1,
|
||||||
|
height = 12,
|
||||||
|
tooltip = describeEntry(history[i]),
|
||||||
|
onHover = function(state)
|
||||||
|
if state == "true" then
|
||||||
|
activityHoverIndex = i
|
||||||
|
elseif activityHoverIndex == i then
|
||||||
|
activityHoverIndex = nil
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
render()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return ui.row({ key = "activity-hits", gap = 1 }, segments)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A small trend graph of pending-update counts across recent checks, plus
|
||||||
|
-- when the last update ran (or, while hovering a point, that point's own
|
||||||
|
-- description). Off entirely when show_activity_graph is off, and hidden
|
||||||
|
-- until there is enough history to draw a line.
|
||||||
|
local function activitySection()
|
||||||
|
if snapshot == nil or noctalia.getConfig("show_activity_graph") ~= true then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local history = type(snapshot.history) == "table" and snapshot.history or {}
|
||||||
|
if #history < 2 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local hoveredEntry = activityHoverIndex ~= nil and history[activityHoverIndex] or nil
|
||||||
|
local caption
|
||||||
|
if hoveredEntry ~= nil then
|
||||||
|
caption = describeEntry(hoveredEntry)
|
||||||
|
else
|
||||||
|
local lastUpdateAt = tonumber(snapshot.lastUpdateAt)
|
||||||
|
if lastUpdateAt == nil then
|
||||||
|
caption = tr("activity.never_updated")
|
||||||
|
else
|
||||||
|
local days = math.floor((os.time() - lastUpdateAt) / 86400)
|
||||||
|
caption = days <= 0 and tr("activity.updated_today")
|
||||||
|
or noctalia.trp("activity.updated_days_ago", days, { count = days })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ui.column({ key = "activity", gap = 4 }, {
|
||||||
|
ui.row({ justify = "space_between", align = "center" }, {
|
||||||
|
ui.label({ text = tr("activity.title"), fontSize = 11, fontWeight = "bold", color = "on_surface_variant" }),
|
||||||
|
ui.label({ text = caption, fontSize = 10, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.graph({
|
||||||
|
values = padGraphLookbehind(upsampleLinear(activityValues(history), ACTIVITY_GRAPH_SUBDIVISIONS)),
|
||||||
|
color = "primary",
|
||||||
|
fillOpacity = 0.15,
|
||||||
|
lineWidth = 2,
|
||||||
|
height = 36,
|
||||||
|
}),
|
||||||
|
activityHoverRow(history),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
local function body()
|
local function body()
|
||||||
local children = {}
|
local children = {}
|
||||||
|
|
||||||
local rows = sourceRows()
|
local rows = sourceRows()
|
||||||
if #rows > 0 then
|
if #rows > 0 then
|
||||||
table.insert(children, ui.scroll({ key = "sources", flexGrow = 1, gap = 6 }, rows))
|
table.insert(children, ui.scroll({ key = "sources", flexGrow = 1, gap = 3 }, rows))
|
||||||
else
|
else
|
||||||
table.insert(children, ui.spacer({ key = "filler", flexGrow = 1 }))
|
table.insert(children, ui.spacer({ key = "filler", flexGrow = 1 }))
|
||||||
end
|
end
|
||||||
@@ -323,6 +503,11 @@ local function body()
|
|||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local activity = activitySection()
|
||||||
|
if activity ~= nil then
|
||||||
|
table.insert(children, activity)
|
||||||
|
end
|
||||||
|
|
||||||
return children
|
return children
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -389,9 +574,10 @@ render = function()
|
|||||||
}),
|
}),
|
||||||
ui.button({
|
ui.button({
|
||||||
key = "dismiss" .. (hasUpdates and "" or "-off"),
|
key = "dismiss" .. (hasUpdates and "" or "-off"),
|
||||||
text = tr("action_dismiss"), variant = "ghost", enabled = hasUpdates and snapshot.dismissed ~= true,
|
text = tr("action_dismiss"), variant = "ghost", enabled = hasUpdates,
|
||||||
onClick = function()
|
onClick = function()
|
||||||
request("dismiss")
|
request("dismiss")
|
||||||
|
panel.close()
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
ui.button({
|
ui.button({
|
||||||
@@ -400,6 +586,7 @@ render = function()
|
|||||||
tooltip = tr("tip_update"),
|
tooltip = tr("tip_update"),
|
||||||
onClick = function()
|
onClick = function()
|
||||||
request("update")
|
request("update")
|
||||||
|
panel.close()
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
@@ -413,6 +600,7 @@ function onOpen(_context)
|
|||||||
expanded = {}
|
expanded = {}
|
||||||
hoverKey = nil
|
hoverKey = nil
|
||||||
hoverText = ""
|
hoverText = ""
|
||||||
|
activityHoverIndex = nil
|
||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -424,6 +612,7 @@ noctalia.state.watch(STATE_KEY, function(value)
|
|||||||
expanded = {}
|
expanded = {}
|
||||||
hoverKey = nil
|
hoverKey = nil
|
||||||
hoverText = ""
|
hoverText = ""
|
||||||
|
activityHoverIndex = nil
|
||||||
end
|
end
|
||||||
snapshot = value
|
snapshot = value
|
||||||
render()
|
render()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id = "yuuto/arch-updater"
|
id = "yuuto/arch-updater"
|
||||||
name = "Arch Updater"
|
name = "Arch Updater"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
plugin_api = 9
|
plugin_api = 9
|
||||||
author = "yuuto"
|
author = "yuuto"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -86,6 +86,25 @@ label_key = "settings.check_reboot_needed.label"
|
|||||||
description_key = "settings.check_reboot_needed.description"
|
description_key = "settings.check_reboot_needed.description"
|
||||||
default = true
|
default = true
|
||||||
|
|
||||||
|
# ── Activity ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "show_activity_graph"
|
||||||
|
type = "bool"
|
||||||
|
label_key = "settings.show_activity_graph.label"
|
||||||
|
description_key = "settings.show_activity_graph.description"
|
||||||
|
default = true
|
||||||
|
|
||||||
|
[[setting]]
|
||||||
|
key = "activity_history_length"
|
||||||
|
type = "int"
|
||||||
|
label_key = "settings.activity_history_length.label"
|
||||||
|
description_key = "settings.activity_history_length.description"
|
||||||
|
default = 10
|
||||||
|
min = 3
|
||||||
|
max = 30
|
||||||
|
visible_when = { key = "show_activity_graph", values = ["true"] }
|
||||||
|
|
||||||
# ── Update run ───────────────────────────────────────────────────────────────
|
# ── Update run ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[[setting]]
|
[[setting]]
|
||||||
|
|||||||
+106
-9
@@ -4,8 +4,8 @@
|
|||||||
--
|
--
|
||||||
-- state "arch_state" = { nonce, phase, step, total, pacman, aur,
|
-- state "arch_state" = { nonce, phase, step, total, pacman, aur,
|
||||||
-- flatpak, downloadSizeMiB, rebootRecommended,
|
-- flatpak, downloadSizeMiB, rebootRecommended,
|
||||||
-- newsUnread, newsLatestTitle, dismissed, err,
|
-- newsUnread, newsLatestTitle, err,
|
||||||
-- checkedAt, ignoredCount }
|
-- checkedAt, ignoredCount, history, lastUpdateAt }
|
||||||
-- requests "arch_request" = { nonce, action } -- check|update|dismiss
|
-- requests "arch_request" = { nonce, action } -- check|update|dismiss
|
||||||
--
|
--
|
||||||
-- Checking runs pacman, then the AUR helper, then Flatpak, then (if pacman
|
-- Checking runs pacman, then the AUR helper, then Flatpak, then (if pacman
|
||||||
@@ -20,6 +20,7 @@ local REQUEST_KEY = "arch_request"
|
|||||||
local NEWS_FILE = "news_state.json"
|
local NEWS_FILE = "news_state.json"
|
||||||
local NEWS_URL = "https://archlinux.org/feeds/news/"
|
local NEWS_URL = "https://archlinux.org/feeds/news/"
|
||||||
local NEWS_PAGE = "https://archlinux.org/news/"
|
local NEWS_PAGE = "https://archlinux.org/news/"
|
||||||
|
local HISTORY_FILE = "history_state.json"
|
||||||
|
|
||||||
local CHECK_TIMEOUT_MS = 45000 -- pacman/AUR/flatpak checks: each may sync a mirror
|
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 SIZE_TIMEOUT_MS = 20000 -- pacman -Si: local db, no mirror sync
|
||||||
@@ -40,7 +41,6 @@ local newsUnread = 0
|
|||||||
local newsLatestTitle = nil
|
local newsLatestTitle = nil
|
||||||
local newsItems = {}
|
local newsItems = {}
|
||||||
local newsLastSeenGuid = nil
|
local newsLastSeenGuid = nil
|
||||||
local dismissed = false
|
|
||||||
local errMsg = nil
|
local errMsg = nil
|
||||||
local checkedAt = ""
|
local checkedAt = ""
|
||||||
local stateNonce = 0
|
local stateNonce = 0
|
||||||
@@ -55,6 +55,10 @@ local startupTicks = 0
|
|||||||
local sinceNewsCheck = 0
|
local sinceNewsCheck = 0
|
||||||
local newsStateLoaded = false
|
local newsStateLoaded = false
|
||||||
local newsDirty = false
|
local newsDirty = false
|
||||||
|
local history = {} -- { n, at, afterUpdate } per check, oldest first, trimmed to activity_history_length
|
||||||
|
local lastUpdateAt = nil -- os.time() of the last update run that finished
|
||||||
|
local historyStateLoaded = false
|
||||||
|
local checkIsPostUpdate = false -- next finished check followed an update run
|
||||||
|
|
||||||
local startCheck
|
local startCheck
|
||||||
local checkNews
|
local checkNews
|
||||||
@@ -197,10 +201,11 @@ local function publish()
|
|||||||
rebootRecommended = rebootRecommended,
|
rebootRecommended = rebootRecommended,
|
||||||
newsUnread = newsUnread,
|
newsUnread = newsUnread,
|
||||||
newsLatestTitle = newsLatestTitle,
|
newsLatestTitle = newsLatestTitle,
|
||||||
dismissed = dismissed,
|
|
||||||
err = errMsg,
|
err = errMsg,
|
||||||
checkedAt = checkedAt,
|
checkedAt = checkedAt,
|
||||||
ignoredCount = #ignoreList(),
|
ignoredCount = #ignoreList(),
|
||||||
|
history = history,
|
||||||
|
lastUpdateAt = lastUpdateAt,
|
||||||
flatpakEnabled = cfg("flatpak_enabled") == true,
|
flatpakEnabled = cfg("flatpak_enabled") == true,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -211,6 +216,7 @@ local checkFlatpak
|
|||||||
local checkSize
|
local checkSize
|
||||||
local checkReboot
|
local checkReboot
|
||||||
local finishCheck
|
local finishCheck
|
||||||
|
local recordCheck
|
||||||
|
|
||||||
local function failCheck(message)
|
local function failCheck(message)
|
||||||
phase = "error"
|
phase = "error"
|
||||||
@@ -423,9 +429,9 @@ finishCheck = function()
|
|||||||
total = sources.pacman.n + sources.aur.n + sources.flatpak.n
|
total = sources.pacman.n + sources.aur.n + sources.flatpak.n
|
||||||
step = ""
|
step = ""
|
||||||
phase = total > 0 and "ready" or "clean"
|
phase = total > 0 and "ready" or "clean"
|
||||||
dismissed = false
|
|
||||||
checkedAt = noctalia.formatTime("%H:%M")
|
checkedAt = noctalia.formatTime("%H:%M")
|
||||||
sinceCheck = 0
|
sinceCheck = 0
|
||||||
|
recordCheck()
|
||||||
publish()
|
publish()
|
||||||
if total > 0 and cfg("notify_on_updates") == true then
|
if total > 0 and cfg("notify_on_updates") == true then
|
||||||
noctalia.notify(tr("title"), noctalia.trp("notify_updates", total, { count = total }))
|
noctalia.notify(tr("title"), noctalia.trp("notify_updates", total, { count = total }))
|
||||||
@@ -469,6 +475,92 @@ local function saveNewsState()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- ── Activity history ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function historyStatePath()
|
||||||
|
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 .. "/" .. HISTORY_FILE
|
||||||
|
end
|
||||||
|
|
||||||
|
local function loadHistoryState()
|
||||||
|
if historyStateLoaded then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
historyStateLoaded = true
|
||||||
|
local path = historyStatePath()
|
||||||
|
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" then
|
||||||
|
if type(decoded.history) == "table" then
|
||||||
|
-- Migrates the old format (a plain array of counts) to entries with
|
||||||
|
-- a timestamp and an afterUpdate flag, both unknown for old data.
|
||||||
|
local migrated = {}
|
||||||
|
for _, entry in ipairs(decoded.history) do
|
||||||
|
if type(entry) == "table" then
|
||||||
|
table.insert(migrated, {
|
||||||
|
n = tonumber(entry.n) or 0,
|
||||||
|
at = tonumber(entry.at),
|
||||||
|
afterUpdate = entry.afterUpdate == true,
|
||||||
|
})
|
||||||
|
elseif type(entry) == "number" then
|
||||||
|
table.insert(migrated, { n = entry, at = nil, afterUpdate = false })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
history = migrated
|
||||||
|
end
|
||||||
|
if type(decoded.lastUpdateAt) == "number" then
|
||||||
|
lastUpdateAt = decoded.lastUpdateAt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function saveHistoryState()
|
||||||
|
local path = historyStatePath()
|
||||||
|
if path == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local encoded = noctalia.json.encode({ history = history, lastUpdateAt = lastUpdateAt })
|
||||||
|
if encoded ~= nil then
|
||||||
|
noctalia.writeFile(path, encoded)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Appends the current total to the activity history, trimmed to the
|
||||||
|
-- configured length. A no-op when the graph is turned off, so disabling it
|
||||||
|
-- also stops collecting data, not just hides it.
|
||||||
|
recordCheck = function()
|
||||||
|
local wasPostUpdate = checkIsPostUpdate
|
||||||
|
checkIsPostUpdate = false
|
||||||
|
if cfg("show_activity_graph") ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
loadHistoryState()
|
||||||
|
table.insert(history, { n = total, at = os.time(), afterUpdate = wasPostUpdate })
|
||||||
|
local maxLen = math.max(3, math.min(30, tonumber(cfg("activity_history_length")) or 10))
|
||||||
|
while #history > maxLen do
|
||||||
|
table.remove(history, 1)
|
||||||
|
end
|
||||||
|
saveHistoryState()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Marks the check that follows as the one that verifies an update run, so its
|
||||||
|
-- history entry can say "Updated" instead of just a pending count.
|
||||||
|
local function recordUpdateRun()
|
||||||
|
checkIsPostUpdate = true
|
||||||
|
if cfg("show_activity_graph") ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
loadHistoryState()
|
||||||
|
lastUpdateAt = os.time()
|
||||||
|
saveHistoryState()
|
||||||
|
end
|
||||||
|
|
||||||
local HTML_ENTITIES = {
|
local HTML_ENTITIES = {
|
||||||
["<"] = "<",
|
["<"] = "<",
|
||||||
[">"] = ">",
|
[">"] = ">",
|
||||||
@@ -665,6 +757,7 @@ local function pollRun()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if runSeen or runTicks >= RUN_GRACE_SECONDS then
|
if runSeen or runTicks >= RUN_GRACE_SECONDS then
|
||||||
|
recordUpdateRun()
|
||||||
startCheck()
|
startCheck()
|
||||||
end
|
end
|
||||||
end, updateProcessName)
|
end, updateProcessName)
|
||||||
@@ -681,10 +774,13 @@ local function handle(action)
|
|||||||
elseif action == "update" then
|
elseif action == "update" then
|
||||||
runUpdate()
|
runUpdate()
|
||||||
elseif action == "dismiss" then
|
elseif action == "dismiss" then
|
||||||
if not dismissed then
|
sources.pacman = { n = 0, items = {} }
|
||||||
dismissed = true
|
sources.aur = { n = 0, items = {}, helper = sources.aur.helper }
|
||||||
publish()
|
sources.flatpak = { n = 0, items = {} }
|
||||||
end
|
total = 0
|
||||||
|
downloadSizeMiB = nil
|
||||||
|
phase = "clean"
|
||||||
|
publish()
|
||||||
elseif action == "open_news" then
|
elseif action == "open_news" then
|
||||||
openNews()
|
openNews()
|
||||||
end
|
end
|
||||||
@@ -750,4 +846,5 @@ if not noctalia.commandExists("checkupdates") then
|
|||||||
phase = "missing"
|
phase = "missing"
|
||||||
errMsg = tr("err_no_checkupdates")
|
errMsg = tr("err_no_checkupdates")
|
||||||
end
|
end
|
||||||
|
loadHistoryState() -- so the graph shows prior sessions' data before the first check runs
|
||||||
publish()
|
publish()
|
||||||
|
|||||||
@@ -3,6 +3,33 @@
|
|||||||
"action_dismiss": "Verwerfen",
|
"action_dismiss": "Verwerfen",
|
||||||
"action_open_news": "News öffnen",
|
"action_open_news": "News öffnen",
|
||||||
"action_update": "Aktualisieren",
|
"action_update": "Aktualisieren",
|
||||||
|
"activity": {
|
||||||
|
"days_ago": {
|
||||||
|
"one": "vor 1 Tag",
|
||||||
|
"other": "vor {count} Tagen"
|
||||||
|
},
|
||||||
|
"hours_ago": {
|
||||||
|
"one": "vor 1 Stunde",
|
||||||
|
"other": "vor {count} Stunden"
|
||||||
|
},
|
||||||
|
"just_now": "gerade eben",
|
||||||
|
"minutes_ago": {
|
||||||
|
"one": "vor 1 Minute",
|
||||||
|
"other": "vor {count} Minuten"
|
||||||
|
},
|
||||||
|
"never_updated": "Noch nie aktualisiert",
|
||||||
|
"pending_at": {
|
||||||
|
"one": "1 ausstehendes Update",
|
||||||
|
"other": "{count} ausstehende Updates"
|
||||||
|
},
|
||||||
|
"title": "Aktivität",
|
||||||
|
"updated": "Aktualisiert",
|
||||||
|
"updated_days_ago": {
|
||||||
|
"one": "Vor 1 Tag aktualisiert",
|
||||||
|
"other": "Vor {count} Tagen aktualisiert"
|
||||||
|
},
|
||||||
|
"updated_today": "Heute aktualisiert"
|
||||||
|
},
|
||||||
"caption_checked": "geprüft {time}",
|
"caption_checked": "geprüft {time}",
|
||||||
"caption_ignored": {
|
"caption_ignored": {
|
||||||
"one": "1 Paket ignoriert",
|
"one": "1 Paket ignoriert",
|
||||||
@@ -33,6 +60,10 @@
|
|||||||
"press_key": "Beliebige Taste zum Schließen drücken"
|
"press_key": "Beliebige Taste zum Schließen drücken"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"activity_history_length": {
|
||||||
|
"description": "Wie viele der letzten Prüfungen für das Aktivitätsdiagramm aufbewahrt werden.",
|
||||||
|
"label": "Länge des Aktivitätsverlaufs"
|
||||||
|
},
|
||||||
"assume_yes": {
|
"assume_yes": {
|
||||||
"description": "Übergibt --noconfirm / -y, damit Paketmanager nicht nach Bestätigung fragen. Aus bedeutet, du bestätigst jedes Paket im Terminal-Fenster selbst.",
|
"description": "Übergibt --noconfirm / -y, damit Paketmanager nicht nach Bestätigung fragen. Aus bedeutet, du bestätigst jedes Paket im Terminal-Fenster selbst.",
|
||||||
"label": "Automatisch mit Ja bestätigen"
|
"label": "Automatisch mit Ja bestätigen"
|
||||||
@@ -84,6 +115,10 @@
|
|||||||
"description": "Sendet eine Desktop-Benachrichtigung, wenn eine Prüfung aktualisierbare Pakete findet.",
|
"description": "Sendet eine Desktop-Benachrichtigung, wenn eine Prüfung aktualisierbare Pakete findet.",
|
||||||
"label": "Bei gefundenen Updates benachrichtigen"
|
"label": "Bei gefundenen Updates benachrichtigen"
|
||||||
},
|
},
|
||||||
|
"show_activity_graph": {
|
||||||
|
"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_count": {
|
"show_count": {
|
||||||
"description": "Zeigt die Anzahl ausstehender Updates neben dem Bar-Symbol.",
|
"description": "Zeigt die Anzahl ausstehender Updates neben dem Bar-Symbol.",
|
||||||
"label": "Anzahl der Updates anzeigen"
|
"label": "Anzahl der Updates anzeigen"
|
||||||
|
|||||||
@@ -3,6 +3,33 @@
|
|||||||
"action_dismiss": "Dismiss",
|
"action_dismiss": "Dismiss",
|
||||||
"action_open_news": "Open news",
|
"action_open_news": "Open news",
|
||||||
"action_update": "Update",
|
"action_update": "Update",
|
||||||
|
"activity": {
|
||||||
|
"days_ago": {
|
||||||
|
"one": "1 day ago",
|
||||||
|
"other": "{count} days ago"
|
||||||
|
},
|
||||||
|
"hours_ago": {
|
||||||
|
"one": "1 hour ago",
|
||||||
|
"other": "{count} hours ago"
|
||||||
|
},
|
||||||
|
"just_now": "just now",
|
||||||
|
"minutes_ago": {
|
||||||
|
"one": "1 minute ago",
|
||||||
|
"other": "{count} minutes ago"
|
||||||
|
},
|
||||||
|
"never_updated": "Never updated",
|
||||||
|
"pending_at": {
|
||||||
|
"one": "1 pending update",
|
||||||
|
"other": "{count} pending updates"
|
||||||
|
},
|
||||||
|
"title": "Activity",
|
||||||
|
"updated": "Updated",
|
||||||
|
"updated_days_ago": {
|
||||||
|
"one": "Updated 1 day ago",
|
||||||
|
"other": "Updated {count} days ago"
|
||||||
|
},
|
||||||
|
"updated_today": "Updated today"
|
||||||
|
},
|
||||||
"caption_checked": "checked {time}",
|
"caption_checked": "checked {time}",
|
||||||
"caption_ignored": {
|
"caption_ignored": {
|
||||||
"one": "1 package ignored",
|
"one": "1 package ignored",
|
||||||
@@ -36,6 +63,10 @@
|
|||||||
"press_key": "Press any key to close"
|
"press_key": "Press any key to close"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"activity_history_length": {
|
||||||
|
"description": "How many of the most recent checks to keep for the activity graph.",
|
||||||
|
"label": "Activity history length"
|
||||||
|
},
|
||||||
"assume_yes": {
|
"assume_yes": {
|
||||||
"description": "Pass --noconfirm / -y so package managers do not ask for confirmation. Off means you confirm each one in the terminal window.",
|
"description": "Pass --noconfirm / -y so package managers do not ask for confirmation. Off means you confirm each one in the terminal window.",
|
||||||
"label": "Answer yes automatically"
|
"label": "Answer yes automatically"
|
||||||
@@ -87,6 +118,10 @@
|
|||||||
"description": "Send a desktop notification when a check finds packages to upgrade.",
|
"description": "Send a desktop notification when a check finds packages to upgrade.",
|
||||||
"label": "Notify when updates are found"
|
"label": "Notify when updates are found"
|
||||||
},
|
},
|
||||||
|
"show_activity_graph": {
|
||||||
|
"description": "Track pending-update counts across recent checks and when you last updated, shown as a small graph in the panel. Turning this off also stops recording the history.",
|
||||||
|
"label": "Show activity graph"
|
||||||
|
},
|
||||||
"show_count": {
|
"show_count": {
|
||||||
"description": "Show the number of pending updates next to the bar glyph.",
|
"description": "Show the number of pending updates next to the bar glyph.",
|
||||||
"label": "Show the update count"
|
"label": "Show the update count"
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ end
|
|||||||
local function pending()
|
local function pending()
|
||||||
return snapshot ~= nil
|
return snapshot ~= nil
|
||||||
and snapshot.phase == "ready"
|
and snapshot.phase == "ready"
|
||||||
and snapshot.dismissed ~= true
|
|
||||||
and (snapshot.total or 0) > 0
|
and (snapshot.total or 0) > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user