Add built-in updater, bump to 1.2.0

- predatord checks the Gitea releases every 6 hours, downloads the
  newest pacman package, verifies its SHA-256 from the release notes
  and installs it with `pacman -U` in a separate systemd unit
- Update notice and install button on the System page, in the sidebar
  and in the panel widget; `predatorctl update [--check] [--yes]`
- Optional API token for private repositories
- packaging/release.py builds and publishes releases in the format
  the updater expects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 17:42:02 +02:00
co-authored by Claude Opus 5.5
parent 2ef62e85cd
commit cc4fdb9cd0
14 changed files with 376 additions and 9 deletions
+63 -2
View File
@@ -4,9 +4,10 @@ import argparse
import os
import sys
import threading
import time
from PySide6.QtCore import QObject, Qt, QTimer, Signal
from PySide6.QtGui import QColor, QIcon
from PySide6.QtGui import QColor, QIcon, QPalette
from PySide6.QtNetwork import QLocalServer, QLocalSocket
from PySide6.QtWidgets import (QApplication, QButtonGroup, QCheckBox, QComboBox, QFrame,
QGridLayout, QHBoxLayout, QInputDialog, QLabel, QListWidget,
@@ -673,6 +674,29 @@ class SystemPage(Page):
self.fw_card.body.addWidget(_muted("Values are stored in the laptop firmware (EC/BIOS)."))
self.lay.addWidget(self.fw_card)
upd = Card("Updates")
self.upd_head = QLabel("")
self.upd_head.setObjectName("big")
upd.body.addWidget(self.upd_head)
self.upd_text = _muted("")
self.upd_text.setTextFormat(Qt.MarkdownText)
self.upd_text.setOpenExternalLinks(True)
upd.body.addWidget(self.upd_text)
h = QHBoxLayout()
self.upd_check = QPushButton("Check now")
self.upd_check.clicked.connect(lambda: (self.upd_check.setEnabled(False), win.backend.run("check_update")))
h.addWidget(self.upd_check)
self.upd_install = QPushButton("Install update")
self.upd_install.setObjectName("primary")
self.upd_install.clicked.connect(self._install)
h.addWidget(self.upd_install)
self.upd_auto = QCheckBox("Check for updates automatically")
self.upd_auto.clicked.connect(lambda v: win.backend.run("set_updates", auto_check=bool(v)))
h.addWidget(self.upd_auto)
h.addStretch(1)
upd.body.addLayout(h)
self.lay.addWidget(upd)
svc = Card("Status")
self.status = _muted("")
svc.body.addWidget(self.status)
@@ -728,7 +752,37 @@ class SystemPage(Page):
box.setChecked(bool(info.get("value")))
box.setToolTip(info.get("error", "") or "")
def _install(self):
u = self.win.state.get("update") or {}
if QMessageBox.question(self, "Install update",
"Install Predator Control %s now?\n\nThe background service restarts briefly; "
"reopen the app afterwards." % u.get("latest")) == QMessageBox.Yes:
self.upd_install.setEnabled(False)
self.win.backend.run("install_update")
def update_state(self, st):
u = st.get("update") or {}
status = st.get("update_status")
self.upd_check.setEnabled(True)
if status == "running":
self.upd_head.setText("Installing update…")
self.upd_text.setText("pacman is installing the new version.")
self.upd_install.hide()
elif u.get("available"):
self.upd_head.setText("Version %s available" % u["latest"])
text = "Installed: %s · [Release page](%s)\n\n%s" % (u["current"], u.get("url"), u.get("notes", "").split("### Install")[0].strip())
if not u.get("can_install"):
text += "\n\n*Automatic install needs the pacman package – otherwise run `git pull && sudo ./install.sh`.*"
self.upd_text.setText(text)
self.upd_install.setVisible(bool(u.get("can_install")))
else:
self.upd_head.setText("Up to date" if u else "Not checked yet")
when = time.strftime("%d.%m.%Y %H:%M", time.localtime(u["checked"])) if u.get("checked") else "–"
extra = "\n\nLast update failed – see: journalctl -u predator-control-update" if status == "failed" else ""
err = ("\n\n⚠ %s" % st["update_error"]) if st.get("update_error") else ""
self.upd_text.setText("Installed version %s · last check %s%s%s" % (st.get("version"), when, err, extra))
self.upd_install.hide()
self.upd_auto.setChecked(st["config"].get("updates", {}).get("auto_check", True))
a = st["available"]
yes = lambda v: "✔" if v else "✘"
lines = ["%s Service predatord %s" % (yes(True), st.get("version")),
@@ -833,13 +887,17 @@ class MainWindow(QMainWindow):
if self.stack.currentWidget() is not bat_page:
bat_page.indicator.set_battery(st["battery"]) # keep the power history going
s = st["sensors"]
self.footer.setText("%s\nCPU %s °C · GPU %s" % (
u = st.get("update") or {}
badge = ("⬆ Update %s available\n" % u["latest"]) if u.get("available") else ""
self.footer.setText(badge + "%s\nCPU %s °C · GPU %s" % (
PROFILE_INFO.get(st["profile"], (st["profile"],))[0], s.get("cpu_temp"),
("%s °C" % s["gpu_temp"]) if s.get("gpu_temp") else "off"))
def _done(self, cmd, result, err):
if err:
self.statusBar().showMessage("Error: %s" % err, 8000)
elif cmd == "install_update":
self.statusBar().showMessage("Update is being installed…", 8000)
elif cmd == "get_firmware":
self.pages["system"][0].firmware(result)
self.pages["battery"][0].firmware(result)
@@ -860,6 +918,9 @@ def main(argv=None):
app.setDesktopFileName("predator-control")
app.setStyle("Fusion")
app.setStyleSheet(theme.QSS)
pal = app.palette()
pal.setColor(QPalette.Link, QColor(theme.ACCENT))
app.setPalette(pal)
# single instance: forward the requested page to a running window
name = "predator-control-%d" % os.getuid()