diff --git a/shelly/README.md b/shelly/README.md new file mode 100644 index 0000000..cadadd4 --- /dev/null +++ b/shelly/README.md @@ -0,0 +1,28 @@ +# Shelly + +Shelly is a plugin that uses the [Shelly Arch Package Manager](https://github.com/Seafoam-Labs/Shelly-ALPM) to display package update information in a bar widget. + +## Plugin + +| Field | Value | +| ------- | --------------------------------------- | +| ID | `joshuaslate/shelly` | +| Entries | Service: `poller`; bar widget: `shelly` | + +## Requirements + +Install the Shelly Arch Package Manager from the [AUR](https://aur.archlinux.org/packages/shelly). + +Test that it works by running `shelly check-updates` in a terminal. If it returns a list of packages, then it is working correctly. + +## Settings + +| Setting | Type | Default | Description | +| ---------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------- | +| `interval` | `int` | `300` | The interval (in seconds) between update checks | +| `notify` | `bool` | `false` | Whether or not you want to receive notifications when there are new package updates available | +| `color` | `color` | `on_surface` | The text color for the bar widget | +| `glyph_color` | `color` | `on_surface` | The color of the glyph on the bar widget | +| `glyph` | `glyph` | `package` | Bar widget icon. | +| `click_action` | `select` | `open_gui` | The action to perform when the bar widget is clicked. Options: `open_gui`, `open_updater`, `none`. | +| `hide_when_up_to_date` | `bool` | `false` | Whether or not to hide the bar widget when there are no package updates available | diff --git a/shelly/plugin.toml b/shelly/plugin.toml new file mode 100644 index 0000000..4978028 --- /dev/null +++ b/shelly/plugin.toml @@ -0,0 +1,74 @@ +id = "joshuaslate/shelly" +name = "Shelly" +icon = "package" +version = "1.0.0" +min_noctalia = "5.0.0" +license = "MIT" +author = "joshuaslate" +dependencies = ["shelly"] +description = "A plugin that provides a widget for displaying system update information." +tags = ["system", "arch"] +deprecated = false + +[[setting]] +key = "interval" +type = "int" +label_key = "settings.interval.label" +description_key = "settings.interval.description" +default = 300 +min = 0 + +[[setting]] +key = "notify" +type = "bool" +label_key = "settings.notify.label" +description_key = "settings.notify.description" +default = false + +[[service]] +id = "update_poller" +entry = "poller.luau" + +[[widget]] +id = "shelly" +entry = "widget.luau" + + [[widget.setting]] + key = "hide_when_up_to_date" + type = "bool" + label_key = "settings.hide_when_up_to_date.label" + description_key = "settings.hide_when_up_to_date.description" + default = false + + [[widget.setting]] + key = "color" + type = "color" + label_key = "settings.color.label" + description_key = "settings.color.description" + default = "on_surface" + + [[widget.setting]] + key = "glyph" + type = "glyph" + label_key = "settings.glyph.label" + description_key = "settings.glyph.description" + default = "package" + + [[widget.setting]] + key = "glyph_color" + type = "color" + label_key = "settings.glyph_color.label" + description_key = "settings.glyph_color.description" + default = "on_surface" + + [[widget.setting]] + key = "click_action" + type = "select" + label_key = "settings.click_action.label" + description_key = "settings.click_action.description" + default = "open_gui" + options = [ + { value = "none", label_key = "settings.click_action.options.none" }, + { value = "open_gui", label_key = "settings.click_action.options.open_gui" }, + { value = "open_updater", label_key = "settings.click_action.options.open_updater" } + ] diff --git a/shelly/poller.luau b/shelly/poller.luau new file mode 100644 index 0000000..fdfd089 --- /dev/null +++ b/shelly/poller.luau @@ -0,0 +1,57 @@ +--!nonstrict + +type Update = { Name: string, Version: string, OldVersion: string?, DownloadSize: string? } + +type UpdateResult = { + Packages: { [number]: Update }, + Aur: { [number]: Update }, + Flatpak: { [number]: Update }, +} + +local updates: UpdateResult = { Packages = {}, Aur = {}, Flatpak = {} } +local updateError: string | nil = nil +local updateInterval = (noctalia.getConfig("interval") or 300)*1000 +local isPolling = false + +noctalia.setUpdateInterval(updateInterval) + +function refresh() + isPolling = true + + noctalia.runAsync("shelly check-updates --json", function(result) + isPolling = false + + if result.exitCode ~= 0 then + updateError = result.stderr + noctalia.state.set("updates", nil) + noctalia.state.set("update_error", updateError) + return + end + + local decoded = noctalia.json.decode(result.stdout) + if type(decoded) ~= "table" then + updateError = noctalia.tr("bar.tooltip.error.decode_json") + noctalia.state.set("update_error", updateError) + return + end + + updates = { + Packages = decoded.Packages or {}, + Aur = decoded.Aur or {}, + Flatpak = decoded.Flatpak or {}, + } + + noctalia.state.set("updates", updates) + noctalia.state.set("update_error", nil) + end) +end + +function update() + if isPolling then + return + end + + refresh() +end + +refresh() diff --git a/shelly/thumbnail.webp b/shelly/thumbnail.webp new file mode 100644 index 0000000..0cbd49a Binary files /dev/null and b/shelly/thumbnail.webp differ diff --git a/shelly/translations/en.json b/shelly/translations/en.json new file mode 100644 index 0000000..25430a7 --- /dev/null +++ b/shelly/translations/en.json @@ -0,0 +1,68 @@ +{ + "title": "Shelly", + "notify": { + "new_updates": { + "title": "New Updates Available", + "description": "There are {count} new updates available for your system." + } + }, + "bar": { + "text": { + "error": "?", + "updates": { + "one": "{count} Update", + "other": "{count} Updates" + } + }, + "tooltip": { + "aur_title": "AUR", + "flatpak_title": "Flatpak", + "packages_title": "Packages", + "no_updates": "System is up to date", + "error": { + "decode_json": "Failed to decode output from `shelly check-updates --json`" + } + }, + "click_handler": { + "error": { + "missing_gui": "Shelly GUI is not installed. Please install it to use this feature.", + "missing_cli": "Shelly CLI is not installed. Please install it to use this feature." + } + } + }, + "settings": { + "interval": { + "label": "Refresh Seconds", + "description": "How often the service checks for updates in the background." + }, + "notify": { + "label": "Notify", + "description": "Whether to show a notification when new updates are available." + }, + "click_action": { + "label": "Click Action", + "description": "What happens when you click on the bar.", + "options": { + "none": "Nothing", + "open_gui": "Open Shelly GUI", + "open_updater": "Start Update in Terminal" + } + }, + "hide_when_up_to_date": { + "label": "Hide When Up-to-Date", + "description": "Whether to hide the bar when there are no updates available." + }, + "color": { + "label": "Color", + "description": "The color of the bar text." + }, + "glyph": { + "label": "Glyph", + "description": "The glyph shown before the label." + }, + "glyph_color": { + "label": "Glyph Color", + "description": "The color of the glyph." + } + } +} diff --git a/shelly/widget.luau b/shelly/widget.luau new file mode 100644 index 0000000..6c9ec87 --- /dev/null +++ b/shelly/widget.luau @@ -0,0 +1,116 @@ +--!nonstrict +local glyph = barWidget.getConfig("glyph") +local color = barWidget.getConfig("color") or "on_surface" +local glyphColor = barWidget.getConfig("glyph_color") or color +local updateError = noctalia.state.get("update_error") +local notifyOnNewUpdates = barWidget.getConfig("notify") +local hideWhenUpToDate = barWidget.getConfig("hide_when_up_to_date") +local click_action = barWidget.getConfig("click_action") or "open_gui" +local updates = noctalia.state.get("updates") or { Packages = {}, Aur = {}, Flatpak = {} } + +local function countUpdates(scopedUpdates) + return #scopedUpdates.Packages + #scopedUpdates.Aur + #scopedUpdates.Flatpak +end + +local function getUpdateLabel(update) + if update.OldVersion then + return string.format("%s → %s", update.OldVersion, update.Version) + else + return update.Version + end +end + +local function appendUpdateSection(rows, title, list) + if #list == 0 then + return + end + + table.insert(rows, { + key = title, + value = string.format("%d", #list), + }) + + for _, pkg in ipairs(list) do + table.insert(rows, { + key = " " .. pkg.Name, + value = getUpdateLabel(pkg), + }) + + if pkg.DownloadSize then + table.insert(rows, { + key = "", + value = pkg.DownloadSize, + }) + end + end +end + +local function updateTooltip(count) + if updateError then + barWidget.setTooltip(updateError) + return + end + + if count == 0 then + barWidget.setTooltip(noctalia.tr("bar.tooltip.no_updates")) + return + end + + local rows = {} + + appendUpdateSection(rows, noctalia.tr("bar.tooltip.packages_title"), updates.Packages) + appendUpdateSection(rows, noctalia.tr("bar.tooltip.aur_title"), updates.Aur) + appendUpdateSection(rows, noctalia.tr("bar.tooltip.flatpak_title"), updates.Flatpak) + + barWidget.setTooltip(rows) +end + +local function render() + local container = barWidget.isVertical() and ui.column or ui.row + + local count = countUpdates(updates) + + barWidget.render(container({ gap = 6, align = "center", visible = not hideWhenUpToDate or count > 0 }, { + ui.glyph({ name = glyph, size = 14, color = glyphColor, visible = glyph ~= "" }), + ui.label({ text = noctalia.trp("bar.text.updates", count), color = color }), + })) + + updateTooltip(count) +end + +noctalia.state.watch("updates", function(value) + local prevCount = countUpdates(updates) + local newCount = countUpdates(value) + + updates = value + + if notifyOnNewUpdates and newCount > prevCount then + noctalia.notify(noctalia.tr("notify.new_updates.title"), noctalia.trp("notify.new_updates.description", newCount)) + end + + render() +end) + +function onClick() + if click_action == "open_gui" then + local cmd = "shelly-ui" + + if not noctalia.commandExists(cmd) then + noctalia.notifyError("Shelly", noctalia.tr("bar.click_handler.error.missing_gui")) + return + end + + noctalia.runAsync(cmd) + elseif click_action == "open_updater" then + local cmd = "shelly" + + if not noctalia.commandExists(cmd) then + noctalia.notifyError("Shelly", noctalia.tr("bar.click_handler.error.missing_cli")) + return + end + + noctalia.runInTerminal(cmd .. " upgrade-all") + end +end + +render()