From 83f5af522dc0ff1c00f2518af0c18d0fc44a428a Mon Sep 17 00:00:00 2001 From: Derock Date: Wed, 13 Aug 2025 12:52:16 -0400 Subject: [PATCH 01/23] core/log: fix nullptr crash in ThreadLogging --- src/core/logging.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index cb3a214..909da03 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -458,13 +458,13 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) { } if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) { + this->detailedWriter.setDevice(nullptr); + if (this->detailedFile) { + this->detailedFile->close(); + this->detailedFile = nullptr; qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; } - - this->detailedWriter.setDevice(nullptr); - this->detailedFile->close(); - this->detailedFile = nullptr; } } From e4d33fa52f0b82883dfbd80634f8fb9a42f82e62 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Tue, 2 Sep 2025 12:38:34 -0400 Subject: [PATCH 02/23] hyprland/ipc: fix focusedWorkspaceChanged connection --- src/wayland/hyprland/ipc/qml.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp index 89eec9e..eb5fdc6 100644 --- a/src/wayland/hyprland/ipc/qml.cpp +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -24,9 +24,9 @@ HyprlandIpcQml::HyprlandIpcQml() { QObject::connect( instance, - &HyprlandIpc::focusedMonitorChanged, + &HyprlandIpc::focusedWorkspaceChanged, this, - &HyprlandIpcQml::focusedMonitorChanged + &HyprlandIpcQml::focusedWorkspaceChanged ); QObject::connect( From 2115f314167dd8d797de2853b1fad35f3f2213f9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 16 Sep 2025 00:15:13 -0700 Subject: [PATCH 03/23] ci: use latest wayland-protocol for all test cases Fixes missing protocols on old nixpkgs versions --- ci/matrix.nix | 7 +++++-- ci/nix-checkouts.nix | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ci/matrix.nix b/ci/matrix.nix index be2da61..dd20fa5 100644 --- a/ci/matrix.nix +++ b/ci/matrix.nix @@ -2,7 +2,10 @@ qtver, compiler, }: let - nixpkgs = (import ./nix-checkouts.nix).${builtins.replaceStrings ["."] ["_"] qtver}; + checkouts = import ./nix-checkouts.nix; + nixpkgs = checkouts.${builtins.replaceStrings ["."] ["_"] qtver}; compilerOverride = (nixpkgs.callPackage ./variations.nix {}).${compiler}; - pkg = (nixpkgs.callPackage ../default.nix {}).override compilerOverride; + pkg = (nixpkgs.callPackage ../default.nix {}).override (compilerOverride // { + wayland-protocols = checkouts.latest.wayland-protocols; + }); in pkg diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix index 73c2415..d3e0159 100644 --- a/ci/nix-checkouts.nix +++ b/ci/nix-checkouts.nix @@ -7,10 +7,12 @@ let url = "https://github.com/nixos/nixpkgs/archive/${commit}.tar.gz"; inherit sha256; }) {}; -in { +in rec { # For old qt versions, grab the commit before the version bump that has all the patches # instead of the bumped version. + latest = qt6_9_0; + qt6_9_0 = byCommit { commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6"; sha256 = "0562lbi67a9brfwzpqs4n3l0i8zvgla368aakcy5mghr7ps80567"; From 996efc93b737842322103d8d5e7c0ac473b5ace5 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 11:09:34 -0400 Subject: [PATCH 04/23] core/desktopentry: watch for changes and rescan entries --- src/core/CMakeLists.txt | 1 + src/core/desktopentry.cpp | 381 ++++++++++++++------ src/core/desktopentry.hpp | 190 ++++++++-- src/core/desktopentrymonitor.cpp | 68 ++++ src/core/desktopentrymonitor.hpp | 32 ++ src/services/notifications/notification.cpp | 2 +- 6 files changed, 521 insertions(+), 153 deletions(-) create mode 100644 src/core/desktopentrymonitor.cpp create mode 100644 src/core/desktopentrymonitor.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7cef987..6029b42 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -23,6 +23,7 @@ qt_add_library(quickshell-core STATIC model.cpp elapsedtimer.cpp desktopentry.cpp + desktopentrymonitor.cpp objectrepeater.cpp platformmenu.cpp qsmenu.cpp diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 95fcb89..cb9710e 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -1,22 +1,27 @@ #include "desktopentry.hpp" #include +#include #include #include #include +#include #include -#include -#include #include #include #include #include +#include #include -#include +#include +#include #include +#include +#include #include #include "../io/processcore.hpp" +#include "desktopentrymonitor.hpp" #include "logcat.hpp" #include "model.hpp" #include "qmlglobal.hpp" @@ -87,57 +92,60 @@ struct Locale { QDebug operator<<(QDebug debug, const Locale& locale) { auto saver = QDebugStateSaver(debug); debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory - << ", modifier" << locale.modifier << ')'; + << ", modifier=" << locale.modifier << ')'; return debug; } -void DesktopEntry::parseEntry(const QString& text) { +ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& text) { + ParsedDesktopEntryData data; + data.id = id; const auto& system = Locale::system(); auto groupName = QString(); auto entries = QHash>(); - auto finishCategory = [this, &groupName, &entries]() { + auto finishCategory = [&data, &groupName, &entries]() { if (groupName == "Desktop Entry") { - if (entries["Type"].second != "Application") return; - if (entries.contains("Hidden") && entries["Hidden"].second == "true") return; + if (entries.value("Type").second != "Application") return; + if (entries.value("Hidden").second == "true") return; for (const auto& [key, pair]: entries.asKeyValueRange()) { auto& [_, value] = pair; - this->mEntries.insert(key, value); + data.entries.insert(key, value); - if (key == "Name") this->mName = value; - else if (key == "GenericName") this->mGenericName = value; - else if (key == "StartupWMClass") this->mStartupClass = value; - else if (key == "NoDisplay") this->mNoDisplay = value == "true"; - else if (key == "Comment") this->mComment = value; - else if (key == "Icon") this->mIcon = value; + if (key == "Name") data.name = value; + else if (key == "GenericName") data.genericName = value; + else if (key == "StartupWMClass") data.startupClass = value; + else if (key == "NoDisplay") data.noDisplay = value == "true"; + else if (key == "Comment") data.comment = value; + else if (key == "Icon") data.icon = value; else if (key == "Exec") { - this->mExecString = value; - this->mCommand = DesktopEntry::parseExecString(value); - } else if (key == "Path") this->mWorkingDirectory = value; - else if (key == "Terminal") this->mTerminal = value == "true"; - else if (key == "Categories") this->mCategories = value.split(u';', Qt::SkipEmptyParts); - else if (key == "Keywords") this->mKeywords = value.split(u';', Qt::SkipEmptyParts); + data.execString = value; + data.command = DesktopEntry::parseExecString(value); + } else if (key == "Path") data.workingDirectory = value; + else if (key == "Terminal") data.terminal = value == "true"; + else if (key == "Categories") data.categories = value.split(u';', Qt::SkipEmptyParts); + else if (key == "Keywords") data.keywords = value.split(u';', Qt::SkipEmptyParts); } } else if (groupName.startsWith("Desktop Action ")) { auto actionName = groupName.sliced(16); - auto* action = new DesktopAction(actionName, this); + DesktopActionData action; + action.id = actionName; for (const auto& [key, pair]: entries.asKeyValueRange()) { const auto& [_, value] = pair; - action->mEntries.insert(key, value); + action.entries.insert(key, value); - if (key == "Name") action->mName = value; - else if (key == "Icon") action->mIcon = value; + if (key == "Name") action.name = value; + else if (key == "Icon") action.icon = value; else if (key == "Exec") { - action->mExecString = value; - action->mCommand = DesktopEntry::parseExecString(value); + action.execString = value; + action.command = DesktopEntry::parseExecString(value); } } - this->mActions.insert(actionName, action); + data.actions.insert(actionName, action); } entries.clear(); @@ -183,14 +191,62 @@ void DesktopEntry::parseEntry(const QString& text) { } finishCategory(); + return data; +} + +void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) { + Qt::beginPropertyUpdateGroup(); + this->bName = newState.name; + this->bGenericName = newState.genericName; + this->bStartupClass = newState.startupClass; + this->bNoDisplay = newState.noDisplay; + this->bComment = newState.comment; + this->bIcon = newState.icon; + this->bExecString = newState.execString; + this->bCommand = newState.command; + this->bWorkingDirectory = newState.workingDirectory; + this->bRunInTerminal = newState.terminal; + this->bCategories = newState.categories; + this->bKeywords = newState.keywords; + Qt::endPropertyUpdateGroup(); + + this->state = newState; + this->updateActions(newState.actions); +} + +void DesktopEntry::updateActions(const QHash& newActions) { + auto old = this->mActions; + + for (const auto& [key, d]: newActions.asKeyValueRange()) { + DesktopAction* act = nullptr; + if (auto found = old.find(key); found != old.end()) { + act = found.value(); + old.erase(found); + } else { + act = new DesktopAction(d.id, this); + this->mActions.insert(key, act); + } + + Qt::beginPropertyUpdateGroup(); + act->bName = d.name; + act->bIcon = d.icon; + act->bExecString = d.execString; + act->bCommand = d.command; + Qt::endPropertyUpdateGroup(); + + act->mEntries = d.entries; + } + + for (auto* leftover: old) { + leftover->deleteLater(); + } } void DesktopEntry::execute() const { - DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory); + DesktopEntry::doExec(this->bCommand.value(), this->bWorkingDirectory.value()); } -bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } -bool DesktopEntry::noDisplay() const { return this->mNoDisplay; } +bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } QVector DesktopEntry::actions() const { return this->mActions.values(); } @@ -266,59 +322,44 @@ void DesktopEntry::doExec(const QList& execString, const QString& worki } void DesktopAction::execute() const { - DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory); + DesktopEntry::doExec(this->bCommand.value(), this->entry->bWorkingDirectory.value()); } -DesktopEntryManager::DesktopEntryManager() { - this->scanDesktopEntries(); - this->populateApplications(); +DesktopEntryScanner::DesktopEntryScanner(DesktopEntryManager* manager): manager(manager) { + this->setAutoDelete(true); } -void DesktopEntryManager::scanDesktopEntries() { - QList dataPaths; +void DesktopEntryScanner::run() { + const auto& desktopPaths = DesktopEntryManager::desktopPaths(); + auto scanResults = QList(); - if (qEnvironmentVariableIsSet("XDG_DATA_HOME")) { - dataPaths.push_back(qEnvironmentVariable("XDG_DATA_HOME")); - } else if (qEnvironmentVariableIsSet("HOME")) { - dataPaths.push_back(qEnvironmentVariable("HOME") + "/.local/share"); + for (const auto& path: desktopPaths | std::views::reverse) { + auto file = QFileInfo(path); + if (!file.isDir()) continue; + + this->scanDirectory(QDir(path), QString(), scanResults); } - if (qEnvironmentVariableIsSet("XDG_DATA_DIRS")) { - auto var = qEnvironmentVariable("XDG_DATA_DIRS"); - dataPaths += var.split(u':', Qt::SkipEmptyParts); - } else { - dataPaths.push_back("/usr/local/share"); - dataPaths.push_back("/usr/share"); - } - - qCDebug(logDesktopEntry) << "Creating desktop entry scanners"; - - for (auto& path: std::ranges::reverse_view(dataPaths)) { - auto p = QDir(path).filePath("applications"); - auto file = QFileInfo(p); - - if (!file.isDir()) { - qCDebug(logDesktopEntry) << "Not scanning path" << p << "as it is not a directory"; - continue; - } - - qCDebug(logDesktopEntry) << "Scanning path" << p; - this->scanPath(p); - } + QMetaObject::invokeMethod( + this->manager, + "onScanCompleted", + Qt::QueuedConnection, + Q_ARG(QList, scanResults) + ); } -void DesktopEntryManager::populateApplications() { - for (auto& entry: this->desktopEntries.values()) { - if (!entry->noDisplay()) this->mApplications.insertObject(entry); - } -} +void DesktopEntryScanner::scanDirectory( + const QDir& dir, + const QString& idPrefix, + QList& entries +) { + auto dirEntries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); -void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { - auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); - - for (auto& entry: entries) { - if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-"); - else if (entry.isFile()) { + for (auto& entry: dirEntries) { + if (entry.isDir()) { + auto subdirPrefix = idPrefix.isEmpty() ? entry.fileName() : idPrefix + '-' + entry.fileName(); + this->scanDirectory(QDir(entry.absoluteFilePath()), subdirPrefix, entries); + } else if (entry.isFile()) { auto path = entry.filePath(); if (!path.endsWith(".desktop")) { qCDebug(logDesktopEntry) << "Skipping file" << path << "as it has no .desktop extension"; @@ -331,46 +372,42 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { continue; } - auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); - auto lowerId = id.toLower(); + auto basename = QFileInfo(entry.fileName()).completeBaseName(); + auto id = idPrefix.isEmpty() ? basename : idPrefix + '-' + basename; + auto content = QString::fromUtf8(file.readAll()); - auto text = QString::fromUtf8(file.readAll()); - auto* dentry = new DesktopEntry(id, this); - dentry->parseEntry(text); - - if (!dentry->isValid()) { - qCDebug(logDesktopEntry) << "Skipping desktop entry" << path; - delete dentry; - continue; - } - - qCDebug(logDesktopEntry) << "Found desktop entry" << id << "at" << path; - - auto conflictingId = this->desktopEntries.contains(id); - - if (conflictingId) { - qCDebug(logDesktopEntry) << "Replacing old entry for" << id; - delete this->desktopEntries.value(id); - this->desktopEntries.remove(id); - this->lowercaseDesktopEntries.remove(lowerId); - } - - this->desktopEntries.insert(id, dentry); - - if (this->lowercaseDesktopEntries.contains(lowerId)) { - qCInfo(logDesktopEntry).nospace() - << "Multiple desktop entries have the same lowercased id " << lowerId - << ". This can cause ambiguity when byId requests are not made with the correct case " - "already."; - - this->lowercaseDesktopEntries.remove(lowerId); - } - - this->lowercaseDesktopEntries.insert(lowerId, dentry); + auto data = DesktopEntry::parseText(id, content); + entries.append(std::move(data)); } } } +DesktopEntryManager::DesktopEntryManager(): monitor(new DesktopEntryMonitor(this)) { + QObject::connect( + this->monitor, + &DesktopEntryMonitor::desktopEntriesChanged, + this, + &DesktopEntryManager::handleFileChanges + ); + + DesktopEntryScanner(this).run(); +} + +void DesktopEntryManager::scanDesktopEntries() { + qCDebug(logDesktopEntry) << "Starting desktop entry scan"; + + if (this->scanInProgress) { + qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan"; + this->scanQueued = true; + return; + } + + this->scanInProgress = true; + this->scanQueued = false; + auto* scanner = new DesktopEntryScanner(this); + QThreadPool::globalInstance()->start(scanner); +} + DesktopEntryManager* DesktopEntryManager::instance() { static auto* instance = new DesktopEntryManager(); // NOLINT return instance; @@ -391,14 +428,14 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { auto list = this->desktopEntries.values(); - auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { - return name == entry->mStartupClass; + auto iter = std::ranges::find_if(list, [&](DesktopEntry* entry) { + return name == entry->bStartupClass.value(); }); if (iter != list.end()) return *iter; - iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { - return name.toLower() == entry->mStartupClass.toLower(); + iter = std::ranges::find_if(list, [&](DesktopEntry* entry) { + return name.toLower() == entry->bStartupClass.value().toLower(); }); if (iter != list.end()) return *iter; @@ -407,7 +444,123 @@ DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } -DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } +void DesktopEntryManager::handleFileChanges() { + qCDebug(logDesktopEntry) << "Directory change detected, performing full rescan"; + + if (this->scanInProgress) { + qCDebug(logDesktopEntry) << "Scan already in progress, queuing another scan"; + this->scanQueued = true; + return; + } + + this->scanInProgress = true; + this->scanQueued = false; + auto* scanner = new DesktopEntryScanner(this); + QThreadPool::globalInstance()->start(scanner); +} + +const QStringList& DesktopEntryManager::desktopPaths() { + static const auto paths = []() { + auto dataPaths = QStringList(); + + auto dataHome = qEnvironmentVariable("XDG_DATA_HOME"); + if (dataHome.isEmpty() && qEnvironmentVariableIsSet("HOME")) + dataHome = qEnvironmentVariable("HOME") + "/.local/share"; + if (!dataHome.isEmpty()) dataPaths.append(dataHome + "/applications"); + + auto dataDirs = qEnvironmentVariable("XDG_DATA_DIRS"); + if (dataDirs.isEmpty()) dataDirs = "/usr/local/share:/usr/share"; + + for (const auto& dir: dataDirs.split(':', Qt::SkipEmptyParts)) { + dataPaths.append(dir + "/applications"); + } + + return dataPaths; + }(); + + return paths; +} + +void DesktopEntryManager::onScanCompleted(const QList& scanResults) { + auto guard = qScopeGuard([this] { + this->scanInProgress = false; + if (this->scanQueued) { + this->scanQueued = false; + this->scanDesktopEntries(); + } + }); + + auto oldEntries = this->desktopEntries; + auto newEntries = QHash(); + auto newLowercaseEntries = QHash(); + + for (const auto& data: scanResults) { + DesktopEntry* dentry = nullptr; + + if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { + dentry = it.value(); + oldEntries.erase(it); + dentry->updateState(data); + } else { + dentry = new DesktopEntry(data.id, this); + dentry->updateState(data); + } + + if (!dentry->isValid()) { + qCDebug(logDesktopEntry) << "Skipping desktop entry" << data.id; + if (!oldEntries.contains(data.id)) { + dentry->deleteLater(); + } + continue; + } + + qCDebug(logDesktopEntry) << "Found desktop entry" << data.id; + + auto lowerId = data.id.toLower(); + auto conflictingId = newEntries.contains(data.id); + + if (conflictingId) { + qCDebug(logDesktopEntry) << "Replacing old entry for" << data.id; + if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); + newLowercaseEntries.remove(lowerId); + } + + newEntries.insert(data.id, dentry); + + if (newLowercaseEntries.contains(lowerId)) { + qCInfo(logDesktopEntry).nospace() + << "Multiple desktop entries have the same lowercased id " << lowerId + << ". This can cause ambiguity when byId requests are not made with the correct case " + "already."; + + newLowercaseEntries.remove(lowerId); + } + + newLowercaseEntries.insert(lowerId, dentry); + } + + this->desktopEntries = newEntries; + this->lowercaseDesktopEntries = newLowercaseEntries; + + auto newApplications = QVector(); + for (auto* entry: this->desktopEntries.values()) + if (!entry->bNoDisplay) newApplications.append(entry); + + this->mApplications.diffUpdate(newApplications); + + emit this->applicationsChanged(); + + for (auto* e: oldEntries) e->deleteLater(); +} + +DesktopEntries::DesktopEntries() { + QObject::connect( + DesktopEntryManager::instance(), + &DesktopEntryManager::applicationsChanged, + this, + &DesktopEntries::applicationsChanged + ); +} DesktopEntry* DesktopEntries::byId(const QString& id) { return DesktopEntryManager::instance()->byId(id); diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 827a618..b124aaf 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -6,35 +6,67 @@ #include #include #include +#include #include +#include #include +#include "desktopentrymonitor.hpp" #include "doc.hpp" #include "model.hpp" class DesktopAction; +class DesktopEntryMonitor; + +struct DesktopActionData { + QString id; + QString name; + QString icon; + QString execString; + QVector command; + QHash entries; +}; + +struct ParsedDesktopEntryData { + QString id; + QString name; + QString genericName; + QString startupClass; + bool noDisplay = false; + QString comment; + QString icon; + QString execString; + QVector command; + QString workingDirectory; + bool terminal = false; + QVector categories; + QVector keywords; + QHash entries; + QHash actions; +}; /// A desktop entry. See @@DesktopEntries for details. class DesktopEntry: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); /// Name of the specific application, such as "Firefox". - Q_PROPERTY(QString name MEMBER mName CONSTANT); + // clang-format off + Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); /// Short description of the application, such as "Web Browser". May be empty. - Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT); + Q_PROPERTY(QString genericName READ default WRITE default NOTIFY genericNameChanged BINDABLE bindableGenericName); /// Initial class or app id the app intends to use. May be useful for matching running apps /// to desktop entries. - Q_PROPERTY(QString startupClass MEMBER mStartupClass CONSTANT); + Q_PROPERTY(QString startupClass READ default WRITE default NOTIFY startupClassChanged BINDABLE bindableStartupClass); /// If true, this application should not be displayed in menus and launchers. - Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT); + Q_PROPERTY(bool noDisplay READ default WRITE default NOTIFY noDisplayChanged BINDABLE bindableNoDisplay); /// Long description of the application, such as "View websites on the internet". May be empty. - Q_PROPERTY(QString comment MEMBER mComment CONSTANT); + Q_PROPERTY(QString comment READ default WRITE default NOTIFY commentChanged BINDABLE bindableComment); /// Name of the icon associated with this application. May be empty. - Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); + Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the desktop entry. /// /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); + Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString); /// The parsed `Exec` command in the desktop entry. /// /// The entry can be run with @@execute(), or by using this command in @@ -43,13 +75,14 @@ class DesktopEntry: public QObject { /// the invoked process. See @@execute() for details. /// /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. - Q_PROPERTY(QVector command MEMBER mCommand CONSTANT); + Q_PROPERTY(QVector command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand); /// The working directory to execute from. - Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); + Q_PROPERTY(QString workingDirectory READ default WRITE default NOTIFY workingDirectoryChanged BINDABLE bindableWorkingDirectory); /// If the application should run in a terminal. - Q_PROPERTY(bool runInTerminal MEMBER mTerminal CONSTANT); - Q_PROPERTY(QVector categories MEMBER mCategories CONSTANT); - Q_PROPERTY(QVector keywords MEMBER mKeywords CONSTANT); + Q_PROPERTY(bool runInTerminal READ default WRITE default NOTIFY runInTerminalChanged BINDABLE bindableRunInTerminal); + Q_PROPERTY(QVector categories READ default WRITE default NOTIFY categoriesChanged BINDABLE bindableCategories); + Q_PROPERTY(QVector keywords READ default WRITE default NOTIFY keywordsChanged BINDABLE bindableKeywords); + // clang-format on Q_PROPERTY(QVector actions READ actions CONSTANT); QML_ELEMENT; QML_UNCREATABLE("DesktopEntry instances must be retrieved from DesktopEntries"); @@ -57,7 +90,8 @@ class DesktopEntry: public QObject { public: explicit DesktopEntry(QString id, QObject* parent): QObject(parent), mId(std::move(id)) {} - void parseEntry(const QString& text); + static ParsedDesktopEntryData parseText(const QString& id, const QString& text); + void updateState(const ParsedDesktopEntryData& newState); /// Run the application. Currently ignores @@runInTerminal and field codes. /// @@ -73,30 +107,65 @@ public: Q_INVOKABLE void execute() const; [[nodiscard]] bool isValid() const; - [[nodiscard]] bool noDisplay() const; [[nodiscard]] QVector actions() const; + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableGenericName() const { return &this->bGenericName; } + [[nodiscard]] QBindable bindableStartupClass() const { return &this->bStartupClass; } + [[nodiscard]] QBindable bindableNoDisplay() const { return &this->bNoDisplay; } + [[nodiscard]] QBindable bindableComment() const { return &this->bComment; } + [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } + [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; } + [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; } + [[nodiscard]] QBindable bindableWorkingDirectory() const { + return &this->bWorkingDirectory; + } + [[nodiscard]] QBindable bindableRunInTerminal() const { return &this->bRunInTerminal; } + [[nodiscard]] QBindable> bindableCategories() const { + return &this->bCategories; + } + [[nodiscard]] QBindable> bindableKeywords() const { return &this->bKeywords; } + // currently ignores all field codes. static QVector parseExecString(const QString& execString); static void doExec(const QList& execString, const QString& workingDirectory); +signals: + void nameChanged(); + void genericNameChanged(); + void startupClassChanged(); + void noDisplayChanged(); + void commentChanged(); + void iconChanged(); + void execStringChanged(); + void commandChanged(); + void workingDirectoryChanged(); + void runInTerminalChanged(); + void categoriesChanged(); + void keywordsChanged(); + public: QString mId; - QString mName; - QString mGenericName; - QString mStartupClass; - bool mNoDisplay = false; - QString mComment; - QString mIcon; - QString mExecString; - QVector mCommand; - QString mWorkingDirectory; - bool mTerminal = false; - QVector mCategories; - QVector mKeywords; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bName, &DesktopEntry::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bGenericName, &DesktopEntry::genericNameChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bStartupClass, &DesktopEntry::startupClassChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bNoDisplay, &DesktopEntry::noDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bComment, &DesktopEntry::commentChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bIcon, &DesktopEntry::iconChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bExecString, &DesktopEntry::execStringChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCommand, &DesktopEntry::commandChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QString, bWorkingDirectory, &DesktopEntry::workingDirectoryChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, bool, bRunInTerminal, &DesktopEntry::runInTerminalChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bCategories, &DesktopEntry::categoriesChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopEntry, QVector, bKeywords, &DesktopEntry::keywordsChanged); + // clang-format on private: - QHash mEntries; + void updateActions(const QHash& newActions); + + ParsedDesktopEntryData state; QHash mActions; friend class DesktopAction; @@ -106,12 +175,13 @@ private: class DesktopAction: public QObject { Q_OBJECT; Q_PROPERTY(QString id MEMBER mId CONSTANT); - Q_PROPERTY(QString name MEMBER mName CONSTANT); - Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); + // clang-format off + Q_PROPERTY(QString name READ default WRITE default NOTIFY nameChanged BINDABLE bindableName); + Q_PROPERTY(QString icon READ default WRITE default NOTIFY iconChanged BINDABLE bindableIcon); /// The raw `Exec` string from the action. /// /// > [!WARNING] This cannot be reliably run as a command. See @@command for one you can run. - Q_PROPERTY(QString execString MEMBER mExecString CONSTANT); + Q_PROPERTY(QString execString READ default WRITE default NOTIFY execStringChanged BINDABLE bindableExecString); /// The parsed `Exec` command in the action. /// /// The entry can be run with @@execute(), or by using this command in @@ -120,7 +190,8 @@ class DesktopAction: public QObject { /// the invoked process. /// /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. - Q_PROPERTY(QVector command MEMBER mCommand CONSTANT); + Q_PROPERTY(QVector command READ default WRITE default NOTIFY commandChanged BINDABLE bindableCommand); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); @@ -136,18 +207,47 @@ public: /// and @@DesktopEntry.workingDirectory. Q_INVOKABLE void execute() const; + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } + [[nodiscard]] QBindable bindableExecString() const { return &this->bExecString; } + [[nodiscard]] QBindable> bindableCommand() const { return &this->bCommand; } + +signals: + void nameChanged(); + void iconChanged(); + void execStringChanged(); + void commandChanged(); + private: DesktopEntry* entry; QString mId; - QString mName; - QString mIcon; - QString mExecString; - QVector mCommand; QHash mEntries; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bName, &DesktopAction::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bIcon, &DesktopAction::iconChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QString, bExecString, &DesktopAction::execStringChanged); + Q_OBJECT_BINDABLE_PROPERTY(DesktopAction, QVector, bCommand, &DesktopAction::commandChanged); + // clang-format on + friend class DesktopEntry; }; +class DesktopEntryManager; + +class DesktopEntryScanner: public QRunnable { +public: + explicit DesktopEntryScanner(DesktopEntryManager* manager); + + void run() override; + // clang-format off + void scanDirectory(const QDir& dir, const QString& idPrefix, QList& entries); + // clang-format on + +private: + DesktopEntryManager* manager; +}; + class DesktopEntryManager: public QObject { Q_OBJECT; @@ -161,15 +261,26 @@ public: static DesktopEntryManager* instance(); + static const QStringList& desktopPaths(); + +signals: + void applicationsChanged(); + +private slots: + void handleFileChanges(); + void onScanCompleted(const QList& scanResults); + private: explicit DesktopEntryManager(); - void populateApplications(); - void scanPath(const QDir& dir, const QString& prefix = QString()); - QHash desktopEntries; QHash lowercaseDesktopEntries; ObjectModel mApplications {this}; + DesktopEntryMonitor* monitor = nullptr; + bool scanInProgress = false; + bool scanQueued = false; + + friend class DesktopEntryScanner; }; ///! Desktop entry index. @@ -201,4 +312,7 @@ public: Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] static ObjectModel* applications(); + +signals: + void applicationsChanged(); }; diff --git a/src/core/desktopentrymonitor.cpp b/src/core/desktopentrymonitor.cpp new file mode 100644 index 0000000..bed6ef1 --- /dev/null +++ b/src/core/desktopentrymonitor.cpp @@ -0,0 +1,68 @@ +#include "desktopentrymonitor.hpp" + +#include +#include +#include +#include +#include +#include + +#include "desktopentry.hpp" + +namespace { +void addPathAndParents(QFileSystemWatcher& watcher, const QString& path) { + watcher.addPath(path); + + auto p = QFileInfo(path).absolutePath(); + while (!p.isEmpty()) { + watcher.addPath(p); + const auto parent = QFileInfo(p).dir().absolutePath(); + if (parent == p) break; + p = parent; + } +} +} // namespace + +DesktopEntryMonitor::DesktopEntryMonitor(QObject* parent): QObject(parent) { + this->debounceTimer.setSingleShot(true); + this->debounceTimer.setInterval(100); + + QObject::connect( + &this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &DesktopEntryMonitor::onDirectoryChanged + ); + QObject::connect( + &this->debounceTimer, + &QTimer::timeout, + this, + &DesktopEntryMonitor::processChanges + ); + + this->startMonitoring(); +} + +void DesktopEntryMonitor::startMonitoring() { + for (const auto& path: DesktopEntryManager::desktopPaths()) { + if (!QDir(path).exists()) continue; + addPathAndParents(this->watcher, path); + this->scanAndWatch(path); + } +} + +void DesktopEntryMonitor::scanAndWatch(const QString& dirPath) { + auto dir = QDir(dirPath); + if (!dir.exists()) return; + + this->watcher.addPath(dirPath); + + auto subdirs = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (const auto& subdir: subdirs) this->watcher.addPath(subdir.absoluteFilePath()); +} + +void DesktopEntryMonitor::onDirectoryChanged(const QString& /*path*/) { + this->debounceTimer.start(); +} + +void DesktopEntryMonitor::processChanges() { emit this->desktopEntriesChanged(); } \ No newline at end of file diff --git a/src/core/desktopentrymonitor.hpp b/src/core/desktopentrymonitor.hpp new file mode 100644 index 0000000..eb3251d --- /dev/null +++ b/src/core/desktopentrymonitor.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +class DesktopEntryMonitor: public QObject { + Q_OBJECT + +public: + explicit DesktopEntryMonitor(QObject* parent = nullptr); + ~DesktopEntryMonitor() override = default; + DesktopEntryMonitor(const DesktopEntryMonitor&) = delete; + DesktopEntryMonitor& operator=(const DesktopEntryMonitor&) = delete; + DesktopEntryMonitor(DesktopEntryMonitor&&) = delete; + DesktopEntryMonitor& operator=(DesktopEntryMonitor&&) = delete; + +signals: + void desktopEntriesChanged(); + +private slots: + void onDirectoryChanged(const QString& path); + void processChanges(); + +private: + void startMonitoring(); + void scanAndWatch(const QString& dirPath); + + QFileSystemWatcher watcher; + QTimer debounceTimer; +}; diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index c5269f3..d048bde 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -127,7 +127,7 @@ void Notification::updateProperties( if (appIcon.isEmpty() && !this->bDesktopEntry.value().isEmpty()) { if (auto* entry = DesktopEntryManager::instance()->byId(this->bDesktopEntry.value())) { - appIcon = entry->mIcon; + appIcon = entry->bIcon.value(); } } From b9cce250618f87ac1c84f6bba5064d6cc912127a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 19 Sep 2025 00:16:26 -0700 Subject: [PATCH 05/23] core: derive incubation controllers from tracked windows list Replaces the attempts to track incubation controllers directly with a list of all known windows, then pulls the first usable incubation controller when an assignment is requested. This should finally fix incubation controller related use after free crashes. --- src/core/generation.cpp | 112 +++++++------------------------------ src/core/generation.hpp | 8 +-- src/window/proxywindow.cpp | 10 +--- 3 files changed, 27 insertions(+), 103 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index fee9441..e15103a 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -11,12 +11,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include "iconimageprovider.hpp" @@ -242,90 +242,6 @@ void EngineGeneration::onDirectoryChanged() { } } -void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { - // We only want controllers that we can swap out if destroyed. - // This happens if the window owning the active controller dies. - auto* obj = dynamic_cast(controller); - if (!obj) { - qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject" - << controller; - - return; - } - - QObject::connect( - obj, - &QObject::destroyed, - this, - &EngineGeneration::incubationControllerDestroyed, - Qt::UniqueConnection - ); - - this->incubationControllers.push_back(obj); - qCDebug(logIncubator) << "Registered incubation controller" << obj << "to generation" << this; - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (this->engine->incubationController() == &this->delayedIncubationController) { - this->assignIncubationController(); - } -} - -// Multiple controllers may be destroyed at once. Dynamic casts must be performed before working -// with any controllers. The QQmlIncubationController destructor will already have run by the -// point QObject::destroyed is called, so we can't cast to that. -void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) { - auto* obj = dynamic_cast(controller); - if (!obj) { - qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, " - "however only QObject controllers should be registered."; - } - - QObject::disconnect(obj, nullptr, this, nullptr); - - if (this->incubationControllers.removeOne(obj)) { - qCDebug(logIncubator) << "Deregistered incubation controller" << obj << "from" << this; - } else { - qCCritical(logIncubator) << "Failed to deregister incubation controller" << obj << "from" - << this << "as it was not registered to begin with"; - qCCritical(logIncubator) << "Current registered incuabation controllers" - << this->incubationControllers; - } - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (this->engine->incubationController() == controller) { - qCDebug(logIncubator - ) << "Destroyed incubation controller was currently active, reassigning from pool"; - this->assignIncubationController(); - } -} - -void EngineGeneration::incubationControllerDestroyed() { - auto* sender = this->sender(); - - if (this->incubationControllers.removeAll(sender) != 0) { - qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from" - << this; - } else { - qCCritical(logIncubator) << "Destroyed incubation controller" << sender - << "was not registered, but its destruction was observed by" << this; - - return; - } - - // This function can run during destruction. - if (this->engine == nullptr) return; - - if (dynamic_cast(this->engine->incubationController()) == sender) { - qCDebug(logIncubator - ) << "Destroyed incubation controller was currently active, reassigning from pool"; - this->assignIncubationController(); - } -} - void EngineGeneration::onEngineWarnings(const QList& warnings) { for (const auto& error: warnings) { const auto& url = error.url(); @@ -367,13 +283,27 @@ void EngineGeneration::exit(int code) { this->destroy(); } -void EngineGeneration::assignIncubationController() { - QQmlIncubationController* controller = nullptr; +void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) { + if (this->trackedWindows.contains(window)) return; - if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) { - controller = &this->delayedIncubationController; - } else { - controller = dynamic_cast(this->incubationControllers.first()); + QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed); + this->trackedWindows.append(window); + this->assignIncubationController(); +} + +void EngineGeneration::onTrackedWindowDestroyed(QObject* object) { + this->trackedWindows.removeAll(static_cast(object)); // NOLINT + this->assignIncubationController(); +} + +void EngineGeneration::assignIncubationController() { + QQmlIncubationController* controller = &this->delayedIncubationController; + + for (auto* window: this->trackedWindows) { + if (auto* wctl = window->incubationController()) { + controller = wctl; + break; + } } qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 3c0c4ae..fef8363 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "incubator.hpp" @@ -40,8 +41,7 @@ public: void setWatchingFiles(bool watching); bool setExtraWatchedFiles(const QVector& files); - void registerIncubationController(QQmlIncubationController* controller); - void deregisterIncubationController(QQmlIncubationController* controller); + void trackWindowIncubationController(QQuickWindow* window); // takes ownership void registerExtension(const void* key, EngineGenerationExt* extension); @@ -84,13 +84,13 @@ public slots: private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); - void incubationControllerDestroyed(); + void onTrackedWindowDestroyed(QObject* object); static void onEngineWarnings(const QList& warnings); private: void postReload(); void assignIncubationController(); - QVector incubationControllers; + QVector trackedWindows; bool incubationControllersLocked = false; QHash extensions; diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 618751a..ea2904b 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -153,13 +153,7 @@ void ProxyWindowBase::createWindow() { void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { if (this->window != nullptr) emit this->windowDestroyed(); - if (auto* window = this->disownWindow(keepItemOwnership)) { - if (auto* generation = EngineGeneration::findObjectGeneration(this)) { - generation->deregisterIncubationController(window->incubationController()); - } - - window->deleteLater(); - } + if (auto* window = this->disownWindow(keepItemOwnership)) window->deleteLater(); } ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { @@ -185,7 +179,7 @@ void ProxyWindowBase::connectWindow() { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { // All windows have effectively the same incubation controller so it dosen't matter // which window it belongs to. We do want to replace the delay one though. - generation->registerIncubationController(this->window->incubationController()); + generation->trackWindowIncubationController(this->window); } this->window->setProxy(this); From d4b19e4a30563e9ed0fcc761eccfcf1a479cae8c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 18:55:45 -0700 Subject: [PATCH 06/23] build: fix cross compilation --- default.nix | 6 ++++-- src/wayland/CMakeLists.txt | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/default.nix b/default.nix index 71c949e..2cb3ac8 100644 --- a/default.nix +++ b/default.nix @@ -54,11 +54,13 @@ nativeBuildInputs = [ cmake ninja - qt6.qtshadertools spirv-tools pkg-config ] - ++ lib.optional withWayland wayland-scanner; + ++ lib.optionals withWayland [ + qt6.qtwayland # qtwaylandscanner required at build time + wayland-scanner + ]; buildInputs = [ qt6.qtbase diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 1d6543e..b0ee3aa 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -1,6 +1,6 @@ find_package(PkgConfig REQUIRED) find_package(WaylandScanner REQUIRED) -pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols) +pkg_check_modules(wayland REQUIRED IMPORTED_TARGET wayland-client wayland-protocols>=1.41) # wayland protocols @@ -12,13 +12,13 @@ if(NOT TARGET Qt6::qtwaylandscanner) message(FATAL_ERROR "qtwaylandscanner executable not found. Most likely there is an issue with your Qt installation.") endif() -execute_process( - COMMAND pkg-config --variable=pkgdatadir wayland-protocols - OUTPUT_VARIABLE WAYLAND_PROTOCOLS - OUTPUT_STRIP_TRAILING_WHITESPACE -) +pkg_get_variable(WAYLAND_PROTOCOLS wayland-protocols pkgdatadir) -message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}") +if(WAYLAND_PROTOCOLS) + message(STATUS "Found wayland protocols at ${WAYLAND_PROTOCOLS}") +else() + message(FATAL_ERROR "Could not find wayland protocols") +endif() qs_add_pchset(wayland-protocol DEPENDENCIES Qt::Core Qt::WaylandClient Qt::WaylandClientPrivate From 4b825e705170ab8d889fc7964805c696a5e5ba8c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 20:24:43 -0700 Subject: [PATCH 07/23] nix: add overlay --- ci/variations.nix | 4 ++-- default.nix | 4 ++-- flake.nix | 17 +++++++++++------ overlay.nix | 5 +++++ shell.nix | 3 ++- 5 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 overlay.nix diff --git a/ci/variations.nix b/ci/variations.nix index b0889be..b1d2947 100644 --- a/ci/variations.nix +++ b/ci/variations.nix @@ -2,6 +2,6 @@ clangStdenv, gccStdenv, }: { - clang = { buildStdenv = clangStdenv; }; - gcc = { buildStdenv = gccStdenv; }; + clang = { stdenv = clangStdenv; }; + gcc = { stdenv = gccStdenv; }; } diff --git a/default.nix b/default.nix index 2cb3ac8..3908e3c 100644 --- a/default.nix +++ b/default.nix @@ -2,8 +2,8 @@ lib, nix-gitignore, pkgs, + stdenv, keepDebugInfo, - buildStdenv ? pkgs.clangStdenv, pkg-config, cmake, @@ -44,7 +44,7 @@ withHyprland ? true, withI3 ? true, }: let - unwrapped = buildStdenv.mkDerivation { + unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.2.0"; src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; diff --git a/flake.nix b/flake.nix index 5de9c96..8edda2c 100644 --- a/flake.nix +++ b/flake.nix @@ -4,23 +4,28 @@ }; outputs = { self, nixpkgs }: let + overlayPkgs = p: p.appendOverlays [ self.overlays.default ]; + forEachSystem = fn: nixpkgs.lib.genAttrs nixpkgs.lib.platforms.linux - (system: fn system nixpkgs.legacyPackages.${system}); + (system: fn system (overlayPkgs nixpkgs.legacyPackages.${system})); in { - packages = forEachSystem (system: pkgs: rec { - quickshell = pkgs.callPackage ./default.nix { - gitRev = self.rev or self.dirtyRev; - }; + overlays.default = import ./overlay.nix { + rev = self.rev or self.dirtyRev; + }; + packages = forEachSystem (system: pkgs: rec { + quickshell = pkgs.quickshell; default = quickshell; }); devShells = forEachSystem (system: pkgs: rec { default = import ./shell.nix { inherit pkgs; - inherit (self.packages.${system}) quickshell; + quickshell = self.packages.${system}.quickshell.override { + stdenv = pkgs.clangStdenv; + }; }; }); }; diff --git a/overlay.nix b/overlay.nix new file mode 100644 index 0000000..d8ea137 --- /dev/null +++ b/overlay.nix @@ -0,0 +1,5 @@ +{ rev ? null }: (final: prev: { + quickshell = final.callPackage ./default.nix { + gitRev = rev; + }; +}) diff --git a/shell.nix b/shell.nix index 82382f9..b768862 100644 --- a/shell.nix +++ b/shell.nix @@ -1,6 +1,7 @@ { pkgs ? import {}, - quickshell ? pkgs.callPackage ./default.nix {}, + stdenv ? pkgs.clangStdenv, # faster compiles than gcc + quickshell ? pkgs.callPackage ./default.nix { inherit stdenv; }, ... }: let tidyfox = import (pkgs.fetchFromGitea { From 2f0f2d1201e240202a90e2d99e90478e70a49774 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 22:27:23 -0700 Subject: [PATCH 08/23] ci: add qt 6.9.2 and 6.9.1 checkouts --- .github/workflows/build.yml | 2 +- ci/nix-checkouts.nix | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93b8458..35729a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Nix strategy: matrix: - qtver: [qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] + qtver: [qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] compiler: [clang, gcc] runs-on: ubuntu-latest steps: diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix index d3e0159..5a95a34 100644 --- a/ci/nix-checkouts.nix +++ b/ci/nix-checkouts.nix @@ -8,11 +8,18 @@ let inherit sha256; }) {}; in rec { - # For old qt versions, grab the commit before the version bump that has all the patches - # instead of the bumped version. - latest = qt6_9_0; + qt6_9_2 = byCommit { + commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127"; + sha256 = "0s2mhbrgzxlgkg2yxb0q0hpk8lby1a7w67dxvfmaz4gsmc0bnvfj"; + }; + + qt6_9_1 = byCommit { + commit = "4c202d26483c5ccf3cb95e0053163facde9f047e"; + sha256 = "06l2w4bcgfw7dfanpzpjcf25ydf84in240yplqsss82qx405y9di"; + }; + qt6_9_0 = byCommit { commit = "546c545bd0594809a28ab7e869b5f80dd7243ef6"; sha256 = "0562lbi67a9brfwzpqs4n3l0i8zvgla368aakcy5mghr7ps80567"; From 62df8d917f54638b036d471fde0d0e539c5a1d18 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 22:27:31 -0700 Subject: [PATCH 09/23] ci: use unwrapped package for dependencies derivation Since adding the wrapper, CI built qs as it was a dependency of the wrapper instead of dependencies of qs itself. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 35729a8..dcfc546 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - uses: DeterminateSystems/nix-installer-action@main - name: Download Dependencies - run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).inputDerivation' + run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation' - name: Build run: nix-build --no-out-link --expr '(import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }' From 354afeaa358c084ff25e7bd1bbda8e17fb3fd9cd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 22:38:03 -0700 Subject: [PATCH 10/23] ci: add detsys nix cache --- .github/workflows/build.yml | 1 + .github/workflows/lint.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcfc546..c2e3976 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,7 @@ jobs: # Use cachix action over detsys for testing with act. # - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main - name: Download Dependencies run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da329cc..35ac4e0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,6 +10,7 @@ jobs: # Use cachix action over detsys for testing with act. # - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main - uses: nicknovitski/nix-develop@v1 - name: Check formatting From 6cc1b6f36a801f350928211e0d5815d86a48643c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 20:25:13 -0700 Subject: [PATCH 11/23] nix: update flake + tidyfox --- flake.lock | 6 +++--- shell.nix | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 7c25aa2..6971438 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1749285348, - "narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", + "lastModified": 1758690382, + "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", + "rev": "e643668fd71b949c53f8626614b21ff71a07379d", "type": "github" }, "original": { diff --git a/shell.nix b/shell.nix index b768862..03a446d 100644 --- a/shell.nix +++ b/shell.nix @@ -8,8 +8,8 @@ domain = "git.outfoxxed.me"; owner = "outfoxxed"; repo = "tidyfox"; - rev = "1f062cc198d1112d13e5128fa1f2ee3dbffe613b"; - sha256 = "kbt0Zc1qHE5fhqBkKz8iue+B+ZANjF1AR/RdgmX1r0I="; + rev = "9d85d7e7dea2602aa74ec3168955fee69967e92f"; + hash = "sha256-77ERiweF6lumonp2c/124rAoVG6/o9J+Aajhttwtu0w="; }) { inherit pkgs; }; in pkgs.mkShell.override { stdenv = quickshell.stdenv; } { inputsFrom = [ quickshell ]; From 344ca340bad320a7c57c021c1ab2c174417da76c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 23:51:06 -0700 Subject: [PATCH 12/23] all: fix lints --- src/core/colorquantizer.cpp | 55 ++++++++++--------- src/core/colorquantizer.hpp | 6 +- src/core/scriptmodel.cpp | 4 +- src/dbus/dbusmenu/dbusmenu.cpp | 2 +- src/dbus/dbusmenu/dbusmenu.hpp | 2 +- src/io/fileview.cpp | 5 +- src/io/jsonadapter.cpp | 20 +++---- src/services/mpris/player.cpp | 2 +- src/services/pipewire/device.cpp | 4 +- src/services/upower/device.cpp | 2 +- src/ui/reload_popup.cpp | 2 +- src/wayland/buffer/dmabuf.cpp | 4 +- src/wayland/buffer/dmabuf.hpp | 2 +- .../wlr_screencopy/wlr_screencopy.cpp | 6 +- 14 files changed, 60 insertions(+), 56 deletions(-) diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp index 6cfb05d..4ac850b 100644 --- a/src/core/colorquantizer.cpp +++ b/src/core/colorquantizer.cpp @@ -28,26 +28,28 @@ ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qrea : source(source) , maxDepth(depth) , rescaleSize(rescaleSize) { - setAutoDelete(false); + this->setAutoDelete(false); } void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCancel) { - if (shouldCancel.loadAcquire() || source->isEmpty()) return; + if (shouldCancel.loadAcquire() || this->source->isEmpty()) return; - colors.clear(); + this->colors.clear(); - auto image = QImage(source->toLocalFile()); - if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { + auto image = QImage(this->source->toLocalFile()); + if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize) + && this->rescaleSize > 0) + { image = image.scaled( - static_cast(rescaleSize), - static_cast(rescaleSize), + static_cast(this->rescaleSize), + static_cast(this->rescaleSize), Qt::KeepAspectRatio, Qt::SmoothTransformation ); } if (image.isNull()) { - qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString(); + qCWarning(logColorQuantizer) << "Failed to load image from" << this->source->toString(); return; } @@ -63,7 +65,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCa auto startTime = QDateTime::currentDateTime(); - colors = quantization(pixels, 0); + this->colors = this->quantization(pixels, 0); auto endTime = QDateTime::currentDateTime(); auto milliseconds = startTime.msecsTo(endTime); @@ -77,7 +79,7 @@ QList ColorQuantizerOperation::quantization( ) { if (shouldCancel.loadAcquire()) return QList(); - if (depth >= maxDepth || rgbValues.isEmpty()) { + if (depth >= this->maxDepth || rgbValues.isEmpty()) { if (rgbValues.isEmpty()) return QList(); auto totalR = 0; @@ -114,8 +116,8 @@ QList ColorQuantizerOperation::quantization( auto rightHalf = rgbValues.mid(mid); QList result; - result.append(quantization(leftHalf, depth + 1)); - result.append(quantization(rightHalf, depth + 1)); + result.append(this->quantization(leftHalf, depth + 1)); + result.append(this->quantization(rightHalf, depth + 1)); return result; } @@ -159,7 +161,7 @@ void ColorQuantizerOperation::finishRun() { } void ColorQuantizerOperation::finished() { - emit this->done(colors); + emit this->done(this->colors); delete this; } @@ -178,39 +180,39 @@ void ColorQuantizerOperation::run() { void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } void ColorQuantizer::componentComplete() { - componentCompleted = true; - if (!mSource.isEmpty()) quantizeAsync(); + this->componentCompleted = true; + if (!this->mSource.isEmpty()) this->quantizeAsync(); } void ColorQuantizer::setSource(const QUrl& source) { - if (mSource != source) { - mSource = source; + if (this->mSource != source) { + this->mSource = source; emit this->sourceChanged(); - if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); + if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); } } void ColorQuantizer::setDepth(qreal depth) { - if (mDepth != depth) { - mDepth = depth; + if (this->mDepth != depth) { + this->mDepth = depth; emit this->depthChanged(); - if (this->componentCompleted) quantizeAsync(); + if (this->componentCompleted) this->quantizeAsync(); } } void ColorQuantizer::setRescaleSize(int rescaleSize) { - if (mRescaleSize != rescaleSize) { - mRescaleSize = rescaleSize; + if (this->mRescaleSize != rescaleSize) { + this->mRescaleSize = rescaleSize; emit this->rescaleSizeChanged(); - if (this->componentCompleted) quantizeAsync(); + if (this->componentCompleted) this->quantizeAsync(); } } void ColorQuantizer::operationFinished(const QList& result) { - bColors = result; + this->bColors = result; this->liveOperation = nullptr; emit this->colorsChanged(); } @@ -219,7 +221,8 @@ void ColorQuantizer::quantizeAsync() { if (this->liveOperation) this->cancelAsync(); qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; - this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); + this->liveOperation = + new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize); QObject::connect( this->liveOperation, diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp index d35a15a..f6e158d 100644 --- a/src/core/colorquantizer.hpp +++ b/src/core/colorquantizer.hpp @@ -91,13 +91,13 @@ public: [[nodiscard]] QBindable> bindableColors() { return &this->bColors; } - [[nodiscard]] QUrl source() const { return mSource; } + [[nodiscard]] QUrl source() const { return this->mSource; } void setSource(const QUrl& source); - [[nodiscard]] qreal depth() const { return mDepth; } + [[nodiscard]] qreal depth() const { return this->mDepth; } void setDepth(qreal depth); - [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } + [[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; } void setRescaleSize(int rescaleSize); signals: diff --git a/src/core/scriptmodel.cpp b/src/core/scriptmodel.cpp index 6837c4a..a8271e7 100644 --- a/src/core/scriptmodel.cpp +++ b/src/core/scriptmodel.cpp @@ -19,7 +19,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) { auto newIter = newValues.begin(); // TODO: cache this - auto getCmpKey = [&](const QVariant& v) { + auto getCmpKey = [this](const QVariant& v) { if (v.canConvert()) { auto vMap = v.value(); if (vMap.contains(this->cmpKey)) { @@ -30,7 +30,7 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) { return v; }; - auto variantCmp = [&](const QVariant& a, const QVariant& b) { + auto variantCmp = [&, this](const QVariant& a, const QVariant& b) { if (!this->cmpKey.isEmpty()) return getCmpKey(a) == getCmpKey(b); else return a == b; }; diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 186b133..bcb354d 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -183,7 +183,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString } } else if (removed.isEmpty() || removed.contains("icon-data")) { imageChanged = this->image.hasData(); - image.data.clear(); + this->image.data.clear(); } auto type = properties.value("type"); diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 1192baa..06cbc34 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -36,7 +36,7 @@ class DBusMenuPngImage: public QsIndexedImageHandle { public: explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} - [[nodiscard]] bool hasData() const { return !data.isEmpty(); } + [[nodiscard]] bool hasData() const { return !this->data.isEmpty(); } QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; QByteArray data; diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 1585f26..04d77bd 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -93,7 +93,8 @@ void FileViewReader::run() { FileViewReader::read(this->owner, this->state, this->doStringConversion, this->shouldCancel); if (this->shouldCancel.loadAcquire()) { - qCDebug(logFileView) << "Read" << this << "of" << state.path << "canceled for" << this->owner; + qCDebug(logFileView) << "Read" << this << "of" << this->state.path << "canceled for" + << this->owner; } } @@ -206,7 +207,7 @@ void FileViewWriter::run() { FileViewWriter::write(this->owner, this->state, this->doAtomicWrite, this->shouldCancel); if (this->shouldCancel.loadAcquire()) { - qCDebug(logFileView) << "Write" << this << "of" << state.path << "canceled for" + qCDebug(logFileView) << "Write" << this << "of" << this->state.path << "canceled for" << this->owner; } } diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 80ac091..e80c6f2 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -44,7 +44,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) { this->deserializeRec(json.object(), this, &JsonAdapter::staticMetaObject); - for (auto* object: oldCreatedObjects) { + for (auto* object: this->oldCreatedObjects) { delete object; // FIXME: QMetaType::destroy? } @@ -56,7 +56,7 @@ void JsonAdapter::deserializeAdapter(const QByteArray& data) { void JsonAdapter::connectNotifiers() { auto notifySlot = JsonAdapter::staticMetaObject.indexOfSlot("onPropertyChanged()"); - connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject); + this->connectNotifiersRec(notifySlot, this, &JsonAdapter::staticMetaObject); } void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaObject* base) { @@ -71,7 +71,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO auto val = prop.read(obj); if (val.canView()) { auto* pobj = prop.read(obj).view(); - if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); + if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); } else if (val.canConvert>()) { auto listVal = val.value>(); @@ -79,7 +79,7 @@ void JsonAdapter::connectNotifiersRec(int notifySlot, QObject* obj, const QMetaO for (auto i = 0; i != len; i++) { auto* pobj = listVal.at(&listVal, i); - if (pobj) connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); + if (pobj) this->connectNotifiersRec(notifySlot, pobj, &JsonObject::staticMetaObject); } } } @@ -111,7 +111,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas auto* pobj = val.view(); if (pobj) { - json.insert(prop.name(), serializeRec(pobj, &JsonObject::staticMetaObject)); + json.insert(prop.name(), this->serializeRec(pobj, &JsonObject::staticMetaObject)); } else { json.insert(prop.name(), QJsonValue::Null); } @@ -124,7 +124,7 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas auto* pobj = listVal.at(&listVal, i); if (pobj) { - array.push_back(serializeRec(pobj, &JsonObject::staticMetaObject)); + array.push_back(this->serializeRec(pobj, &JsonObject::staticMetaObject)); } else { array.push_back(QJsonValue::Null); } @@ -178,8 +178,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM currentValue->setParent(this); this->createdObjects.push_back(currentValue); - } else if (oldCreatedObjects.removeOne(currentValue)) { - createdObjects.push_back(currentValue); + } else if (this->oldCreatedObjects.removeOne(currentValue)) { + this->createdObjects.push_back(currentValue); } this->deserializeRec(jval.toObject(), currentValue, &JsonObject::staticMetaObject); @@ -212,8 +212,8 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM if (jsonValue.isObject()) { if (isNew) { currentValue = lp.at(&lp, i); - if (oldCreatedObjects.removeOne(currentValue)) { - createdObjects.push_back(currentValue); + if (this->oldCreatedObjects.removeOne(currentValue)) { + this->createdObjects.push_back(currentValue); } } else { // FIXME: should be the type inside the QQmlListProperty but how can we get that? diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 751a4e7..85b2b3b 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -378,7 +378,7 @@ void MprisPlayer::onPlaybackStatusUpdated() { // For exceptionally bad players that update playback timestamps at an indeterminate time AFTER // updating playback state. (Youtube) - QTimer::singleShot(100, this, [&]() { this->pPosition.requestUpdate(); }); + QTimer::singleShot(100, this, [this]() { this->pPosition.requestUpdate(); }); // For exceptionally bad players that don't update length (or other metadata) until a new track actually // starts playing, and then don't trigger a metadata update when they do. (Jellyfin) diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 616e7d0..0c111fa 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -135,8 +135,8 @@ void PwDevice::polled() { // It is far more likely that the list content has not come in yet than it having no entries, // and there isn't a way to check in the case that there *aren't* actually any entries. if (!this->stagingIndexes.isEmpty()) { - this->routeDeviceIndexes.removeIf([&](const std::pair& entry) { - if (!stagingIndexes.contains(entry.first)) { + this->routeDeviceIndexes.removeIf([&, this](const std::pair& entry) { + if (!this->stagingIndexes.contains(entry.first)) { qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first << ", index: " << entry.second << "] for" << this; return true; diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index 2492b1f..b2964f2 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -101,7 +101,7 @@ QString UPowerDevice::address() const { return this->device ? this->device->serv QString UPowerDevice::path() const { return this->device ? this->device->path() : QString(); } void UPowerDevice::onGetAllFinished() { - qCDebug(logUPowerDevice) << "UPowerDevice" << device->path() << "ready."; + qCDebug(logUPowerDevice) << "UPowerDevice" << this->device->path() << "ready."; this->bReady = true; } diff --git a/src/ui/reload_popup.cpp b/src/ui/reload_popup.cpp index 8e58dc9..f000374 100644 --- a/src/ui/reload_popup.cpp +++ b/src/ui/reload_popup.cpp @@ -25,7 +25,7 @@ ReloadPopup::ReloadPopup(QString instanceId, bool failed, QString errorString) this->popup = component.createWithInitialProperties({{"reloadInfo", QVariant::fromValue(this)}}); - if (!popup) { + if (!this->popup) { qCritical() << "Failed to open reload popup:" << component.errorString(); } diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 4593389..b33e118 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -77,7 +77,7 @@ QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) { } GbmDeviceHandle::~GbmDeviceHandle() { - if (device) { + if (this->device) { MANAGER->unrefGbmDevice(this->device); } } @@ -522,7 +522,7 @@ WlDmaBuffer::~WlDmaBuffer() { bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { if (request.width != this->width || request.height != this->height) return false; - auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) { + auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [this](const auto& format) { return format.format == this->format && (format.modifiers.isEmpty() || std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end()); diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp index a05e82a..1e4ef1a 100644 --- a/src/wayland/buffer/dmabuf.hpp +++ b/src/wayland/buffer/dmabuf.hpp @@ -40,7 +40,7 @@ public: '\0'} ) { for (auto i = 3; i != 0; i--) { - if (chars[i] == ' ') chars[i] = '\0'; + if (this->chars[i] == ' ') this->chars[i] = '\0'; else break; } } diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index e1553f5..43a2543 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -68,11 +68,11 @@ void WlrScreencopyContext::captureFrame() { this->request.reset(); if (this->region.isEmpty()) { - this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output())); + this->init(this->manager->capture_output(this->paintCursors ? 1 : 0, this->screen->output())); } else { - this->init(manager->capture_output_region( + this->init(this->manager->capture_output_region( this->paintCursors ? 1 : 0, - screen->output(), + this->screen->output(), this->region.x(), this->region.y(), this->region.width(), From f73729754d86fe6093691f9470b344aea6c4acb5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 29 Sep 2025 21:19:36 -0700 Subject: [PATCH 13/23] build: explicitly depend on private qt modules In Qt 6.10, private Qt modules must be depended on explicitly. --- CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 55b5e5d..3c2a0e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ if (NOT CMAKE_BUILD_TYPE) endif() set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) +set(QT_PRIVDEPS QuickPrivate) include(cmake/pch.cmake) @@ -115,6 +116,7 @@ endif() if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) + list(APPEND QT_PRIVDEPS WaylandClientPrivate) endif() if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) @@ -127,6 +129,13 @@ endif() find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) +# In Qt 6.10, private dependencies are required to be explicit, +# but they could not be explicitly depended on prior to 6.9. +if (Qt6_VERSION VERSION_GREATER_EQUAL "6.9.0") + set(QT_NO_PRIVATE_MODULE_WARNING ON) + find_package(Qt6 REQUIRED COMPONENTS ${QT_PRIVDEPS}) +endif() + set(CMAKE_AUTOUIC OFF) qt_standard_project_setup(REQUIRES 6.6) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml_modules) From e81e6da08f6790c23b0cc6b2e4c01348c84004cb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 29 Sep 2025 21:49:34 -0700 Subject: [PATCH 14/23] ci: fix magic-nix-cache write permissions --- .github/workflows/build.yml | 5 +++++ .github/workflows/lint.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2e3976..83957dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,12 +9,17 @@ jobs: qtver: [qt6.9.2, qt6.9.1, qt6.9.0, qt6.8.3, qt6.8.2, qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] compiler: [clang, gcc] runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 # Use cachix action over detsys for testing with act. # - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-flakehub: false - name: Download Dependencies run: nix-build --no-out-link --expr '((import ./ci/matrix.nix) { qtver = "${{ matrix.qtver }}"; compiler = "${{ matrix.compiler }}"; }).unwrapped.inputDerivation' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 35ac4e0..de0c304 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,12 +5,17 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 # Use cachix action over detsys for testing with act. # - uses: cachix/install-nix-action@v27 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-flakehub: false - uses: nicknovitski/nix-develop@v1 - name: Check formatting From f5ca8453c0f2ef98108425dfabb91c09c4938f26 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 30 Sep 2025 23:28:05 -0700 Subject: [PATCH 15/23] docs: start tracking qs-next changelog --- changelog/next.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog/next.md diff --git a/changelog/next.md b/changelog/next.md new file mode 100644 index 0000000..d0e9895 --- /dev/null +++ b/changelog/next.md @@ -0,0 +1,9 @@ +## New Features + +- Changes to desktop entries are now tracked in real time. + +## Bug Fixes + +- Fixed a crash when running out of disk space to write log files. +- Fixed a rare crash when disconnecting a monitor. +- Fixed build issues preventing cross compilation from working. From 522d126d1b17f09efbf6e63d54f1cda84e451e6e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 1 Oct 2025 00:29:45 -0700 Subject: [PATCH 16/23] services/pipewire: consider device volume step when sending updates Previously a hardcoded 0.0001 offset was used to determine if a volume change was significant enough to send to a device, however some devices have a much more granular step size, which caused future volume updates to be blocked. This change replaces the hardcoded offset with the volumeStep device route property which should be large enough for the device to work with. Fixes #279 --- changelog/next.md | 1 + src/services/pipewire/node.cpp | 59 +++++++++++++++++++++------------- src/services/pipewire/node.hpp | 2 ++ 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index d0e9895..fdd62d8 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -4,6 +4,7 @@ ## Bug Fixes +- Fixed volumes getting stuck on change for pipewire devices with few volume steps. - Fixed a crash when running out of disk space to write log files. - Fixed a rare crash when disconnecting a monitor. - Fixed build issues preventing cross compilation from working. diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 3e68149..031a68f 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -304,6 +304,8 @@ void PwNodeBoundAudio::updateVolumeProps(const PwVolumeProps& volumeProps) { return; } + this->volumeStep = volumeProps.volumeStep; + // It is important that the lengths of channels and volumes stay in sync whenever you read them. auto channelsChanged = false; auto volumesChanged = false; @@ -435,31 +437,35 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { << "via device"; this->waitingVolumes = realVolumes; } else { - auto significantChange = this->mServerVolumes.isEmpty(); - for (auto i = 0; i < this->mServerVolumes.length(); i++) { - auto serverVolume = this->mServerVolumes.value(i); - auto targetVolume = realVolumes.value(i); - if (targetVolume == 0 || abs(targetVolume - serverVolume) >= 0.0001) { - significantChange = true; - break; - } - } - - if (significantChange) { - qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes - << "via device"; - if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) { - return; + if (this->volumeStep != -1) { + auto significantChange = this->mServerVolumes.isEmpty(); + for (auto i = 0; i < this->mServerVolumes.length(); i++) { + auto serverVolume = this->mServerVolumes.value(i); + auto targetVolume = realVolumes.value(i); + if (targetVolume == 0 || abs(targetVolume - serverVolume) >= this->volumeStep) { + significantChange = true; + break; + } } - this->mDeviceVolumes = realVolumes; - this->node->device->waitForDevice(); - } else { - // Insignificant changes won't cause an info event on the device, leaving qs hung in the - // "waiting for acknowledgement" state forever. - qCInfo(logNode) << "Ignoring volume change for" << this->node << "to" << realVolumes - << "from" << this->mServerVolumes - << "as it is a device node and the change is too small."; + if (significantChange) { + qCInfo(logNode) << "Changing volumes of" << this->node << "to" << realVolumes + << "via device"; + if (!this->node->device->setVolumes(this->node->routeDevice, realVolumes)) { + return; + } + + this->mDeviceVolumes = realVolumes; + this->node->device->waitForDevice(); + } else { + // Insignificant changes won't cause an info event on the device, leaving qs hung in the + // "waiting for acknowledgement" state forever. + qCInfo(logNode).nospace() + << "Ignoring volume change for " << this->node << " to " << realVolumes << " from " + << this->mServerVolumes + << " as it is a device node and the change is too small (min step: " + << this->volumeStep << ")."; + } } } } else { @@ -519,6 +525,7 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes); const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap); const auto* muteProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute); + const auto* volumeStepProp = spa_pod_find_prop(param, nullptr, SPA_PROP_volumeStep); const auto* volumes = reinterpret_cast(&volumesProp->value); const auto* channels = reinterpret_cast(&channelsProp->value); @@ -537,6 +544,12 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { spa_pod_get_bool(&muteProp->value, &props.mute); + if (volumeStepProp) { + spa_pod_get_float(&volumeStepProp->value, &props.volumeStep); + } else { + props.volumeStep = -1; + } + return props; } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 0d4c92e..b53015f 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -158,6 +158,7 @@ struct PwVolumeProps { QVector channels; QVector volumes; bool mute = false; + float volumeStep = -1; static PwVolumeProps parseSpaPod(const spa_pod* param); }; @@ -214,6 +215,7 @@ private: QVector mServerVolumes; QVector mDeviceVolumes; QVector waitingVolumes; + float volumeStep = -1; PwNode* node; }; From 7fc6635ca8fc176af49037bacca857df025b6b4a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Oct 2025 12:36:28 -0700 Subject: [PATCH 17/23] wayland/lock: support Qt 6.10 --- changelog/next.md | 3 ++ .../session_lock/shell_integration.cpp | 5 ++- src/wayland/session_lock/surface.cpp | 45 ++++++++++++------- src/wayland/session_lock/surface.hpp | 12 ++++- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index fdd62d8..639eef0 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,6 +2,9 @@ - Changes to desktop entries are now tracked in real time. +## Other Changes +- Added support for Qt 6.10 + ## Bug Fixes - Fixed volumes getting stuck on change for pipewire devices with few volume steps. diff --git a/src/wayland/session_lock/shell_integration.cpp b/src/wayland/session_lock/shell_integration.cpp index 2b5fdbf..207ef42 100644 --- a/src/wayland/session_lock/shell_integration.cpp +++ b/src/wayland/session_lock/shell_integration.cpp @@ -10,10 +10,11 @@ QtWaylandClient::QWaylandShellSurface* QSWaylandSessionLockIntegration::createShellSurface(QtWaylandClient::QWaylandWindow* window) { auto* lock = LockWindowExtension::get(window->window()); - if (lock == nullptr || lock->surface == nullptr || !lock->surface->isExposed()) { + if (lock == nullptr || lock->surface == nullptr) { qFatal() << "Visibility canary failed. A window with a LockWindowExtension MUST be set to " "visible via LockWindowExtension::setVisible"; } - return lock->surface; + QSWaylandSessionLockSurface* surface = lock->surface; // shut up the unused include linter + return surface; } diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index bc0e75d..6ec4eb6 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -48,16 +48,6 @@ void QSWaylandSessionLockSurface::applyConfigure() { this->window()->resizeFromApplyConfigure(this->size); } -bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) { - if (this->initBuf != nullptr) { - // at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one. - delete this->initBuf; - this->initBuf = nullptr; - } - - return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region); -} - void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { if (ext == nullptr) { if (this->window() != nullptr) this->window()->window()->close(); @@ -71,11 +61,6 @@ void QSWaylandSessionLockSurface::setExtension(LockWindowExtension* ext) { } } -void QSWaylandSessionLockSurface::setVisible() { - if (this->configured && !this->visible) this->initVisible(); - this->visible = true; -} - void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( quint32 serial, quint32 width, @@ -97,13 +82,41 @@ void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( #else this->window()->updateExposure(); #endif + +#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0) if (this->visible) this->initVisible(); +#endif } else { // applyConfigureWhenPossible runs too late and causes a protocol error on reconfigure. this->window()->resizeFromApplyConfigure(this->size); } } +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) + +bool QSWaylandSessionLockSurface::commitSurfaceRole() const { return false; } + +void QSWaylandSessionLockSurface::setVisible() { this->window()->window()->setVisible(true); } + +#else + +bool QSWaylandSessionLockSurface::handleExpose(const QRegion& region) { + if (this->initBuf != nullptr) { + // at this point qt's next commit to the surface will have a new buffer, and we can safely delete this one. + delete this->initBuf; + this->initBuf = nullptr; + } + + return this->QtWaylandClient::QWaylandShellSurface::handleExpose(region); +} + +void QSWaylandSessionLockSurface::setVisible() { + if (this->configured && !this->visible) this->initVisible(); + this->visible = true; +} + +#endif + #if QT_VERSION < QT_VERSION_CHECK(6, 9, 0) #include @@ -123,7 +136,7 @@ void QSWaylandSessionLockSurface::initVisible() { this->window()->window()->setVisible(true); } -#else +#elif QT_VERSION < QT_VERSION_CHECK(6, 10, 0) #include diff --git a/src/wayland/session_lock/surface.hpp b/src/wayland/session_lock/surface.hpp index f7abc77..39be88d 100644 --- a/src/wayland/session_lock/surface.hpp +++ b/src/wayland/session_lock/surface.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -20,7 +21,12 @@ public: [[nodiscard]] bool isExposed() const override; void applyConfigure() override; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) + [[nodiscard]] bool commitSurfaceRole() const override; +#else bool handleExpose(const QRegion& region) override; +#endif void setExtension(LockWindowExtension* ext); void setVisible(); @@ -29,11 +35,13 @@ private: void ext_session_lock_surface_v1_configure(quint32 serial, quint32 width, quint32 height) override; +#if QT_VERSION < QT_VERSION_CHECK(6, 10, 0) void initVisible(); + bool visible = false; + QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr; +#endif LockWindowExtension* ext = nullptr; QSize size; bool configured = false; - bool visible = false; - QtWaylandClient::QWaylandShmBuffer* initBuf = nullptr; }; From b254f6dabc854c3e85cf9d3c3d0981e47f4a5175 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Oct 2025 12:49:36 -0700 Subject: [PATCH 18/23] nix: remove qtwayland dependency when qt >= 6.10 QtWaylandClient was moved into QtBase in 6.10. The QtWayland packages is now only the wayland server code which Quickshell does not need. --- default.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 3908e3c..adb978b 100644 --- a/default.nix +++ b/default.nix @@ -57,6 +57,7 @@ spirv-tools pkg-config ] + ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optionals withWayland [ qt6.qtwayland # qtwaylandscanner required at build time wayland-scanner @@ -70,7 +71,8 @@ ++ lib.optional withQtSvg qt6.qtsvg ++ lib.optional withCrashReporter breakpad ++ lib.optional withJemalloc jemalloc - ++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] + ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland + ++ lib.optionals withWayland [ wayland wayland-protocols ] ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam From e10747addd0f0bc331fafaa1282d8194d8bf4abb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Oct 2025 13:22:17 -0700 Subject: [PATCH 19/23] all: fix gcc warnings and lints --- Justfile | 3 +++ src/core/logging.cpp | 27 ++++++++++++------- src/crash/handler.cpp | 6 ++++- src/crash/main.cpp | 5 +++- src/launch/main.cpp | 5 +++- src/services/mpris/player.cpp | 2 +- src/services/upower/device.cpp | 2 +- src/services/upower/powerprofiles.cpp | 1 + src/wayland/wlr_layershell/wlr_layershell.cpp | 3 ++- src/x11/i3/ipc/connection.cpp | 2 +- src/x11/panel_window.cpp | 2 ++ 11 files changed, 42 insertions(+), 16 deletions(-) diff --git a/Justfile b/Justfile index f60771a..2d6377e 100644 --- a/Justfile +++ b/Justfile @@ -12,6 +12,9 @@ lint-ci: lint-changed: git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} +lint-staged: + git diff --staged --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} + configure target='debug' *FLAGS='': cmake -GNinja -B {{builddir}} \ -DCMAKE_BUILD_TYPE={{ if target == "debug" { "Debug" } else { "RelWithDebInfo" } }} \ diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 909da03..034a14d 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -313,8 +313,12 @@ void ThreadLogging::init() { if (logMfd != -1) { this->file = new QFile(); - this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle); - this->fileStream.setDevice(this->file); + + if (this->file->open(logMfd, QFile::ReadWrite, QFile::AutoCloseHandle)) { + this->fileStream.setDevice(this->file); + } else { + qCCritical(logLogging) << "Failed to open early logging memfd."; + } } if (dlogMfd != -1) { @@ -322,14 +326,19 @@ void ThreadLogging::init() { this->detailedFile = new QFile(); // buffered by WriteBuffer - this->detailedFile->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle); - this->detailedWriter.setDevice(this->detailedFile); + if (this->detailedFile + ->open(dlogMfd, QFile::ReadWrite | QFile::Unbuffered, QFile::AutoCloseHandle)) + { + this->detailedWriter.setDevice(this->detailedFile); - if (!this->detailedWriter.writeHeader()) { - qCCritical(logLogging) << "Could not write header for detailed logs."; - this->detailedWriter.setDevice(nullptr); - delete this->detailedFile; - this->detailedFile = nullptr; + if (!this->detailedWriter.writeHeader()) { + qCCritical(logLogging) << "Could not write header for detailed logs."; + this->detailedWriter.setDevice(nullptr); + delete this->detailedFile; + this->detailedFile = nullptr; + } + } else { + qCCritical(logLogging) << "Failed to open early detailed logging memfd."; } } diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 1433a87..43a9792 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -77,7 +77,11 @@ void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { } QFile file; - file.open(this->d->infoFd, QFile::ReadWrite); + + if (!file.open(this->d->infoFd, QFile::ReadWrite)) { + qCCritical(logCrashHandler + ) << "Failed to open instance info memfd, crash recovery will not work."; + } QDataStream ds(&file); ds << info; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index b9f0eab..6571660 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -161,7 +161,10 @@ void qsCheckCrash(int argc, char** argv) { auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); QFile file; - file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + if (!file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qFatal() << "Failed to open instance info fd."; + } + file.seek(0); auto ds = QDataStream(&file); diff --git a/src/launch/main.cpp b/src/launch/main.cpp index 2bcbebd..7a801fc 100644 --- a/src/launch/main.cpp +++ b/src/launch/main.cpp @@ -32,7 +32,10 @@ void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { auto lastInfoFd = lastInfoFdStr.toInt(); QFile file; - file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + if (!file.open(lastInfoFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qFatal() << "Failed to open crash info fd. Cannot restart."; + } + file.seek(0); auto ds = QDataStream(&file); diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 85b2b3b..fe8b349 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -100,7 +100,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren } else return static_cast(-1); }); - this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; }); + this->bLengthSupported.setBinding([this]() { return this->bInternalLength.value() != -1; }); this->bIsPlaying.setBinding([this]() { return this->bPlaybackState == MprisPlaybackState::Playing; diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index b2964f2..adf5923 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -73,7 +73,7 @@ UPowerDevice::UPowerDevice(QObject* parent): QObject(parent) { return this->bType == UPowerDeviceType::Battery && this->bPowerSupply; }); - this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage != 0; }); + this->bHealthSupported.setBinding([this]() { return this->bHealthPercentage.value() != 0; }); } void UPowerDevice::init(const QString& path) { diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp index 4c40798..43615ae 100644 --- a/src/services/upower/powerprofiles.cpp +++ b/src/services/upower/powerprofiles.cpp @@ -164,6 +164,7 @@ QString DBusDataTransform::toWire(Data data) { case PowerProfile::PowerSaver: return QStringLiteral("power-saver"); case PowerProfile::Balanced: return QStringLiteral("balanced"); case PowerProfile::Performance: return QStringLiteral("performance"); + default: qFatal() << "Attempted to convert invalid power profile" << data << "to wire format."; } } diff --git a/src/wayland/wlr_layershell/wlr_layershell.cpp b/src/wayland/wlr_layershell/wlr_layershell.cpp index 2b77690..947c51a 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell/wlr_layershell.cpp @@ -28,9 +28,10 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) { case Qt::BottomEdge: return this->bImplicitHeight + margins.top; case Qt::LeftEdge: return this->bImplicitWidth + margins.right; case Qt::RightEdge: return this->bImplicitWidth + margins.left; - default: return 0; } } + + return 0; }); this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index ba010ed..c5d2db2 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -532,7 +532,7 @@ QString I3IpcEvent::eventToString(EventCode event) { case EventCode::BarStateUpdate: return "bar_state_update"; break; case EventCode::Input: return "input"; break; - case EventCode::Unknown: return "unknown"; break; + default: return "unknown"; break; } } diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 5d53fdd..c78b548 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -115,6 +115,8 @@ XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) { return 0; } } + + return 0; }); this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); From 8d2a2d3dd291c9735f82aedd40a5f43b193295a8 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 18 Sep 2025 09:33:55 -0400 Subject: [PATCH 20/23] core/desktopentry: mask entries with priority less than hidden entry --- changelog/next.md | 1 + src/core/desktopentry.cpp | 18 ++++++++++++++++-- src/core/desktopentry.hpp | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 639eef0..e6b3ac0 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -11,3 +11,4 @@ - Fixed a crash when running out of disk space to write log files. - Fixed a rare crash when disconnecting a monitor. - Fixed build issues preventing cross compilation from working. +- Fixed dekstop entries with lower priority than a hidden entry not being hidden. diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index cb9710e..b453988 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -108,7 +108,6 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& auto finishCategory = [&data, &groupName, &entries]() { if (groupName == "Desktop Entry") { if (entries.value("Type").second != "Application") return; - if (entries.value("Hidden").second == "true") return; for (const auto& [key, pair]: entries.asKeyValueRange()) { auto& [_, value] = pair; @@ -118,6 +117,7 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& else if (key == "GenericName") data.genericName = value; else if (key == "StartupWMClass") data.startupClass = value; else if (key == "NoDisplay") data.noDisplay = value == "true"; + else if (key == "Hidden") data.hidden = value == "true"; else if (key == "Comment") data.comment = value; else if (key == "Icon") data.icon = value; else if (key == "Exec") { @@ -495,6 +495,21 @@ void DesktopEntryManager::onScanCompleted(const QList& s auto newLowercaseEntries = QHash(); for (const auto& data: scanResults) { + auto lowerId = data.id.toLower(); + + if (data.hidden) { + if (auto* victim = newEntries.take(data.id)) victim->deleteLater(); + newLowercaseEntries.remove(lowerId); + + if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { + it.value()->deleteLater(); + oldEntries.erase(it); + } + + qCDebug(logDesktopEntry) << "Masking hidden desktop entry" << data.id; + continue; + } + DesktopEntry* dentry = nullptr; if (auto it = oldEntries.find(data.id); it != oldEntries.end()) { @@ -516,7 +531,6 @@ void DesktopEntryManager::onScanCompleted(const QList& s qCDebug(logDesktopEntry) << "Found desktop entry" << data.id; - auto lowerId = data.id.toLower(); auto conflictingId = newEntries.contains(data.id); if (conflictingId) { diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index b124aaf..623019d 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -33,6 +33,7 @@ struct ParsedDesktopEntryData { QString genericName; QString startupClass; bool noDisplay = false; + bool hidden = false; QString comment; QString icon; QString execString; From a94418f45dd2a978da54abbe30ad3b7436d9fe8a Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 30 Sep 2025 08:17:03 -0400 Subject: [PATCH 21/23] core/desktopentry: don't match keys with wrong modifier or country --- changelog/next.md | 1 + src/core/desktopentry.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index e6b3ac0..b97e845 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -12,3 +12,4 @@ - Fixed a rare crash when disconnecting a monitor. - Fixed build issues preventing cross compilation from working. - Fixed dekstop entries with lower priority than a hidden entry not being hidden. +- Fixed desktop entry keys with mismatched modifier or country not being discarded. diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index b453988..941a405 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -61,12 +61,14 @@ struct Locale { [[nodiscard]] int matchScore(const Locale& other) const { if (this->language != other.language) return 0; - auto territoryMatches = !this->territory.isEmpty() && this->territory == other.territory; - auto modifierMatches = !this->modifier.isEmpty() && this->modifier == other.modifier; + + if (!other.modifier.isEmpty() && this->modifier != other.modifier) return 0; + if (!other.territory.isEmpty() && this->territory != other.territory) return 0; auto score = 1; - if (territoryMatches) score += 2; - if (modifierMatches) score += 1; + + if (!other.territory.isEmpty()) score += 2; + if (!other.modifier.isEmpty()) score += 1; return score; } From 40bc09b800e05b72d64836fa4f0816fb0269a917 Mon Sep 17 00:00:00 2001 From: Gregor Kleen <20089782+gkleen@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:50:46 +0200 Subject: [PATCH 22/23] service/greetd: always send responses --- changelog/next.md | 1 + src/services/greetd/connection.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index b97e845..5869425 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -13,3 +13,4 @@ - Fixed build issues preventing cross compilation from working. - Fixed dekstop entries with lower priority than a hidden entry not being hidden. - Fixed desktop entry keys with mismatched modifier or country not being discarded. +- Fixed greetd hanging when authenticating with a fingerprint. diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index bf0d1fd..cb237a0 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -225,6 +225,10 @@ void GreetdConnection::onSocketReady() { this->mResponseRequired = responseRequired; emit this->authMessage(message, error, responseRequired, echoResponse); + + if (!responseRequired) { + this->sendRequest({{"type", "post_auth_message_response"}}); + } } else goto unexpected; return; From a1a150fab00a93ea983aaca5df55304bc837f51b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 11 Oct 2025 17:14:14 -0700 Subject: [PATCH 23/23] version: bump to 0.2.1 --- BUILD.md | 4 ++-- CMakeLists.txt | 2 +- changelog/{next.md => v0.2.1.md} | 1 + src/launch/command.cpp | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) rename changelog/{next.md => v0.2.1.md} (99%) diff --git a/BUILD.md b/BUILD.md index aa7c98a..742baa7 100644 --- a/BUILD.md +++ b/BUILD.md @@ -55,7 +55,7 @@ On some distros, private Qt headers are in separate packages which you may have We currently require private headers for the following libraries: - `qt6declarative` -- `qt6wayland` +- `qt6wayland` (for Qt versions prior to 6.10) We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and svg icons will not work, including system ones. @@ -104,7 +104,7 @@ Currently supported Qt versions: `6.6`, `6.7`. To disable: `-DWAYLAND=OFF` Dependencies: - - `qt6wayland` + - `qt6wayland` (for Qt versions prior to 6.10) - `wayland` (libwayland-client) - `wayland-scanner` (build time) - `wayland-protocols` (static library) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c2a0e1..880b9ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(quickshell VERSION "0.2.0" LANGUAGES CXX C) +project(quickshell VERSION "0.2.1" LANGUAGES CXX C) set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) diff --git a/changelog/next.md b/changelog/v0.2.1.md similarity index 99% rename from changelog/next.md rename to changelog/v0.2.1.md index 5869425..596b82f 100644 --- a/changelog/next.md +++ b/changelog/v0.2.1.md @@ -3,6 +3,7 @@ - Changes to desktop entries are now tracked in real time. ## Other Changes + - Added support for Qt 6.10 ## Bug Fixes diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 8a9c6de..e63498a 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -509,7 +509,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { if (state.misc.printVersion) { qCInfo(logBare).noquote().nospace() - << "quickshell 0.2.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; + << "quickshell 0.2.1, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; if (state.log.verbosity > 1) { qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR;