Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions system-updater/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# System Updates Manager

Manage system updates inside the noctalia panel. Currently supported PackageKit, Flatpak and Cargo.

# Features

* Check updates with [PackageKit](https://www.freedesktop.org/software/PackageKit), [Flatpak](https://flatpak.org/) and Cargo.
* Extensible with user created adapters
* Support for offline updates with PackageKit
* Single package update is supported
* Show brief package information: Name, description, installed version, updated version
* Show application icon if found
* Show number of pending packages in the bar widget

![System Updates Manager panel](screenshots/panel.png)

## Plugin

| Field | Value |
| --- | --- |
| ID | `tordex/system-updater` |
| Entries | Bar widget: `status`; panel: `panel`; service: `service` |

## Requirements

All adapters require ```python``` to run check update scripts. You have to install ```pycairo``` ```PyGObject``` modules with ```pip```:

```sh
pip install pycairo PyGObject
```

Other requirements are depend of update adapter. Plugin disables update adapter if any of its own dependences are not installed.

### PackageKit

PackageKit update adapter requires ```python``` and ```pkgcli``` to be installed into ```$PATH```.

### Flatpak

Flatpak update adapter requires ```python``` and ```flatpak``` to be installed into ```$PATH```.

### Cargo

Cargo update adapter requires ```cargo```, ```python```, ```cargo-install-update```, `awk` and `bash` to be installed into ```$PATH```.

## Usage

Add the system-updater widget from Noctalia's widget picker, then click it to open the panel. You can also open the panel directly or bind it in your compositor:

```sh
noctalia msg panel-toggle tordex/system-updater:panel
```

| Action | Effect |
|-----------------------------------|-------------------------------------------------------------|
| Left click (bar glyph) | Open/close the panel |
| **Check Updates** (panel) | Check for new updates |
| **Update** (panel) | Start applying updates |
| ↻ refresh (panel header) | Same as **Check Updates** |
| ⚙ settings (panel header) | Open this plugin's page in *Settings → Plugins* |

## Settings

| Setting | Type | Default | Description |
| --- | --- | --- | --- |
| `enable_packagekit` | `bool` | `true` | Enable the PackageKit adapter for managing system updates. |
| `enable_flatpak` | `bool` | `true` | Enable the Flatpak adapter for managing system updates. |
| `enable_cargo` | `bool` | `true` | Enable the Cargo adapter for managing system updates. |
| `auto_check_minutes` | `int` | `60` | Check for updates automatically every N minutes. if <= 10 never checks on its own — nothing runs until you ask for it. |
| `adapters_folder` | `folder` | | The folder where adapters definitions are stored. One adapter per subfolder. Each adapter folder must contain an adapter.json with the adapter definition. |
| `notify_on_updates` | `bool` | `true` | Send a desktop notification when a check finds packages to upgrade or after applying updates. |
| `notify_on_updates` | `bool` | `true` | Send a desktop notification when a check finds packages to upgrade or after applying updates. |
| `glyph` | `glyph` | `package` | The glyph shown for the system updater widget on the bar. |
| `show_count` | `bool` | `true` | Show the number of pending updates next to the bar glyph. |

## IPC

IPC command to start updates checking:
```sh
noctalia msg plugin tordex/system-updater:service all check
```

IPC command to start update applying updates:
```sh
noctalia msg plugin tordex/system-updater:service all update
```

## Notes

You can define the folder with your own adapters the setting `adapters_folder`. Plugin will read custom adapters and include them into ckecking/updating process.

### How to write update adapter

1. Adapter must be located inside single folder
2. Adapter folder must have `adapter.json` file with adapter definitions
3. You have to write a script to check updates that provides output in the formap plugin understand

### adapter.json file

Adpater definitions in format:
```json
{
"name": "PackageKit",
"enabled": true,
"dependencies": ["python", "pkgcli"],
"check_command": "python {adapter_dir}/check.py",
"update_package_command": "pkgcli -y -q update {package_name}",
"update_all_command": "pkgcli -y -q offline-update prepare",
"actions": {
"reboot": {
"command": "noctalia msg session reboot",
"check_after": false
},
"cancel": {
"command": "pkgcli -y -q offline-update cancel",
"check_after": true
}
}
}
```
| Field | Type | Description |
| --- | --- | --- |
| `name` | `string` | The update adapter name. Please don't use spaces. |
| `enables` | `bool` | Disable or enable adapter. |
| `check_command` | `string` | The shell command to check updates. Output should be in the specified format. |
| `update_package_command` | `string` | The shell command to update the single package. Use {package_name} placeholder as the package parameter |
| `update_all_command` | `string` | The shell command to update all pending packages. |
| `actions` | `string` | The supported by adapter actions dictinary. |

For commands use `{adapter_dir}` placeholder as the full path to the adapter folder.

Actions are the spesial buttons shown after update checking. `adapter.json` has the disctionary of available actions. Every action has two fileds:

| Field | Type | Description |
| --- | --- | --- |
| `command` | `string` | The shell script to run. |
| `check_after` | `bool` | When `true` plugin will run check update for this adapter after running the action command. |

### `check_command` output format

```json
{
"info": "",
"actions": [
{
"name": "actions name/label",
"command": "action_id",
}
],
"updates": [
{
"id": "package_id",
"name": "package_name",
"icon": "path to the icon",
"description": "package description",
"from_version": "current version",
"to_version": "new version"
}
...
}
```
| Field | Type | Description |
| --- | --- | --- |
| `info` | `string` | The message for actions. |
| `actions` | `list` | List of actions to show. |
| `actions.name` | `string` | The name/label of action. Will be shown on the action button |
| `actions.command` | `string` | Actions ID. Refers to the `adapter.json` |
| `updates` | `list` | List of available updates. |
| `updates.id` | `string` | The package ID. Plugin pass it as `{package_name}` placeholder |
| `updates.name` | `string` | The package name to be shown on the panel |
| `updates.icon` | `string` | (optional) Full path to the icon file |
| `updates.description` | `string` | (optional) The description of the package |
| `updates.from_version` | `string` | (optional) The current version of the package |
| `updates.to_version` | `string` | (optional) The new version of the package |
9 changes: 9 additions & 0 deletions system-updater/adapters/cargo/adapter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Cargo",
"enabled": true,
"dependencies": ["cargo", "python", "cargo-install-update", "awk", "bash"],
"check_command": "python {adapter_dir}/check.py",
"update_package_command": "cargo install \"{package_name}\"",
"update_all_command": "cargo-install-update install-update --all --git",
"actions": {}
}
86 changes: 86 additions & 0 deletions system-updater/adapters/cargo/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import subprocess
import json
import re
import sys

