noctalia
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"dnf":{"count":13,"packages":[{"arch":"x86_64","download_bytes":150994773,"download_formatted":"144.0 MiB","evr":"157.0.8103.38-1","install_bytes":473654166,"install_formatted":"451.7 MiB","name":"google-chrome-stable","repo":"google-chrome"},{"arch":"x86_64","download_bytes":237159,"download_formatted":"231.6 KiB","evr":"710-2.fc44","install_bytes":500346,"install_formatted":"488.6 KiB","name":"less","repo":"updates"},{"arch":"x86_64","download_bytes":11154,"download_formatted":"10.9 KiB","evr":"710-2.fc44","install_bytes":2453,"install_formatted":"2.4 KiB","name":"less-color","repo":"updates"},{"arch":"x86_64","download_bytes":14509767,"download_formatted":"13.8 MiB","evr":"26.2.6-1.fc44","install_bytes":55835303,"install_formatted":"53.2 MiB","name":"mesa-dri-drivers","repo":"updates"},{"arch":"x86_64","download_bytes":9467,"download_formatted":"9.2 KiB","evr":"26.2.6-1.fc44","install_bytes":3682,"install_formatted":"3.6 KiB","name":"mesa-filesystem","repo":"updates"},{"arch":"x86_64","download_bytes":144525,"download_formatted":"141.1 KiB","evr":"26.2.6-1.fc44","install_bytes":407983,"install_formatted":"398.4 KiB","name":"mesa-libEGL","repo":"updates"},{"arch":"x86_64","download_bytes":142203,"download_formatted":"138.9 KiB","evr":"26.2.6-1.fc44","install_bytes":387286,"install_formatted":"378.2 KiB","name":"mesa-libGL","repo":"updates"},{"arch":"x86_64","download_bytes":15617,"download_formatted":"15.3 KiB","evr":"26.2.6-1.fc44","install_bytes":19996,"install_formatted":"19.5 KiB","name":"mesa-libgbm","repo":"updates"},{"arch":"x86_64","download_bytes":29137089,"download_formatted":"27.8 MiB","evr":"26.2.6-1.fc44","install_bytes":182459359,"install_formatted":"174.0 MiB","name":"mesa-vulkan-drivers","repo":"updates"},{"arch":"noarch","download_bytes":11984,"download_formatted":"11.7 KiB","evr":"3.14.10-1.fc44","install_bytes":23,"install_formatted":"23 B","name":"python-unversioned-command","repo":"updates"},{"arch":"x86_64","download_bytes":30236,"download_formatted":"29.5 KiB","evr":"3.14.10-1.fc44","install_bytes":29354,"install_formatted":"28.7 KiB","name":"python3","repo":"updates"},{"arch":"x86_64","download_bytes":10672326,"download_formatted":"10.2 MiB","evr":"3.14.10-1.fc44","install_bytes":46317997,"install_formatted":"44.2 MiB","name":"python3-libs","repo":"updates"},{"arch":"x86_64","download_bytes":643432,"download_formatted":"628.4 KiB","evr":"5.7-1.fc44","install_bytes":1437679,"install_formatted":"1.4 MiB","name":"swayimg","repo":"updates"}],"status":"done","total_download_bytes":206559732,"total_download_formatted":"197.0 MiB","total_install_bytes":761055627,"total_install_formatted":"725.8 MiB"},"flatpak":{"count":0,"packages":{},"status":"done","total_download_bytes":0,"total_download_formatted":"0 B","total_install_bytes":0,"total_install_formatted":"0 B"},"last_check":"04/11 20:23","last_check_timestamp":1793820226,"last_update_timestamp":1793730402,"reboot_required":false,"status":"IDLE","total_count":13,"total_download_bytes":206559732,"total_download_formatted":"197.0 MiB","total_install_bytes":761055627,"total_install_formatted":"725.8 MiB"}
|
||||
@@ -0,0 +1,59 @@
|
||||
## [1.1.0] - 2026-10-26
|
||||
|
||||
### Added
|
||||
- **Reboot Required Detection**: Detects pending system restart via `dnf needs-restarting -r` with warning banner in panel and widget status indicator.
|
||||
- **Package Search Filter**: Search bar to filter pending DNF and Flatpak packages by name, repository, or runtime.
|
||||
- **Header Overview**: Relative last check and last full upgrade timestamps in panel header.
|
||||
- **Weekly Check Interval**: Extended check interval limit up to 7 days (10080 min).
|
||||
- **Terminal Confirmation**: Added `-y` flag to terminal updates for promptless execution.
|
||||
|
||||
### Fixed
|
||||
- **Badge Overflow**: Truncated COPR repo names, suppressed redundant native arch and official Fedora repo badges, and capped badge width.
|
||||
- **State Persistence**: Cached check results in `pluginDataDir()` across restarts to prevent redundant checks on login.
|
||||
- **Terminal Update Sync**: Triggered auto-recheck via IPC when terminal updates complete, and closed panel on launch.
|
||||
|
||||
## [1.0.3] - 2026-10-06
|
||||
|
||||
### Added
|
||||
- **Plugin Version in Header**: Display installed version next to panel title in `panel.luau`.
|
||||
- **Flatpak Runtime Updates**: Added `include_flatpak_runtimes` setting and row badge in `panel.luau`.
|
||||
|
||||
### Fixed
|
||||
- Fixes noctalia-dev/community-plugins#922: **Flatpak Ghost Updates**: Added `--app` flag to `flatpak remote-ls --updates` to filter unmatched theme extensions and EOL runtimes.
|
||||
- **Flatpak Selection Crash**: Defined `flatpakCount` scope in `updateFlatpak()` to fix nil comparison crash and infinite loading.
|
||||
- **Background Upgrade Protection**: Raised background transaction timeout to 10m and wrapped helper with `systemd-inhibit` to prevent SIGKILL and mid-upgrade aborts.
|
||||
- **Flatpak Parameter Limits**: Removed duplicate Flatpak refs, relaxed validation, and simplified full updates.
|
||||
- **Timeout Error Reporting**: Added `result.timedOut` handling to DNF and Flatpak update errors advising terminal execution.
|
||||
|
||||
## [1.0.2] - 2026-09-22
|
||||
|
||||
### Added
|
||||
- **Installer Script for passwordless background updates**: Added `scripts/install.sh` for remote/local install, group checks (`wheel`/`sudo`), SELinux restore, and uninstallation.
|
||||
|
||||
### Fixed
|
||||
- **Check Retry**: Retried DNF/Flatpak checks once when they time out.
|
||||
- **Stream Exclusions**: Fixed exclusions for RPM modules and epochs containing colons.
|
||||
- **Polkit Compatibility**: Added support for both `wheel` and `sudo` groups.
|
||||
- **Error Capture**: Captured DNF error output when `stderr` is empty.
|
||||
|
||||
## [1.0.1] - 2026-09-21
|
||||
|
||||
### Security
|
||||
- **Helper**: Restricted privileged DNF commands to supported operations.
|
||||
- **Input Validation**: Added validation for DNF package names and Flatpak refs.
|
||||
- **Shell Quoting**: Safely escaped dynamic terminal arguments.
|
||||
|
||||
### Fixed
|
||||
- **Terminal Updates**: Combined DNF and Flatpak updates into one terminal session.
|
||||
- **Flatpak Updates**: Fixed full Flatpak updates to include runtimes.
|
||||
- **Size Parsing**: Improved Flatpak update size parsing.
|
||||
- **Package Keys**: Fixed inconsistent Flatpak package keys.
|
||||
|
||||
## [1.0.0] - 2026-09-19
|
||||
|
||||
### Added
|
||||
- Initial release of DNF Updater.
|
||||
- DNF5 and Flatpak update monitoring and management.
|
||||
- Flyout panel with package selection and size estimation.
|
||||
- Terminal and passwordless Polkit background update modes.
|
||||
- Status bar widget with update count badges and indicators.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 strob3
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,119 @@
|
||||
# DNF Updater
|
||||

|
||||
|
||||
DNF Updater monitors and manages pending DNF5 and Flatpak updates directly from the Noctalia bar. It provides a lightweight update badge in the bar, per-package selection to skip or apply specific upgrades, package search filtering, reboot required detection, download size estimation, and safe execution through Polkit or an interactive terminal.
|
||||
|
||||
## Plugin
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| ID | `strob3/dnf-updater` |
|
||||
| Entries | Bar widget: `widget`; panel: `panel`; service: `service` |
|
||||
|
||||
## Requirements
|
||||
|
||||
The plugin expects the following dependencies on `$PATH`:
|
||||
|
||||
- `dnf5` - required for DNF, COPR, and RPM package management. Fedora 41+ uses DNF5 by default.
|
||||
- `flatpak` - optional, Flatpak application manager
|
||||
- `pkexec` - (Polkit authentication for non-interactive privileged package installation)
|
||||
|
||||
### Passwordless Background Upgrades (Optional)
|
||||
|
||||
To enable background upgrades without a password prompt, install the included hardened helper script and Polkit rule with:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/strob3/dnf-updater/main/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
Alternatively, if you have cloned the repository locally:
|
||||
|
||||
```sh
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
To remove the helper and Polkit rule later:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/strob3/dnf-updater/main/scripts/install.sh | sudo bash -s -- --uninstall
|
||||
# or: sudo ./scripts/install.sh --uninstall
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add the DNF Updater widget to the bar using Noctalia's bar widget picker. Left-click the widget to open the updates panel. You can also toggle the panel or bind it to a compositor shortcut:
|
||||
|
||||
```sh
|
||||
noctalia msg panel-toggle strob3/dnf-updater:panel
|
||||
```
|
||||
|
||||
### Shortcuts & Keybindings
|
||||
|
||||
You can also toggle the panel with a hotkey or shell alias:
|
||||
|
||||
**Niri** (`~/.config/niri/config.kdl`):
|
||||
```kdl
|
||||
binds {
|
||||
Mod+U { spawn "noctalia" "msg" "panel-toggle" "strob3/dnf-updater:panel"; }
|
||||
}
|
||||
```
|
||||
|
||||
**Hyprland** (`hyprland.conf`):
|
||||
```conf
|
||||
bind = $mainMod, U, exec, noctalia msg panel-toggle strob3/dnf-updater:panel
|
||||
```
|
||||
|
||||
**Shell Alias** (`~/.bashrc` or `~/.zshrc`):
|
||||
```sh
|
||||
alias dnf-updater="noctalia msg panel-toggle strob3/dnf-updater:panel"
|
||||
alias dnf-check="noctalia msg plugin strob3/dnf-updater:service service check"
|
||||
```
|
||||
|
||||
### Controls
|
||||
|
||||
| Action | Target | Description |
|
||||
| --- | --- | --- |
|
||||
| **Left Click** | Bar Widget | Open or close the updates panel |
|
||||
| **Right Click** | Bar Widget | Refresh the updates |
|
||||
| **Middle Click** | Bar Widget | Open plugin settings in *Settings → Plugins* |
|
||||
| **Search Input** | Panel Top | Filter packages in real time by name, repo, or ref |
|
||||
| **Click Row** | Package Row | Toggle package selection (include or exclude from update) |
|
||||
| **Chevron Click** | Section Header | Collapse or expand DNF or Flatpak package section |
|
||||
| **Check Click** | Section Header | Select all or deselect all packages in section |
|
||||
| **Refresh Icon** | Panel Header | Re-check repositories for latest updates |
|
||||
| **Update Button** | Panel Bottom | Apply updates for all selected packages |
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `check_interval` | `int` | `30` | Interval in minutes between automatic checks (`0` to `10080`, `0` disables). |
|
||||
| `enable_dnf` | `bool` | `true` | Enable checking and upgrading DNF5 system packages. |
|
||||
| `enable_flatpak` | `bool` | `true` | Enable checking and upgrading Flatpak applications. |
|
||||
| `include_flatpak_runtimes` | `bool` | `false` | Include Flatpak runtime and extension updates alongside applications. |
|
||||
| `show_widget_when_clean` | `bool` | `true` | Show the widget icon in the bar even when system is up to date. |
|
||||
| `notify_on_updates` | `bool` | `true` | Send a desktop notification when new pending updates are found. |
|
||||
| `update_in_terminal` | `bool` | `true` | Launch update commands inside your terminal emulator instead of background Polkit execution. |
|
||||
| `glyph` | `glyph` | `package` | Widget icon glyph displayed in the bar. |
|
||||
|
||||
## IPC
|
||||
|
||||
Trigger background service operations using `noctalia msg`:
|
||||
|
||||
```sh
|
||||
# Check for updates immediately
|
||||
noctalia msg plugin strob3/dnf-updater:service service check
|
||||
|
||||
# Apply updates for all pending packages
|
||||
noctalia msg plugin strob3/dnf-updater:service service update
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Privilege Separation**: Noctalia never runs as root. System upgrades invoke hardened helper script with explicit arguments via Polkit, or launch inside the user's terminal emulator when configured.
|
||||
- **Selective Upgrades**: When packages are deselected, DNF5 is invoked with `--exclude=<pkg>` arguments and Flatpak is invoked with only selected application IDs.
|
||||
- **Background Checks**: Update queries use non-blocking JSON queries (`dnf5 check-upgrade --json` and `flatpak remote-ls --updates -j`) to prevent freezing the shell.
|
||||
- **Reboot Detection**: Checks `dnf needs-restarting -r` to detect when core packages (kernel, systemd, glibc) require a system restart.
|
||||
|
||||
## License
|
||||
[MIT](LICENSE)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
id = "strob3/dnf-updater"
|
||||
name = "DNF Updater"
|
||||
version = "1.1.0"
|
||||
plugin_api = 43
|
||||
author = "strob3"
|
||||
license = "MIT"
|
||||
icon = "package"
|
||||
description = "Manage DNF and Flatpak updates."
|
||||
dependencies = ["dnf5", "flatpak", "pkexec"]
|
||||
tags = ["bar", "panel", "service", "system", "fedora", "utility"]
|
||||
|
||||
[[setting]]
|
||||
key = "check_interval"
|
||||
type = "int"
|
||||
label_key = "settings.check_interval.label"
|
||||
description_key = "settings.check_interval.description"
|
||||
default = 30
|
||||
min = 0
|
||||
max = 10080
|
||||
|
||||
[[setting]]
|
||||
key = "enable_dnf"
|
||||
type = "bool"
|
||||
label_key = "settings.enable_dnf.label"
|
||||
description_key = "settings.enable_dnf.description"
|
||||
default = true
|
||||
|
||||
[[setting]]
|
||||
key = "enable_flatpak"
|
||||
type = "bool"
|
||||
label_key = "settings.enable_flatpak.label"
|
||||
description_key = "settings.enable_flatpak.description"
|
||||
default = true
|
||||
|
||||
[[setting]]
|
||||
key = "include_flatpak_runtimes"
|
||||
type = "bool"
|
||||
label_key = "settings.include_flatpak_runtimes.label"
|
||||
description_key = "settings.include_flatpak_runtimes.description"
|
||||
default = false
|
||||
|
||||
[[setting]]
|
||||
key = "show_widget_when_clean"
|
||||
type = "bool"
|
||||
label_key = "settings.show_widget_when_clean.label"
|
||||
description_key = "settings.show_widget_when_clean.description"
|
||||
default = true
|
||||
|
||||
[[setting]]
|
||||
key = "notify_on_updates"
|
||||
type = "bool"
|
||||
label_key = "settings.notify_on_updates.label"
|
||||
description_key = "settings.notify_on_updates.description"
|
||||
default = true
|
||||
|
||||
[[setting]]
|
||||
key = "update_in_terminal"
|
||||
type = "bool"
|
||||
label_key = "settings.update_in_terminal.label"
|
||||
description_key = "settings.update_in_terminal.description"
|
||||
default = true
|
||||
|
||||
[[service]]
|
||||
id = "service"
|
||||
entry = "service.luau"
|
||||
|
||||
[[widget]]
|
||||
id = "widget"
|
||||
entry = "widget.luau"
|
||||
|
||||
[[widget.setting]]
|
||||
key = "glyph"
|
||||
type = "glyph"
|
||||
label_key = "settings.glyph.label"
|
||||
description_key = "settings.glyph.description"
|
||||
default = "package"
|
||||
|
||||
[[panel]]
|
||||
id = "panel"
|
||||
entry = "panel.luau"
|
||||
width = 420
|
||||
height = 500
|
||||
placement = "attached"
|
||||
open_near_click = true
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/* Noctalia DNF Updater - Passwordless Update Policy
|
||||
*
|
||||
* Grants authorization for the dedicated dnf-updater-helper script
|
||||
* to active local users in the wheel or sudo group.
|
||||
*
|
||||
* The helper strictly restricts operations to 'upgrade' with validated
|
||||
* exclusions, preventing arbitrary command execution as root.
|
||||
*
|
||||
* Install:
|
||||
* curl -fsSL https://raw.githubusercontent.com/strob3/dnf-updater/main/scripts/install.sh | sudo bash
|
||||
* (or locally: sudo ./scripts/install.sh)
|
||||
*
|
||||
* Remove:
|
||||
* sudo ./scripts/install.sh --uninstall
|
||||
*/
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.policykit.exec" &&
|
||||
action.lookup("program") == "/usr/local/bin/dnf-updater-helper" &&
|
||||
subject.active == true && subject.local == true &&
|
||||
(subject.isInGroup("wheel") || subject.isInGroup("sudo"))) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
|
||||
return polkit.Result.NOT_HANDLED;
|
||||
});
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# DNF Updater - Privileged Update Helper
|
||||
#
|
||||
# Restricts privileged execution strictly to system package upgrades
|
||||
# with validated exclusions. Prevents arbitrary command execution or
|
||||
# malicious package installations.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Help / usage
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
echo "Usage: dnf-updater-helper upgrade [--exclude=<pkg1,pkg2,...>]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ensure standard, safe environment
|
||||
export PATH="/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
unset IFS
|
||||
unset LD_PRELOAD LD_LIBRARY_PATH PYTHONPATH PYTHONHOME DNF5_PLUGINS
|
||||
|
||||
# Require root
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
echo "Error: dnf-updater-helper must be run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTION="${1:-}"
|
||||
if [[ "$ACTION" != "upgrade" ]]; then
|
||||
echo "Error: unsupported action '${ACTION}'. Only 'upgrade' is allowed." >&2
|
||||
echo "Usage: dnf-updater-helper upgrade [--exclude=<pkg1,pkg2,...>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
|
||||
# Validate any remaining arguments.
|
||||
VALID_ARGS=()
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" =~ ^--exclude=[a-zA-Z0-9._+*:-][a-zA-Z0-9._+*:,-]*$ ]]; then
|
||||
VALID_ARGS+=("$arg")
|
||||
else
|
||||
echo "Error: invalid argument rejected: '$arg'" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Resolve DNF binary
|
||||
DNF_BIN=""
|
||||
for bin in "/usr/bin/dnf5" "/usr/bin/dnf"; do
|
||||
if [[ -x "$bin" ]]; then
|
||||
DNF_BIN="$bin"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$DNF_BIN" ]]; then
|
||||
echo "Error: neither /usr/bin/dnf5 nor /usr/bin/dnf was found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute upgrade w/o interaction, inhibiting sleep/shutdown if systemd-inhibit is available
|
||||
if command -v systemd-inhibit >/dev/null 2>&1; then
|
||||
exec systemd-inhibit --what=shutdown:sleep:idle --who="dnf-updater" --why="Performing system package upgrade" "$DNF_BIN" upgrade -y "${VALID_ARGS[@]}"
|
||||
else
|
||||
exec "$DNF_BIN" upgrade -y "${VALID_ARGS[@]}"
|
||||
fi
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# DNF Updater - Privileged Helper & Polkit Policy Installer
|
||||
#
|
||||
# Installs or uninstalls the privileged helper and Polkit rule.
|
||||
# Supports both local repository invocation and remote execution via:
|
||||
# curl -fsSL https://raw.githubusercontent.com/strob3/dnf-updater/main/scripts/install.sh | sudo bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEST_BIN="/usr/local/bin/dnf-updater-helper"
|
||||
DEST_POLKIT="/etc/polkit-1/rules.d/50-dnf-updater.rules"
|
||||
REPO_RAW_URL="https://raw.githubusercontent.com/strob3/dnf-updater/main"
|
||||
|
||||
# Help / usage
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
echo "Usage: $0 [install|--uninstall]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " install Install helper to $DEST_BIN and rule to $DEST_POLKIT (default)"
|
||||
echo " --uninstall Remove helper and Polkit rule from the system"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Verify root privileges
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
echo "Error: Installation requires root privileges. Please run with sudo:" >&2
|
||||
echo " sudo $0 $@" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTION="${1:-install}"
|
||||
|
||||
case "$ACTION" in
|
||||
uninstall|--uninstall|-u)
|
||||
echo "Removing DNF Updater privileged helper and Polkit rule..."
|
||||
rm -f "$DEST_BIN" "$DEST_POLKIT"
|
||||
echo "✓ Successfully removed:"
|
||||
echo " - $DEST_BIN"
|
||||
echo " - $DEST_POLKIT"
|
||||
exit 0
|
||||
;;
|
||||
install|--install|-i)
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown argument '$ACTION'" >&2
|
||||
echo "Usage: $0 [install|--uninstall]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "==> Installing DNF Updater privileged helper and Polkit rule..."
|
||||
|
||||
# Determine script location if running locally
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
|
||||
|
||||
SRC_HELPER=""
|
||||
SRC_POLKIT=""
|
||||
CLEANUP_TMP=false
|
||||
TMP_DIR=""
|
||||
|
||||
if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/dnf-updater-helper.sh" && -f "${SCRIPT_DIR}/../polkit/50-dnf-updater.rules" ]]; then
|
||||
echo " → Using local repository files..."
|
||||
SRC_HELPER="${SCRIPT_DIR}/dnf-updater-helper.sh"
|
||||
SRC_POLKIT="${SCRIPT_DIR}/../polkit/50-dnf-updater.rules"
|
||||
elif [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/scripts/dnf-updater-helper.sh" && -f "${SCRIPT_DIR}/polkit/50-dnf-updater.rules" ]]; then
|
||||
echo " → Using local repository files..."
|
||||
SRC_HELPER="${SCRIPT_DIR}/scripts/dnf-updater-helper.sh"
|
||||
SRC_POLKIT="${SCRIPT_DIR}/polkit/50-dnf-updater.rules"
|
||||
else
|
||||
echo " → Fetching latest files from GitHub ($REPO_RAW_URL)..."
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
CLEANUP_TMP=true
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "${REPO_RAW_URL}/scripts/dnf-updater-helper.sh" -o "${TMP_DIR}/dnf-updater-helper.sh"
|
||||
curl -fsSL "${REPO_RAW_URL}/polkit/50-dnf-updater.rules" -o "${TMP_DIR}/50-dnf-updater.rules"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO "${TMP_DIR}/dnf-updater-helper.sh" "${REPO_RAW_URL}/scripts/dnf-updater-helper.sh"
|
||||
wget -qO "${TMP_DIR}/50-dnf-updater.rules" "${REPO_RAW_URL}/polkit/50-dnf-updater.rules"
|
||||
else
|
||||
echo "Error: neither curl nor wget was found to download required files." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_HELPER="${TMP_DIR}/dnf-updater-helper.sh"
|
||||
SRC_POLKIT="${TMP_DIR}/50-dnf-updater.rules"
|
||||
fi
|
||||
|
||||
# Install privileged helper
|
||||
install -Dm755 "$SRC_HELPER" "$DEST_BIN"
|
||||
echo " ✓ Installed privileged helper: $DEST_BIN (mode 0755)"
|
||||
|
||||
# Install Polkit rule
|
||||
install -Dm644 "$SRC_POLKIT" "$DEST_POLKIT"
|
||||
echo " ✓ Installed Polkit policy: $DEST_POLKIT (mode 0644)"
|
||||
|
||||
# Fix SELinux context if SELinux is available
|
||||
if command -v restorecon >/dev/null 2>&1; then
|
||||
restorecon -F "$DEST_BIN" "$DEST_POLKIT" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check if target user belongs to wheel or sudo
|
||||
INVOKER="${SUDO_USER:-$USER}"
|
||||
if [[ -n "$INVOKER" && "$INVOKER" != "root" ]]; then
|
||||
if ! id -nG "$INVOKER" 2>/dev/null | grep -Ewq "wheel|sudo"; then
|
||||
echo ""
|
||||
echo " ⚠ Warning: User '$INVOKER' is not in the 'wheel' or 'sudo' group."
|
||||
echo " Polkit requires administrative group membership for passwordless updates."
|
||||
echo " Add your user with: sudo usermod -aG wheel $INVOKER"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Installation complete!"
|
||||
echo "💡 Tip: To enable background updates without terminal popups, ensure 'Run Updates in Terminal'"
|
||||
echo " is turned off in Noctalia Settings → Plugins → DNF Updater."
|
||||
@@ -0,0 +1,983 @@
|
||||
--!nonstrict
|
||||
-- Headless [[service]] entry: monitors and manages DNF5 and Flatpak updates.
|
||||
-- Publishes state updates to noctalia.state so the bar widget and panel stay synchronized.
|
||||
|
||||
local checkInterval = noctalia.getConfig("check_interval") or 30
|
||||
local enableDnf = noctalia.getConfig("enable_dnf") ~= false
|
||||
local enableFlatpak = noctalia.getConfig("enable_flatpak") ~= false
|
||||
local notifyOnUpdates = noctalia.getConfig("notify_on_updates") ~= false
|
||||
local updateInTerminal = noctalia.getConfig("update_in_terminal") ~= false
|
||||
local includeFlatpakRuntimes = noctalia.getConfig("include_flatpak_runtimes") == true
|
||||
|
||||
local state = {
|
||||
status = "IDLE", -- "IDLE" | "CHECKING" | "UPDATING" | "SUCCESS" | "ERROR"
|
||||
total_count = 0,
|
||||
total_download_bytes = 0,
|
||||
total_install_bytes = 0,
|
||||
total_download_formatted = "0 B",
|
||||
total_install_formatted = "0 B",
|
||||
dnf = {
|
||||
count = 0,
|
||||
packages = {},
|
||||
total_download_bytes = 0,
|
||||
total_install_bytes = 0,
|
||||
total_download_formatted = "0 B",
|
||||
total_install_formatted = "0 B",
|
||||
status = "idle", -- "idle" | "checking" | "updating" | "done" | "error"
|
||||
error = nil,
|
||||
},
|
||||
flatpak = {
|
||||
count = 0,
|
||||
packages = {},
|
||||
total_download_bytes = 0,
|
||||
total_install_bytes = 0,
|
||||
total_download_formatted = "0 B",
|
||||
total_install_formatted = "0 B",
|
||||
status = "idle", -- "idle" | "checking" | "updating" | "done" | "error"
|
||||
error = nil,
|
||||
},
|
||||
last_check = nil,
|
||||
last_check_timestamp = 0,
|
||||
last_update_timestamp = 0,
|
||||
reboot_required = false,
|
||||
error_message = nil,
|
||||
}
|
||||
|
||||
local previousCount = 0
|
||||
local isChecking = false
|
||||
local isUpdating = false
|
||||
|
||||
local function formatBytes(bytes)
|
||||
if not bytes or bytes <= 0 then
|
||||
return "0 B"
|
||||
end
|
||||
local units = { "B", "KiB", "MiB", "GiB", "TiB" }
|
||||
local b = tonumber(bytes) or 0
|
||||
local uIndex = 1
|
||||
while b >= 1024 and uIndex < #units do
|
||||
b = b / 1024
|
||||
uIndex = uIndex + 1
|
||||
end
|
||||
if uIndex == 1 then
|
||||
return string.format("%d %s", b, units[uIndex])
|
||||
else
|
||||
return string.format("%.1f %s", b, units[uIndex])
|
||||
end
|
||||
end
|
||||
|
||||
local function parseSizeToBytes(sizeStr)
|
||||
if not sizeStr or type(sizeStr) ~= "string" or sizeStr == "" then
|
||||
return 0
|
||||
end
|
||||
local s = sizeStr:gsub(",", ".")
|
||||
local numStr, unit = s:match("([%d%.]+).-([%a]+)")
|
||||
if not numStr or not unit then
|
||||
return 0
|
||||
end
|
||||
local num = tonumber(numStr)
|
||||
if not num or num <= 0 then
|
||||
return 0
|
||||
end
|
||||
unit = string.upper(unit)
|
||||
if unit == "B" or unit == "BYTES" then
|
||||
return math.floor(num)
|
||||
elseif unit == "KIB" then
|
||||
return math.floor(num * 1024)
|
||||
elseif unit == "KB" or unit == "K" then
|
||||
return math.floor(num * 1000)
|
||||
elseif unit == "MIB" then
|
||||
return math.floor(num * 1024 * 1024)
|
||||
elseif unit == "MB" or unit == "M" then
|
||||
return math.floor(num * 1000 * 1000)
|
||||
elseif unit == "GIB" then
|
||||
return math.floor(num * 1024 * 1024 * 1024)
|
||||
elseif unit == "GB" or unit == "G" then
|
||||
return math.floor(num * 1000 * 1000 * 1000)
|
||||
elseif unit == "TIB" then
|
||||
return math.floor(num * 1024 * 1024 * 1024 * 1024)
|
||||
elseif unit == "TB" or unit == "T" then
|
||||
return math.floor(num * 1000 * 1000 * 1000 * 1000)
|
||||
end
|
||||
return math.floor(num)
|
||||
end
|
||||
|
||||
-- Persisted so a shell restart shows the last result instead of re-checking: with a
|
||||
-- daily/weekly interval, checking on every login defeated the point.
|
||||
local function stateFile()
|
||||
local dir = noctalia.pluginDataDir()
|
||||
return dir and (dir .. "/state.json") or nil
|
||||
end
|
||||
|
||||
local function saveState()
|
||||
local path = stateFile()
|
||||
local encoded = path and noctalia.json.encode(state)
|
||||
if encoded then
|
||||
noctalia.writeFile(path, encoded)
|
||||
end
|
||||
end
|
||||
|
||||
local function loadState()
|
||||
local path = stateFile()
|
||||
if not path or not noctalia.fileExists(path) then
|
||||
return false
|
||||
end
|
||||
local contents = noctalia.readFile(path)
|
||||
local saved = contents and noctalia.json.decode(contents)
|
||||
if type(saved) ~= "table" or type(saved.last_check_timestamp) ~= "number" then
|
||||
return false
|
||||
end
|
||||
state = saved
|
||||
state.status = state.error_message and "ERROR" or (state.total_count == 0 and "SUCCESS" or "IDLE")
|
||||
state.reboot_required = saved.reboot_required == true
|
||||
previousCount = state.total_count or 0
|
||||
return true
|
||||
end
|
||||
|
||||
local function checkDue()
|
||||
return checkInterval > 0 and os.time() - (state.last_check_timestamp or 0) >= checkInterval * 60
|
||||
end
|
||||
|
||||
local function publishState()
|
||||
noctalia.state.set("update_state", state)
|
||||
end
|
||||
|
||||
local function getDnfBinary()
|
||||
if noctalia.commandExists("dnf5") then
|
||||
return "dnf5"
|
||||
elseif noctalia.commandExists("dnf") then
|
||||
return "dnf"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function isValidPackageName(name)
|
||||
if type(name) ~= "string" or name == "" then
|
||||
return false
|
||||
end
|
||||
if name:sub(1, 1) == "-" then
|
||||
return false
|
||||
end
|
||||
return name:match("^[%w%._+%-]+$") ~= nil
|
||||
end
|
||||
|
||||
local function isValidFlatpakId(id)
|
||||
if type(id) ~= "string" or id == "" then
|
||||
return false
|
||||
end
|
||||
if id:sub(1, 1) == "-" then
|
||||
return false
|
||||
end
|
||||
return id:match("^[%w%._%-%/]+$") ~= nil
|
||||
end
|
||||
|
||||
local function shellQuote(str)
|
||||
return "'" .. string.gsub(str, "'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function checkDnf(onFinished, isRetry)
|
||||
if not enableDnf then
|
||||
state.dnf.count = 0
|
||||
state.dnf.packages = {}
|
||||
state.dnf.total_download_bytes = 0
|
||||
state.dnf.total_install_bytes = 0
|
||||
state.dnf.total_download_formatted = "0 B"
|
||||
state.dnf.total_install_formatted = "0 B"
|
||||
state.dnf.status = "idle"
|
||||
state.dnf.error = nil
|
||||
onFinished(true)
|
||||
return
|
||||
end
|
||||
|
||||
local dnfBin = getDnfBinary()
|
||||
if not dnfBin then
|
||||
state.dnf.count = 0
|
||||
state.dnf.packages = {}
|
||||
state.dnf.total_download_bytes = 0
|
||||
state.dnf.total_install_bytes = 0
|
||||
state.dnf.total_download_formatted = "0 B"
|
||||
state.dnf.total_install_formatted = "0 B"
|
||||
state.dnf.status = "error"
|
||||
state.dnf.error = "DNF package manager not found"
|
||||
onFinished(false)
|
||||
return
|
||||
end
|
||||
|
||||
state.dnf.status = "checking"
|
||||
state.dnf.error = nil
|
||||
|
||||
noctalia.runAsync({ dnfBin, "check-upgrade", "--json" }, function(result)
|
||||
if result.timedOut then
|
||||
if not isRetry then
|
||||
print("[dnf-updater] DNF check timed out; retrying once...")
|
||||
checkDnf(onFinished, true)
|
||||
return
|
||||
end
|
||||
state.dnf.status = "error"
|
||||
state.dnf.error = "DNF check timed out"
|
||||
onFinished(false)
|
||||
return
|
||||
end
|
||||
|
||||
local raw = noctalia.string.trim(result.stdout)
|
||||
if raw == "" then
|
||||
state.dnf.count = 0
|
||||
state.dnf.packages = {}
|
||||
state.dnf.total_download_bytes = 0
|
||||
state.dnf.total_install_bytes = 0
|
||||
state.dnf.total_download_formatted = "0 B"
|
||||
state.dnf.total_install_formatted = "0 B"
|
||||
state.dnf.status = "done"
|
||||
state.dnf.error = nil
|
||||
onFinished(true)
|
||||
return
|
||||
end
|
||||
|
||||
local decoded, err = noctalia.json.decode(raw)
|
||||
if type(decoded) == "table" and type(decoded.upgrades) == "table" then
|
||||
local packages = {}
|
||||
for _, item in ipairs(decoded.upgrades) do
|
||||
if type(item) == "table" and item.name then
|
||||
table.insert(packages, {
|
||||
name = item.name,
|
||||
evr = item.evr or "",
|
||||
arch = item.arch or "",
|
||||
repo = item.repository or "",
|
||||
download_bytes = 0,
|
||||
install_bytes = 0,
|
||||
download_formatted = "",
|
||||
install_formatted = "",
|
||||
})
|
||||
end
|
||||
end
|
||||
state.dnf.count = #packages
|
||||
state.dnf.packages = packages
|
||||
|
||||
if #packages > 0 then
|
||||
-- Query individual package download and install sizes
|
||||
noctalia.runAsync({ dnfBin, "repoquery", "--upgrades", "--queryformat", "%{name}\t%{downloadsize}\t%{installsize}\n" }, function(resSizes)
|
||||
local dnfTotalDownload = 0
|
||||
local dnfTotalInstall = 0
|
||||
if resSizes.exitCode == 0 and resSizes.stdout and resSizes.stdout ~= "" then
|
||||
local sizeMap = {}
|
||||
for line in resSizes.stdout:gmatch("[^\r\n]+") do
|
||||
local name, dl, inst = line:match("([^\t]+)\t([^\t]+)\t([^\t]+)")
|
||||
if name then
|
||||
sizeMap[name] = {
|
||||
dl = tonumber(dl) or 0,
|
||||
inst = tonumber(inst) or 0,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
for _, pkg in ipairs(packages) do
|
||||
local s = sizeMap[pkg.name]
|
||||
if s then
|
||||
pkg.download_bytes = s.dl
|
||||
pkg.install_bytes = s.inst
|
||||
pkg.download_formatted = formatBytes(s.dl)
|
||||
pkg.install_formatted = formatBytes(s.inst)
|
||||
dnfTotalDownload += s.dl
|
||||
dnfTotalInstall += s.inst
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
state.dnf.total_download_bytes = dnfTotalDownload
|
||||
state.dnf.total_install_bytes = dnfTotalInstall
|
||||
state.dnf.total_download_formatted = formatBytes(dnfTotalDownload)
|
||||
state.dnf.total_install_formatted = formatBytes(dnfTotalInstall)
|
||||
state.dnf.status = "done"
|
||||
state.dnf.error = nil
|
||||
onFinished(true)
|
||||
end, 30000)
|
||||
else
|
||||
state.dnf.total_download_bytes = 0
|
||||
state.dnf.total_install_bytes = 0
|
||||
state.dnf.total_download_formatted = "0 B"
|
||||
state.dnf.total_install_formatted = "0 B"
|
||||
state.dnf.status = "done"
|
||||
state.dnf.error = nil
|
||||
onFinished(true)
|
||||
end
|
||||
else
|
||||
-- If JSON parsing failed or command returned non-standard error
|
||||
if result.exitCode ~= 0 and result.exitCode ~= 100 then
|
||||
state.dnf.status = "error"
|
||||
state.dnf.error = result.stderr ~= "" and result.stderr or (err or "DNF check failed")
|
||||
onFinished(false)
|
||||
else
|
||||
state.dnf.count = 0
|
||||
state.dnf.packages = {}
|
||||
state.dnf.total_download_bytes = 0
|
||||
state.dnf.total_install_bytes = 0
|
||||
state.dnf.total_download_formatted = "0 B"
|
||||
state.dnf.total_install_formatted = "0 B"
|
||||
state.dnf.status = "done"
|
||||
state.dnf.error = nil
|
||||
onFinished(true)
|
||||
end
|
||||
end
|
||||
end, 30000)
|
||||
end
|
||||
|
||||
local function checkFlatpak(onFinished, isRetry)
|
||||
if not enableFlatpak then
|
||||
state.flatpak.count = 0
|
||||
state.flatpak.packages = {}
|
||||
state.flatpak.total_download_bytes = 0
|
||||
state.flatpak.total_install_bytes = 0
|
||||
state.flatpak.total_download_formatted = "0 B"
|
||||
state.flatpak.total_install_formatted = "0 B"
|
||||
state.flatpak.status = "idle"
|
||||
state.flatpak.error = nil
|
||||
onFinished(true)
|
||||
return
|
||||
end
|
||||
|
||||
if not noctalia.commandExists("flatpak") then
|
||||
state.flatpak.count = 0
|
||||
state.flatpak.packages = {}
|
||||
state.flatpak.total_download_bytes = 0
|
||||
state.flatpak.total_install_bytes = 0
|
||||
state.flatpak.total_download_formatted = "0 B"
|
||||
state.flatpak.total_install_formatted = "0 B"
|
||||
state.flatpak.status = "error"
|
||||
state.flatpak.error = "Flatpak not installed"
|
||||
onFinished(false)
|
||||
return
|
||||
end
|
||||
|
||||
state.flatpak.status = "checking"
|
||||
state.flatpak.error = nil
|
||||
|
||||
local flatpakArgs = { "flatpak", "remote-ls", "--updates", "--columns=name,application,version,branch,origin,installed-size,download-size,ref", "-j" }
|
||||
if not includeFlatpakRuntimes then
|
||||
table.insert(flatpakArgs, 4, "--app")
|
||||
end
|
||||
|
||||
noctalia.runAsync(flatpakArgs, function(result)
|
||||
if result.timedOut then
|
||||
if not isRetry then
|
||||
print("[dnf-updater] Flatpak check timed out; retrying once...")
|
||||
checkFlatpak(onFinished, true)
|
||||
return
|
||||
end
|
||||
state.flatpak.status = "error"
|
||||
state.flatpak.error = "Flatpak check timed out"
|
||||
onFinished(false)
|
||||
return
|
||||
end
|
||||
|
||||
local raw = noctalia.string.trim(result.stdout)
|
||||
if raw == "" then
|
||||
state.flatpak.count = 0
|
||||
state.flatpak.packages = {}
|
||||
state.flatpak.total_download_bytes = 0
|
||||
state.flatpak.total_install_bytes = 0
|
||||
state.flatpak.total_download_formatted = "0 B"
|
||||
state.flatpak.total_install_formatted = "0 B"
|
||||
state.flatpak.status = "done"
|
||||
state.flatpak.error = nil
|
||||
onFinished(true)
|
||||
return
|
||||
end
|
||||
|
||||
local decoded, err = noctalia.json.decode(raw)
|
||||
if type(decoded) == "table" then
|
||||
local packages = {}
|
||||
local flatpakTotalDownload = 0
|
||||
local flatpakTotalInstall = 0
|
||||
for _, item in ipairs(decoded) do
|
||||
if type(item) == "table" then
|
||||
local dlBytes = parseSizeToBytes(item.download_size)
|
||||
local instBytes = parseSizeToBytes(item.installed_size)
|
||||
flatpakTotalDownload += dlBytes
|
||||
flatpakTotalInstall += instBytes
|
||||
|
||||
local isRuntime = (item.ref and item.ref:find("^runtime/") ~= nil) or false
|
||||
table.insert(packages, {
|
||||
name = item.name or (item.application_id or "Unknown"),
|
||||
id = (item.application_id and item.application_id ~= "") and item.application_id or (item.ref or (item.name or "")),
|
||||
ref = item.ref or "",
|
||||
version = item.version or (item.branch or ""),
|
||||
branch = item.branch or "",
|
||||
origin = item.origin or "",
|
||||
is_runtime = isRuntime,
|
||||
download_bytes = dlBytes,
|
||||
install_bytes = instBytes,
|
||||
download_formatted = (item.download_size and item.download_size ~= "") and item.download_size or formatBytes(dlBytes),
|
||||
install_formatted = (item.installed_size and item.installed_size ~= "") and item.installed_size or formatBytes(instBytes),
|
||||
})
|
||||
end
|
||||
end
|
||||
state.flatpak.count = #packages
|
||||
state.flatpak.packages = packages
|
||||
state.flatpak.total_download_bytes = flatpakTotalDownload
|
||||
state.flatpak.total_install_bytes = flatpakTotalInstall
|
||||
state.flatpak.total_download_formatted = formatBytes(flatpakTotalDownload)
|
||||
state.flatpak.total_install_formatted = formatBytes(flatpakTotalInstall)
|
||||
state.flatpak.status = "done"
|
||||
state.flatpak.error = nil
|
||||
onFinished(true)
|
||||
else
|
||||
if result.exitCode ~= 0 then
|
||||
state.flatpak.status = "error"
|
||||
state.flatpak.error = result.stderr ~= "" and result.stderr or (err or "Flatpak check failed")
|
||||
onFinished(false)
|
||||
else
|
||||
state.flatpak.count = 0
|
||||
state.flatpak.packages = {}
|
||||
state.flatpak.total_download_bytes = 0
|
||||
state.flatpak.total_install_bytes = 0
|
||||
state.flatpak.total_download_formatted = "0 B"
|
||||
state.flatpak.total_install_formatted = "0 B"
|
||||
state.flatpak.status = "done"
|
||||
state.flatpak.error = nil
|
||||
onFinished(true)
|
||||
end
|
||||
end
|
||||
end, 30000)
|
||||
end
|
||||
|
||||
-- Read from dnf history rather than recorded on our own runs: terminal updates are
|
||||
-- fire-and-forget, and a manual `dnf upgrade` counts too. Only an upgrade with no
|
||||
-- package args is a full update; Flatpak keeps no history, so it is not tracked.
|
||||
local function isFullUpgrade(commandLine)
|
||||
local verbSeen = false
|
||||
for word in (commandLine or ""):gmatch("%S+") do
|
||||
if verbSeen then
|
||||
if word:sub(1, 1) ~= "-" then
|
||||
return false
|
||||
end
|
||||
elseif word == "upgrade" or word == "update" or word == "distro-sync" then
|
||||
verbSeen = true
|
||||
end
|
||||
end
|
||||
return verbSeen
|
||||
end
|
||||
|
||||
local function refreshLastUpdate()
|
||||
local dnfBin = getDnfBinary()
|
||||
if not dnfBin then
|
||||
return
|
||||
end
|
||||
noctalia.runAsync({ dnfBin, "history", "list", "--json" }, function(result)
|
||||
local transactions = result and result.exitCode == 0 and noctalia.json.decode(result.stdout or "")
|
||||
if type(transactions) ~= "table" then
|
||||
return
|
||||
end
|
||||
local latest = 0
|
||||
for _, t in ipairs(transactions) do
|
||||
if t.status == "Ok" and (t.end_time or 0) > latest and isFullUpgrade(t.command_line) then
|
||||
latest = t.end_time
|
||||
end
|
||||
end
|
||||
if latest > 0 and latest ~= state.last_update_timestamp then
|
||||
state.last_update_timestamp = latest
|
||||
publishState()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function checkReboot()
|
||||
if noctalia.fileExists("/run/reboot-required") or noctalia.fileExists("/var/run/reboot-required") then
|
||||
if not state.reboot_required then
|
||||
state.reboot_required = true
|
||||
publishState()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local dnfBin = getDnfBinary()
|
||||
if not dnfBin then
|
||||
return
|
||||
end
|
||||
|
||||
noctalia.runAsync({ dnfBin, "--disablerepo=*", "needs-restarting", "-r", "--json" }, function(result)
|
||||
local isReboot = false
|
||||
if result then
|
||||
if result.exitCode == 1 then
|
||||
isReboot = true
|
||||
elseif result.exitCode == 0 and result.stdout and result.stdout ~= "" then
|
||||
local data = noctalia.json.decode(result.stdout)
|
||||
if type(data) == "table" and data[1] and data[1].reboot_required == true then
|
||||
isReboot = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if isReboot ~= state.reboot_required then
|
||||
state.reboot_required = isReboot
|
||||
publishState()
|
||||
end
|
||||
end, 10000)
|
||||
end
|
||||
|
||||
local function checkAll(force)
|
||||
if isChecking or isUpdating then
|
||||
return
|
||||
end
|
||||
|
||||
isChecking = true
|
||||
state.status = "CHECKING"
|
||||
publishState()
|
||||
refreshLastUpdate()
|
||||
checkReboot()
|
||||
|
||||
local dnfDone = false
|
||||
local flatpakDone = false
|
||||
local dnfSuccess = false
|
||||
local flatpakSuccess = false
|
||||
|
||||
local function finalize()
|
||||
if not dnfDone or not flatpakDone then
|
||||
return
|
||||
end
|
||||
|
||||
isChecking = false
|
||||
state.total_count = state.dnf.count + state.flatpak.count
|
||||
state.total_download_bytes = (state.dnf.total_download_bytes or 0) + (state.flatpak.total_download_bytes or 0)
|
||||
state.total_install_bytes = (state.dnf.total_install_bytes or 0) + (state.flatpak.total_install_bytes or 0)
|
||||
state.total_download_formatted = formatBytes(state.total_download_bytes)
|
||||
state.total_install_formatted = formatBytes(state.total_install_bytes)
|
||||
state.last_check = noctalia.formatTime("%d/%m %H:%M")
|
||||
state.last_check_timestamp = os.time()
|
||||
|
||||
if state.dnf.error and state.flatpak.error then
|
||||
state.status = "ERROR"
|
||||
state.error_message = state.dnf.error .. " | " .. state.flatpak.error
|
||||
elseif state.dnf.error then
|
||||
state.status = "ERROR"
|
||||
state.error_message = state.dnf.error
|
||||
elseif state.flatpak.error then
|
||||
state.status = "ERROR"
|
||||
state.error_message = state.flatpak.error
|
||||
else
|
||||
state.status = state.total_count == 0 and "SUCCESS" or "IDLE"
|
||||
state.error_message = nil
|
||||
end
|
||||
|
||||
-- Send notification when new updates are detected
|
||||
if notifyOnUpdates and state.total_count > previousCount and state.total_count > 0 then
|
||||
noctalia.notify(
|
||||
noctalia.tr("notifications.updates_available_title"),
|
||||
noctalia.trp("notifications.updates_available_body", state.total_count, { count = state.total_count })
|
||||
)
|
||||
end
|
||||
previousCount = state.total_count
|
||||
|
||||
publishState()
|
||||
saveState()
|
||||
end
|
||||
|
||||
checkDnf(function(ok)
|
||||
dnfDone = true
|
||||
dnfSuccess = ok
|
||||
finalize()
|
||||
end)
|
||||
|
||||
checkFlatpak(function(ok)
|
||||
flatpakDone = true
|
||||
flatpakSuccess = ok
|
||||
finalize()
|
||||
end)
|
||||
end
|
||||
|
||||
-- runInTerminal is fire-and-forget, so the terminal itself pings us back when done
|
||||
-- (`;`, not `&&`: a failed or partial run still changes what is pending).
|
||||
local function runInTerminalThenRecheck(cmd)
|
||||
noctalia.runInTerminal(cmd .. "; noctalia msg plugin strob3/dnf-updater:service all check")
|
||||
end
|
||||
|
||||
local function updateDnf(excludedList, onFinished)
|
||||
local dnfBin = getDnfBinary()
|
||||
if not dnfBin then
|
||||
state.dnf.status = "error"
|
||||
state.dnf.error = "DNF not found"
|
||||
if onFinished then onFinished(false) end
|
||||
return
|
||||
end
|
||||
|
||||
local validExcluded = {}
|
||||
if excludedList and type(excludedList) == "table" then
|
||||
for _, name in ipairs(excludedList) do
|
||||
if isValidPackageName(name) then
|
||||
table.insert(validExcluded, name)
|
||||
else
|
||||
print(string.format("[dnf-updater] Warning: invalid DNF package name skipped: %s", tostring(name)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local dnfCount = state.dnf.count or 0
|
||||
if excludedList and type(excludedList) == "table" and #validExcluded >= dnfCount and dnfCount > 0 then
|
||||
state.dnf.status = "done"
|
||||
publishState()
|
||||
if onFinished then onFinished(true) end
|
||||
return
|
||||
end
|
||||
|
||||
local excludeArg = nil
|
||||
if #validExcluded > 0 then
|
||||
excludeArg = "--exclude=" .. table.concat(validExcluded, ",")
|
||||
end
|
||||
|
||||
if updateInTerminal then
|
||||
local sudoCmd = noctalia.commandExists("sudo") and "sudo" or "pkexec"
|
||||
local cmd = sudoCmd .. " " .. dnfBin .. " upgrade -y"
|
||||
if excludeArg then
|
||||
cmd = cmd .. " " .. shellQuote(excludeArg)
|
||||
end
|
||||
runInTerminalThenRecheck(cmd)
|
||||
state.dnf.status = "done"
|
||||
publishState()
|
||||
if onFinished then onFinished(true) end
|
||||
return
|
||||
end
|
||||
|
||||
state.dnf.status = "updating"
|
||||
state.status = "UPDATING"
|
||||
publishState()
|
||||
|
||||
local helperBin = "/usr/local/bin/dnf-updater-helper"
|
||||
local args
|
||||
if noctalia.fileExists(helperBin) then
|
||||
args = { "pkexec", helperBin, "upgrade" }
|
||||
else
|
||||
args = { "pkexec", dnfBin, "upgrade", "-y" }
|
||||
end
|
||||
if excludeArg then
|
||||
table.insert(args, excludeArg)
|
||||
end
|
||||
|
||||
noctalia.runAsync(args, function(result)
|
||||
if result.exitCode == 0 then
|
||||
state.dnf.status = "done"
|
||||
state.dnf.error = nil
|
||||
if onFinished then onFinished(true) end
|
||||
else
|
||||
state.dnf.status = "error"
|
||||
local errMsg = "DNF upgrade failed"
|
||||
if result.timedOut then
|
||||
errMsg = "DNF upgrade timed out (consider running updates in terminal)"
|
||||
elseif result.stderr and result.stderr ~= "" then
|
||||
errMsg = result.stderr
|
||||
elseif result.stdout and result.stdout ~= "" then
|
||||
errMsg = result.stdout
|
||||
end
|
||||
state.dnf.error = errMsg
|
||||
if onFinished then onFinished(false) end
|
||||
end
|
||||
publishState()
|
||||
end, 600000)
|
||||
end
|
||||
|
||||
local function updateFlatpak(selectedList, onFinished)
|
||||
if not noctalia.commandExists("flatpak") then
|
||||
state.flatpak.status = "error"
|
||||
state.flatpak.error = "Flatpak not found"
|
||||
if onFinished then onFinished(false) end
|
||||
return
|
||||
end
|
||||
|
||||
local validSelected = nil
|
||||
if selectedList and type(selectedList) == "table" then
|
||||
validSelected = {}
|
||||
local seen = {}
|
||||
for _, ref in ipairs(selectedList) do
|
||||
if isValidFlatpakId(ref) and not seen[ref] then
|
||||
seen[ref] = true
|
||||
table.insert(validSelected, ref)
|
||||
elseif not isValidFlatpakId(ref) then
|
||||
print(string.format("[dnf-updater] Warning: invalid Flatpak ID skipped: %s", tostring(ref)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if selectedList and type(selectedList) == "table" and #selectedList == 0 and (state.flatpak.count or 0) > 0 then
|
||||
state.flatpak.status = "done"
|
||||
publishState()
|
||||
if onFinished then onFinished(true) end
|
||||
return
|
||||
end
|
||||
|
||||
if selectedList and type(selectedList) == "table" and #selectedList > 0 and validSelected and #validSelected == 0 then
|
||||
print("[dnf-updater] Warning: all selected Flatpak IDs were invalid; skipping update")
|
||||
state.flatpak.status = "done"
|
||||
publishState()
|
||||
if onFinished then onFinished(true) end
|
||||
return
|
||||
end
|
||||
|
||||
local flatpakCount = (state.flatpak and state.flatpak.count) or 0
|
||||
local hasSubset = validSelected and #validSelected > 0 and #validSelected < flatpakCount
|
||||
|
||||
if updateInTerminal then
|
||||
local cmd = "flatpak update -y"
|
||||
if hasSubset then
|
||||
for _, ref in ipairs(validSelected) do
|
||||
cmd = cmd .. " " .. shellQuote(ref)
|
||||
end
|
||||
end
|
||||
runInTerminalThenRecheck(cmd)
|
||||
state.flatpak.status = "done"
|
||||
publishState()
|
||||
if onFinished then onFinished(true) end
|
||||
return
|
||||
end
|
||||
|
||||
state.flatpak.status = "updating"
|
||||
state.status = "UPDATING"
|
||||
publishState()
|
||||
|
||||
local args = { "flatpak", "update", "-y", "--noninteractive" }
|
||||
if hasSubset then
|
||||
for _, ref in ipairs(validSelected) do
|
||||
table.insert(args, ref)
|
||||
end
|
||||
end
|
||||
|
||||
noctalia.runAsync(args, function(result)
|
||||
if result.exitCode == 0 then
|
||||
state.flatpak.status = "done"
|
||||
state.flatpak.error = nil
|
||||
if onFinished then onFinished(true) end
|
||||
else
|
||||
state.flatpak.status = "error"
|
||||
local errMsg = "Flatpak update failed"
|
||||
if result.timedOut then
|
||||
errMsg = "Flatpak update timed out (consider running updates in terminal)"
|
||||
elseif result.stderr and result.stderr ~= "" then
|
||||
errMsg = result.stderr
|
||||
elseif result.stdout and result.stdout ~= "" then
|
||||
errMsg = result.stdout
|
||||
end
|
||||
state.flatpak.error = errMsg
|
||||
if onFinished then onFinished(false) end
|
||||
end
|
||||
publishState()
|
||||
end, 600000)
|
||||
end
|
||||
|
||||
local function updateAll(excludedDnf, selectedFlatpaks)
|
||||
if isUpdating or isChecking then
|
||||
return
|
||||
end
|
||||
|
||||
isUpdating = true
|
||||
state.status = "UPDATING"
|
||||
|
||||
local dnfCount = (state.dnf and state.dnf.count) or 0
|
||||
local flatpakCount = (state.flatpak and state.flatpak.count) or 0
|
||||
|
||||
local validExcluded = {}
|
||||
if excludedDnf and type(excludedDnf) == "table" then
|
||||
for _, name in ipairs(excludedDnf) do
|
||||
if isValidPackageName(name) then
|
||||
table.insert(validExcluded, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
local dnfNeedsUpdate = enableDnf and dnfCount > 0 and (#validExcluded < dnfCount)
|
||||
|
||||
local validSelected = nil
|
||||
if selectedFlatpaks and type(selectedFlatpaks) == "table" then
|
||||
validSelected = {}
|
||||
local seen = {}
|
||||
for _, ref in ipairs(selectedFlatpaks) do
|
||||
if isValidFlatpakId(ref) and not seen[ref] then
|
||||
seen[ref] = true
|
||||
table.insert(validSelected, ref)
|
||||
end
|
||||
end
|
||||
end
|
||||
local flatpakNeedsUpdate = enableFlatpak and flatpakCount > 0
|
||||
if flatpakNeedsUpdate and selectedFlatpaks and type(selectedFlatpaks) == "table" and #validSelected == 0 then
|
||||
flatpakNeedsUpdate = false
|
||||
end
|
||||
|
||||
if not dnfNeedsUpdate and not flatpakNeedsUpdate then
|
||||
isUpdating = false
|
||||
checkAll(true)
|
||||
return
|
||||
end
|
||||
|
||||
if updateInTerminal then
|
||||
local cmds = {}
|
||||
local dnfBin = getDnfBinary()
|
||||
|
||||
if dnfNeedsUpdate and dnfBin then
|
||||
local sudoCmd = noctalia.commandExists("sudo") and "sudo" or "pkexec"
|
||||
local dnfCmd = sudoCmd .. " " .. dnfBin .. " upgrade -y"
|
||||
if #validExcluded > 0 then
|
||||
dnfCmd = dnfCmd .. " " .. shellQuote("--exclude=" .. table.concat(validExcluded, ","))
|
||||
end
|
||||
table.insert(cmds, dnfCmd)
|
||||
end
|
||||
|
||||
if flatpakNeedsUpdate then
|
||||
local fpCmd = "flatpak update -y"
|
||||
if validSelected and #validSelected > 0 and #validSelected < flatpakCount then
|
||||
for _, ref in ipairs(validSelected) do
|
||||
fpCmd = fpCmd .. " " .. shellQuote(ref)
|
||||
end
|
||||
end
|
||||
table.insert(cmds, fpCmd)
|
||||
end
|
||||
|
||||
if #cmds > 0 then
|
||||
local fullCmd = table.concat(cmds, " && ")
|
||||
runInTerminalThenRecheck(fullCmd)
|
||||
if dnfNeedsUpdate then state.dnf.status = "done" end
|
||||
if flatpakNeedsUpdate then state.flatpak.status = "done" end
|
||||
state.status = "SUCCESS"
|
||||
publishState()
|
||||
end
|
||||
|
||||
isUpdating = false
|
||||
return
|
||||
end
|
||||
|
||||
if dnfNeedsUpdate and flatpakNeedsUpdate then
|
||||
state.dnf.status = "updating"
|
||||
state.flatpak.status = "waiting"
|
||||
publishState()
|
||||
|
||||
updateDnf(excludedDnf, function(dnfOk)
|
||||
state.flatpak.status = "updating"
|
||||
publishState()
|
||||
|
||||
updateFlatpak(selectedFlatpaks, function(flatpakOk)
|
||||
isUpdating = false
|
||||
if dnfOk and flatpakOk then
|
||||
noctalia.notify(
|
||||
noctalia.tr("notifications.update_complete_title"),
|
||||
noctalia.tr("notifications.update_complete_body")
|
||||
)
|
||||
else
|
||||
noctalia.notifyError(
|
||||
noctalia.tr("notifications.update_failed_title"),
|
||||
noctalia.tr("notifications.update_failed_body")
|
||||
)
|
||||
end
|
||||
checkAll(true)
|
||||
end)
|
||||
end)
|
||||
elseif dnfNeedsUpdate then
|
||||
state.dnf.status = "updating"
|
||||
publishState()
|
||||
updateDnf(excludedDnf, function(ok)
|
||||
isUpdating = false
|
||||
if ok then
|
||||
noctalia.notify(
|
||||
noctalia.tr("notifications.update_complete_title"),
|
||||
noctalia.tr("notifications.update_complete_body")
|
||||
)
|
||||
else
|
||||
noctalia.notifyError(
|
||||
noctalia.tr("notifications.update_failed_title"),
|
||||
noctalia.tr("notifications.update_failed_body")
|
||||
)
|
||||
end
|
||||
checkAll(true)
|
||||
end)
|
||||
elseif flatpakNeedsUpdate then
|
||||
state.flatpak.status = "updating"
|
||||
publishState()
|
||||
updateFlatpak(selectedFlatpaks, function(ok)
|
||||
isUpdating = false
|
||||
if ok then
|
||||
noctalia.notify(
|
||||
noctalia.tr("notifications.update_complete_title"),
|
||||
noctalia.tr("notifications.update_complete_body")
|
||||
)
|
||||
else
|
||||
noctalia.notifyError(
|
||||
noctalia.tr("notifications.update_failed_title"),
|
||||
noctalia.tr("notifications.update_failed_body")
|
||||
)
|
||||
end
|
||||
checkAll(true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Watch for commands from widget and panel
|
||||
noctalia.state.watch("service_command", function(cmd)
|
||||
if type(cmd) ~= "table" or not cmd.action then
|
||||
return
|
||||
end
|
||||
|
||||
if cmd.action == "check_now" then
|
||||
checkAll(true)
|
||||
elseif cmd.action == "update_all" then
|
||||
updateAll(cmd.excluded_dnf, cmd.selected_flatpak)
|
||||
elseif cmd.action == "update_dnf" then
|
||||
if not isUpdating and not isChecking then
|
||||
isUpdating = true
|
||||
updateDnf(cmd.excluded_dnf, function()
|
||||
isUpdating = false
|
||||
checkAll(true)
|
||||
end)
|
||||
end
|
||||
elseif cmd.action == "update_flatpak" then
|
||||
if not isUpdating and not isChecking then
|
||||
isUpdating = true
|
||||
updateFlatpak(cmd.selected_flatpak, function()
|
||||
isUpdating = false
|
||||
checkAll(true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function onConfigChanged()
|
||||
checkInterval = noctalia.getConfig("check_interval") or 30
|
||||
enableDnf = noctalia.getConfig("enable_dnf") ~= false
|
||||
enableFlatpak = noctalia.getConfig("enable_flatpak") ~= false
|
||||
includeFlatpakRuntimes = noctalia.getConfig("include_flatpak_runtimes") == true
|
||||
notifyOnUpdates = noctalia.getConfig("notify_on_updates") ~= false
|
||||
updateInTerminal = noctalia.getConfig("update_in_terminal") ~= false
|
||||
if checkDue() then
|
||||
checkAll(true)
|
||||
end
|
||||
end
|
||||
|
||||
function onIpc(event, payload)
|
||||
local p = (type(payload) == "table") and payload or {}
|
||||
if event == "check" or event == "check_now" then
|
||||
checkAll(true)
|
||||
elseif event == "update" or event == "update_all" then
|
||||
updateAll(p.excluded_dnf, p.selected_flatpak)
|
||||
elseif event == "update_dnf" then
|
||||
if not isUpdating then
|
||||
isUpdating = true
|
||||
updateDnf(p.excluded_dnf, function()
|
||||
isUpdating = false
|
||||
checkAll(true)
|
||||
end)
|
||||
end
|
||||
elseif event == "update_flatpak" then
|
||||
if not isUpdating then
|
||||
isUpdating = true
|
||||
updateFlatpak(p.selected_flatpak, function()
|
||||
isUpdating = false
|
||||
checkAll(true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Timer tick: runs once every 60 seconds (60000ms)
|
||||
noctalia.setUpdateInterval(60000)
|
||||
|
||||
function update()
|
||||
if checkDue() then
|
||||
checkAll(false)
|
||||
end
|
||||
end
|
||||
|
||||
local restored = loadState()
|
||||
publishState()
|
||||
if not restored or checkDue() then
|
||||
checkAll(true)
|
||||
end
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
+117
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"actions": {
|
||||
"check_now": "Check Now",
|
||||
"clear_search": "Clear",
|
||||
"close": "Close",
|
||||
"collapse": "Collapse section",
|
||||
"deselect_all": "Deselect all",
|
||||
"exclude_pkg": "Exclude from update",
|
||||
"expand": "Expand section",
|
||||
"include_pkg": "Include in update",
|
||||
"open_settings": "Settings",
|
||||
"search_placeholder": "Search packages…",
|
||||
"select_all": "Select all",
|
||||
"update_all": "Update All",
|
||||
"update_all_size": "Update All ({count} · {size})",
|
||||
"update_dnf": "Update DNF",
|
||||
"update_flatpak": "Update Flatpak",
|
||||
"update_selected": "Update Selected ({count})",
|
||||
"update_selected_size": "Update Selected ({count} · {size})"
|
||||
},
|
||||
"notifications": {
|
||||
"update_complete_body": "System updates have completed successfully.",
|
||||
"update_complete_title": "Update Complete",
|
||||
"update_failed_body": "An error occurred while installing updates.",
|
||||
"update_failed_title": "Update Failed",
|
||||
"updates_available_body": "{count} pending updates are ready to install.",
|
||||
"updates_available_title": "Updates Available"
|
||||
},
|
||||
"sections": {
|
||||
"dnf": "DNF · {count} updates",
|
||||
"dnf_selected": "DNF · {selected}/{total} updates",
|
||||
"flatpak": "Flatpak · {count} updates",
|
||||
"flatpak_selected": "Flatpak · {selected}/{total} updates",
|
||||
"runtime": "Runtime"
|
||||
},
|
||||
"settings": {
|
||||
"check_interval": {
|
||||
"description": "How often to automatically check for updates in minutes (0 to disable).",
|
||||
"label": "Check Interval (Minutes)"
|
||||
},
|
||||
"enable_dnf": {
|
||||
"description": "Check for and manage DNF5 system package updates.",
|
||||
"label": "Enable DNF"
|
||||
},
|
||||
"enable_flatpak": {
|
||||
"description": "Check for and manage Flatpak application updates.",
|
||||
"label": "Enable Flatpak"
|
||||
},
|
||||
"include_flatpak_runtimes": {
|
||||
"description": "Include Flatpak runtime and extension updates alongside applications.",
|
||||
"label": "Include Flatpak Runtimes"
|
||||
},
|
||||
"glyph": {
|
||||
"description": "Icon glyph to display in the Noctalia bar widget.",
|
||||
"label": "Widget Icon"
|
||||
},
|
||||
"notify_on_updates": {
|
||||
"description": "Send a desktop notification when new pending updates are found.",
|
||||
"label": "Notify on New Updates"
|
||||
},
|
||||
"show_widget_when_clean": {
|
||||
"description": "Show the bar widget even when there are no pending updates.",
|
||||
"label": "Show Widget When Clean"
|
||||
},
|
||||
"update_in_terminal": {
|
||||
"description": "Launch update commands inside a terminal emulator instead of running in background with polkit.",
|
||||
"label": "Run Updates in Terminal"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"checking": "Checking for updates…",
|
||||
"dnf_checking": "Checking DNF…",
|
||||
"dnf_done": "DNF up to date",
|
||||
"dnf_error": "DNF error",
|
||||
"dnf_idle": "DNF idle",
|
||||
"dnf_updating": "Updating DNF…",
|
||||
"everything_up_to_date": "Everything is up to date.",
|
||||
"flatpak_checking": "Checking Flatpak…",
|
||||
"flatpak_done": "Flatpak up to date",
|
||||
"flatpak_error": "Flatpak error",
|
||||
"flatpak_idle": "Flatpak idle",
|
||||
"flatpak_updating": "Updating Flatpak…",
|
||||
"flatpak_waiting": "Flatpak waiting…",
|
||||
"last_checked": "Last check {time}",
|
||||
"last_update": "last update {time}",
|
||||
"never_checked": "Never checked",
|
||||
"reboot_required": "Restart required to complete updates",
|
||||
"skipped": "Skipped",
|
||||
"updating": "Updating packages…"
|
||||
},
|
||||
"search": {
|
||||
"no_matches": "No packages match \"{query}\""
|
||||
},
|
||||
"ago": {
|
||||
"days": "{n} d ago",
|
||||
"hours": "{n} h ago",
|
||||
"minutes": "{n} min ago",
|
||||
"now": "just now"
|
||||
},
|
||||
"summary": {
|
||||
"disk_size": "Disk Size",
|
||||
"dnf": "DNF",
|
||||
"download": "Download",
|
||||
"flatpak": "Flatpak",
|
||||
"last_checked": "Last Checked",
|
||||
"storage": "Storage",
|
||||
"system": "System",
|
||||
"total": "Total"
|
||||
},
|
||||
"title": "DNF Updater",
|
||||
"tooltip": {
|
||||
"all_up_to_date": "All packages up to date",
|
||||
"disk_size": "Disk size",
|
||||
"download_size": "Download size",
|
||||
"updates_available": "{count} updates available"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
--!nonstrict
|
||||
-- Bar widget entry: displays pending update badge and status in Noctalia bar.
|
||||
|
||||
local glyph = noctalia.getConfig("glyph") or "package"
|
||||
local showWhenClean = noctalia.getConfig("show_widget_when_clean") ~= false
|
||||
|
||||
local updateState = noctalia.state.get("update_state")
|
||||
|
||||
local function render()
|
||||
glyph = noctalia.getConfig("glyph") or "package"
|
||||
showWhenClean = noctalia.getConfig("show_widget_when_clean") ~= false
|
||||
|
||||
if not updateState then
|
||||
barWidget.setGlyph(glyph)
|
||||
barWidget.setText("…")
|
||||
barWidget.setTooltip(noctalia.tr("status.checking"))
|
||||
barWidget.setVisible(true)
|
||||
return
|
||||
end
|
||||
|
||||
local status = updateState.status or "IDLE"
|
||||
local total = updateState.total_count or 0
|
||||
|
||||
if status == "CHECKING" and total == 0 then
|
||||
barWidget.setGlyph(glyph)
|
||||
barWidget.setText("…")
|
||||
barWidget.setTooltip(noctalia.tr("status.checking"))
|
||||
barWidget.setVisible(true)
|
||||
elseif status == "UPDATING" then
|
||||
barWidget.setGlyph("refresh")
|
||||
barWidget.setText(total > 0 and tostring(total) or "…")
|
||||
barWidget.setTooltip(noctalia.tr("status.updating"))
|
||||
barWidget.setVisible(true)
|
||||
elseif status == "ERROR" and total == 0 then
|
||||
barWidget.setGlyph("alert-circle")
|
||||
barWidget.setText("!")
|
||||
barWidget.setTooltip(updateState.error_message or noctalia.tr("status.dnf_error"))
|
||||
barWidget.setVisible(true)
|
||||
elseif total > 0 then
|
||||
barWidget.setGlyph(glyph)
|
||||
barWidget.setText(tostring(total))
|
||||
local tooltipEntries = {}
|
||||
if updateState.reboot_required then
|
||||
table.insert(tooltipEntries, { key = noctalia.tr("summary.system"), value = noctalia.tr("status.reboot_required") })
|
||||
end
|
||||
table.insert(tooltipEntries, { key = noctalia.tr("summary.dnf"), value = tostring(updateState.dnf and updateState.dnf.count or 0) })
|
||||
table.insert(tooltipEntries, { key = noctalia.tr("summary.flatpak"), value = tostring(updateState.flatpak and updateState.flatpak.count or 0) })
|
||||
table.insert(tooltipEntries, { key = noctalia.tr("summary.total"), value = tostring(total) })
|
||||
if updateState.total_download_formatted and updateState.total_download_formatted ~= "0 B" then
|
||||
table.insert(tooltipEntries, { key = noctalia.tr("tooltip.download_size"), value = updateState.total_download_formatted })
|
||||
end
|
||||
table.insert(tooltipEntries, { key = noctalia.tr("summary.last_checked"), value = updateState.last_check or "–" })
|
||||
barWidget.setTooltip(tooltipEntries)
|
||||
barWidget.setVisible(true)
|
||||
else
|
||||
-- Total == 0 (all up to date)
|
||||
if updateState.reboot_required then
|
||||
barWidget.setVisible(true)
|
||||
barWidget.setGlyph("rotate-cw")
|
||||
barWidget.setText("")
|
||||
barWidget.setTooltip(noctalia.tr("status.reboot_required"))
|
||||
elseif showWhenClean then
|
||||
barWidget.setVisible(true)
|
||||
barWidget.setGlyph(glyph)
|
||||
barWidget.setText("")
|
||||
local tooltipText = noctalia.tr("tooltip.all_up_to_date")
|
||||
if updateState.last_check then
|
||||
tooltipText = tooltipText .. " (" .. updateState.last_check .. ")"
|
||||
end
|
||||
barWidget.setTooltip(tooltipText)
|
||||
else
|
||||
barWidget.setVisible(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Live updates from background service
|
||||
noctalia.state.watch("update_state", function(newState)
|
||||
updateState = newState
|
||||
render()
|
||||
end)
|
||||
|
||||
function update()
|
||||
render()
|
||||
end
|
||||
|
||||
function onClick()
|
||||
noctalia.togglePanel("strob3/dnf-updater:panel")
|
||||
end
|
||||
|
||||
function onRightClick()
|
||||
noctalia.state.set("service_command", { action = "check_now", ts = os.time() })
|
||||
end
|
||||
|
||||
function onMiddleClick()
|
||||
noctalia.openSettings()
|
||||
end
|
||||
|
||||
render()
|
||||
+1
Submodule .local/state/noctalia/plugins/sources/community/repo added at 08bcf05199
+1
Submodule .local/state/noctalia/plugins/sources/official/repo added at 510773a6eb
Reference in New Issue
Block a user