Add GitHub release update check to Python GNOME and KDE KCM UIs (#160)

This commit is contained in:
Copilot 2026-02-28 18:29:27 -08:00 committed by GitHub
parent cc33623aa7
commit afb2dd4f4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 205 additions and 43 deletions

View File

@ -35,7 +35,7 @@ include(cmake/info.cmake)
find_package(epoxy REQUIRED)
find_package(XCB REQUIRED COMPONENTS XCB)
find_package(KWinDBusInterface CONFIG REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS Core)
find_package(Qt6 REQUIRED COMPONENTS Core Network)
# Qt6 sets QT6_INSTALL_QML which is distro-aware
get_target_property(QT6_QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION)

View File

@ -15,6 +15,7 @@ kcoreaddons_add_plugin(breezy_desktop_config INSTALL_NAMESPACE "plasma/kcms" SOU
kconfig_add_kcfg_files(breezy_desktop_config ../breezydesktopconfig.kcfgc)
target_link_libraries(breezy_desktop_config
Qt6::DBus
Qt6::Network
KF6::ConfigCore
KF6::ConfigGui
KF6::ConfigWidgets

View File

@ -51,6 +51,8 @@
#include <QDebug>
#include <QLocale>
#include <QSignalBlocker>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <cmath>
#include <algorithm>
@ -237,6 +239,9 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
// One-time check if the KWin effect backend is actually loaded. If not, disable UI early.
checkEffectLoaded();
// Asynchronously check GitHub for a newer release.
checkForUpdates();
// Show/enable Virtual Display controls only when we're on Wayland
const bool isWaylandSession = QGuiApplication::platformName().contains(QStringLiteral("wayland"), Qt::CaseInsensitive)
|| qEnvironmentVariable("XDG_SESSION_TYPE").compare(QStringLiteral("wayland"), Qt::CaseInsensitive) == 0;
@ -588,6 +593,72 @@ void BreezyDesktopEffectConfig::checkEffectLoaded() {
}
}
void BreezyDesktopEffectConfig::checkForUpdates() {
#ifdef BREEZY_DESKTOP_VERSION_STR
// Skip update check for system-wide installs (e.g. AUR) — the package
// manager handles updates there. Scripted installs put the plugin under
// the user's home directory, so we use that as the heuristic.
const QString pluginPath = metaData().fileName();
const QString home = QDir::homePath();
if (!pluginPath.startsWith(home + QLatin1Char('/')))
return;
if (!m_networkManager)
m_networkManager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl(QStringLiteral("https://api.github.com/repos/wheaney/breezy-desktop/releases/latest")));
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("breezy-desktop-kcm"));
auto *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qCDebug(KWIN_XR) << "Update check failed:" << reply->errorString();
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (!doc.isObject()) return;
const QString latestTag = doc.object().value(QStringLiteral("tag_name")).toString();
if (latestTag.isEmpty()) return;
QString latest = latestTag;
if (latest.startsWith(QLatin1Char('v'))) latest.remove(0, 1);
// Compare version tuples
const QString current = QLatin1String(BREEZY_DESKTOP_VERSION_STR);
auto parseParts = [](const QString &v) -> QList<int> {
QList<int> parts;
for (const QString &p : v.split(QLatin1Char('.'))) {
bool ok;
int n = p.toInt(&ok);
if (!ok) return {};
parts.append(n);
}
return parts;
};
const QList<int> latestParts = parseParts(latest);
const QList<int> currentParts = parseParts(current);
if (latestParts.isEmpty() || currentParts.isEmpty()) return;
bool isNewer = false;
for (int i = 0; i < qMax(latestParts.size(), currentParts.size()); ++i) {
int lv = i < latestParts.size() ? latestParts[i] : 0;
int cv = i < currentParts.size() ? currentParts[i] : 0;
if (lv != cv) {
isNewer = lv > cv;
break;
}
}
if (isNewer) {
if (auto label = widget()->findChild<QLabel*>(QStringLiteral("labelUpdateAvailable"))) {
label->setText(tr("A newer version (%1) is available. To update, rerun the breezy_kwin_setup script.").arg(latest));
label->setVisible(true);
}
}
});
#endif
}
static QDBusInterface makeVDInterface() {
return QDBusInterface(
QStringLiteral("org.kde.KWin"),

View File

@ -4,6 +4,7 @@
#include <KConfigWatcher>
#include <memory>
#include <QNetworkAccessManager>
#include <QTimer>
#include <QVariant>
#include <QVariantList>
@ -56,6 +57,7 @@ private:
void pollDriverState();
void refreshLicenseUi(const QJsonObject &rootObj);
void checkEffectLoaded();
void checkForUpdates();
void showStatus(QLabel *label, bool success, const QString &message);
void setRequestInProgress(std::initializer_list<QObject*> widgets, bool inProgress);
bool eventFilter(QObject *watched, QEvent *event) override;
@ -71,6 +73,7 @@ private:
::Ui::BreezyDesktopEffectConfig ui;
KConfigWatcher::Ptr m_configWatcher;
QNetworkAccessManager *m_networkManager = nullptr;
bool m_updatingFromConfig = false;
bool m_driverStateInitialized = false;
bool m_deviceConnected = false;

View File

@ -39,6 +39,25 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelUpdateAvailable">
<property name="text">
<string/>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignVCenter</set>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(0,100,200); font-weight: bold;</string>
</property>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="tabPosition">

@ -1 +1 @@
Subproject commit 9c6bd2b8260540a50b315bb0571ff0c2ac846dbc
Subproject commit 40c9979d7e79d31047cb474c154018ce43afb63a

View File

@ -3,6 +3,7 @@
<requires lib="gtk" version="4.0"/>
<template class="FailedVerification" parent="GtkBox">
<property name="orientation">1</property>
<property name="vexpand">True</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
@ -10,9 +11,11 @@
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="vexpand">True</property>
<property name="title" translatable="yes">Breezy Desktop GNOME invalid setup</property>
<property name="description" translatable="yes">Your Breezy GNOME setup is invalid or incomplete. Please re-run the setup script. Report this issue if it persists.</property>
<property name="width-request">650</property>
<property name="height-request">250</property>
</object>
</child>
</template>

View File

@ -3,6 +3,7 @@
<requires lib="gtk" version="4.0"/>
<template class="NoDevice" parent="GtkBox">
<property name="orientation">1</property>
<property name="vexpand">True</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
@ -10,6 +11,7 @@
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="vexpand">True</property>
<property name="title" translatable="yes">No device connected</property>
<property name="description" translatable="yes">Breezy Desktop was unable to detect any supported XR devices.</property>
<property name="width-request">800</property>

View File

@ -3,6 +3,7 @@
<requires lib="gtk" version="4.0"/>
<template class="NoDriver" parent="GtkBox">
<property name="orientation">1</property>
<property name="vexpand">True</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
@ -10,6 +11,7 @@
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="vexpand">True</property>
<property name="title" translatable="yes">No driver running</property>
<property name="description" translatable="yes">
If you installed via AUR, make sure you ran the recommended post-install command:
@ -18,6 +20,7 @@
Otherwise, please file an issue on GitHub, or create a new thread in the #troubleshooting channel on Discord.
</property>
<property name="width-request">800</property>
<property name="height-request">300</property>
</object>
</child>
</template>

View File

@ -3,6 +3,7 @@
<requires lib="gtk" version="4.0"/>
<template class="NoExtension" parent="GtkBox">
<property name="orientation">1</property>
<property name="vexpand">True</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
@ -10,9 +11,11 @@
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="vexpand">True</property>
<property name="title" translatable="yes">Breezy Desktop GNOME extension not ready</property>
<property name="description" translatable="yes">If you have just run the setup, then you may need to log out and back in to use it. Otherwise, please follow the Breezy GNOME setup instructions.</property>
<property name="width-request">800</property>
<property name="height-request">250</property>
</object>
</child>
</template>

View File

@ -3,6 +3,7 @@
<requires lib="gtk" version="4.0"/>
<template class="NoLicense" parent="GtkBox">
<property name="orientation">1</property>
<property name="vexpand">True</property>
<property name="margin-top">20</property>
<property name="margin-bottom">20</property>
<property name="margin-start">20</property>
@ -10,6 +11,7 @@
<property name="spacing">20</property>
<child>
<object class="AdwStatusPage">
<property name="vexpand">True</property>
<property name="title" translatable="yes">No license file was found</property>
<property name="description" translatable="yes">
The first time you use Breezy Desktop, an internet connection is required to retrieve your device's license.

View File

@ -80,7 +80,29 @@
</object>
</child>
<child>
<object class="GtkBox" id="main_content" />
<object class="GtkInfoBar" id="update_available_banner">
<property name="revealed">0</property>
<property name="show-close-button">False</property>
<property name="message-type">info</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">A newer version is available. To update, rerun the breezy_gnome_setup script.</property>
<property name="hexpand">True</property>
<property name="wrap">True</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_content">
<property name="vexpand">True</property>
</object>
</child>
</object>
</property>

View File

@ -1,22 +1,3 @@
# main.py
#
# Copyright 2024 Unknown
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import sys
import gi
@ -90,7 +71,7 @@ class BreezydesktopApplication(Adw.Application):
"""
win = self.props.active_window
if not win:
win = BreezydesktopWindow(self._skip_verification, application=self)
win = BreezydesktopWindow(self.version, self._skip_verification, application=self)
win.connect('close-request', lambda *_: self.on_quit_action())
win.connect('destroy', lambda *_: self.on_quit_action())
win.present()

View File

@ -61,6 +61,7 @@ breezydesktop_sources = [
'shortcutdialog.py',
'statemanager.py',
'time.py',
'updatechecker.py',
'virtualdisplay.py',
'virtualdisplaymanager.py',
'verify.py',

63
ui/src/updatechecker.py Normal file
View File

@ -0,0 +1,63 @@
import json
import logging
import os
import sys
import threading
from urllib.request import urlopen, Request
from urllib.error import URLError
logger = logging.getLogger('breezy_ui')
GITHUB_RELEASES_URL = 'https://api.github.com/repos/wheaney/breezy-desktop/releases/latest'
def _is_user_local_install():
"""Return True if the app is running from a user-local installation.
Scripted installs put the binary under the user's home directory (e.g.
~/.local/bin/breezydesktop). System-wide package manager installs (e.g.
AUR) put the binary in a system path like /usr/bin and don't need a
version-update prompt because the package manager handles updates.
"""
home = os.path.expanduser('~')
script_path = os.path.realpath(sys.argv[0])
return script_path.startswith(home + os.sep)
def _parse_version(version_str):
"""Parse a version string like '2.8.10' or 'v2.8.9' into a tuple of ints."""
v = version_str.strip().lstrip('v')
try:
return tuple(int(x) for x in v.split('.'))
except (ValueError, AttributeError):
return None
def check_for_update(current_version, callback):
"""
Asynchronously check for a newer version on GitHub.
Calls callback(latest_version_str) on the calling thread's GLib main loop
if a newer version is found, or callback(None) if no update is available
or if the check fails. Does nothing (no callback) when not running from a
user-local installation (e.g. installed via AUR).
"""
if not _is_user_local_install():
return
def _check():
latest_version = None
try:
req = Request(GITHUB_RELEASES_URL, headers={'User-Agent': 'breezy-desktop-ui'})
with urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode())
latest_tag = data.get('tag_name', '')
latest = _parse_version(latest_tag)
current = _parse_version(current_version)
if latest and current and latest > current:
latest_version = latest_tag.lstrip('v')
except (URLError, json.JSONDecodeError, ValueError, OSError) as e:
logger.debug('Update check failed: %s', e)
callback(latest_version)
threading.Thread(target=_check, daemon=True).start()

View File

@ -1,22 +1,3 @@
# window.py
#
# Copyright 2024 Unknown
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Gtk, GLib
from .extensionsmanager import ExtensionsManager
from .license import BREEZY_GNOME_FEATURES
@ -29,6 +10,7 @@ from .nodevice import NoDevice
from .nodriver import NoDriver
from .noextension import NoExtension
from .nolicense import NoLicense
from .updatechecker import check_for_update
from .verify import verify_installation
@Gtk.Template(resource_path='/com/xronlinux/BreezyDesktop/gtk/window.ui')
@ -40,8 +22,9 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
license_action_needed_button = Gtk.Template.Child()
missing_breezy_features_banner = Gtk.Template.Child()
missing_breezy_features_button = Gtk.Template.Child()
update_available_banner = Gtk.Template.Child()
def __init__(self, skip_verification, **kwargs):
def __init__(self, version, skip_verification, **kwargs):
super().__init__(**kwargs)
self.connected_device = ConnectedDevice()
@ -70,6 +53,8 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
self.connect("destroy", self._on_window_destroy)
check_for_update(version, self._on_update_check_result)
def _handle_settings_update(self, settings_manager, key):
self._handle_state_update(self.state_manager, None)
@ -112,5 +97,8 @@ class BreezydesktopWindow(Gtk.ApplicationWindow):
dialog.set_transient_for(widget.get_ancestor(Gtk.Window))
dialog.present()
def _on_update_check_result(self, latest_version):
GLib.idle_add(self.update_available_banner.set_revealed, latest_version is not None)
def _on_window_destroy(self, widget):
self.state_manager.disconnect_by_func(self._handle_state_update)