def get_updates_list():
proc = subprocess.run(["bash", "-c", "cargo install-update --list 2>/dev/null | awk '$NF==\"Yes\"{print $1\"\t\"$2\"\t\"$3}'"], capture_output=True)
if proc.returncode != 0:
print(proc.stderr.decode(), file=sys.stderr)
exit(1)
stdout = proc.stdout.decode().strip()
for line in stdout.splitlines():
parts = line.split("\t")
if len(parts) >= 3:
yield {
"name": parts[0].strip(),
"from_version": parts[1].strip(),
"to_version": parts[2].strip()
}

def get_package_info(package_name):
try:
proc = subprocess.Popen(
["pkgcli", "show", package_name, "--json", "--filter=newest;installed"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, _ = proc.communicate(input="1\n")
lines = stdout.splitlines()
if len(lines) == 1:
return json.loads(lines[0])
for line in lines[1:]:
index = line.find("{")
if index != -1:
line = line[index:]
return json.loads(line)
return {
"summary": "",
"version": ""
}
except Exception as e:
return {
"summary": "",
"version": ""
}

def get_package_description(package_name):
proc = subprocess.run(["cargo", "-q", "info", package_name], capture_output=True)
if proc.returncode != 0:
print(proc.stderr.decode(), file=sys.stderr)
exit(1)
stdout = proc.stdout.decode().strip()
lines = stdout.splitlines()
if len(lines) >= 2:
return lines[1].strip() # The second line usually contains the description
return ""


def package_kit_updates():

out = []

for update in get_updates_list():
# Extract relevant information from the update dictionary
pkg_info = get_package_info(update.get("name"))
update_info = {
"id": update.get("name"),
"name": update.get("name"),
"icon": "", # Placeholder for icon path, as Cargo doesn't provide icons
"glyph": "brand-rust", # Placeholder glyph for Cargo packages
"description": get_package_description(update.get("name")),
"from_version": update.get("from_version", ""),
"to_version": update.get("to_version", "")
}
out.append(update_info)

out.sort(key=lambda x: x["name"].lower())
return {
"info": "",
"updates": out
}

if __name__ == "__main__":
print(json.dumps(package_kit_updates(), indent=2, ensure_ascii=False))
9 changes: 9 additions & 0 deletions system-updater/adapters/flatpak/adapter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Flatpak",
"enabled": true,
"dependencies": ["flatpak", "python"],
"check_command": "python {adapter_dir}/check.py",
"update_package_command": "flatpak -y --noninteractive update \"{package_name}\"",
"update_all_command": "flatpak -y --noninteractive update",
"actions": {}
}
95 changes: 95 additions & 0 deletions system-updater/adapters/flatpak/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import subprocess
import json
import re
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

def get_icon_path(icon_name, size=48):
theme = Gtk.IconTheme.get_default()
icon_info = theme.lookup_icon(icon_name, size, Gtk.IconLookupFlags.USE_BUILTIN)
if icon_info:
return icon_info.get_filename()
return None

def run_process(command, input_text=None):
try:
proc = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, _ = proc.communicate(input=input_text)
return stdout.splitlines()
except Exception as e:
return []

def parse_flatpak_list_output(lines):
apps = {}
the_caption_line = True
for line in lines:
if the_caption_line:
the_caption_line = False
continue

parts = line.split("\t")
if len(parts) >= 4:
app_id = parts[0].strip()
apps[app_id] = {
"name": parts[1].strip(),
"description": parts[2].strip(),
"version": parts[3].strip()
}
return apps

def get_real_flatpak_updates():
# Run flatpak list --columns 'application,name,description,version'
list_lines = run_process(["flatpak", "list", "--app", "--columns", "application,name,description,version"])
# Parse the output of flatpak list to create a mapping of app_id to its details
apps = parse_flatpak_list_output(list_lines)

# Run flatpak list --app --columns 'application,name,description,version'
remote_ls_lines = run_process(["flatpak", "remote-ls", "--updates", "--columns", "application,name,description,version"])
# Parse the output of flatpak remote-ls to create a mapping of app_id to its details
remote_ls_apps = parse_flatpak_list_output(remote_ls_lines)

# Run flatpak update in non-interactive mode with an "n" (no) response
# This makes flatpak print the exact table and exit immediately
updates_lines = run_process(["flatpak", "update"], input_text="n\n")

updates = []

# Parse the output of flatpak update to extract the list of updates
# And enrich them with the details from the apps mapping
for line in updates_lines:
line_str = line.strip()

# Parse numbered lines (for example: "1. [i] org.mozilla.firefox stable flathub ...")
if re.match(r'^\d+\.', line_str):
# Remove the number and flags like [i], [u]
cleaned = re.sub(r'^\d+\.\s*(\[\w+\]\s*)?', '', line_str)
parts = cleaned.split()

if len(parts) >= 1:
app_id = parts[0].strip()
if app_id in apps:
updates.append({
"id": app_id,
"name": apps[app_id]["name"],
"description": apps[app_id]["description"],
"icon": get_icon_path(app_id),
"from_version": apps[app_id]["version"],
"to_version": remote_ls_apps[app_id]["version"] if app_id in remote_ls_apps and remote_ls_apps[app_id]["version"] != apps[app_id]["version"] else ""
})

updates.sort(key=lambda x: x["name"].lower())
return {
"info": "",
"updates": updates
}


if __name__ == "__main__":
print(json.dumps(get_real_flatpak_updates(), indent=2, ensure_ascii=False))
18 changes: 18 additions & 0 deletions system-updater/adapters/packagekit/adapter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "PackageKit",
"enabled": true,
"dependencies": ["python", "pkgcli"],
"check_command": "python {adapter_dir}/check.py",
"update_package_command": "pkgcli -y -q update \"{package_name}\"",
"update_all_command": "pkgcli -y -q offline-update prepare",
"actions": {
"reboot": {
"command": "noctalia msg session reboot",
"check_after": false
},
"cancel": {
"command": "pkgcli -y -q offline-update cancel",
"check_after": true
}
}
}
Loading