From 05b5eccf2eab51eb6c585f177149aacff5f916b1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 12 Jun 2025 04:48:09 -0700 Subject: [PATCH 001/226] build: update build guide, nix and guix packages --- BUILD.md | 2 +- default.nix | 15 +++++++++------ flake.lock | 6 +++--- flake.nix | 7 ++++--- quickshell.scm | 6 +++--- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/BUILD.md b/BUILD.md index 3172dbe..afcb2e4 100644 --- a/BUILD.md +++ b/BUILD.md @@ -44,7 +44,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qtshadertools` (build-time only) - `spirv-tools` (build-time only) - `pkg-config` (build-time only) -- `cli11` +- `cli11` (build-time only) On some distros, private Qt headers are in separate packages which you may have to install. We currently require private headers for the following libraries: diff --git a/default.nix b/default.nix index 79c9b7a..53c238e 100644 --- a/default.nix +++ b/default.nix @@ -5,18 +5,20 @@ keepDebugInfo, buildStdenv ? pkgs.clangStdenv, + pkg-config, cmake, ninja, - qt6, spirv-tools, - cli11, + qt6, breakpad, jemalloc, + cli11, wayland, wayland-protocols, + wayland-scanner, + xorg, libdrm, libgbm ? null, - xorg, pipewire, pam, @@ -46,11 +48,12 @@ version = "0.1.0"; src = nix-gitignore.gitignoreSource "/docs\n/examples\n" ./.; - nativeBuildInputs = with pkgs; [ + nativeBuildInputs = [ cmake ninja qt6.qtshadertools spirv-tools + cli11 qt6.wrapQtAppsHook pkg-config ] ++ (lib.optionals withWayland [ @@ -61,7 +64,6 @@ buildInputs = [ qt6.qtbase qt6.qtdeclarative - cli11 ] ++ lib.optional withCrashReporter breakpad ++ lib.optional withJemalloc jemalloc @@ -96,9 +98,10 @@ dontStrip = debug; meta = with lib; { - homepage = "https://git.outfoxxed.me/outfoxxed/quickshell"; + homepage = "https://quickshell.outfoxxed.me"; description = "Flexbile QtQuick based desktop shell toolkit"; license = licenses.lgpl3Only; platforms = platforms.linux; + mainProgram = "quickshell"; }; } diff --git a/flake.lock b/flake.lock index df5aa3f..7c25aa2 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1736012469, - "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=", + "lastModified": 1749285348, + "narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d", + "rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index a0bc18d..5de9c96 100644 --- a/flake.nix +++ b/flake.nix @@ -4,9 +4,10 @@ }; outputs = { self, nixpkgs }: let - forEachSystem = fn: nixpkgs.lib.genAttrs - [ "x86_64-linux" "aarch64-linux" ] - (system: fn system nixpkgs.legacyPackages.${system}); + forEachSystem = fn: + nixpkgs.lib.genAttrs + nixpkgs.lib.platforms.linux + (system: fn system nixpkgs.legacyPackages.${system}); in { packages = forEachSystem (system: pkgs: rec { quickshell = pkgs.callPackage ./default.nix { diff --git a/quickshell.scm b/quickshell.scm index 05bf269..26abdc0 100644 --- a/quickshell.scm +++ b/quickshell.scm @@ -35,9 +35,9 @@ pkg-config qtshadertools spirv-tools - wayland-protocols)) - (inputs (list cli11 - jemalloc + wayland-protocols + cli11)) + (inputs (list jemalloc libdrm libxcb libxkbcommon From 517143adf97fb0df7e9a7584061deecdffb19faf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 12 Jun 2025 16:51:02 -0700 Subject: [PATCH 002/226] all: fix new lints --- src/core/test/scriptmodel.cpp | 2 +- src/services/pipewire/defaults.cpp | 5 ++++- src/wayland/hyprland/surface/surface.cpp | 1 - src/wayland/hyprland/surface/surface.hpp | 1 - src/wayland/session_lock/surface.cpp | 1 + src/x11/i3/ipc/connection.cpp | 3 ++- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/core/test/scriptmodel.cpp b/src/core/test/scriptmodel.cpp index 6674683..0abfdbf 100644 --- a/src/core/test/scriptmodel.cpp +++ b/src/core/test/scriptmodel.cpp @@ -115,7 +115,7 @@ void TestScriptModel::unique_data() { void TestScriptModel::unique() { QFETCH(const QString, oldstr); QFETCH(const QString, newstr); - QFETCH(OpList, operations); + QFETCH(const OpList, operations); auto strToVariantList = [](const QString& str) -> QVariantList { QVariantList list; diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 9ff37e0..23252e7 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -16,6 +16,9 @@ #include "node.hpp" #include "registry.hpp" +// This and spa_json_init are part of json-core.h, which is missing from older pw versions. +struct spa_json; + namespace qs::service::pipewire { namespace { @@ -72,7 +75,7 @@ void PwDefaultTracker::onMetadataProperty(const char* key, const char* type, con if (type != nullptr && value != nullptr && strcmp(type, "Spa:String:JSON") == 0) { auto failed = true; auto iter = std::array(); - spa_json_init(&iter[0], value, strlen(value)); + spa_json_init(&iter[0], value, strlen(value)); // NOLINT (misc-include-cleaner) if (spa_json_enter_object(&iter[0], &iter[1]) > 0) { auto buf = std::array(); diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp index 487da40..f49ab8f 100644 --- a/src/wayland/hyprland/surface/surface.cpp +++ b/src/wayland/hyprland/surface/surface.cpp @@ -18,7 +18,6 @@ HyprlandSurface::HyprlandSurface( QtWaylandClient::QWaylandWindow* backer ) : QtWayland::hyprland_surface_v1(surface) - , backer(backer) , backerSurface(backer->surface()) {} HyprlandSurface::~HyprlandSurface() { this->destroy(); } diff --git a/src/wayland/hyprland/surface/surface.hpp b/src/wayland/hyprland/surface/surface.hpp index 1c8b548..48a2cda 100644 --- a/src/wayland/hyprland/surface/surface.hpp +++ b/src/wayland/hyprland/surface/surface.hpp @@ -24,7 +24,6 @@ public: void setVisibleRegion(const QRegion& region); private: - QtWaylandClient::QWaylandWindow* backer; wl_surface* backerSurface = nullptr; }; diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index 6b1f652..a2608dd 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -169,6 +169,7 @@ void QSWaylandSessionLockSurface::initVisible() { auto& surfacePointer = reinterpret_cast(this->window())->surfacePointer(); // Swap out the surface for a dummy during initWindow. + QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED // swap() { surfacePointer.swap(*tempSurface); diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index 3c1015f..cce9ba0 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -156,7 +156,8 @@ QVector I3Ipc::parseResponse() { break; } - QJsonParseError e; + // Importing this makes CI builds fail for some reason. + QJsonParseError e; // NOLINT (misc-include-cleaner) auto data = QJsonDocument::fromJson(payload, &e); if (e.error != QJsonParseError::NoError) { From 71fe3d916576d062f072e2b9e1b5669d247414e9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Jun 2025 20:12:32 -0700 Subject: [PATCH 003/226] x11/panelwindow: do not look up engine generation in ~XPanelWindow() Looking up engine generation in the destructor causes occasional crashes. This commit caches it to prevent that from happening. --- src/x11/panel_window.cpp | 12 ++++++++---- src/x11/panel_window.hpp | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 9ffbeb5..ef271b5 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -40,17 +40,20 @@ public: } void addPanel(XPanelWindow* panel) { - auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + panel->engineGeneration = EngineGeneration::findObjectGeneration(panel); + auto& panels = this->mPanels[panel->engineGeneration]; if (!panels.contains(panel)) { panels.push_back(panel); } } void removePanel(XPanelWindow* panel) { - auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(panel)]; + if (!panel->engineGeneration) return; + + auto& panels = this->mPanels[panel->engineGeneration]; if (panels.removeOne(panel)) { if (panels.isEmpty()) { - this->mPanels.erase(EngineGeneration::findObjectGeneration(panel)); + this->mPanels.erase(panel->engineGeneration); } // from the bottom up, update all panels @@ -61,7 +64,8 @@ public: } void updateLowerDimensions(XPanelWindow* exclude) { - auto& panels = this->mPanels[EngineGeneration::findObjectGeneration(exclude)]; + if (!exclude->engineGeneration) return; + auto& panels = this->mPanels[exclude->engineGeneration]; // update all panels lower than the one we start from auto found = false; diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index d8cc966..c6c1de7 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -98,6 +98,7 @@ private: Margins mMargins; qint32 mExclusiveZone = 0; ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto; + EngineGeneration* knownGeneration = nullptr; QRect lastScreenVirtualGeometry; XPanelEventFilter eventFilter; From 0140356d99f28261f8d59f37adb624832ab3fe80 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Jun 2025 14:45:04 -0700 Subject: [PATCH 004/226] core/qmlglobal!: rename shellRoot to configDir + add configPath --- src/core/qmlglobal.cpp | 14 ++++++++------ src/core/qmlglobal.hpp | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 71626be..cbddee4 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -177,12 +177,6 @@ void QuickshellGlobal::reload(bool hard) { root->reloadGraph(hard); } -QString QuickshellGlobal::shellRoot() const { - auto* generation = EngineGeneration::findObjectGeneration(this); - // already canonical - return generation->rootPath.path(); -} - QString QuickshellGlobal::workingDirectory() const { // NOLINT return QuickshellSettings::instance()->workingDirectory(); } @@ -213,6 +207,10 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) { if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged(); } +QString QuickshellGlobal::configDir() const { + return EngineGeneration::findObjectGeneration(this)->rootPath.path(); +} + QString QuickshellGlobal::dataDir() const { // NOLINT return QsPaths::instance()->shellDataDir().path(); } @@ -225,6 +223,10 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT return QsPaths::instance()->shellCacheDir().path(); } +QString QuickshellGlobal::configPath(const QString& path) const { + return this->configDir() % '/' % path; +} + QString QuickshellGlobal::dataPath(const QString& path) const { return this->dataDir() % '/' % path; } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 83a9718..afb2f7e 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -104,7 +104,7 @@ class QuickshellGlobal: public QObject { /// /// The root directory is the folder containing the entrypoint to your shell, often referred /// to as `shell.qml`. - Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); + Q_PROPERTY(QString configDir READ configDir CONSTANT); /// Quickshell's working directory. Defaults to whereever quickshell was launched from. Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); /// If true then the configuration will be reloaded whenever any files change. @@ -167,6 +167,8 @@ public: /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback /// icon if the requested one could not be loaded. Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); + /// Equivalent to `${Quickshell.configDir}/${path}` + Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const; /// Equivalent to `${Quickshell.dataDir}/${path}` Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const; /// Equivalent to `${Quickshell.stateDir}/${path}` @@ -182,7 +184,7 @@ public: void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } - [[nodiscard]] QString shellRoot() const; + [[nodiscard]] QString configDir() const; [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); From 0499518143b232b949a177c5fea929f2ceed58ec Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Jun 2025 00:07:01 -0700 Subject: [PATCH 005/226] core/qmlglobal: add execDetached functions for spawning processes --- src/core/qmlglobal.cpp | 33 ++++++++++++++++++ src/core/qmlglobal.hpp | 63 ++++++++++++++++++++++++++++++++++ src/core/test/CMakeLists.txt | 2 +- src/io/CMakeLists.txt | 1 + src/io/process.cpp | 28 ++++----------- src/io/process.hpp | 13 ++++--- src/io/processcore.cpp | 37 ++++++++++++++++++++ src/io/processcore.hpp | 16 +++++++++ src/io/test/CMakeLists.txt | 2 +- src/window/test/CMakeLists.txt | 2 +- 10 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 src/io/processcore.cpp create mode 100644 src/io/processcore.hpp diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index cbddee4..c447c55 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -8,8 +8,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -21,6 +23,7 @@ #include #include +#include "../io/processcore.hpp" #include "generation.hpp" #include "iconimageprovider.hpp" #include "paths.hpp" @@ -246,6 +249,36 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } +void QuickshellGlobal::execDetached(QList command) { + QuickshellGlobal::execDetached(ProcessContext(std::move(command))); +} + +void QuickshellGlobal::execDetached(const ProcessContext& context) { + if (context.command.isEmpty()) { + qWarning() << "Cannot start process as command is empty."; + return; + } + + const auto& cmd = context.command.first(); + auto args = context.command.sliced(1); + + QProcess process; + + qs::core::process::setupProcessEnvironment( + &process, + context.clearEnvironment, + context.environment + ); + + if (!context.workingDirectory.isEmpty()) { + process.setWorkingDirectory(context.workingDirectory); + } + + process.setProgram(cmd); + process.setArguments(args); + process.startDetached(); +} + QString QuickshellGlobal::iconPath(const QString& icon) { return IconImageProvider::requestString(icon); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index afb2f7e..d5b9844 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -1,8 +1,12 @@ #pragma once +#include + #include #include +#include #include +#include #include #include #include @@ -15,6 +19,25 @@ #include "qmlscreen.hpp" +class ProcessContext { + Q_PROPERTY(QList command MEMBER command); + Q_PROPERTY(QHash environment MEMBER environment); + Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment); + Q_PROPERTY(QString workingDirectory MEMBER workingDirectory); + Q_GADGET; + QML_STRUCTURED_VALUE; + QML_VALUE_TYPE(processContext); + +public: + ProcessContext() = default; + explicit ProcessContext(QList command): command(std::move(command)) {} + + QList command; + QHash environment; + bool clearEnvironment = false; + QString workingDirectory; +}; + ///! Accessor for some options under the Quickshell type. class QuickshellSettings: public QObject { Q_OBJECT; @@ -152,6 +175,46 @@ public: /// Returns the string value of an environment variable or null if it is not set. Q_INVOKABLE QVariant env(const QString& variable); + /// Launch a process detached from Quickshell. + /// + /// Each command argument is its own string, meaning arguments do + /// not have to be escaped. + /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// + /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with + /// > the system shell. + /// + /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). + Q_INVOKABLE static void execDetached(QList command); + /// Launch a process detached from Quickshell. + /// + /// The context parameter is a JS object with the following fields: + /// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command. + /// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment. + /// - `clearEnvironment`: Removes all variables from the environment if true. + /// - `workingDirectory`: The working directory the command should run in. + /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// + /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with + /// > the system shell. + /// + /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). + Q_INVOKABLE static void execDetached(const ProcessContext& context); + /// Returns a string usable for a @@QtQuick.Image.source for a given system icon. /// /// > [!INFO] By default, icons are loaded from the theme selected by the qt platform theme, diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index c4005c8..4e66c62 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-core quickshell-window quickshell-ui quickshell-io) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 6bb8e70..8b5c20a 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -1,5 +1,6 @@ qt_add_library(quickshell-io STATIC datastream.cpp + processcore.cpp process.cpp fileview.cpp jsonadapter.cpp diff --git a/src/io/process.cpp b/src/io/process.cpp index 143fdec..43637d4 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -3,9 +3,9 @@ #include #include +#include #include #include -#include #include #include #include @@ -13,10 +13,10 @@ #include #include -#include "../core/common.hpp" #include "../core/generation.hpp" #include "../core/qmlglobal.hpp" #include "datastream.hpp" +#include "processcore.hpp" Process::Process(QObject* parent): QObject(parent) { QObject::connect( @@ -79,9 +79,10 @@ void Process::onGlobalWorkingDirectoryChanged() { } } -QMap Process::environment() const { return this->mEnvironment; } +QHash Process::environment() const { return this->mEnvironment; } -void Process::setEnvironment(QMap environment) { +void Process::setEnvironment(QHash environment) { + qDebug() << "setEnv" << environment; if (environment == this->mEnvironment) return; this->mEnvironment = std::move(environment); emit this->environmentChanged(); @@ -224,24 +225,7 @@ void Process::setupEnvironment(QProcess* process) { process->setWorkingDirectory(this->mWorkingDirectory); } - const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT; - auto env = this->mClearEnvironment ? QProcessEnvironment() : sysenv; - - for (auto& name: this->mEnvironment.keys()) { - auto value = this->mEnvironment.value(name); - if (!value.isValid()) continue; - - if (this->mClearEnvironment) { - if (value.isNull()) { - if (sysenv.contains(name)) env.insert(name, sysenv.value(name)); - } else env.insert(name, value.toString()); - } else { - if (value.isNull()) env.remove(name); - else env.insert(name, value.toString()); - } - } - - process->setProcessEnvironment(env); + qs::core::process::setupProcessEnvironment(process, this->mClearEnvironment, this->mEnvironment); } void Process::onStarted() { diff --git a/src/io/process.hpp b/src/io/process.hpp index e93004f..2d7e1fd 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -98,7 +99,7 @@ class Process: public QObject { /// If the process is already running changing this property will affect the next /// started process. If the property has been changed after starting a process it will /// return the new value, not the one for the currently running process. - Q_PROPERTY(QMap environment READ environment WRITE setEnvironment NOTIFY environmentChanged); + Q_PROPERTY(QHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged); /// If the process's environment should be cleared prior to applying @@environment. /// Defaults to false. /// @@ -140,10 +141,12 @@ public: /// Writes to the process's stdin. Does nothing if @@running is false. Q_INVOKABLE void write(const QString& data); - /// Launches an instance of the process detached from quickshell. + /// Launches an instance of the process detached from Quickshell. /// /// The subprocess will not be tracked, @@running will be false, /// and the subprocess will not be killed by Quickshell. + /// + /// This function is equivalent to @@Quickshell.Quickshell.execDetached(). Q_INVOKABLE void startDetached(); [[nodiscard]] bool isRunning() const; @@ -157,8 +160,8 @@ public: [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(const QString& workingDirectory); - [[nodiscard]] QMap environment() const; - void setEnvironment(QMap environment); + [[nodiscard]] QHash environment() const; + void setEnvironment(QHash environment); [[nodiscard]] bool environmentCleared() const; void setEnvironmentCleared(bool cleared); @@ -203,7 +206,7 @@ private: QProcess* process = nullptr; QList mCommand; QString mWorkingDirectory; - QMap mEnvironment; + QHash mEnvironment; DataStreamParser* mStdoutParser = nullptr; DataStreamParser* mStderrParser = nullptr; QByteArray stdoutBuffer; diff --git a/src/io/processcore.cpp b/src/io/processcore.cpp new file mode 100644 index 0000000..572045e --- /dev/null +++ b/src/io/processcore.cpp @@ -0,0 +1,37 @@ +#include "processcore.hpp" + +#include +#include +#include +#include + +#include "../core/common.hpp" + +namespace qs::core::process { + +void setupProcessEnvironment( + QProcess* process, + bool clear, + const QHash& envChanges +) { + const auto& sysenv = qs::Common::INITIAL_ENVIRONMENT; + auto env = clear ? QProcessEnvironment() : sysenv; + + for (auto& name: envChanges.keys()) { + auto value = envChanges.value(name); + if (!value.isValid()) continue; + + if (clear) { + if (value.isNull()) { + if (sysenv.contains(name)) env.insert(name, sysenv.value(name)); + } else env.insert(name, value.toString()); + } else { + if (value.isNull()) env.remove(name); + else env.insert(name, value.toString()); + } + } + + process->setProcessEnvironment(env); +} + +} // namespace qs::core::process diff --git a/src/io/processcore.hpp b/src/io/processcore.hpp new file mode 100644 index 0000000..fe8bda7 --- /dev/null +++ b/src/io/processcore.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include +#include + +namespace qs::core::process { + +void setupProcessEnvironment( + QProcess* process, + bool clear, + const QHash& envChanges +); + +} diff --git a/src/io/test/CMakeLists.txt b/src/io/test/CMakeLists.txt index 4ab5173..8875566 100644 --- a/src/io/test/CMakeLists.txt +++ b/src/io/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test quickshell-io) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() diff --git a/src/window/test/CMakeLists.txt b/src/window/test/CMakeLists.txt index 7061511..8a0d65c 100644 --- a/src/window/test/CMakeLists.txt +++ b/src/window/test/CMakeLists.txt @@ -1,6 +1,6 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-window quickshell-core quickshell-ui) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Test quickshell-window quickshell-core quickshell-ui quickshell-io) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() From 20322484b937a862bbf0c3a0d1900930e6c48a8c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Jun 2025 02:26:21 -0700 Subject: [PATCH 006/226] wayland/layershell: fix bridge destructor use after free on reload Under some conditions, Qt will recreate the layer surface. The layer surface destructor tries to destroy the bridge, but doesn't actually need to because the bridge is a child of the QWindow owning the layer, meaning not destroying it is actually completely fine. --- src/wayland/wlr_layershell/surface.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 3188c6b..26d7558 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -174,7 +174,10 @@ LayerSurface::LayerSurface(LayerShellIntegration* shell, QtWaylandClient::QWayla } LayerSurface::~LayerSurface() { - delete this->bridge; + if (this->bridge && this->bridge->surface == this) { + this->bridge->surface = nullptr; + } + this->destroy(); } From d9164578a2fc4f0721aaf61c2af8bc6c5cdb11b4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Jun 2025 03:12:51 -0700 Subject: [PATCH 007/226] core/window: add title property to floating windows --- src/window/floatingwindow.cpp | 7 +++++++ src/window/floatingwindow.hpp | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index 6a3a708..2f196fc 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -13,6 +13,7 @@ void ProxyFloatingWindow::connectWindow() { this->ProxyWindowBase::connectWindow(); + this->window->setTitle(this->bTitle); this->window->setMinimumSize(this->bMinimumSize); this->window->setMaximumSize(this->bMaximumSize); } @@ -29,6 +30,11 @@ void ProxyFloatingWindow::trySetHeight(qint32 implicitHeight) { } } +void ProxyFloatingWindow::onTitleChanged() { + if (this->window) this->window->setTitle(this->bTitle); + emit this->titleChanged(); +} + void ProxyFloatingWindow::onMinimumSizeChanged() { if (this->window) this->window->setMinimumSize(this->bMinimumSize); emit this->minimumSizeChanged(); @@ -59,6 +65,7 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged); QObject::connect(this->window, &ProxyWindowBase::surfaceFormatChanged, this, &FloatingWindowInterface::surfaceFormatChanged); + QObject::connect(this->window, &ProxyFloatingWindow::titleChanged, this, &FloatingWindowInterface::titleChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); // clang-format on diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index a175a1a..a98e9f4 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -24,12 +24,21 @@ public: signals: void minimumSizeChanged(); void maximumSizeChanged(); + void titleChanged(); private: void onMinimumSizeChanged(); void onMaximumSizeChanged(); + void onTitleChanged(); public: + Q_OBJECT_BINDABLE_PROPERTY( + ProxyFloatingWindow, + QString, + bTitle, + &ProxyFloatingWindow::onTitleChanged + ); + Q_OBJECT_BINDABLE_PROPERTY( ProxyFloatingWindow, QSize, @@ -49,6 +58,8 @@ public: class FloatingWindowInterface: public WindowInterface { Q_OBJECT; // clang-format off + /// Window title. + Q_PROPERTY(QString title READ default WRITE default NOTIFY titleChanged BINDABLE bindableTitle); /// Minimum window size given to the window system. Q_PROPERTY(QSize minimumSize READ default WRITE default NOTIFY minimumSizeChanged BINDABLE bindableMinimumSize); /// Maximum window size given to the window system. @@ -100,10 +111,12 @@ public: QBindable bindableMinimumSize() { return &this->window->bMinimumSize; } QBindable bindableMaximumSize() { return &this->window->bMaximumSize; } + QBindable bindableTitle() { return &this->window->bTitle; } signals: void minimumSizeChanged(); void maximumSizeChanged(); + void titleChanged(); private: ProxyFloatingWindow* window; From 9a3033340529881ae5e564d1aedf6884f53e3ea1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Jun 2025 23:00:56 -0700 Subject: [PATCH 008/226] build: clarify shared libraries --- BUILD.md | 24 ++++++++++++++++-------- default.nix | 15 +++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/BUILD.md b/BUILD.md index afcb2e4..aa7c98a 100644 --- a/BUILD.md +++ b/BUILD.md @@ -41,10 +41,15 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `cmake` - `qt6base` - `qt6declarative` -- `qtshadertools` (build-time only) -- `spirv-tools` (build-time only) -- `pkg-config` (build-time only) -- `cli11` (build-time only) +- `qtshadertools` (build-time) +- `spirv-tools` (build-time) +- `pkg-config` (build-time) +- `cli11` (static library) + +Build time dependencies and static libraries don't have to exist at runtime, +however build time dependencies must be compiled for the architecture of +the builder, while static libraries must be compiled for the architecture +of the target. On some distros, private Qt headers are in separate packages which you may have to install. We currently require private headers for the following libraries: @@ -66,7 +71,7 @@ enable us to fix bugs far more easily. To disable: `-DCRASH_REPORTER=OFF` -Dependencies: `google-breakpad` +Dependencies: `google-breakpad` (static library) ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused @@ -101,8 +106,11 @@ To disable: `-DWAYLAND=OFF` Dependencies: - `qt6wayland` - `wayland` (libwayland-client) - - `wayland-scanner` (may be part of your distro's wayland package) - - `wayland-protocols` + - `wayland-scanner` (build time) + - `wayland-protocols` (static library) + +Note that one or both of `wayland-scanner` and `wayland-protocols` may be bundled +with you distro's wayland package. #### Wlroots Layershell Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, @@ -220,7 +228,7 @@ To disable: `-DI3_IPC=OFF` ## Building *For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* -We highly recommend using `ninja` to run the build, but you can use makefiles if you must. +Only `ninja` builds are tested, but makefiles may work. #### Configuring the build ```sh diff --git a/default.nix b/default.nix index 53c238e..73cd8d1 100644 --- a/default.nix +++ b/default.nix @@ -46,29 +46,28 @@ }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; - src = nix-gitignore.gitignoreSource "/docs\n/examples\n" ./.; + src = nix-gitignore.gitignoreSource [] ./.; nativeBuildInputs = [ cmake ninja qt6.qtshadertools spirv-tools - cli11 qt6.wrapQtAppsHook pkg-config - ] ++ (lib.optionals withWayland [ - wayland-protocols - wayland-scanner - ]); + ] + ++ lib.optional withWayland wayland-scanner; buildInputs = [ qt6.qtbase qt6.qtdeclarative + cli11 ] + ++ lib.optional withQtSvg qt6.qtsvg ++ lib.optional withCrashReporter breakpad ++ lib.optional withJemalloc jemalloc - ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else [])) + ++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] + ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire; From 579d589290f1aa330ea1087da08c13baaff1e415 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 18 Jun 2025 13:41:14 -0700 Subject: [PATCH 009/226] core/popupanchor: ensure item-derived rect is at least 1x1 pixels --- src/core/popupanchor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index a1ada03..bbcc3a5 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -189,11 +189,15 @@ void PopupAnchor::updateAnchor() { if (this->mItem && this->mProxyWindow) { auto baseRect = this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect(); + auto rect = this->mProxyWindow->contentItem()->mapFromItem( this->mItem, baseRect.marginsRemoved(this->mMargins.qmargins()) ); + if (rect.width() < 1) rect.setWidth(1); + if (rect.height() < 1) rect.setHeight(1); + this->setWindowRect(rect.toRect()); } From 95d0af8113394b1fdb71c94ac5160c83b8b829cb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 19 Jun 2025 05:12:24 -0700 Subject: [PATCH 010/226] services/pipewire: update volume props from device for device nodes Yet another device node edge case. In addition to only writing via a pw_device when present, now we only read from one as well. This fixes missing state changes not conveyed by the pw_node. Fixes #35 --- src/services/pipewire/device.cpp | 27 +++++++++++++++++++++------ src/services/pipewire/device.hpp | 5 ++++- src/services/pipewire/node.cpp | 31 +++++++++++++++++++++++++++---- src/services/pipewire/node.hpp | 3 ++- src/services/pipewire/qml.hpp | 14 +++++++------- 5 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 0a1e1b6..649b9c6 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,7 @@ #include #include "core.hpp" +#include "node.hpp" namespace qs::service::pipewire { @@ -104,30 +106,43 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { qint32 device = 0; qint32 index = 0; + spa_pod* props = nullptr; + // clang-format off quint32 id = SPA_PARAM_Route; spa_pod_parser_get_object( &parser, SPA_TYPE_OBJECT_ParamRoute, &id, SPA_PARAM_ROUTE_device, SPA_POD_Int(&device), - SPA_PARAM_ROUTE_index, SPA_POD_Int(&index) + SPA_PARAM_ROUTE_index, SPA_POD_Int(&index), + SPA_PARAM_ROUTE_props, SPA_POD_PodObject(&props) ); // clang-format on - this->stagingIndexes.insert(device, index); + auto volumeProps = PwVolumeProps::parseSpaPod(props); + + this->stagingIndexes.append(device); // Insert into the main map as well, staging's purpose is to remove old entries. this->routeDeviceIndexes.insert(device, index); qCDebug(logDevice).nospace() << "Registered device/index pair for " << this << ": [device: " << device << ", index: " << index << ']'; + + emit this->routeVolumesChanged(device, volumeProps); } 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->stagingIndexes != this->routeDeviceIndexes) { - this->routeDeviceIndexes = this->stagingIndexes; - qCDebug(logDevice) << "Updated device/index pair list for" << this << "to" - << this->routeDeviceIndexes; + if (!this->stagingIndexes.isEmpty()) { + this->routeDeviceIndexes.removeIf([&](const std::pair& entry) { + if (!stagingIndexes.contains(entry.first)) { + qCDebug(logDevice).nospace() << "Removed device/index pair [device: " << entry.first + << ", index: " << entry.second << "] for" << this; + return true; + } + + return false; + }); } } diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 2e14d61..1a1f705 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include "core.hpp" +#include "node.hpp" #include "registry.hpp" namespace qs::service::pipewire { @@ -32,6 +34,7 @@ public: signals: void deviceReady(); + void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps); private slots: void polled(); @@ -43,7 +46,7 @@ private: onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); QHash routeDeviceIndexes; - QHash stagingIndexes; + QList stagingIndexes; void addDeviceIndexPairs(const spa_pod* param); bool diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 2aff595..ed65fca 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -255,6 +255,13 @@ void PwNode::onParam( PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): node(node) { if (node->device) { QObject::connect(node->device, &PwDevice::deviceReady, this, &PwNodeBoundAudio::onDeviceReady); + + QObject::connect( + node->device, + &PwDevice::routeVolumesChanged, + this, + &PwNodeBoundAudio::onDeviceVolumesChanged + ); } } @@ -278,13 +285,17 @@ void PwNodeBoundAudio::onInfo(const pw_node_info* info) { void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) { if (id == SPA_PARAM_Props && index == 0) { - this->updateVolumeProps(param); + if (this->node->device) { + qCDebug(logNode) << "Skipping node volume props update for" << this->node + << "in favor of device updates."; + return; + } + + this->updateVolumeProps(PwVolumeProps::parseSpaPod(param)); } } -void PwNodeBoundAudio::updateVolumeProps(const spa_pod* param) { - auto volumeProps = PwVolumeProps::parseSpaPod(param); - +void PwNodeBoundAudio::updateVolumeProps(const PwVolumeProps& volumeProps) { if (volumeProps.volumes.size() != volumeProps.channels.size()) { qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelVolumes and channelMap are not the same size. Sizes:" @@ -489,6 +500,18 @@ void PwNodeBoundAudio::onDeviceReady() { } } +void PwNodeBoundAudio::onDeviceVolumesChanged( + qint32 routeDevice, + const PwVolumeProps& volumeProps +) { + if (this->node->device && this->node->routeDevice == routeDevice) { + qCDebug(logNode) << "Got updated device volume props for" << this->node << "via" + << this->node->device; + + this->updateVolumeProps(volumeProps); + } +} + PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { auto props = PwVolumeProps(); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index aca949f..0235217 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -203,9 +203,10 @@ signals: private slots: void onDeviceReady(); + void onDeviceVolumesChanged(qint32 routeDevice, const PwVolumeProps& props); private: - void updateVolumeProps(const spa_pod* param); + void updateVolumeProps(const PwVolumeProps& volumeProps); bool mMuted = false; QVector mChannels; diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 2ff7a7a..5bcc70d 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -219,22 +219,22 @@ class PwNodeAudioIface: public QObject { // clang-format off /// If the node is currently muted. Setting this property changes the mute state. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged); /// The average volume over all channels of the node. /// Setting this property modifies the volume of all channels proportionately. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(float volume READ averageVolume WRITE setAverageVolume NOTIFY volumesChanged); /// The audio channels present on the node. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); /// The volumes of each audio channel individually. Each entry corrosponds to /// the volume of the channel at the same index in @@channels. @@volumes and @@channels /// will always be the same length. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(QVector volumes READ volumes WRITE setVolumes NOTIFY volumesChanged); // clang-format on QML_NAMED_ELEMENT(PwNodeAudio); @@ -300,7 +300,7 @@ class PwNodeIface: public PwObjectIface { /// - `media.title` - The title of the currently playing media. /// - `media.artist` - The artist of the currently playing media. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged); /// Extra information present only if the node sends or receives audio. /// @@ -357,7 +357,7 @@ class PwLinkIface: public PwObjectIface { Q_PROPERTY(qs::service::pipewire::PwNodeIface* source READ source CONSTANT); /// The current state of the link. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLink); QML_UNCREATABLE("PwLinks cannot be created directly"); @@ -392,7 +392,7 @@ class PwLinkGroupIface Q_PROPERTY(qs::service::pipewire::PwNodeIface* source READ source CONSTANT); /// The current state of the link group. /// - /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). + /// > [!WARNING] This property is invalid unless the node is bound using @@PwObjectTracker. Q_PROPERTY(qs::service::pipewire::PwLinkState::Enum state READ state NOTIFY stateChanged); QML_NAMED_ELEMENT(PwLinkGroup); QML_UNCREATABLE("PwLinkGroups cannot be created directly"); From 79b2204af8360a81a8f210a5c2dc097506776c08 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 19 Jun 2025 13:50:42 -0700 Subject: [PATCH 011/226] io/socketserver: correctly order startup/teardown across generations Fixes #60 --- src/io/socket.cpp | 27 ++++++++++++++++++++------- src/io/socket.hpp | 10 +++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/io/socket.cpp b/src/io/socket.cpp index a102c7b..2cf9b62 100644 --- a/src/io/socket.cpp +++ b/src/io/socket.cpp @@ -107,7 +107,11 @@ void Socket::flush() { SocketServer::~SocketServer() { this->disableServer(); } -void SocketServer::onPostReload() { +void SocketServer::onReload(QObject* oldInstance) { + if (auto* old = qobject_cast(oldInstance)) { + old->disableServer(); + } + this->postReload = true; if (this->isActivatable()) this->enableServer(); } @@ -152,6 +156,8 @@ bool SocketServer::isActivatable() { void SocketServer::enableServer() { this->disableServer(); + qCDebug(logSocket) << "Enabling socket server" << this << "at" << this->mPath; + this->server = new QLocalServer(this); QObject::connect( this->server, @@ -160,31 +166,38 @@ void SocketServer::enableServer() { &SocketServer::onNewConnection ); + if (QFile::remove(this->mPath)) { + qCWarning(logSocket) << "Deleted existing file at" << this->mPath << "to create socket"; + } + if (!this->server->listen(this->mPath)) { - qWarning() << "could not start socket server at" << this->mPath; + qCWarning(logSocket) << "Could not start socket server at" << this->mPath; this->disableServer(); } this->activeTarget = false; + this->activePath = this->mPath; emit this->activeStatusChanged(); } void SocketServer::disableServer() { auto wasActive = this->server != nullptr; - if (this->server != nullptr) { + if (wasActive) { + qCDebug(logSocket) << "Disabling socket server" << this << "at" << this->activePath; for (auto* socket: this->mSockets) { socket->deleteLater(); } this->mSockets.clear(); + this->server->close(); this->server->deleteLater(); this->server = nullptr; - } - if (this->mPath != nullptr) { - if (QFile::exists(this->mPath) && !QFile::remove(this->mPath)) { - qWarning() << "failed to delete socket file at" << this->mPath; + if (!this->activePath.isEmpty()) { + if (QFile::exists(this->activePath) && !QFile::remove(this->activePath)) { + qWarning() << "Failed to delete socket file at" << this->activePath; + } } } diff --git a/src/io/socket.hpp b/src/io/socket.hpp index c710dbd..64605f8 100644 --- a/src/io/socket.hpp +++ b/src/io/socket.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -90,9 +91,7 @@ private: /// } /// } /// ``` -class SocketServer - : public QObject - , public PostReloadHook { +class SocketServer: public Reloadable { Q_OBJECT; /// If the socket server is currently active. Defaults to false. /// @@ -115,11 +114,11 @@ class SocketServer QML_ELEMENT; public: - explicit SocketServer(QObject* parent = nullptr): QObject(parent) {} + explicit SocketServer(QObject* parent = nullptr): Reloadable(parent) {} ~SocketServer() override; Q_DISABLE_COPY_MOVE(SocketServer); - void onPostReload() override; + void onReload(QObject* oldInstance) override; [[nodiscard]] bool isActive() const; void setActive(bool active); @@ -149,4 +148,5 @@ private: bool activeTarget = false; bool postReload = false; QString mPath; + QString activePath; }; From 3d3b7f1c05b0dca2fef889e7ef7cedbee5e01592 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 19 Jun 2025 14:54:52 -0700 Subject: [PATCH 012/226] wayland/lock: avoid creating lock surfaces for the fallback screen Fixes #61 --- src/wayland/session_lock.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index 5f0bb67..ccba9bc 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -1,5 +1,6 @@ #include "session_lock.hpp" +#include #include #include #include @@ -52,6 +53,17 @@ void WlSessionLock::onReload(QObject* oldInstance) { void WlSessionLock::updateSurfaces(bool show, WlSessionLock* old) { auto screens = QGuiApplication::screens(); + screens.removeIf([](QScreen* screen) { + if (dynamic_cast(screen->handle()) == nullptr) { + qDebug() << "Not creating lock surface for screen" << screen + << "as it is not backed by a wayland screen."; + + return true; + } + + return false; + }); + auto map = this->surfaces.toStdMap(); for (auto& [screen, surface]: map) { if (!screens.contains(screen)) { From 02362c3e94b0c14e064664232aff59856daad6da Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 20 Jun 2025 02:53:30 -0700 Subject: [PATCH 013/226] services/pipewire: add missing ; after Q_ENUM for docgen --- src/services/pipewire/node.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 0235217..0d4c92e 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -145,8 +145,8 @@ public: // @@PwNodeType.Video and @@PwNodeType.Sink flags. VideoSink = Video | Sink, }; - Q_ENUM(Flag) - Q_DECLARE_FLAGS(Flags, Flag) + Q_ENUM(Flag); + Q_DECLARE_FLAGS(Flags, Flag); Q_INVOKABLE static QString toString(qs::service::pipewire::PwNodeType::Flags type); }; From c115df8d34053ba7f0b1dc7f320090b557579930 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 20 Jun 2025 03:34:05 -0700 Subject: [PATCH 014/226] docs: mention github mirror in README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index adefcb8..4491d24 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ See the [website](https://quickshell.outfoxxed.me) for more information and installation instructions. +This repo is hosted at: +- https://git.outfoxxed.me/quickshell/quickshell +- https://github.com/quickshell-mirror/quickshell + # Contributing / Development See [CONTRIBUTING.md](CONTRIBUTING.md) for details. From 362c8e1b693d99254c2be979641d9c1188d2629a Mon Sep 17 00:00:00 2001 From: Maeeen Date: Fri, 20 Jun 2025 04:09:37 -0700 Subject: [PATCH 015/226] hyprland/ipc: expose Hyprland toplevels --- src/wayland/hyprland/ipc/connection.cpp | 235 ++++++++++++++++++ src/wayland/hyprland/ipc/connection.hpp | 25 ++ .../hyprland/ipc/hyprland_toplevel.cpp | 155 ++++++++++-- .../hyprland/ipc/hyprland_toplevel.hpp | 95 +++++-- src/wayland/hyprland/ipc/qml.cpp | 15 ++ src/wayland/hyprland/ipc/qml.hpp | 8 + src/wayland/hyprland/ipc/workspace.cpp | 68 +++++ src/wayland/hyprland/ipc/workspace.hpp | 22 +- .../test/manual/toplevel-association.qml | 37 +++ .../hyprland/test/manual/toplevels.qml | 34 +++ .../hyprland/test/manual/workspaces.qml | 34 +++ 11 files changed, 685 insertions(+), 43 deletions(-) create mode 100644 src/wayland/hyprland/test/manual/toplevel-association.qml create mode 100644 src/wayland/hyprland/test/manual/toplevels.qml create mode 100644 src/wayland/hyprland/test/manual/workspaces.qml diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 1c9f4eb..90cb8a2 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -21,7 +23,10 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" +#include "../../toplevel_management/handle.hpp" +#include "hyprland_toplevel.hpp" #include "monitor.hpp" +#include "toplevel_mapping.hpp" #include "workspace.hpp" namespace qs::hyprland::ipc { @@ -62,11 +67,16 @@ HyprlandIpc::HyprlandIpc() { QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError); QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged); QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady); + + auto *instance = HyprlandToplevelMappingManager::instance(); + QObject::connect(instance, &HyprlandToplevelMappingManager::toplevelAddressed, this, &HyprlandIpc::toplevelAddressed); + // clang-format on this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); this->refreshMonitors(true); this->refreshWorkspaces(true); + this->refreshToplevels(); } QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } @@ -113,6 +123,36 @@ void HyprlandIpc::eventSocketReady() { } } +void HyprlandIpc::toplevelAddressed( + wayland::toplevel_management::impl::ToplevelHandle* handle, + quint64 address +) { + auto* waylandToplevel = + wayland::toplevel_management::ToplevelManager::instance()->forImpl(handle); + + if (!waylandToplevel) return; + + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(waylandToplevel, false) + ); + + auto* hyprToplevel = this->findToplevelByAddress(address, true); + + if (attached) { + if (attached->address()) { + qCDebug(logHyprlandIpc) << "Toplevel" << attached->addressStr() << "already has address" + << address; + + return; + } + + attached->setAddress(address); + attached->setHyprlandHandle(hyprToplevel); + } + + hyprToplevel->setWaylandHandle(waylandToplevel->implHandle()); +} + void HyprlandIpc::makeRequest( const QByteArray& request, const std::function& callback @@ -166,6 +206,8 @@ ObjectModel* HyprlandIpc::monitors() { return &this->mMonitors; ObjectModel* HyprlandIpc::workspaces() { return &this->mWorkspaces; } +ObjectModel* HyprlandIpc::toplevels() { return &this->mToplevels; } + QVector HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) { auto args = QVector(); @@ -218,6 +260,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { if (event->name == "configreloaded") { this->refreshMonitors(true); this->refreshWorkspaces(true); + this->refreshToplevels(); } else if (event->name == "monitoraddedv2") { auto args = event->parseView(3); @@ -390,6 +433,133 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { // the fullscreen state changed, but this falls apart if you move a fullscreen // window between workspaces. this->refreshWorkspaces(false); + } else if (event->name == "openwindow") { + auto args = event->parseView(4); + auto ok = false; + auto windowAddress = args.at(0).toULongLong(&ok, 16); + + if (!ok) return; + + auto workspaceName = QString::fromUtf8(args.at(1)); + auto windowTitle = QString::fromUtf8(args.at(2)); + auto windowClass = QString::fromUtf8(args.at(3)); + + auto* workspace = this->findWorkspaceByName(workspaceName, false); + if (!workspace) { + qCWarning(logHyprlandIpc) << "Got openwindow for workspace" << workspaceName + << "which was not previously tracked."; + return; + } + + auto* toplevel = this->findToplevelByAddress(windowAddress, false); + const bool existed = toplevel != nullptr; + + if (!toplevel) toplevel = new HyprlandToplevel(this); + toplevel->updateInitial(windowAddress, windowTitle, workspaceName); + + workspace->insertToplevel(toplevel); + + if (!existed) { + this->mToplevels.insertObject(toplevel); + qCDebug(logHyprlandIpc) << "New toplevel created with address" << windowAddress << ", title" + << windowTitle << ", workspace" << workspaceName; + } + } else if (event->name == "closewindow") { + auto args = event->parseView(1); + auto ok = false; + auto windowAddress = args.at(0).toULongLong(&ok, 16); + + if (!ok) return; + + const auto& mList = this->mToplevels.valueList(); + auto toplevelIter = std::ranges::find_if(mList, [windowAddress](HyprlandToplevel* m) { + return m->address() == windowAddress; + }); + + if (toplevelIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got closewindow for address" << windowAddress + << "which was not previously tracked."; + return; + } + + auto* toplevel = *toplevelIter; + auto index = toplevelIter - mList.begin(); + this->mToplevels.removeAt(index); + + // Remove from workspace + auto* workspace = toplevel->bindableWorkspace().value(); + if (workspace) { + workspace->toplevels()->removeObject(toplevel); + } + + delete toplevel; + } else if (event->name == "movewindowv2") { + auto args = event->parseView(3); + auto ok = false; + auto windowAddress = args.at(0).toULongLong(&ok, 16); + auto workspaceName = QString::fromUtf8(args.at(2)); + + auto* toplevel = this->findToplevelByAddress(windowAddress, false); + if (!toplevel) { + qCWarning(logHyprlandIpc) << "Got movewindowv2 event for client with address" << windowAddress + << "which was not previously tracked."; + return; + } + + HyprlandWorkspace* workspace = this->findWorkspaceByName(workspaceName, false); + if (!workspace) { + qCWarning(logHyprlandIpc) << "Got movewindowv2 event for workspace" << args.at(2) + << "which was not previously tracked."; + return; + } + + auto* oldWorkspace = toplevel->bindableWorkspace().value(); + toplevel->setWorkspace(workspace); + + if (oldWorkspace) { + oldWorkspace->removeToplevel(toplevel); + } + + workspace->insertToplevel(toplevel); + } else if (event->name == "windowtitlev2") { + auto args = event->parseView(2); + auto ok = false; + auto windowAddress = args.at(0).toULongLong(&ok, 16); + auto windowTitle = QString::fromUtf8(args.at(1)); + + if (!ok) return; + + // It happens that Hyprland sends windowtitlev2 events before event + // "openwindow" is emitted, so let's preemptively create it + auto* toplevel = this->findToplevelByAddress(windowAddress, true); + if (!toplevel) { + qCWarning(logHyprlandIpc) << "Got windowtitlev2 event for client with address" + << windowAddress << "which was not previously tracked."; + return; + } + + toplevel->bindableTitle().setValue(windowTitle); + } else if (event->name == "activewindowv2") { + auto args = event->parseView(1); + auto ok = false; + auto windowAddress = args.at(0).toULongLong(&ok, 16); + + if (!ok) return; + + // Did not observe "activewindowv2" event before "openwindow", + // but better safe than sorry, so create if missing. + auto* toplevel = this->findToplevelByAddress(windowAddress, true); + this->bActiveToplevel = toplevel; + } else if (event->name == "urgent") { + auto args = event->parseView(1); + auto ok = false; + auto windowAddress = args.at(0).toULongLong(&ok, 16); + + if (!ok) return; + + // It happens that Hyprland sends urgent before "openwindow" + auto* toplevel = this->findToplevelByAddress(windowAddress, true); + toplevel->bindableUrgent().setValue(true); } } @@ -496,6 +666,71 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { }); } +HyprlandToplevel* HyprlandIpc::findToplevelByAddress(quint64 address, bool createIfMissing) { + const auto& mList = this->mToplevels.valueList(); + HyprlandToplevel* toplevel = nullptr; + + auto toplevelIter = + std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; }); + + toplevel = toplevelIter == mList.end() ? nullptr : *toplevelIter; + + if (!toplevel && createIfMissing) { + qCDebug(logHyprlandIpc) << "Toplevel with address" << address + << "requested before creation, performing early init"; + + toplevel = new HyprlandToplevel(this); + toplevel->updateInitial(address, "", ""); + this->mToplevels.insertObject(toplevel); + } + + return toplevel; +} + +void HyprlandIpc::refreshToplevels() { + if (this->requestingToplevels) return; + this->requestingToplevels = true; + + this->makeRequest("j/clients", [this](bool success, const QByteArray& resp) { + this->requestingToplevels = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "Parsing j/clients response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mToplevels.valueList(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + + bool ok = false; + auto address = object.value("address").toString().toULongLong(&ok, 16); + + if (!ok) { + qCWarning(logHyprlandIpc) << "Invalid address in j/clients entry:" << object; + continue; + } + + auto toplevelsIter = + std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; }); + + auto* toplevel = toplevelsIter == mList.end() ? nullptr : *toplevelsIter; + auto exists = toplevel != nullptr; + + if (!exists) toplevel = new HyprlandToplevel(this); + toplevel->updateFromObject(object); + + if (!exists) { + qCDebug(logHyprlandIpc) << "New toplevel created with address" << address; + this->mToplevels.insertObject(toplevel); + } + + auto* workspace = toplevel->bindableWorkspace().value(); + workspace->insertToplevel(toplevel); + } + }); +} + HyprlandMonitor* HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { const auto& mList = this->mMonitors.valueList(); diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index 5a5783f..e15d5cd 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -14,16 +14,19 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" +#include "../../../wayland/toplevel_management/handle.hpp" namespace qs::hyprland::ipc { class HyprlandMonitor; class HyprlandWorkspace; +class HyprlandToplevel; } // namespace qs::hyprland::ipc Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*); Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*); +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandToplevel*); namespace qs::hyprland::ipc { @@ -85,18 +88,25 @@ public: return &this->bFocusedWorkspace; } + [[nodiscard]] QBindable bindableActiveToplevel() const { + return &this->bActiveToplevel; + } + void setFocusedMonitor(HyprlandMonitor* monitor); [[nodiscard]] ObjectModel* monitors(); [[nodiscard]] ObjectModel* workspaces(); + [[nodiscard]] ObjectModel* toplevels(); // No byId because these preemptively create objects. The given id is set if created. HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = -1); HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); + HyprlandToplevel* findToplevelByAddress(quint64 address, bool createIfMissing); // canCreate avoids making ghost workspaces when the connection races void refreshWorkspaces(bool canCreate); void refreshMonitors(bool canCreate); + void refreshToplevels(); // The last argument may contain commas, so the count is required. [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); @@ -107,12 +117,18 @@ signals: void focusedMonitorChanged(); void focusedWorkspaceChanged(); + void activeToplevelChanged(); private slots: void eventSocketError(QLocalSocket::LocalSocketError error) const; void eventSocketStateChanged(QLocalSocket::LocalSocketState state); void eventSocketReady(); + void toplevelAddressed( + qs::wayland::toplevel_management::impl::ToplevelHandle* handle, + quint64 address + ); + void onFocusedMonitorDestroyed(); private: @@ -128,10 +144,12 @@ private: bool valid = false; bool requestingMonitors = false; bool requestingWorkspaces = false; + bool requestingToplevels = false; bool monitorsRequested = false; ObjectModel mMonitors {this}; ObjectModel mWorkspaces {this}; + ObjectModel mToplevels {this}; HyprlandIpcEvent event {this}; @@ -148,6 +166,13 @@ private: bFocusedWorkspace, &HyprlandIpc::focusedWorkspaceChanged ); + + Q_OBJECT_BINDABLE_PROPERTY( + HyprlandIpc, + HyprlandToplevel*, + bActiveToplevel, + &HyprlandIpc::activeToplevelChanged + ); }; } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp index 93c924c..59ed17e 100644 --- a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp @@ -2,50 +2,159 @@ #include #include +#include #include #include -#include "toplevel_mapping.hpp" -#include "../../toplevel_management/handle.hpp" #include "../../toplevel_management/qml.hpp" +#include "connection.hpp" +#include "toplevel_mapping.hpp" +#include "workspace.hpp" using namespace qs::wayland::toplevel_management; -using namespace qs::wayland::toplevel_management::impl; namespace qs::hyprland::ipc { -HyprlandToplevel::HyprlandToplevel(Toplevel* toplevel) - : QObject(toplevel) - , handle(toplevel->implHandle()) { - auto* instance = HyprlandToplevelMappingManager::instance(); - auto addr = instance->getToplevelAddress(handle); +HyprlandToplevel::HyprlandToplevel(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) { + this->bMonitor.setBinding([this]() { + return this->bWorkspace ? this->bWorkspace->bindableMonitor().value() : nullptr; + }); - if (addr != 0) this->setAddress(addr); - else { - QObject::connect( - instance, - &HyprlandToplevelMappingManager::toplevelAddressed, - this, - &HyprlandToplevel::onToplevelAddressed - ); - } + this->bActivated.setBinding([this]() { + return this->ipc->bindableActiveToplevel().value() == this; + }); + + QObject::connect( + this, + &HyprlandToplevel::activatedChanged, + this, + &HyprlandToplevel::onActivatedChanged + ); } -void HyprlandToplevel::onToplevelAddressed(ToplevelHandle* handle, quint64 address) { - if (handle == this->handle) { - this->setAddress(address); - QObject::disconnect(HyprlandToplevelMappingManager::instance(), nullptr, this, nullptr); +HyprlandToplevel::HyprlandToplevel(HyprlandIpc* ipc, Toplevel* toplevel): HyprlandToplevel(ipc) { + this->mWaylandHandle = toplevel->implHandle(); + auto* instance = HyprlandToplevelMappingManager::instance(); + auto addr = instance->getToplevelAddress(this->mWaylandHandle); + + if (!addr) { + // Address not available, will rely on HyprlandIpc to resolve it. + return; + } + + this->setAddress(addr); + + // Check if client is present in HyprlandIPC + auto* hyprToplevel = ipc->findToplevelByAddress(addr, false); + // HyprlandIpc will eventually resolve it + if (!hyprToplevel) return; + + this->setHyprlandHandle(hyprToplevel); +} + +void HyprlandToplevel::updateInitial( + quint64 address, + const QString& title, + const QString& workspaceName +) { + auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false); + Qt::beginPropertyUpdateGroup(); + this->setAddress(address); + this->bTitle = title; + this->setWorkspace(workspace); + Qt::endPropertyUpdateGroup(); +} + +void HyprlandToplevel::updateFromObject(const QVariantMap& object) { + auto addressStr = object.value("address").value(); + auto title = object.value("title").value(); + + bool ok = false; + auto address = addressStr.toULongLong(&ok, 16); + if (!ok || !address) { + return; + } + + this->setAddress(address); + this->bTitle = title; + + auto workspaceMap = object.value("workspace").toMap(); + auto workspaceName = workspaceMap.value("name").toString(); + + auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false); + if (!workspace) return; + + this->setWorkspace(workspace); +} + +void HyprlandToplevel::setWorkspace(HyprlandWorkspace* workspace) { + auto* oldWorkspace = this->bWorkspace.value(); + if (oldWorkspace == workspace) return; + + if (oldWorkspace) { + QObject::disconnect(oldWorkspace, nullptr, this, nullptr); + } + + this->bWorkspace = workspace; + + if (workspace) { + QObject::connect(workspace, &QObject::destroyed, this, [this]() { + this->bWorkspace = nullptr; + }); } } void HyprlandToplevel::setAddress(quint64 address) { - this->mAddress = QString::number(address, 16); + this->mAddress = address; emit this->addressChanged(); } +Toplevel* HyprlandToplevel::waylandHandle() { + return ToplevelManager::instance()->forImpl(this->mWaylandHandle); +} + +void HyprlandToplevel::setWaylandHandle(impl::ToplevelHandle* handle) { + if (this->mWaylandHandle == handle) return; + if (this->mWaylandHandle) { + QObject::disconnect(this->mWaylandHandle, nullptr, this, nullptr); + } + + this->mWaylandHandle = handle; + if (handle) { + QObject::connect(handle, &QObject::destroyed, this, [this]() { + this->mWaylandHandle = nullptr; + }); + } + + emit this->waylandHandleChanged(); +} + +void HyprlandToplevel::setHyprlandHandle(HyprlandToplevel* handle) { + if (this->mHyprlandHandle == handle) return; + if (this->mHyprlandHandle) { + QObject::disconnect(this->mHyprlandHandle, nullptr, this, nullptr); + } + this->mHyprlandHandle = handle; + if (handle) { + QObject::connect(handle, &QObject::destroyed, this, [this]() { + this->mHyprlandHandle = nullptr; + }); + } + + emit this->hyprlandHandleChanged(); +} + +void HyprlandToplevel::onActivatedChanged() { + if (this->bUrgent.value()) { + // If was urgent, and now active, clear urgent state + this->bUrgent = false; + } +} + HyprlandToplevel* HyprlandToplevel::qmlAttachedProperties(QObject* object) { if (auto* toplevel = qobject_cast(object)) { - return new HyprlandToplevel(toplevel); + auto* ipc = HyprlandIpc::instance(); + return new HyprlandToplevel(ipc, toplevel); } else { return nullptr; } diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.hpp b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp index 2cc70a5..4d61bef 100644 --- a/src/wayland/hyprland/ipc/hyprland_toplevel.hpp +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp @@ -2,49 +2,108 @@ #include #include +#include #include #include #include #include "../../toplevel_management/handle.hpp" #include "../../toplevel_management/qml.hpp" +#include "connection.hpp" namespace qs::hyprland::ipc { -//! Exposes Hyprland window address for a Toplevel -/// Attached object of @@Quickshell.Wayland.Toplevel which exposes -/// a Hyprland window address for the window. +//! Hyprland Toplevel +/// Represents a window as Hyprland exposes it. +/// Can also be used as an attached object of a @@Quickshell.Wayland.Toplevel, +/// to resolve a handle to an Hyprland toplevel. class HyprlandToplevel: public QObject { Q_OBJECT; QML_ELEMENT; QML_UNCREATABLE(""); QML_ATTACHED(HyprlandToplevel); + // clang-format off /// Hexadecimal Hyprland window address. Will be an empty string until /// the address is reported. - Q_PROPERTY(QString address READ address NOTIFY addressChanged); + Q_PROPERTY(QString address READ addressStr NOTIFY addressChanged); + /// The toplevel handle, exposing the Hyprland toplevel. + /// Will be null until the address is reported + Q_PROPERTY(HyprlandToplevel* handle READ hyprlandHandle NOTIFY hyprlandHandleChanged); + /// The wayland toplevel handle. Will be null intil the address is reported + Q_PROPERTY(qs::wayland::toplevel_management::Toplevel* wayland READ waylandHandle NOTIFY waylandHandleChanged); + /// The title of the toplevel + Q_PROPERTY(QString title READ default NOTIFY titleChanged BINDABLE bindableTitle); + /// Whether the toplevel is active or not + Q_PROPERTY(bool activated READ default NOTIFY activatedChanged BINDABLE bindableActivated); + /// Whether the client is urgent or not + Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent); + /// The current workspace of the toplevel (might be null) + Q_PROPERTY(qs::hyprland::ipc::HyprlandWorkspace* workspace READ default NOTIFY workspaceChanged BINDABLE bindableWorkspace); + /// The current monitor of the toplevel (might be null) + Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* monitor READ default NOTIFY monitorChanged BINDABLE bindableMonitor); + // clang-format on public: - explicit HyprlandToplevel(qs::wayland::toplevel_management::Toplevel* toplevel); - - [[nodiscard]] QString address() { return this->mAddress; } + /// When invoked from HyprlandIpc, reacting to Hyprland's IPC events. + explicit HyprlandToplevel(HyprlandIpc* ipc); + /// When attached from a Toplevel + explicit HyprlandToplevel(HyprlandIpc* ipc, qs::wayland::toplevel_management::Toplevel* toplevel); static HyprlandToplevel* qmlAttachedProperties(QObject* object); -signals: - void addressChanged(); + void updateInitial(quint64 address, const QString& title, const QString& workspaceName); -private slots: - void onToplevelAddressed( - qs::wayland::toplevel_management::impl::ToplevelHandle* handle, - quint64 address - ); + void updateFromObject(const QVariantMap& object); -private: + [[nodiscard]] QString addressStr() const { return QString::number(this->mAddress, 16); } + [[nodiscard]] quint64 address() const { return this->mAddress; } void setAddress(quint64 address); - QString mAddress; - // doesn't have to be nulled on destroy, only used for comparison - qs::wayland::toplevel_management::impl::ToplevelHandle* handle; + // clang-format off + [[nodiscard]] HyprlandToplevel* hyprlandHandle() { return this->mHyprlandHandle; } + void setHyprlandHandle(HyprlandToplevel* handle); + + [[nodiscard]] wayland::toplevel_management::Toplevel* waylandHandle(); + void setWaylandHandle(wayland::toplevel_management::impl::ToplevelHandle* handle); + // clang-format on + + [[nodiscard]] QBindable bindableTitle() { return &this->bTitle; } + [[nodiscard]] QBindable bindableActivated() { return &this->bActivated; } + [[nodiscard]] QBindable bindableUrgent() { return &this->bUrgent; } + + [[nodiscard]] QBindable bindableWorkspace() { return &this->bWorkspace; } + void setWorkspace(HyprlandWorkspace* workspace); + + [[nodiscard]] QBindable bindableMonitor() { return &this->bMonitor; } + +signals: + void addressChanged(); + QSDOC_HIDE void waylandHandleChanged(); + QSDOC_HIDE void hyprlandHandleChanged(); + + void titleChanged(); + void activatedChanged(); + void urgentChanged(); + void workspaceChanged(); + void monitorChanged(); + +private slots: + void onActivatedChanged(); + +private: + quint64 mAddress = 0; + HyprlandIpc* ipc; + + qs::wayland::toplevel_management::impl::ToplevelHandle* mWaylandHandle = nullptr; + HyprlandToplevel* mHyprlandHandle = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, QString, bTitle, &HyprlandToplevel::titleChanged); + Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, bool, bActivated, &HyprlandToplevel::activatedChanged); + Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, bool, bUrgent, &HyprlandToplevel::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, HyprlandWorkspace*, bWorkspace, &HyprlandToplevel::workspaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, HyprlandMonitor*, bMonitor, &HyprlandToplevel::monitorChanged); + // clang-format on }; } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp index a497ab3..3694c97 100644 --- a/src/wayland/hyprland/ipc/qml.cpp +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -28,6 +28,13 @@ HyprlandIpcQml::HyprlandIpcQml() { this, &HyprlandIpcQml::focusedMonitorChanged ); + + QObject::connect( + instance, + &HyprlandIpc::activeToplevelChanged, + this, + &HyprlandIpcQml::activeToplevelChanged + ); } void HyprlandIpcQml::dispatch(const QString& request) { @@ -51,6 +58,10 @@ QBindable HyprlandIpcQml::bindableFocusedWorkspace() { return HyprlandIpc::instance()->bindableFocusedWorkspace(); } +QBindable HyprlandIpcQml::bindableActiveToplevel() { + return HyprlandIpc::instance()->bindableActiveToplevel(); +} + ObjectModel* HyprlandIpcQml::monitors() { return HyprlandIpc::instance()->monitors(); } @@ -59,4 +70,8 @@ ObjectModel* HyprlandIpcQml::workspaces() { return HyprlandIpc::instance()->workspaces(); } +ObjectModel* HyprlandIpcQml::toplevels() { + return HyprlandIpc::instance()->toplevels(); +} + } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp index f776ef6..fce50cc 100644 --- a/src/wayland/hyprland/ipc/qml.hpp +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -24,6 +24,8 @@ class HyprlandIpcQml: public QObject { Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* focusedMonitor READ default NOTIFY focusedMonitorChanged BINDABLE bindableFocusedMonitor); /// The currently focused hyprland workspace. May be null. Q_PROPERTY(qs::hyprland::ipc::HyprlandWorkspace* focusedWorkspace READ default NOTIFY focusedWorkspaceChanged BINDABLE bindableFocusedWorkspace); + /// Currently active toplevel (might be null) + Q_PROPERTY(qs::hyprland::ipc::HyprlandToplevel* activeToplevel READ default NOTIFY activeToplevelChanged BINDABLE bindableActiveToplevel); /// All hyprland monitors. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* monitors READ monitors CONSTANT); @@ -32,6 +34,9 @@ class HyprlandIpcQml: public QObject { /// > [!NOTE] Named workspaces have a negative id, and will appear before unnamed workspaces. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); + /// All hyprland toplevels + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* toplevels READ toplevels CONSTANT); // clang-format on QML_NAMED_ELEMENT(Hyprland); QML_SINGLETON; @@ -61,8 +66,10 @@ public: [[nodiscard]] static QString eventSocketPath(); [[nodiscard]] static QBindable bindableFocusedMonitor(); [[nodiscard]] static QBindable bindableFocusedWorkspace(); + [[nodiscard]] static QBindable bindableActiveToplevel(); [[nodiscard]] static ObjectModel* monitors(); [[nodiscard]] static ObjectModel* workspaces(); + [[nodiscard]] static ObjectModel* toplevels(); signals: /// Emitted for every event that comes in through the hyprland event socket (socket2). @@ -72,6 +79,7 @@ signals: void focusedMonitorChanged(); void focusedWorkspaceChanged(); + void activeToplevelChanged(); }; } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp index bc0070d..d16c821 100644 --- a/src/wayland/hyprland/ipc/workspace.cpp +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -1,4 +1,5 @@ #include "workspace.hpp" +#include #include #include @@ -25,6 +26,12 @@ HyprlandWorkspace::HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) { return this->ipc->bindableFocusedWorkspace().value() == this; }); + QObject::connect(this, &HyprlandWorkspace::focusedChanged, this, [this]() { + if (this->bFocused.value()) { + this->updateUrgent(); + } + }); + Qt::endPropertyUpdateGroup(); } @@ -82,6 +89,67 @@ void HyprlandWorkspace::setMonitor(HyprlandMonitor* monitor) { void HyprlandWorkspace::onMonitorDestroyed() { this->bMonitor = nullptr; } +void HyprlandWorkspace::insertToplevel(HyprlandToplevel* toplevel) { + if (!toplevel) return; + + const auto& mList = this->mToplevels.valueList(); + + if (std::ranges::find(mList, toplevel) != mList.end()) { + return; + } + + this->mToplevels.insertObject(toplevel); + + QObject::connect(toplevel, &QObject::destroyed, this, [this, toplevel]() { + this->removeToplevel(toplevel); + }); + + QObject::connect( + toplevel, + &HyprlandToplevel::urgentChanged, + this, + &HyprlandWorkspace::updateUrgent + ); + + this->updateUrgent(); +} + +void HyprlandWorkspace::removeToplevel(HyprlandToplevel* toplevel) { + if (!toplevel) return; + + this->mToplevels.removeObject(toplevel); + emit this->updateUrgent(); + QObject::disconnect(toplevel, nullptr, this, nullptr); +} + +// Triggered when there is an update either on the toplevel list, on a toplevel's urgent state +void HyprlandWorkspace::updateUrgent() { + const auto& mList = this->mToplevels.valueList(); + + const bool hasUrgentToplevel = std::ranges::any_of(mList, [&](HyprlandToplevel* toplevel) { + return toplevel->bindableUrgent().value(); + }); + + if (this->bFocused && hasUrgentToplevel) { + this->clearUrgent(); + return; + } + + if (hasUrgentToplevel != this->bUrgent.value()) { + this->bUrgent = hasUrgentToplevel; + } +} + +void HyprlandWorkspace::clearUrgent() { + this->bUrgent = false; + + // Clear all urgent toplevels + const auto& mList = this->mToplevels.valueList(); + for (auto* toplevel: mList) { + toplevel->bindableUrgent().setValue(false); + } +} + void HyprlandWorkspace::activate() { this->ipc->dispatch(QString("workspace %1").arg(this->bId.value())); } diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp index 3493c5f..957639a 100644 --- a/src/wayland/hyprland/ipc/workspace.hpp +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -9,6 +9,7 @@ #include #include "connection.hpp" +#include "hyprland_toplevel.hpp" namespace qs::hyprland::ipc { @@ -24,8 +25,11 @@ class HyprlandWorkspace: public QObject { /// If this workspace is currently active on a monitor and that monitor is currently /// focused. See also @@active. Q_PROPERTY(bool focused READ default NOTIFY focusedChanged BINDABLE bindableFocused); + /// If this workspace has a window that is urgent. + /// Becomes always falsed after the workspace is @@focused. + Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent); /// If this workspace currently has a fullscreen client. - Q_PROPERTY(bool hasFullscreen READ default NOTIFY focusedChanged BINDABLE bindableHasFullscreen); + Q_PROPERTY(bool hasFullscreen READ default NOTIFY hasFullscreenChanged BINDABLE bindableHasFullscreen); /// Last json returned for this workspace, as a javascript object. /// /// > [!WARNING] This is *not* updated unless the workspace object is fetched again from @@ -33,6 +37,9 @@ class HyprlandWorkspace: public QObject { /// > property, run @@Hyprland.refreshWorkspaces() and wait for this property to update. Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* monitor READ default NOTIFY monitorChanged BINDABLE bindableMonitor); + /// List of toplevels on this workspace. + QSDOC_TYPE_OVERRIDE(ObjectModel bindableName() { return &this->bName; } [[nodiscard]] QBindable bindableActive() { return &this->bActive; } [[nodiscard]] QBindable bindableFocused() { return &this->bFocused; } + [[nodiscard]] QBindable bindableUrgent() { return &this->bUrgent; } [[nodiscard]] QBindable bindableHasFullscreen() { return &this->bHasFullscreen; } [[nodiscard]] QBindable bindableMonitor() { return &this->bMonitor; } + [[nodiscard]] ObjectModel* toplevels() { return &this->mToplevels; } [[nodiscard]] QVariantMap lastIpcObject() const; void setMonitor(HyprlandMonitor* monitor); + void insertToplevel(HyprlandToplevel* toplevel); + void removeToplevel(HyprlandToplevel* toplevel); + signals: void idChanged(); void nameChanged(); void activeChanged(); void focusedChanged(); + void urgentChanged(); void hasFullscreenChanged(); void lastIpcObjectChanged(); void monitorChanged(); private slots: void onMonitorDestroyed(); + void updateUrgent(); private: - HyprlandIpc* ipc; + void clearUrgent(); + HyprlandIpc* ipc; QVariantMap mLastIpcObject; + ObjectModel mToplevels {this}; + // clang-format off Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(HyprlandWorkspace, qint32, bId, -1, &HyprlandWorkspace::idChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, QString, bName, &HyprlandWorkspace::nameChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bActive, &HyprlandWorkspace::activeChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bFocused, &HyprlandWorkspace::focusedChanged); + Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bUrgent, &HyprlandWorkspace::urgentChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bHasFullscreen, &HyprlandWorkspace::hasFullscreenChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, HyprlandMonitor*, bMonitor, &HyprlandWorkspace::monitorChanged); // clang-format on diff --git a/src/wayland/hyprland/test/manual/toplevel-association.qml b/src/wayland/hyprland/test/manual/toplevel-association.qml new file mode 100644 index 0000000..042b915 --- /dev/null +++ b/src/wayland/hyprland/test/manual/toplevel-association.qml @@ -0,0 +1,37 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland + +FloatingWindow { + ColumnLayout { + anchors.fill: parent + + Text { text: "Hyprland -> Wayland" } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: Hyprland.toplevels + delegate: Text { + required property HyprlandToplevel modelData + text: `${modelData} -> ${modelData.wayland}` + } + } + + Text { text: "Wayland -> Hyprland" } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: ToplevelManager.toplevels + delegate: Text { + required property Toplevel modelData + text: `${modelData} -> ${modelData.HyprlandToplevel.handle}` + } + } + } +} diff --git a/src/wayland/hyprland/test/manual/toplevels.qml b/src/wayland/hyprland/test/manual/toplevels.qml new file mode 100644 index 0000000..da54e5c --- /dev/null +++ b/src/wayland/hyprland/test/manual/toplevels.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland + +FloatingWindow { + ColumnLayout { + anchors.fill: parent + + Text { text: "Current toplevel:" } + + ToplevelFromHyprland { + modelData: Hyprland.activeToplevel + } + + Text { text: "\nAll toplevels:" } + + ListView { + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + model: Hyprland.toplevels + delegate: ToplevelFromHyprland {} + } + } + + component ToplevelFromHyprland: ColumnLayout { + required property HyprlandToplevel modelData + + Text { + text: `Window 0x${modelData.address}, title: ${modelData.title}, activated: ${modelData.activated}, workspace id: ${modelData.workspace.id}, monitor name: ${modelData.monitor.name}, urgent: ${modelData.urgent}` + } + } +} diff --git a/src/wayland/hyprland/test/manual/workspaces.qml b/src/wayland/hyprland/test/manual/workspaces.qml new file mode 100644 index 0000000..ef1bafe --- /dev/null +++ b/src/wayland/hyprland/test/manual/workspaces.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Hyprland + +FloatingWindow { + ListView { + anchors.fill: parent + model: Hyprland.workspaces + spacing: 5 + + delegate: WrapperRectangle { + id: wsDelegate + required property HyprlandWorkspace modelData + color: "lightgray" + + ColumnLayout { + Text { text: `Workspace ${wsDelegate.modelData.id} on ${wsDelegate.modelData.monitor} | urgent: ${wsDelegate.modelData.urgent}`} + + ColumnLayout { + Repeater { + model: wsDelegate.modelData.toplevels + Text { + id: tDelegate + required property HyprlandToplevel modelData; + text: `${tDelegate.modelData}: ${tDelegate.modelData.title}` + } + } + } + } + } + } +} From c17ea5437146075ccfdd1368e14cceb97d02a6f6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 20 Jun 2025 16:37:22 -0700 Subject: [PATCH 016/226] wayland/lock: check for protocol availability before use Fixes #66 --- src/wayland/session_lock.cpp | 7 +++++++ src/wayland/session_lock/session_lock.cpp | 2 ++ src/wayland/session_lock/session_lock.hpp | 2 ++ 3 files changed, 11 insertions(+) diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index ccba9bc..0ecf9ec 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -109,6 +109,13 @@ void WlSessionLock::updateSurfaces(bool show, WlSessionLock* old) { void WlSessionLock::realizeLockTarget(WlSessionLock* old) { if (this->lockTarget) { + if (!SessionLockManager::lockAvailable()) { + qCritical() << "Cannot start session lock: The current compositor does not support the " + "ext-session-lock-v1 protocol."; + this->unlock(); + return; + } + if (this->mSurfaceComponent == nullptr) { qWarning() << "WlSessionLock.surface is null. Aborting lock."; this->unlock(); diff --git a/src/wayland/session_lock/session_lock.cpp b/src/wayland/session_lock/session_lock.cpp index 50e8818..c32dd90 100644 --- a/src/wayland/session_lock/session_lock.cpp +++ b/src/wayland/session_lock/session_lock.cpp @@ -22,6 +22,8 @@ QSWaylandSessionLockManager* manager() { } } // namespace +bool SessionLockManager::lockAvailable() { return manager()->isActive(); } + bool SessionLockManager::lock() { if (this->isLocked() || SessionLockManager::sessionLocked()) return false; this->mLock = manager()->acquireLock(); diff --git a/src/wayland/session_lock/session_lock.hpp b/src/wayland/session_lock/session_lock.hpp index 1ad6ae9..5a55896 100644 --- a/src/wayland/session_lock/session_lock.hpp +++ b/src/wayland/session_lock/session_lock.hpp @@ -15,6 +15,8 @@ class SessionLockManager: public QObject { public: explicit SessionLockManager(QObject* parent = nullptr): QObject(parent) {} + [[nodiscard]] static bool lockAvailable(); + // Returns true if a lock was acquired. // If true is returned the caller must watch the global screen list and create/destroy // windows with an attached LockWindowExtension to match it. From 8fc3e1cb6eabaf0a6598a96db0b8d36e6bce34db Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 20 Jun 2025 19:06:59 -0700 Subject: [PATCH 017/226] docs: include HyprlandToplevel in module file --- src/wayland/hyprland/module.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index 0bdb6a7..921bac9 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -4,6 +4,7 @@ headers = [ "ipc/connection.hpp", "ipc/monitor.hpp", "ipc/workspace.hpp", + "ipc/hyprland_toplevel.hpp", "ipc/qml.hpp", "focus_grab/qml.hpp", "global_shortcuts/qml.hpp", From 98d09b5a36ceb2caea35125068731a1431a55400 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 20 Jun 2025 20:32:42 -0700 Subject: [PATCH 018/226] io/process: add Process.exec() --- src/core/qmlglobal.cpp | 10 +++----- src/core/qmlglobal.hpp | 47 +++++-------------------------------- src/io/process.cpp | 42 +++++++++++++++++++++++---------- src/io/process.hpp | 36 ++++++++++++++++++++++++++++ src/io/processcore.cpp | 4 ++-- src/io/processcore.hpp | 53 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 128 insertions(+), 64 deletions(-) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index c447c55..0aaf06c 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -250,10 +250,10 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT } void QuickshellGlobal::execDetached(QList command) { - QuickshellGlobal::execDetached(ProcessContext(std::move(command))); + QuickshellGlobal::execDetached(qs::io::process::ProcessContext(std::move(command))); } -void QuickshellGlobal::execDetached(const ProcessContext& context) { +void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& context) { if (context.command.isEmpty()) { qWarning() << "Cannot start process as command is empty."; return; @@ -264,11 +264,7 @@ void QuickshellGlobal::execDetached(const ProcessContext& context) { QProcess process; - qs::core::process::setupProcessEnvironment( - &process, - context.clearEnvironment, - context.environment - ); + qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment); if (!context.workingDirectory.isEmpty()) { process.setWorkingDirectory(context.workingDirectory); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index d5b9844..82442ce 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -17,27 +15,10 @@ #include #include +#include "../io/processcore.hpp" +#include "doc.hpp" #include "qmlscreen.hpp" -class ProcessContext { - Q_PROPERTY(QList command MEMBER command); - Q_PROPERTY(QHash environment MEMBER environment); - Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment); - Q_PROPERTY(QString workingDirectory MEMBER workingDirectory); - Q_GADGET; - QML_STRUCTURED_VALUE; - QML_VALUE_TYPE(processContext); - -public: - ProcessContext() = default; - explicit ProcessContext(QList command): command(std::move(command)) {} - - QList command; - QHash environment; - bool clearEnvironment = false; - QString workingDirectory; -}; - ///! Accessor for some options under the Quickshell type. class QuickshellSettings: public QObject { Q_OBJECT; @@ -175,27 +156,11 @@ public: /// Returns the string value of an environment variable or null if it is not set. Q_INVOKABLE QVariant env(const QString& variable); + // MUST be before execDetached(ctx) or the other will be called with a default constructed obj. + QSDOC_HIDE Q_INVOKABLE static void execDetached(QList command); /// Launch a process detached from Quickshell. /// - /// Each command argument is its own string, meaning arguments do - /// not have to be escaped. - /// - /// > [!WARNING] This does not run command in a shell. All arguments to the command - /// > must be in separate values in the list, e.g. `["echo", "hello"]` - /// > and not `["echo hello"]`. - /// > - /// > Additionally, shell scripts must be run by your shell, - /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script - /// > has a shebang. - /// - /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with - /// > the system shell. - /// - /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). - Q_INVOKABLE static void execDetached(QList command); - /// Launch a process detached from Quickshell. - /// - /// The context parameter is a JS object with the following fields: + /// The context parameter can either be a list of command arguments or a JS object with the following fields: /// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command. /// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment. /// - `clearEnvironment`: Removes all variables from the environment if true. @@ -213,7 +178,7 @@ public: /// > the system shell. /// /// This function is equivalent to @@Quickshell.Io.Process.startDetached(). - Q_INVOKABLE static void execDetached(const ProcessContext& context); + Q_INVOKABLE static void execDetached(const qs::io::process::ProcessContext& context); /// Returns a string usable for a @@QtQuick.Image.source for a given system icon. /// diff --git a/src/io/process.cpp b/src/io/process.cpp index 43637d4..8aa6c24 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -46,17 +46,7 @@ void Process::setCommand(QList command) { if (this->mCommand == command) return; this->mCommand = std::move(command); - auto& cmd = this->mCommand.first(); - if (cmd.startsWith("file://")) { - cmd = cmd.sliced(7); - } else if (cmd.startsWith("root://")) { - cmd = cmd.sliced(7); - auto& root = EngineGeneration::findObjectGeneration(this)->rootPath; - cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd); - } - emit this->commandChanged(); - this->startProcessIfReady(); } @@ -82,7 +72,6 @@ void Process::onGlobalWorkingDirectoryChanged() { QHash Process::environment() const { return this->mEnvironment; } void Process::setEnvironment(QHash environment) { - qDebug() << "setEnv" << environment; if (environment == this->mEnvironment) return; this->mEnvironment = std::move(environment); emit this->environmentChanged(); @@ -180,6 +169,14 @@ void Process::startProcessIfReady() { this->targetRunning = false; auto& cmd = this->mCommand.first(); + if (cmd.startsWith("file://")) { + cmd = cmd.sliced(7); + } else if (cmd.startsWith("root://")) { + cmd = cmd.sliced(7); + auto& root = EngineGeneration::findObjectGeneration(this)->rootPath; + cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd); + } + auto args = this->mCommand.sliced(1); this->process = new QProcess(this); @@ -203,6 +200,25 @@ void Process::startProcessIfReady() { this->process->start(cmd, args); } +void Process::exec(QList command) { + this->exec(qs::io::process::ProcessContext(std::move(command))); +} + +void Process::exec(const qs::io::process::ProcessContext& context) { + this->setRunning(false); + if (context.commandSet) this->setCommand(context.command); + if (context.environmentSet) this->setEnvironment(context.environment); + if (context.clearEnvironmentSet) this->setEnvironmentCleared(context.clearEnvironment); + if (context.workingDirectorySet) this->setWorkingDirectory(context.workingDirectory); + + if (this->mCommand.isEmpty()) { + qmlWarning(this) << "Cannot start process as command is empty."; + return; + } + + this->setRunning(true); +} + void Process::startDetached() { if (this->mCommand.isEmpty()) { qmlWarning(this) << "Cannot start process as command is empty."; @@ -225,7 +241,7 @@ void Process::setupEnvironment(QProcess* process) { process->setWorkingDirectory(this->mWorkingDirectory); } - qs::core::process::setupProcessEnvironment(process, this->mClearEnvironment, this->mEnvironment); + qs::io::process::setupProcessEnvironment(process, this->mClearEnvironment, this->mEnvironment); } void Process::onStarted() { @@ -245,6 +261,8 @@ void Process::onFinished(qint32 exitCode, QProcess::ExitStatus exitStatus) { emit this->exited(exitCode, exitStatus); emit this->runningChanged(); emit this->processIdChanged(); + + this->startProcessIfReady(); // for `running = false; running = true` } void Process::onErrorOccurred(QProcess::ProcessError error) { diff --git a/src/io/process.hpp b/src/io/process.hpp index 2d7e1fd..c9e983e 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -10,7 +10,9 @@ #include #include +#include "../core/doc.hpp" #include "datastream.hpp" +#include "processcore.hpp" // Needed when compiling with clang musl-libc++. // Default include paths contain macros that cause name collisions. @@ -135,6 +137,40 @@ class Process: public QObject { public: explicit Process(QObject* parent = nullptr); + // MUST be before exec(ctx) or the other will be called with a default constructed obj. + QSDOC_HIDE Q_INVOKABLE void exec(QList command); + /// Launch a process with the given arguments, stopping any currently running process. + /// + /// The context parameter can either be a list of command arguments or a JS object with the following fields: + /// - `command`: A list containing the command and all its arguments. See @@Quickshell.Io.Process.command. + /// - `environment`: Changes to make to the process environment. See @@Quickshell.Io.Process.environment. + /// - `clearEnvironment`: Removes all variables from the environment if true. + /// - `workingDirectory`: The working directory the command should run in. + /// + /// Passed parameters will change the values currently set in the process. + /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// + /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with + /// > the system shell. + /// + /// Calling this function is equivalent to running: + /// ```qml + /// process.running = false; + /// process.command = ... + /// process.environment = ... + /// process.clearEnvironment = ... + /// process.workingDirectory = ... + /// process.running = true; + /// ``` + Q_INVOKABLE void exec(const qs::io::process::ProcessContext& context); + /// Sends a signal to the process if @@running is true, otherwise does nothing. Q_INVOKABLE void signal(qint32 signal); diff --git a/src/io/processcore.cpp b/src/io/processcore.cpp index 572045e..8b5e80e 100644 --- a/src/io/processcore.cpp +++ b/src/io/processcore.cpp @@ -7,7 +7,7 @@ #include "../core/common.hpp" -namespace qs::core::process { +namespace qs::io::process { void setupProcessEnvironment( QProcess* process, @@ -34,4 +34,4 @@ void setupProcessEnvironment( process->setProcessEnvironment(env); } -} // namespace qs::core::process +} // namespace qs::io::process diff --git a/src/io/processcore.hpp b/src/io/processcore.hpp index fe8bda7..c74f6fb 100644 --- a/src/io/processcore.hpp +++ b/src/io/processcore.hpp @@ -1,11 +1,60 @@ #pragma once +#include + #include #include +#include #include +#include #include -namespace qs::core::process { +namespace qs::io::process { + +class ProcessContext { + Q_PROPERTY(QList command MEMBER command WRITE setCommand); + Q_PROPERTY(QHash environment MEMBER environment WRITE setEnvironment); + Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment); + Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory); + Q_GADGET; + QML_STRUCTURED_VALUE; + QML_VALUE_TYPE(processContext); + +public: + ProcessContext() = default; + // Making this a Q_INVOKABLE does not work with QML_STRUCTURED_VALUe in Qt 6.9. + explicit ProcessContext(QList command): command(std::move(command)), commandSet(true) {} + + void setCommand(QList command) { + this->command = std::move(command); + this->commandSet = true; + } + + void setEnvironment(QHash environment) { + this->environment = std::move(environment); + this->environmentSet = true; + } + + void setClearEnvironment(bool clearEnvironment) { + this->clearEnvironment = clearEnvironment; + this->clearEnvironmentSet = true; + } + + void setWorkingDirectory(QString workingDirectory) { + this->workingDirectory = std::move(workingDirectory); + this->workingDirectorySet = true; + } + + QList command; + QHash environment; + bool clearEnvironment = false; + QString workingDirectory; + + bool commandSet : 1 = false; + bool environmentSet : 1 = false; + bool clearEnvironmentSet : 1 = false; + bool workingDirectorySet : 1 = false; +}; void setupProcessEnvironment( QProcess* process, @@ -13,4 +62,4 @@ void setupProcessEnvironment( const QHash& envChanges ); -} +} // namespace qs::io::process From 8be18c05ed026bd8ca2803103cbd17bb1b18c70b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 20 Jun 2025 21:31:44 -0700 Subject: [PATCH 019/226] hyprland/ipc: expose HyprlandToplevel jsons --- src/wayland/hyprland/ipc/hyprland_toplevel.hpp | 12 ++++++++++++ src/wayland/hyprland/ipc/qml.cpp | 1 + src/wayland/hyprland/ipc/qml.hpp | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.hpp b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp index 4d61bef..ebd4f84 100644 --- a/src/wayland/hyprland/ipc/hyprland_toplevel.hpp +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.hpp @@ -37,6 +37,12 @@ class HyprlandToplevel: public QObject { Q_PROPERTY(bool activated READ default NOTIFY activatedChanged BINDABLE bindableActivated); /// Whether the client is urgent or not Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent); + /// Last json returned for this toplevel, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the toplevel object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run @@Hyprland.refreshToplevels() and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ default BINDABLE bindableLastIpcObject NOTIFY lastIpcObjectChanged); /// The current workspace of the toplevel (might be null) Q_PROPERTY(qs::hyprland::ipc::HyprlandWorkspace* workspace READ default NOTIFY workspaceChanged BINDABLE bindableWorkspace); /// The current monitor of the toplevel (might be null) @@ -71,6 +77,10 @@ public: [[nodiscard]] QBindable bindableActivated() { return &this->bActivated; } [[nodiscard]] QBindable bindableUrgent() { return &this->bUrgent; } + [[nodiscard]] QBindable bindableLastIpcObject() const { + return &this->bLastIpcObject; + }; + [[nodiscard]] QBindable bindableWorkspace() { return &this->bWorkspace; } void setWorkspace(HyprlandWorkspace* workspace); @@ -86,6 +96,7 @@ signals: void urgentChanged(); void workspaceChanged(); void monitorChanged(); + void lastIpcObjectChanged(); private slots: void onActivatedChanged(); @@ -103,6 +114,7 @@ private: Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, bool, bUrgent, &HyprlandToplevel::urgentChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, HyprlandWorkspace*, bWorkspace, &HyprlandToplevel::workspaceChanged); Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, HyprlandMonitor*, bMonitor, &HyprlandToplevel::monitorChanged); + Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, QVariantMap, bLastIpcObject, &HyprlandToplevel::lastIpcObjectChanged); // clang-format on }; diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp index 3694c97..89eec9e 100644 --- a/src/wayland/hyprland/ipc/qml.cpp +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -47,6 +47,7 @@ HyprlandMonitor* HyprlandIpcQml::monitorFor(QuickshellScreenInfo* screen) { void HyprlandIpcQml::refreshMonitors() { HyprlandIpc::instance()->refreshMonitors(false); } void HyprlandIpcQml::refreshWorkspaces() { HyprlandIpc::instance()->refreshWorkspaces(false); } +void HyprlandIpcQml::refreshToplevels() { HyprlandIpc::instance()->refreshToplevels(); } QString HyprlandIpcQml::requestSocketPath() { return HyprlandIpc::instance()->requestSocketPath(); } QString HyprlandIpcQml::eventSocketPath() { return HyprlandIpc::instance()->eventSocketPath(); } diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp index fce50cc..ebf5d80 100644 --- a/src/wayland/hyprland/ipc/qml.hpp +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -62,6 +62,12 @@ public: /// so this function is available if required. Q_INVOKABLE static void refreshWorkspaces(); + /// Refresh toplevel information. + /// + /// Many actions that will invalidate workspace state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshToplevels(); + [[nodiscard]] static QString requestSocketPath(); [[nodiscard]] static QString eventSocketPath(); [[nodiscard]] static QBindable bindableFocusedMonitor(); From 20c3da01f1b2bc038582eee831e4f5055b4f71ff Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Jun 2025 12:57:15 -0700 Subject: [PATCH 020/226] io/fileview: null watcher ptr after deletion to avoid UAF Fixes #69 --- src/io/fileview.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index b6e911f..cbe8417 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -492,7 +492,10 @@ void FileView::updatePath() { void FileView::updateWatchedFiles() { // If inotify events are sent to the watcher after deletion and deleteLater // isn't used, a use after free in the QML engine will occur. - if (this->watcher) this->watcher->deleteLater(); + if (this->watcher) { + this->watcher->deleteLater(); + this->watcher = nullptr; + } if (!this->targetPath.isEmpty() && this->bWatchChanges) { qCDebug(logFileView) << "Creating watcher for" << this << "at" << this->targetPath; From 27f97c3283bfe77e7ddc64e77f08859696e11e7c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 24 Jun 2025 18:52:19 -0700 Subject: [PATCH 021/226] wayland/toplevel: refactor toplevel output tracking to its own file --- src/wayland/CMakeLists.txt | 1 + src/wayland/output_tracking.cpp | 73 ++++++++++++++++++++ src/wayland/output_tracking.hpp | 34 ++++++++++ src/wayland/toplevel_management/handle.cpp | 77 ++-------------------- src/wayland/toplevel_management/handle.hpp | 9 +-- src/wayland/toplevel_management/qml.cpp | 7 +- 6 files changed, 119 insertions(+), 82 deletions(-) create mode 100644 src/wayland/output_tracking.cpp create mode 100644 src/wayland/output_tracking.hpp diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 26d29c8..1d6543e 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -77,6 +77,7 @@ qt_add_library(quickshell-wayland STATIC popupanchor.cpp xdgshell.cpp util.cpp + output_tracking.cpp ) # required to make sure the constructor is linked diff --git a/src/wayland/output_tracking.cpp b/src/wayland/output_tracking.cpp new file mode 100644 index 0000000..9d69ee7 --- /dev/null +++ b/src/wayland/output_tracking.cpp @@ -0,0 +1,73 @@ +#include "output_tracking.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::wayland { + +void WlOutputTracker::addOutput(::wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + + if (auto* platformScreen = display->screenForOutput(output)) { + auto* screen = platformScreen->screen(); + this->mScreens.append(screen); + emit this->screenAdded(screen); + } else { + QObject::connect( + static_cast(QGuiApplication::instance()), // NOLINT + &QGuiApplication::screenAdded, + this, + &WlOutputTracker::onQScreenAdded, + Qt::UniqueConnection + ); + + this->mOutputs.append(output); + } +} + +void WlOutputTracker::removeOutput(::wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + + if (auto* platformScreen = display->screenForOutput(output)) { + auto* screen = platformScreen->screen(); + this->mScreens.removeOne(screen); + emit this->screenRemoved(screen); + } else { + this->mOutputs.removeOne(output); + + if (this->mOutputs.isEmpty()) { + QObject::disconnect( + static_cast(QGuiApplication::instance()), // NOLINT + nullptr, + this, + nullptr + ); + } + } +} + +void WlOutputTracker::onQScreenAdded(QScreen* screen) { + if (auto* platformScreen = dynamic_cast(screen->handle())) { + if (this->mOutputs.removeOne(platformScreen->output())) { + this->mScreens.append(screen); + emit this->screenAdded(screen); + + if (this->mOutputs.isEmpty()) { + QObject::disconnect( + static_cast(QGuiApplication::instance()), // NOLINT + nullptr, + this, + nullptr + ); + } + } + } +} + +} // namespace qs::wayland diff --git a/src/wayland/output_tracking.hpp b/src/wayland/output_tracking.hpp new file mode 100644 index 0000000..988e53b --- /dev/null +++ b/src/wayland/output_tracking.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +struct wl_output; + +namespace qs::wayland { + +class WlOutputTracker: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] const QList& screens() const { return this->mScreens; } + +signals: + void screenAdded(QScreen* screen); + void screenRemoved(QScreen* screen); + +public slots: + void addOutput(::wl_output* output); + void removeOutput(::wl_output* output); + +private slots: + void onQScreenAdded(QScreen* screen); + +private: + QList mScreens; + QList<::wl_output*> mOutputs; +}; + +} // namespace qs::wayland diff --git a/src/wayland/toplevel_management/handle.cpp b/src/wayland/toplevel_management/handle.cpp index 026b439..914e44e 100644 --- a/src/wayland/toplevel_management/handle.cpp +++ b/src/wayland/toplevel_management/handle.cpp @@ -23,7 +23,6 @@ namespace qs::wayland::toplevel_management::impl { QString ToplevelHandle::appId() const { return this->mAppId; } QString ToplevelHandle::title() const { return this->mTitle; } -QVector ToplevelHandle::visibleScreens() const { return this->mVisibleScreens; } ToplevelHandle* ToplevelHandle::parent() const { return this->mParent; } bool ToplevelHandle::activated() const { return this->mActivated; } bool ToplevelHandle::maximized() const { return this->mMaximized; } @@ -181,59 +180,13 @@ void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) } void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) { - auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); - - auto* platformScreen = display->screenForOutput(output); - if (!platformScreen) { - qCDebug(logToplevelManagement) << this << "got pending output enter" << output; - - if (this->mPendingVisibleScreens.isEmpty()) { - QObject::connect( - static_cast(QGuiApplication::instance()), // NOLINT - &QGuiApplication::screenAdded, - this, - &ToplevelHandle::onScreenAdded - ); - } - - this->mPendingVisibleScreens.append(output); - return; - } - - auto* screen = platformScreen->screen(); - - qCDebug(logToplevelManagement) << this << "got output enter" << screen; - - this->mVisibleScreens.append(screen); - emit this->visibleScreenAdded(screen); + qCDebug(logToplevelManagement) << this << "got output enter" << output; + this->visibleScreens.addOutput(output); } void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) { - auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); - auto* platformScreen = display->screenForOutput(output); - - if (!this->mPendingVisibleScreens.isEmpty()) { - this->mPendingVisibleScreens.removeOne(output); - - if (this->mPendingVisibleScreens.isEmpty()) { - qCDebug(logToplevelManagement) << this << "got pending output leave" << output; - - QObject::disconnect( - static_cast(QGuiApplication::instance()), // NOLINT - nullptr, - this, - nullptr - ); - } - } - - if (!platformScreen) return; - auto* screen = platformScreen->screen(); - - qCDebug(logToplevelManagement) << this << "got output leave" << screen; - - this->mVisibleScreens.removeOne(screen); - emit this->visibleScreenRemoved(screen); + qCDebug(logToplevelManagement) << this << "got output leave" << output; + this->visibleScreens.removeOutput(output); } void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_parent( @@ -262,26 +215,4 @@ void ToplevelHandle::onParentClosed() { emit this->parentChanged(); } -void ToplevelHandle::onScreenAdded(QScreen* screen) { - auto* waylandScreen = dynamic_cast(screen->handle()); - if (!waylandScreen) return; - - auto* output = waylandScreen->output(); - - if (this->mPendingVisibleScreens.removeOne(output)) { - qCDebug(logToplevelManagement) << this << "got pending entered output init" << screen; - this->mVisibleScreens.append(screen); - emit this->visibleScreenAdded(screen); - } - - if (this->mPendingVisibleScreens.isEmpty()) { - QObject::disconnect( - static_cast(QGuiApplication::instance()), // NOLINT - nullptr, - this, - nullptr - ); - } -} - } // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/handle.hpp b/src/wayland/toplevel_management/handle.hpp index 991069a..92531a5 100644 --- a/src/wayland/toplevel_management/handle.hpp +++ b/src/wayland/toplevel_management/handle.hpp @@ -6,6 +6,7 @@ #include #include +#include "../output_tracking.hpp" #include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" namespace qs::wayland::toplevel_management::impl { @@ -18,7 +19,6 @@ class ToplevelHandle public: [[nodiscard]] QString appId() const; [[nodiscard]] QString title() const; - [[nodiscard]] QVector visibleScreens() const; [[nodiscard]] ToplevelHandle* parent() const; [[nodiscard]] bool activated() const; [[nodiscard]] bool maximized() const; @@ -32,6 +32,8 @@ public: void fullscreenOn(QScreen* screen); void setRectangle(QWindow* window, QRect rect); + WlOutputTracker visibleScreens; + signals: // sent after the first done event. void ready(); @@ -40,8 +42,6 @@ signals: void appIdChanged(); void titleChanged(); - void visibleScreenAdded(QScreen* screen); - void visibleScreenRemoved(QScreen* screen); void parentChanged(); void activatedChanged(); void maximizedChanged(); @@ -51,7 +51,6 @@ signals: private slots: void onParentClosed(); void onRectWindowDestroyed(); - void onScreenAdded(QScreen* screen); private: void zwlr_foreign_toplevel_handle_v1_done() override; @@ -66,8 +65,6 @@ private: bool isReady = false; QString mAppId; QString mTitle; - QVector mVisibleScreens; - QVector mPendingVisibleScreens; ToplevelHandle* mParent = nullptr; bool mActivated = false; bool mMaximized = false; diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 0d14d4d..0eae3de 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -10,6 +10,7 @@ #include "../../core/util.hpp" #include "../../window/proxywindow.hpp" #include "../../window/windowinterface.hpp" +#include "../output_tracking.hpp" #include "handle.hpp" #include "manager.hpp" @@ -22,8 +23,8 @@ Toplevel::Toplevel(impl::ToplevelHandle* handle, QObject* parent): QObject(paren QObject::connect(handle, &impl::ToplevelHandle::titleChanged, this, &Toplevel::titleChanged); QObject::connect(handle, &impl::ToplevelHandle::parentChanged, this, &Toplevel::parentChanged); QObject::connect(handle, &impl::ToplevelHandle::activatedChanged, this, &Toplevel::activatedChanged); - QObject::connect(handle, &impl::ToplevelHandle::visibleScreenAdded, this, &Toplevel::screensChanged); - QObject::connect(handle, &impl::ToplevelHandle::visibleScreenRemoved, this, &Toplevel::screensChanged); + QObject::connect(&handle->visibleScreens, &WlOutputTracker::screenAdded, this, &Toplevel::screensChanged); + QObject::connect(&handle->visibleScreens, &WlOutputTracker::screenRemoved, this, &Toplevel::screensChanged); QObject::connect(handle, &impl::ToplevelHandle::maximizedChanged, this, &Toplevel::maximizedChanged); QObject::connect(handle, &impl::ToplevelHandle::minimizedChanged, this, &Toplevel::minimizedChanged); QObject::connect(handle, &impl::ToplevelHandle::fullscreenChanged, this, &Toplevel::fullscreenChanged); @@ -50,7 +51,7 @@ bool Toplevel::activated() const { return this->handle->activated(); } QList Toplevel::screens() const { QList screens; - for (auto* screen: this->handle->visibleScreens()) { + for (auto* screen: this->handle->visibleScreens.screens()) { screens.push_back(QuickshellTracked::instance()->screenInfo(screen)); } From d949f913479445e4f0ca3a95a183ee45d98dc359 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 25 Jun 2025 12:34:00 -0700 Subject: [PATCH 022/226] wayland/screencopy: apply output transform to wlr screencopy Note that this only fixes output copies, and not toplevel copies. Toplevel copies are harder because a toplevel can be on more than one output. Hopefully we'll all be using image-copy-capture before this one comes up. Fixes #75 --- src/wayland/buffer/manager.hpp | 1 + src/wayland/screencopy/view.cpp | 8 ++- .../wlr_screencopy/wlr_screencopy.cpp | 69 +++++++++++++++++-- .../wlr_screencopy/wlr_screencopy_p.hpp | 33 +++++++++ 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/wayland/buffer/manager.hpp b/src/wayland/buffer/manager.hpp index c84ee86..b521e89 100644 --- a/src/wayland/buffer/manager.hpp +++ b/src/wayland/buffer/manager.hpp @@ -40,6 +40,7 @@ struct WlBufferTransform { [[nodiscard]] int degrees() const { return 90 * (this->transform & 0b11111011); } [[nodiscard]] bool flip() const { return this->transform & 0b00000100; } + [[nodiscard]] bool flipSize() const { return this->transform & 0b00000001; } void apply(QMatrix4x4& matrix) const { matrix.rotate(this->flip() ? 180 : 0, 0, 1, 0); diff --git a/src/wayland/screencopy/view.cpp b/src/wayland/screencopy/view.cpp index aeafea6..7828c98 100644 --- a/src/wayland/screencopy/view.cpp +++ b/src/wayland/screencopy/view.cpp @@ -120,8 +120,14 @@ void ScreencopyView::captureFrame() { void ScreencopyView::onFrameCaptured() { this->setFlag(QQuickItem::ItemHasContents); this->update(); + + const auto& frontbuffer = this->context->swapchain().frontbuffer(); + + auto size = frontbuffer->size(); + if (frontbuffer->transform.flipSize()) size.transpose(); + + this->bSourceSize = size; this->bHasContent = true; - this->bSourceSize = this->context->swapchain().frontbuffer()->size(); } void ScreencopyView::componentComplete() { diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index 8cc89bc..aa21266 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -1,12 +1,14 @@ #include "wlr_screencopy.hpp" #include +#include #include #include #include #include #include #include +#include #include #include @@ -45,6 +47,7 @@ WlrScreencopyContext::WlrScreencopyContext( , screen(dynamic_cast(screen->handle())) , paintCursors(paintCursors) , region(region) { + this->transform.setScreen(this->screen); QObject::connect(screen, &QObject::destroyed, this, &WlrScreencopyContext::onScreenDestroyed); } @@ -99,9 +102,7 @@ void WlrScreencopyContext::zwlr_screencopy_frame_v1_linux_dmabuf( } void WlrScreencopyContext::zwlr_screencopy_frame_v1_flags(uint32_t flags) { - if (flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT) { - this->mSwapchain.backbuffer()->transform = buffer::WlBufferTransform::Flipped180; - } + this->yInvert = flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT; } void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer_done() { @@ -119,10 +120,7 @@ void WlrScreencopyContext::zwlr_screencopy_frame_v1_ready( uint32_t /*tvSecLo*/, uint32_t /*tvNsec*/ ) { - this->destroy(); - this->copiedFirstFrame = true; - this->mSwapchain.swapBuffers(); - emit this->frameCaptured(); + this->submitFrame(); } void WlrScreencopyContext::zwlr_screencopy_frame_v1_failed() { @@ -130,4 +128,61 @@ void WlrScreencopyContext::zwlr_screencopy_frame_v1_failed() { emit this->stopped(); } +void WlrScreencopyContext::updateTransform(bool previouslyUnset) { + if (previouslyUnset && this->copiedFirstFrame) this->submitFrame(); +} + +void WlrScreencopyContext::submitFrame() { + this->copiedFirstFrame = true; + if (this->transform.transform == -1) return; + + auto flipTransform = + this->yInvert ? buffer::WlBufferTransform::Flipped180 : buffer::WlBufferTransform::Normal0; + + this->mSwapchain.backbuffer()->transform = this->transform.transform ^ flipTransform; + + this->destroy(); + this->mSwapchain.swapBuffers(); + emit this->frameCaptured(); +} + +WlrScreencopyContext::OutputTransformQuery::OutputTransformQuery(WlrScreencopyContext* context) + : context(context) {} + +WlrScreencopyContext::OutputTransformQuery::~OutputTransformQuery() { + if (this->isInitialized()) this->release(); +} + +void WlrScreencopyContext::OutputTransformQuery::setScreen(QtWaylandClient::QWaylandScreen* screen +) { + // cursed hack + class QWaylandScreenReflector: public QtWaylandClient::QWaylandScreen { + public: + [[nodiscard]] int globalId() const { return this->m_outputId; } + }; + + if (this->isInitialized()) this->release(); + + this->init( + screen->display()->wl_registry(), + static_cast(screen)->globalId(), // NOLINT + 3 + ); +} + +void WlrScreencopyContext::OutputTransformQuery::output_geometry( + qint32 /*x*/, + qint32 /*y*/, + qint32 /*width*/, + qint32 /*height*/, + qint32 /*subpixel*/, + const QString& /*make*/, + const QString& /*model*/, + qint32 transform +) { + auto newTransform = this->transform == -1; + this->transform = transform; + this->context->updateTransform(newTransform); +} + } // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp index 7bdbafb..6e7620c 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp @@ -1,7 +1,10 @@ #pragma once +#include #include +#include #include +#include #include #include "../manager.hpp" @@ -24,6 +27,7 @@ public: Q_DISABLE_COPY_MOVE(WlrScreencopyContext); void captureFrame() override; + void updateTransform(bool previouslyUnset); protected: // clang-format off @@ -39,9 +43,38 @@ private slots: void onScreenDestroyed(); private: + void submitFrame(); + + class OutputTransformQuery: public QtWayland::wl_output { + public: + OutputTransformQuery(WlrScreencopyContext* context); + ~OutputTransformQuery() override; + Q_DISABLE_COPY_MOVE(OutputTransformQuery); + + qint32 transform = -1; + void setScreen(QtWaylandClient::QWaylandScreen* screen); + + protected: + void output_geometry( + qint32 x, + qint32 y, + qint32 width, + qint32 height, + qint32 subpixel, + const QString& make, + const QString& model, + qint32 transform + ) override; + + private: + WlrScreencopyContext* context; + }; + WlrScreencopyManager* manager; buffer::WlBufferRequest request; bool copiedFirstFrame = false; + OutputTransformQuery transform {this}; + bool yInvert = false; QtWaylandClient::QWaylandScreen* screen; bool paintCursors; From f842b84a5a215e7c8b9ee9c4a32d88bab4fb6f5e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 26 Jun 2025 12:43:59 -0700 Subject: [PATCH 023/226] widgets/wrapper: round child position when centering Fixes misalignment when resizeChild is false and wrapper width is odd. --- src/widgets/marginwrapper.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp index 020e80a..9960bba 100644 --- a/src/widgets/marginwrapper.cpp +++ b/src/widgets/marginwrapper.cpp @@ -1,4 +1,5 @@ #include "marginwrapper.hpp" +#include #include #include @@ -39,7 +40,7 @@ MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(pare auto total = this->bLeftMargin + this->bRightMargin; auto mul = total == 0 ? 0.5 : this->bLeftMargin / total; auto margin = this->bWrapperWidth - this->bChildImplicitWidth; - return margin * mul; + return std::round(margin * mul); }); this->bChildY.setBinding([this] { @@ -48,7 +49,7 @@ MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(pare auto total = this->bTopMargin + this->bBottomMargin; auto mul = total == 0 ? 0.5 : this->bTopMargin / total; auto margin = this->bWrapperHeight - this->bChildImplicitHeight; - return margin * mul; + return std::round(margin * mul); }); this->bChildWidth.setBinding([this] { From 1d02292fbf24c41f947cf72dd7de57f6dedf2173 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 27 Jun 2025 04:09:14 -0700 Subject: [PATCH 024/226] hyprland/ipc: actually set lastIpcObject --- src/wayland/hyprland/ipc/hyprland_toplevel.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp index 59ed17e..7b07bc8 100644 --- a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp @@ -69,6 +69,7 @@ void HyprlandToplevel::updateFromObject(const QVariantMap& object) { auto addressStr = object.value("address").value(); auto title = object.value("title").value(); + Qt::beginPropertyUpdateGroup(); bool ok = false; auto address = addressStr.toULongLong(&ok, 16); if (!ok || !address) { @@ -85,6 +86,8 @@ void HyprlandToplevel::updateFromObject(const QVariantMap& object) { if (!workspace) return; this->setWorkspace(workspace); + this->bLastIpcObject = object; + Qt::endPropertyUpdateGroup(); } void HyprlandToplevel::setWorkspace(HyprlandWorkspace* workspace) { From f681e2016fd71b42985bd520b3a20c62488582e9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 1 Jul 2025 00:07:20 -0700 Subject: [PATCH 025/226] bluetooth: add bluetooth integration Missing support for things that require an agent, but has most basics. Closes #17 --- CMakeLists.txt | 3 +- src/CMakeLists.txt | 4 + src/bluetooth/CMakeLists.txt | 42 +++ src/bluetooth/adapter.cpp | 217 ++++++++++++ src/bluetooth/adapter.hpp | 173 ++++++++++ src/bluetooth/bluez.cpp | 156 +++++++++ src/bluetooth/bluez.hpp | 81 +++++ src/bluetooth/device.cpp | 318 ++++++++++++++++++ src/bluetooth/device.hpp | 225 +++++++++++++ src/bluetooth/module.md | 12 + src/bluetooth/org.bluez.Adapter.xml | 9 + src/bluetooth/org.bluez.Device.xml | 8 + src/bluetooth/test/manual/test.qml | 200 +++++++++++ src/dbus/CMakeLists.txt | 11 + src/dbus/dbus_objectmanager_types.hpp | 10 + src/dbus/objectmanager.cpp | 86 +++++ src/dbus/objectmanager.hpp | 37 ++ .../org.freedesktop.DBus.ObjectManager.xml | 18 + src/dbus/properties.cpp | 8 + src/dbus/properties.hpp | 7 +- .../status_notifier/dbus_item_types.cpp | 8 - .../status_notifier/dbus_item_types.hpp | 4 - 22 files changed, 1623 insertions(+), 14 deletions(-) create mode 100644 src/bluetooth/CMakeLists.txt create mode 100644 src/bluetooth/adapter.cpp create mode 100644 src/bluetooth/adapter.hpp create mode 100644 src/bluetooth/bluez.cpp create mode 100644 src/bluetooth/bluez.hpp create mode 100644 src/bluetooth/device.cpp create mode 100644 src/bluetooth/device.hpp create mode 100644 src/bluetooth/module.md create mode 100644 src/bluetooth/org.bluez.Adapter.xml create mode 100644 src/bluetooth/org.bluez.Device.xml create mode 100644 src/bluetooth/test/manual/test.qml create mode 100644 src/dbus/dbus_objectmanager_types.hpp create mode 100644 src/dbus/objectmanager.cpp create mode 100644 src/dbus/objectmanager.hpp create mode 100644 src/dbus/org.freedesktop.DBus.ObjectManager.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 846a280..7161c4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ boption(SERVICE_PAM "Pam" ON) boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) +boption(BLUETOOTH "Bluetooth" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -116,7 +117,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) set(DBUS ON) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d3070b6..52db00a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,3 +29,7 @@ if (X11) endif() add_subdirectory(services) + +if (BLUETOOTH) + add_subdirectory(bluetooth) +endif() diff --git a/src/bluetooth/CMakeLists.txt b/src/bluetooth/CMakeLists.txt new file mode 100644 index 0000000..806ff04 --- /dev/null +++ b/src/bluetooth/CMakeLists.txt @@ -0,0 +1,42 @@ +set_source_files_properties(org.bluez.Adapter.xml PROPERTIES + CLASSNAME DBusBluezAdapterInterface +) + +set_source_files_properties(org.bluez.Device.xml PROPERTIES + CLASSNAME DBusBluezDeviceInterface +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.bluez.Adapter.xml + dbus_adapter +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.bluez.Device.xml + dbus_device +) + +qt_add_library(quickshell-bluetooth STATIC + adapter.cpp + bluez.cpp + device.cpp + ${DBUS_INTERFACES} +) + +qt_add_qml_module(quickshell-bluetooth + URI Quickshell.Bluetooth + VERSION 0.1 + DEPENDENCIES QtQml +) + +install_qml_module(quickshell-bluetooth) + +# dbus headers +target_include_directories(quickshell-bluetooth PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +target_link_libraries(quickshell-bluetooth PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-bluetooth quickshell-dbus) + +qs_module_pch(quickshell-bluetooth SET dbus) + +target_link_libraries(quickshell PRIVATE quickshell-bluetoothplugin) diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp new file mode 100644 index 0000000..e24b13a --- /dev/null +++ b/src/bluetooth/adapter.cpp @@ -0,0 +1,217 @@ +#include "adapter.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/properties.hpp" +#include "dbus_adapter.h" + +namespace qs::bluetooth { + +namespace { +Q_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg); +} + +QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) { + switch (state) { + case BluetoothAdapterState::Disabled: return QStringLiteral("Disabled"); + case BluetoothAdapterState::Enabled: return QStringLiteral("Enabled"); + case BluetoothAdapterState::Enabling: return QStringLiteral("Enabling"); + case BluetoothAdapterState::Disabling: return QStringLiteral("Disabling"); + case BluetoothAdapterState::Blocked: return QStringLiteral("Blocked"); + default: return QStringLiteral("Unknown"); + } +} + +BluetoothAdapter::BluetoothAdapter(const QString& path, QObject* parent): QObject(parent) { + this->mInterface = + new DBusBluezAdapterInterface("org.bluez", path, QDBusConnection::systemBus(), this); + + if (!this->mInterface->isValid()) { + qCWarning(logAdapter) << "Could not create DBus interface for adapter at" << path; + this->mInterface = nullptr; + return; + } + + this->properties.setInterface(this->mInterface); +} + +QString BluetoothAdapter::adapterId() const { + auto path = this->path(); + return path.sliced(path.lastIndexOf('/') + 1); +} + +void BluetoothAdapter::setEnabled(bool enabled) { + if (enabled == this->bEnabled) return; + this->bEnabled = enabled; + this->pEnabled.write(); +} + +void BluetoothAdapter::setDiscoverable(bool discoverable) { + if (discoverable == this->bDiscoverable) return; + this->bDiscoverable = discoverable; + this->pDiscoverable.write(); +} + +void BluetoothAdapter::setDiscovering(bool discovering) { + if (discovering) { + this->startDiscovery(); + } else { + this->stopDiscovery(); + } +} + +void BluetoothAdapter::setDiscoverableTimeout(quint32 timeout) { + if (timeout == this->bDiscoverableTimeout) return; + this->bDiscoverableTimeout = timeout; + this->pDiscoverableTimeout.write(); +} + +void BluetoothAdapter::setPairable(bool pairable) { + if (pairable == this->bPairable) return; + this->bPairable = pairable; + this->pPairable.write(); +} + +void BluetoothAdapter::setPairableTimeout(quint32 timeout) { + if (timeout == this->bPairableTimeout) return; + this->bPairableTimeout = timeout; + this->pPairableTimeout.write(); +} + +void BluetoothAdapter::addInterface(const QString& interface, const QVariantMap& properties) { + if (interface == "org.bluez.Adapter1") { + this->properties.updatePropertySet(properties, false); + qCDebug(logAdapter) << "Updated Adapter properties for" << this; + } +} + +void BluetoothAdapter::removeDevice(const QString& devicePath) { + qCDebug(logAdapter) << "Removing device" << devicePath << "from adapter" << this; + + auto reply = this->mInterface->RemoveDevice(QDBusObjectPath(devicePath)); + + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this, devicePath](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + + if (reply.isError()) { + qCWarning(logAdapter).nospace() + << "Failed to remove device " << devicePath << " from adapter" << this << ": " + << reply.error().message(); + } else { + qCDebug(logAdapter) << "Successfully removed device" << devicePath << "from adapter" + << this; + } + + delete watcher; + } + ); +} + +void BluetoothAdapter::startDiscovery() { + if (this->bDiscovering) return; + qCDebug(logAdapter) << "Starting discovery for adapter" << this; + + auto reply = this->mInterface->StartDiscovery(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + + if (reply.isError()) { + qCWarning(logAdapter).nospace() + << "Failed to start discovery on adapter" << this << ": " << reply.error().message(); + } else { + qCDebug(logAdapter) << "Successfully started discovery on adapter" << this; + } + + delete watcher; + } + ); +} + +void BluetoothAdapter::stopDiscovery() { + if (!this->bDiscovering) return; + qCDebug(logAdapter) << "Stopping discovery for adapter" << this; + + auto reply = this->mInterface->StopDiscovery(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + + if (reply.isError()) { + qCWarning(logAdapter).nospace() + << "Failed to stop discovery on adapter " << this << ": " << reply.error().message(); + } else { + qCDebug(logAdapter) << "Successfully stopped discovery on adapter" << this; + } + + delete watcher; + } + ); +} + +} // namespace qs::bluetooth + +namespace qs::dbus { + +using namespace qs::bluetooth; + +DBusResult +DBusDataTransform::fromWire(const Wire& wire) { + if (wire == QStringLiteral("off")) { + return BluetoothAdapterState::Disabled; + } else if (wire == QStringLiteral("on")) { + return BluetoothAdapterState::Enabled; + } else if (wire == QStringLiteral("off-enabling")) { + return BluetoothAdapterState::Enabling; + } else if (wire == QStringLiteral("on-disabling")) { + return BluetoothAdapterState::Disabling; + } else if (wire == QStringLiteral("off-blocked")) { + return BluetoothAdapterState::Blocked; + } else { + return QDBusError( + QDBusError::InvalidArgs, + QString("Invalid BluetoothAdapterState: %1").arg(wire) + ); + } +} + +} // namespace qs::dbus + +QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter) { + auto saver = QDebugStateSaver(debug); + + if (adapter) { + debug.nospace() << "BluetoothAdapter(" << static_cast(adapter) + << ", path=" << adapter->path() << ")"; + } else { + debug << "BluetoothAdapter(nullptr)"; + } + + return debug; +} diff --git a/src/bluetooth/adapter.hpp b/src/bluetooth/adapter.hpp new file mode 100644 index 0000000..d7f21d7 --- /dev/null +++ b/src/bluetooth/adapter.hpp @@ -0,0 +1,173 @@ +#pragma once + +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/model.hpp" +#include "../dbus/properties.hpp" +#include "dbus_adapter.h" + +namespace qs::bluetooth { + +///! Power state of a Bluetooth adapter. +class BluetoothAdapterState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The adapter is powered off. + Disabled = 0, + /// The adapter is powered on. + Enabled = 1, + /// The adapter is transitioning from off to on. + Enabling = 2, + /// The adapter is transitioning from on to off. + Disabling = 3, + /// The adapter is blocked by rfkill. + Blocked = 4, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(BluetoothAdapterState::Enum state); +}; + +} // namespace qs::bluetooth + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::bluetooth::BluetoothAdapterState::Enum; + static DBusResult fromWire(const Wire& wire); +}; + +} // namespace qs::dbus + +namespace qs::bluetooth { + +class BluetoothAdapter; +class BluetoothDevice; + +///! A Bluetooth adapter +class BluetoothAdapter: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// System provided name of the adapter. See @@adapterId for the internal identifier. + Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); + /// True if the adapter is currently enabled. More detailed state is available from @@state. + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + /// Detailed power state of the adapter. + Q_PROPERTY(BluetoothAdapterState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// True if the adapter can be discovered by other bluetooth devices. + Q_PROPERTY(bool discoverable READ discoverable WRITE setDiscoverable NOTIFY discoverableChanged); + /// Timeout in seconds for how long the adapter stays discoverable after @@discoverable is set to true. + /// A value of 0 means the adapter stays discoverable forever. + Q_PROPERTY(quint32 discoverableTimeout READ discoverableTimeout WRITE setDiscoverableTimeout NOTIFY discoverableTimeoutChanged); + /// True if the adapter is scanning for new devices. + Q_PROPERTY(bool discovering READ discovering WRITE setDiscovering NOTIFY discoveringChanged); + /// True if the adapter is accepting incoming pairing requests. + /// + /// This only affects incoming pairing requests and should typically only be changed + /// by system settings applications. Defaults to true. + Q_PROPERTY(bool pairable READ pairable WRITE setPairable NOTIFY pairableChanged); + /// Timeout in seconds for how long the adapter stays pairable after @@pairable is set to true. + /// A value of 0 means the adapter stays pairable forever. Defaults to 0. + Q_PROPERTY(quint32 pairableTimeout READ pairableTimeout WRITE setPairableTimeout NOTIFY pairableTimeoutChanged); + /// Bluetooth devices connected to this adapter. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + /// The internal ID of the adapter (e.g., "hci0"). + Q_PROPERTY(QString adapterId READ adapterId CONSTANT); + /// DBus path of the adapter under the `org.bluez` system service. + Q_PROPERTY(QString dbusPath READ path CONSTANT); + // clang-format on + +public: + explicit BluetoothAdapter(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const { return this->mInterface->isValid(); } + [[nodiscard]] QString path() const { return this->mInterface->path(); } + [[nodiscard]] QString adapterId() const; + + [[nodiscard]] bool enabled() const { return this->bEnabled; } + void setEnabled(bool enabled); + + [[nodiscard]] bool discoverable() const { return this->bDiscoverable; } + void setDiscoverable(bool discoverable); + + [[nodiscard]] bool discovering() const { return this->bDiscovering; } + void setDiscovering(bool discovering); + + [[nodiscard]] quint32 discoverableTimeout() const { return this->bDiscoverableTimeout; } + void setDiscoverableTimeout(quint32 timeout); + + [[nodiscard]] bool pairable() const { return this->bPairable; } + void setPairable(bool pairable); + + [[nodiscard]] quint32 pairableTimeout() const { return this->bPairableTimeout; } + void setPairableTimeout(quint32 timeout); + + [[nodiscard]] QBindable bindableName() { return &this->bName; } + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + [[nodiscard]] QBindable bindableState() { return &this->bState; } + [[nodiscard]] QBindable bindableDiscoverable() { return &this->bDiscoverable; } + [[nodiscard]] QBindable bindableDiscoverableTimeout() { + return &this->bDiscoverableTimeout; + } + [[nodiscard]] QBindable bindableDiscovering() { return &this->bDiscovering; } + [[nodiscard]] QBindable bindablePairable() { return &this->bPairable; } + [[nodiscard]] QBindable bindablePairableTimeout() { return &this->bPairableTimeout; } + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } + + void addInterface(const QString& interface, const QVariantMap& properties); + void removeDevice(const QString& devicePath); + + void startDiscovery(); + void stopDiscovery(); + +signals: + void nameChanged(); + void enabledChanged(); + void stateChanged(); + void discoverableChanged(); + void discoverableTimeoutChanged(); + void discoveringChanged(); + void pairableChanged(); + void pairableTimeoutChanged(); + +private: + DBusBluezAdapterInterface* mInterface = nullptr; + ObjectModel mDevices {this}; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, QString, bName, &BluetoothAdapter::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bEnabled, &BluetoothAdapter::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, BluetoothAdapterState::Enum, bState, &BluetoothAdapter::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscoverable, &BluetoothAdapter::discoverableChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bDiscoverableTimeout, &BluetoothAdapter::discoverableTimeoutChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bDiscovering, &BluetoothAdapter::discoveringChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, bool, bPairable, &BluetoothAdapter::pairableChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothAdapter, quint32, bPairableTimeout, &BluetoothAdapter::pairableTimeoutChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothAdapter, properties); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pName, bName, properties, "Alias"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pEnabled, bEnabled, properties, "Powered"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pState, bState, properties, "PowerState"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverable, bDiscoverable, properties, "Discoverable"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscoverableTimeout, bDiscoverableTimeout, properties, "DiscoverableTimeout"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pDiscovering, bDiscovering, properties, "Discovering"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairable, bPairable, properties, "Pairable"); + QS_DBUS_PROPERTY_BINDING(BluetoothAdapter, pPairableTimeout, bPairableTimeout, properties, "PairableTimeout"); + // clang-format on +}; + +} // namespace qs::bluetooth + +QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothAdapter* adapter); diff --git a/src/bluetooth/bluez.cpp b/src/bluetooth/bluez.cpp new file mode 100644 index 0000000..3c8bf94 --- /dev/null +++ b/src/bluetooth/bluez.cpp @@ -0,0 +1,156 @@ +#include "bluez.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/dbus_objectmanager_types.hpp" +#include "../dbus/objectmanager.hpp" +#include "adapter.hpp" +#include "device.hpp" + +namespace qs::bluetooth { + +namespace { +Q_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg); +} + +Bluez* Bluez::instance() { + static auto* instance = new Bluez(); + return instance; +} + +Bluez::Bluez() { this->init(); } + +void Bluez::init() { + qCDebug(logBluetooth) << "Connecting to BlueZ"; + + auto bus = QDBusConnection::systemBus(); + + if (!bus.isConnected()) { + qCWarning(logBluetooth) << "Could not connect to DBus. Bluetooth integration is not available."; + return; + } + + this->objectManager = new qs::dbus::DBusObjectManager(this); + + QObject::connect( + this->objectManager, + &qs::dbus::DBusObjectManager::interfacesAdded, + this, + &Bluez::onInterfacesAdded + ); + + QObject::connect( + this->objectManager, + &qs::dbus::DBusObjectManager::interfacesRemoved, + this, + &Bluez::onInterfacesRemoved + ); + + if (!this->objectManager->setInterface("org.bluez", "/", bus)) { + qCDebug(logBluetooth) << "BlueZ is not running. Bluetooth integration will not work."; + return; + } +} + +void Bluez::onInterfacesAdded( + const QDBusObjectPath& path, + const DBusObjectManagerInterfaces& interfaces +) { + if (auto* adapter = this->mAdapterMap.value(path.path())) { + for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { + adapter->addInterface(interface, properties); + } + } else if (auto* device = this->mDeviceMap.value(path.path())) { + for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { + device->addInterface(interface, properties); + } + } else if (interfaces.contains("org.bluez.Adapter1")) { + auto* adapter = new BluetoothAdapter(path.path(), this); + + if (!adapter->isValid()) { + qCWarning(logBluetooth) << "Adapter path is not valid, cannot track: " << device; + delete adapter; + return; + } + + qCDebug(logBluetooth) << "Tracked new adapter" << adapter; + + for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { + adapter->addInterface(interface, properties); + } + + for (auto* device: this->mDevices.valueList()) { + if (device->adapterPath() == path) { + adapter->devices()->insertObject(device); + qCDebug(logBluetooth) << "Added tracked device" << device << "to new adapter" << adapter; + emit device->adapterChanged(); + } + } + + this->mAdapterMap.insert(path.path(), adapter); + this->mAdapters.insertObject(adapter); + } else if (interfaces.contains("org.bluez.Device1")) { + auto* device = new BluetoothDevice(path.path(), this); + + if (!device->isValid()) { + qCWarning(logBluetooth) << "Device path is not valid, cannot track: " << device; + delete device; + return; + } + + qCDebug(logBluetooth) << "Tracked new device" << device; + + for (const auto& [interface, properties]: interfaces.asKeyValueRange()) { + device->addInterface(interface, properties); + } + + if (auto* adapter = device->adapter()) { + adapter->devices()->insertObject(device); + qCDebug(logBluetooth) << "Added device" << device << "to adapter" << adapter; + } + + this->mDeviceMap.insert(path.path(), device); + this->mDevices.insertObject(device); + } +} + +void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces) { + if (auto* adapter = this->mAdapterMap.value(path.path())) { + if (interfaces.contains("org.bluez.Adapter1")) { + qCDebug(logBluetooth) << "Adapter removed:" << adapter; + + this->mAdapterMap.remove(path.path()); + this->mAdapters.removeObject(adapter); + delete adapter; + } + } else if (auto* device = this->mDeviceMap.value(path.path())) { + if (interfaces.contains("org.bluez.Device1")) { + qCDebug(logBluetooth) << "Device removed:" << device; + + if (auto* adapter = device->adapter()) { + adapter->devices()->removeObject(device); + } + + this->mDeviceMap.remove(path.path()); + this->mDevices.removeObject(device); + delete device; + } else { + for (const auto& interface: interfaces) { + device->removeInterface(interface); + } + } + } +} + +BluetoothAdapter* Bluez::defaultAdapter() const { + const auto& adapters = this->mAdapters.valueList(); + return adapters.isEmpty() ? nullptr : adapters.first(); +} + +} // namespace qs::bluetooth diff --git a/src/bluetooth/bluez.hpp b/src/bluetooth/bluez.hpp new file mode 100644 index 0000000..d888e8f --- /dev/null +++ b/src/bluetooth/bluez.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" +#include "../core/model.hpp" +#include "../dbus/dbus_objectmanager_types.hpp" +#include "../dbus/objectmanager.hpp" + +namespace qs::bluetooth { + +class BluetoothAdapter; +class BluetoothDevice; + +class Bluez: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] ObjectModel* adapters() { return &this->mAdapters; } + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } + [[nodiscard]] BluetoothAdapter* defaultAdapter() const; + + [[nodiscard]] BluetoothAdapter* adapter(const QString& path) { + return this->mAdapterMap.value(path); + } + + static Bluez* instance(); + +private slots: + void + onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces); + void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces); + +private: + explicit Bluez(); + void init(); + + qs::dbus::DBusObjectManager* objectManager = nullptr; + QHash mAdapterMap; + QHash mDeviceMap; + ObjectModel mAdapters {this}; + ObjectModel mDevices {this}; +}; + +///! Bluetooth manager +/// Provides access to bluetooth devices and adapters. +class BluezQml: public QObject { + Q_OBJECT; + /// The default bluetooth adapter. Usually there is only one. + Q_PROPERTY(BluetoothAdapter* defaultAdapter READ defaultAdapter CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + /// A list of all bluetooth adapters. See @@defaultAdapter for the default. + Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + /// A list of all connected bluetooth devices across all adapters. + /// See @@BluetoothAdapter.devices for the devices connected to a single adapter. + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + QML_NAMED_ELEMENT(Bluetooth); + QML_SINGLETON; + +public: + explicit BluezQml(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] static ObjectModel* adapters() { + return Bluez::instance()->adapters(); + } + + [[nodiscard]] static ObjectModel* devices() { + return Bluez::instance()->devices(); + } + + [[nodiscard]] static BluetoothAdapter* defaultAdapter() { + return Bluez::instance()->defaultAdapter(); + } +}; + +} // namespace qs::bluetooth diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp new file mode 100644 index 0000000..30008a3 --- /dev/null +++ b/src/bluetooth/device.cpp @@ -0,0 +1,318 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../dbus/properties.hpp" +#include "adapter.hpp" +#include "bluez.hpp" +#include "dbus_device.h" + +namespace qs::bluetooth { + +namespace { +Q_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg); +} + +QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) { + switch (state) { + case BluetoothDeviceState::Disconnected: return QStringLiteral("Disconnected"); + case BluetoothDeviceState::Connected: return QStringLiteral("Connected"); + case BluetoothDeviceState::Disconnecting: return QStringLiteral("Disconnecting"); + case BluetoothDeviceState::Connecting: return QStringLiteral("Connecting"); + default: return QStringLiteral("Unknown"); + } +} + +BluetoothDevice::BluetoothDevice(const QString& path, QObject* parent): QObject(parent) { + this->mInterface = + new DBusBluezDeviceInterface("org.bluez", path, QDBusConnection::systemBus(), this); + + if (!this->mInterface->isValid()) { + qCWarning(logDevice) << "Could not create DBus interface for device at" << path; + delete this->mInterface; + this->mInterface = nullptr; + return; + } + + this->properties.setInterface(this->mInterface); +} + +BluetoothAdapter* BluetoothDevice::adapter() const { + return Bluez::instance()->adapter(this->bAdapterPath.value().path()); +} + +void BluetoothDevice::setConnected(bool connected) { + if (connected == this->bConnected) return; + + if (connected) { + this->connect(); + } else { + this->disconnect(); + } +} + +void BluetoothDevice::setTrusted(bool trusted) { + if (trusted == this->bTrusted) return; + this->bTrusted = trusted; + this->pTrusted.write(); +} + +void BluetoothDevice::setBlocked(bool blocked) { + if (blocked == this->bBlocked) return; + this->bBlocked = blocked; + this->pBlocked.write(); +} + +void BluetoothDevice::setName(const QString& name) { + if (name == this->bName) return; + this->bName = name; + this->pName.write(); +} + +void BluetoothDevice::setWakeAllowed(bool wakeAllowed) { + if (wakeAllowed == this->bWakeAllowed) return; + this->bWakeAllowed = wakeAllowed; + this->pWakeAllowed.write(); +} + +void BluetoothDevice::connect() { + if (this->bConnected) { + qCCritical(logDevice) << "Device" << this << "is already connected"; + return; + } + + if (this->bState == BluetoothDeviceState::Connecting) { + qCCritical(logDevice) << "Device" << this << "is already connecting"; + return; + } + + qCDebug(logDevice) << "Connecting to device" << this; + this->bState = BluetoothDeviceState::Connecting; + + auto reply = this->mInterface->Connect(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + + if (reply.isError()) { + qCWarning(logDevice).nospace() + << "Failed to connect to device " << this << ": " << reply.error().message(); + + this->bState = this->bConnected ? BluetoothDeviceState::Connected + : BluetoothDeviceState::Disconnected; + } else { + qCDebug(logDevice) << "Successfully connected to to device" << this; + } + + delete watcher; + } + ); +} + +void BluetoothDevice::disconnect() { + if (!this->bConnected) { + qCCritical(logDevice) << "Device" << this << "is already disconnected"; + return; + } + + if (this->bState == BluetoothDeviceState::Disconnecting) { + qCCritical(logDevice) << "Device" << this << "is already disconnecting"; + return; + } + + qCDebug(logDevice) << "Disconnecting from device" << this; + this->bState = BluetoothDeviceState::Disconnecting; + + auto reply = this->mInterface->Disconnect(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + + if (reply.isError()) { + qCWarning(logDevice).nospace() + << "Failed to disconnect from device " << this << ": " << reply.error().message(); + + this->bState = this->bConnected ? BluetoothDeviceState::Connected + : BluetoothDeviceState::Disconnected; + } else { + qCDebug(logDevice) << "Successfully disconnected from from device" << this; + } + + delete watcher; + } + ); +} + +void BluetoothDevice::pair() { + if (this->bPaired) { + qCCritical(logDevice) << "Device" << this << "is already paired"; + return; + } + + if (this->bPairing) { + qCCritical(logDevice) << "Device" << this << "is already pairing"; + return; + } + + qCDebug(logDevice) << "Pairing with device" << this; + this->bPairing = true; + + auto reply = this->mInterface->Pair(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + if (reply.isError()) { + qCWarning(logDevice).nospace() + << "Failed to pair with device " << this << ": " << reply.error().message(); + } else { + qCDebug(logDevice) << "Successfully initiated pairing with device" << this; + } + + this->bPairing = false; + delete watcher; + } + ); +} + +void BluetoothDevice::cancelPair() { + if (!this->bPairing) { + qCCritical(logDevice) << "Device" << this << "is not currently pairing"; + return; + } + + qCDebug(logDevice) << "Cancelling pairing with device" << this; + + auto reply = this->mInterface->CancelPairing(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply<> reply = *watcher; + if (reply.isError()) { + qCWarning(logDevice) << "Failed to cancel pairing with device" << this << ":" + << reply.error().message(); + } else { + qCDebug(logDevice) << "Successfully cancelled pairing with device" << this; + } + + this->bPairing = false; + delete watcher; + } + ); +} + +void BluetoothDevice::forget() { + if (!this->mInterface || !this->mInterface->isValid()) { + qCCritical(logDevice) << "Cannot forget - device interface is invalid"; + return; + } + + if (auto* adapter = Bluez::instance()->adapter(this->bAdapterPath.value().path())) { + qCDebug(logDevice) << "Forgetting device" << this << "via adapter" << adapter; + adapter->removeDevice(this->path()); + } else { + qCCritical(logDevice) << "Could not find adapter for path" << this->bAdapterPath.value().path() + << "to forget from"; + } +} + +void BluetoothDevice::addInterface(const QString& interface, const QVariantMap& properties) { + if (interface == "org.bluez.Device1") { + this->properties.updatePropertySet(properties, false); + qCDebug(logDevice) << "Updated Device properties for" << this; + } else if (interface == "org.bluez.Battery1") { + if (!this->mBatteryInterface) { + this->mBatteryInterface = new QDBusInterface( + "org.bluez", + this->path(), + "org.bluez.Battery1", + QDBusConnection::systemBus(), + this + ); + + if (!this->mBatteryInterface->isValid()) { + qCWarning(logDevice) << "Could not create Battery interface for device at" << this; + delete this->mBatteryInterface; + this->mBatteryInterface = nullptr; + return; + } + } + + this->batteryProperties.setInterface(this->mBatteryInterface); + this->batteryProperties.updatePropertySet(properties, false); + + emit this->batteryAvailableChanged(); + qCDebug(logDevice) << "Updated Battery properties for" << this; + } +} + +void BluetoothDevice::removeInterface(const QString& interface) { + if (interface == "org.bluez.Battery1" && this->mBatteryInterface) { + this->batteryProperties.setInterface(nullptr); + delete this->mBatteryInterface; + this->mBatteryInterface = nullptr; + this->bBattery = 0; + + emit this->batteryAvailableChanged(); + qCDebug(logDevice) << "Battery interface removed from device" << this; + } +} + +void BluetoothDevice::onConnectedChanged() { + this->bState = + this->bConnected ? BluetoothDeviceState::Connected : BluetoothDeviceState::Disconnected; + emit this->connectedChanged(); +} + +} // namespace qs::bluetooth + +namespace qs::dbus { + +using namespace qs::bluetooth; + +DBusResult DBusDataTransform::fromWire(quint8 percentage) { + return DBusResult(percentage * 0.01); +} + +} // namespace qs::dbus + +QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device) { + auto saver = QDebugStateSaver(debug); + + if (device) { + debug.nospace() << "BluetoothDevice(" << static_cast(device) + << ", path=" << device->path() << ")"; + } else { + debug << "BluetoothDevice(nullptr)"; + } + + return debug; +} diff --git a/src/bluetooth/device.hpp b/src/bluetooth/device.hpp new file mode 100644 index 0000000..23f230f --- /dev/null +++ b/src/bluetooth/device.hpp @@ -0,0 +1,225 @@ +#pragma once + +#include +#include +#include +#include + +#include "../dbus/properties.hpp" +#include "dbus_device.h" + +namespace qs::bluetooth { + +///! Connection state of a Bluetooth device. +class BluetoothDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device is not connected. + Disconnected = 0, + /// The device is connected. + Connected = 1, + /// The device is disconnecting. + Disconnecting = 2, + /// The device is connecting. + Connecting = 3, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(BluetoothDeviceState::Enum state); +}; + +struct BatteryPercentage {}; + +} // namespace qs::bluetooth + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint8; + using Data = qreal; + static DBusResult fromWire(Wire percentage); +}; + +} // namespace qs::dbus + +namespace qs::bluetooth { + +class BluetoothAdapter; + +///! A tracked Bluetooth device. +class BluetoothDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// MAC address of the device. + Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); + /// The name of the Bluetooth device. This property may be written to create an alias, or set to + /// an empty string to fall back to the device provided name. + /// + /// See @@deviceName for the name provided by the device. + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged); + /// The name of the Bluetooth device, ignoring user provided aliases. See also @@name + /// which returns a user provided alias if set. + Q_PROPERTY(QString deviceName READ default NOTIFY deviceNameChanged BINDABLE bindableDeviceName); + /// System icon representing the device type. Use @@Quickshell.Quickshell.iconPath() to display this in an image. + Q_PROPERTY(QString icon READ default NOTIFY iconChanged BINDABLE bindableIcon); + /// Connection state of the device. + Q_PROPERTY(BluetoothDeviceState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// True if the device is currently connected to the computer. + /// + /// Setting this property is equivalent to calling @@connect() and @@disconnect(). + /// + /// > [!NOTE] @@state provides more detailed information if required. + Q_PROPERTY(bool connected READ connected WRITE setConnected NOTIFY connectedChanged); + /// True if the device is paired to the computer. + /// + /// > [!NOTE] @@pair() can be used to pair a device, however you must @@forget() the device to unpair it. + Q_PROPERTY(bool paired READ default NOTIFY pairedChanged BINDABLE bindablePaired); + /// True if pairing information is stored for future connections. + Q_PROPERTY(bool bonded READ default NOTIFY bondedChanged BINDABLE bindableBonded); + /// True if the device is currently being paired. + /// + /// > [!NOTE] @@cancelPair() can be used to cancel the pairing process. + Q_PROPERTY(bool pairing READ pairing NOTIFY pairingChanged); + /// True if the device is considered to be trusted by the system. + /// Trusted devices are allowed to reconnect themselves to the system without intervention. + Q_PROPERTY(bool trusted READ trusted WRITE setTrusted NOTIFY trustedChanged); + /// True if the device is blocked from connecting. + /// If a device is blocked, any connection attempts will be immediately rejected by the system. + Q_PROPERTY(bool blocked READ blocked WRITE setBlocked NOTIFY blockedChanged); + /// True if the device is allowed to wake up the host system from suspend. + Q_PROPERTY(bool wakeAllowed READ wakeAllowed WRITE setWakeAllowed NOTIFY wakeAllowedChanged); + /// True if the connected device reports its battery level. Battery level can be accessed via @@battery. + Q_PROPERTY(bool batteryAvailable READ batteryAvailable NOTIFY batteryAvailableChanged); + /// Battery level of the connected device, from `0.0` to `1.0`. Only valid if @@batteryAvailable is true. + Q_PROPERTY(qreal battery READ default NOTIFY batteryChanged BINDABLE bindableBattery); + /// The Bluetooth adapter this device belongs to. + Q_PROPERTY(BluetoothAdapter* adapter READ adapter NOTIFY adapterChanged); + /// DBus path of the device under the `org.bluez` system service. + Q_PROPERTY(QString dbusPath READ path CONSTANT); + // clang-format on + +public: + explicit BluetoothDevice(const QString& path, QObject* parent = nullptr); + + /// Attempt to connect to the device. + Q_INVOKABLE void connect(); + /// Disconnect from the device. + Q_INVOKABLE void disconnect(); + /// Attempt to pair the device. + /// + /// > [!NOTE] @@paired and @@pairing return the current pairing status of the device. + Q_INVOKABLE void pair(); + /// Cancel an active pairing attempt. + Q_INVOKABLE void cancelPair(); + /// Forget the device. + Q_INVOKABLE void forget(); + + [[nodiscard]] bool isValid() const { return this->mInterface && this->mInterface->isValid(); } + [[nodiscard]] QString path() const { + return this->mInterface ? this->mInterface->path() : QString(); + } + + [[nodiscard]] bool batteryAvailable() const { return this->mBatteryInterface != nullptr; } + [[nodiscard]] BluetoothAdapter* adapter() const; + [[nodiscard]] QDBusObjectPath adapterPath() const { return this->bAdapterPath.value(); } + + [[nodiscard]] bool connected() const { return this->bConnected; } + void setConnected(bool connected); + + [[nodiscard]] bool trusted() const { return this->bTrusted; } + void setTrusted(bool trusted); + + [[nodiscard]] bool blocked() const { return this->bBlocked; } + void setBlocked(bool blocked); + + [[nodiscard]] QString name() const { return this->bName; } + void setName(const QString& name); + + [[nodiscard]] bool wakeAllowed() const { return this->bWakeAllowed; } + void setWakeAllowed(bool wakeAllowed); + + [[nodiscard]] bool pairing() const { return this->bPairing; } + + [[nodiscard]] QBindable bindableAddress() { return &this->bAddress; } + [[nodiscard]] QBindable bindableDeviceName() { return &this->bDeviceName; } + [[nodiscard]] QBindable bindableName() { return &this->bName; } + [[nodiscard]] QBindable bindableConnected() { return &this->bConnected; } + [[nodiscard]] QBindable bindablePaired() { return &this->bPaired; } + [[nodiscard]] QBindable bindableBonded() { return &this->bBonded; } + [[nodiscard]] QBindable bindableTrusted() { return &this->bTrusted; } + [[nodiscard]] QBindable bindableBlocked() { return &this->bBlocked; } + [[nodiscard]] QBindable bindableWakeAllowed() { return &this->bWakeAllowed; } + [[nodiscard]] QBindable bindableIcon() { return &this->bIcon; } + [[nodiscard]] QBindable bindableBattery() { return &this->bBattery; } + [[nodiscard]] QBindable bindableState() { return &this->bState; } + + void addInterface(const QString& interface, const QVariantMap& properties); + void removeInterface(const QString& interface); + +signals: + void addressChanged(); + void deviceNameChanged(); + void nameChanged(); + void connectedChanged(); + void stateChanged(); + void pairedChanged(); + void bondedChanged(); + void pairingChanged(); + void trustedChanged(); + void blockedChanged(); + void wakeAllowedChanged(); + void iconChanged(); + void batteryAvailableChanged(); + void batteryChanged(); + void adapterChanged(); + +private: + void onConnectedChanged(); + + DBusBluezDeviceInterface* mInterface = nullptr; + QDBusInterface* mBatteryInterface = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bAddress, &BluetoothDevice::addressChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bDeviceName, &BluetoothDevice::deviceNameChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bName, &BluetoothDevice::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bConnected, &BluetoothDevice::onConnectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPaired, &BluetoothDevice::pairedChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBonded, &BluetoothDevice::bondedChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bTrusted, &BluetoothDevice::trustedChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bBlocked, &BluetoothDevice::blockedChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bWakeAllowed, &BluetoothDevice::wakeAllowedChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QString, bIcon, &BluetoothDevice::iconChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, QDBusObjectPath, bAdapterPath); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, qreal, bBattery, &BluetoothDevice::batteryChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, BluetoothDeviceState::Enum, bState, &BluetoothDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(BluetoothDevice, bool, bPairing, &BluetoothDevice::pairingChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, properties); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAddress, bAddress, properties, "Address"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pDeviceName, bDeviceName, properties, "Name"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pName, bName, properties, "Alias"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pConnected, bConnected, properties, "Connected"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pPaired, bPaired, properties, "Paired"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBonded, bBonded, properties, "Bonded"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pTrusted, bTrusted, properties, "Trusted"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pBlocked, bBlocked, properties, "Blocked"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pWakeAllowed, bWakeAllowed, properties, "WakeAllowed"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pIcon, bIcon, properties, "Icon"); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, pAdapterPath, bAdapterPath, properties, "Adapter"); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(BluetoothDevice, batteryProperties); + QS_DBUS_PROPERTY_BINDING(BluetoothDevice, BatteryPercentage, pBattery, bBattery, batteryProperties, "Percentage", true); + // clang-format on +}; + +} // namespace qs::bluetooth + +QDebug operator<<(QDebug debug, const qs::bluetooth::BluetoothDevice* device); diff --git a/src/bluetooth/module.md b/src/bluetooth/module.md new file mode 100644 index 0000000..eb797d9 --- /dev/null +++ b/src/bluetooth/module.md @@ -0,0 +1,12 @@ +name = "Quickshell.Bluetooth" +description = "Bluetooth API" +headers = [ + "bluez.hpp", + "adapter.hpp", + "device.hpp", +] +----- +This module exposes Bluetooth management APIs provided by the BlueZ DBus interface. +Both DBus and BlueZ must be running to use it. + +See the @@Quickshell.Bluetooth.Bluetooth singleton. diff --git a/src/bluetooth/org.bluez.Adapter.xml b/src/bluetooth/org.bluez.Adapter.xml new file mode 100644 index 0000000..286991e --- /dev/null +++ b/src/bluetooth/org.bluez.Adapter.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/bluetooth/org.bluez.Device.xml b/src/bluetooth/org.bluez.Device.xml new file mode 100644 index 0000000..274e9fd --- /dev/null +++ b/src/bluetooth/org.bluez.Device.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/bluetooth/test/manual/test.qml b/src/bluetooth/test/manual/test.qml new file mode 100644 index 0000000..21c53b1 --- /dev/null +++ b/src/bluetooth/test/manual/test.qml @@ -0,0 +1,200 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Bluetooth + +FloatingWindow { + color: contentItem.palette.window + + ListView { + anchors.fill: parent + anchors.margins: 5 + model: Bluetooth.adapters + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 5 + + ColumnLayout { + Label { text: `Adapter: ${modelData.name} (${modelData.adapterId})` } + + RowLayout { + Layout.fillWidth: true + + CheckBox { + text: "Enable" + checked: modelData.enabled + onToggled: modelData.enabled = checked + } + + Label { + color: modelData.state === BluetoothAdapterState.Blocked ? palette.errorText : palette.placeholderText + text: BluetoothAdapterState.toString(modelData.state) + } + + CheckBox { + text: "Discoverable" + checked: modelData.discoverable + onToggled: modelData.discoverable = checked + } + + CheckBox { + text: "Discovering" + checked: modelData.discovering + onToggled: modelData.discovering = checked + } + + CheckBox { + text: "Pairable" + checked: modelData.pairable + onToggled: modelData.pairable = checked + } + } + + RowLayout { + Layout.fillWidth: true + + Label { text: "Discoverable timeout:" } + + SpinBox { + from: 0 + to: 3600 + value: modelData.discoverableTimeout + onValueModified: modelData.discoverableTimeout = value + textFromValue: time => time === 0 ? "∞" : time + "s" + } + + Label { text: "Pairable timeout:" } + + SpinBox { + from: 0 + to: 3600 + value: modelData.pairableTimeout + onValueModified: modelData.pairableTimeout = value + textFromValue: time => time === 0 ? "∞" : time + "s" + } + } + + Repeater { + model: modelData.devices + + WrapperRectangle { + Layout.fillWidth: true + color: palette.button + border.color: palette.mid + border.width: 1 + margin: 5 + + RowLayout { + ColumnLayout { + Layout.fillWidth: true + + RowLayout { + IconImage { + Layout.fillHeight: true + implicitWidth: height + source: Quickshell.iconPath(modelData.icon) + } + + TextField { + text: modelData.name + font.bold: true + background: null + readOnly: false + selectByMouse: true + onEditingFinished: modelData.name = text + } + + Label { + visible: modelData.name && modelData.name !== modelData.deviceName + text: `(${modelData.deviceName})` + color: palette.placeholderText + } + } + + RowLayout { + Label { + text: modelData.address + color: palette.placeholderText + } + + Label { + visible: modelData.batteryAvailable + text: `| Battery: ${Math.round(modelData.battery * 100)}%` + color: palette.placeholderText + } + } + + RowLayout { + Label { + text: BluetoothDeviceState.toString(modelData.state) + + color: modelData.connected ? palette.link : palette.placeholderText + } + + Label { + text: modelData.pairing ? "Pairing" : (modelData.paired ? "Paired" : "Not Paired") + color: modelData.paired || modelData.pairing ? palette.link : palette.placeholderText + } + + Label { + visible: modelData.bonded + text: "| Bonded" + color: palette.link + } + + CheckBox { + text: "Trusted" + checked: modelData.trusted + onToggled: modelData.trusted = checked + } + + CheckBox { + text: "Blocked" + checked: modelData.blocked + onToggled: modelData.blocked = checked + } + + CheckBox { + text: "Wake Allowed" + checked: modelData.wakeAllowed + onToggled: modelData.wakeAllowed = checked + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignRight + + Button { + Layout.alignment: Qt.AlignRight + text: modelData.connected ? "Disconnect" : "Connect" + onClicked: modelData.connected = !modelData.connected + } + + Button { + Layout.alignment: Qt.AlignRight + text: modelData.pairing ? "Cancel" : (modelData.paired ? "Forget" : "Pair") + onClicked: { + if (modelData.pairing) { + modelData.cancelPair(); + } else if (modelData.paired) { + modelData.forget(); + } else { + modelData.pair(); + } + } + } + } + } + } + } + } + } + } +} diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index 9948ea7..fc004f3 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -2,13 +2,24 @@ set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES CLASSNAME DBusPropertiesInterface ) +set_source_files_properties(org.freedesktop.DBus.ObjectManager.xml PROPERTIES + CLASSNAME DBusObjectManagerInterface + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_objectmanager_types.hpp +) + qt_add_dbus_interface(DBUS_INTERFACES org.freedesktop.DBus.Properties.xml dbus_properties ) +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.DBus.ObjectManager.xml + dbus_objectmanager +) + qt_add_library(quickshell-dbus STATIC properties.cpp + objectmanager.cpp bus.cpp ${DBUS_INTERFACES} ) diff --git a/src/dbus/dbus_objectmanager_types.hpp b/src/dbus/dbus_objectmanager_types.hpp new file mode 100644 index 0000000..5e0869c --- /dev/null +++ b/src/dbus/dbus_objectmanager_types.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include +#include +#include + +using DBusObjectManagerInterfaces = QHash; +using DBusObjectManagerObjects = QHash; diff --git a/src/dbus/objectmanager.cpp b/src/dbus/objectmanager.cpp new file mode 100644 index 0000000..d7acb74 --- /dev/null +++ b/src/dbus/objectmanager.cpp @@ -0,0 +1,86 @@ +#include "objectmanager.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_objectmanager.h" +#include "dbus_objectmanager_types.hpp" + +namespace { +Q_LOGGING_CATEGORY(logDbusObjectManager, "quickshell.dbus.objectmanager", QtWarningMsg); +} + +namespace qs::dbus { + +DBusObjectManager::DBusObjectManager(QObject* parent): QObject(parent) { + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); +} + +bool DBusObjectManager::setInterface( + const QString& service, + const QString& path, + const QDBusConnection& connection +) { + delete this->mInterface; + this->mInterface = new DBusObjectManagerInterface(service, path, connection, this); + + if (!this->mInterface->isValid()) { + qCWarning(logDbusObjectManager) << "Failed to create DBusObjectManagerInterface for" << service + << path << ":" << this->mInterface->lastError(); + delete this->mInterface; + this->mInterface = nullptr; + return false; + } + + QObject::connect( + this->mInterface, + &DBusObjectManagerInterface::InterfacesAdded, + this, + &DBusObjectManager::interfacesAdded + ); + + QObject::connect( + this->mInterface, + &DBusObjectManagerInterface::InterfacesRemoved, + this, + &DBusObjectManager::interfacesRemoved + ); + + this->fetchInitialObjects(); + return true; +} + +void DBusObjectManager::fetchInitialObjects() { + if (!this->mInterface) return; + + auto reply = this->mInterface->GetManagedObjects(); + auto* watcher = new QDBusPendingCallWatcher(reply, this); + + QObject::connect( + watcher, + &QDBusPendingCallWatcher::finished, + this, + [this](QDBusPendingCallWatcher* watcher) { + const QDBusPendingReply reply = *watcher; + watcher->deleteLater(); + + if (reply.isError()) { + qCWarning(logDbusObjectManager) << "Failed to get managed objects:" << reply.error(); + return; + } + + for (const auto& [path, interfaces]: reply.value().asKeyValueRange()) { + emit this->interfacesAdded(path, interfaces); + } + } + ); +} + +} // namespace qs::dbus diff --git a/src/dbus/objectmanager.hpp b/src/dbus/objectmanager.hpp new file mode 100644 index 0000000..4246ea2 --- /dev/null +++ b/src/dbus/objectmanager.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +#include "dbus_objectmanager_types.hpp" + +class DBusObjectManagerInterface; + +namespace qs::dbus { + +class DBusObjectManager: public QObject { + Q_OBJECT; + +public: + explicit DBusObjectManager(QObject* parent = nullptr); + + bool setInterface( + const QString& service, + const QString& path, + const QDBusConnection& connection = QDBusConnection::sessionBus() + ); + +signals: + void + interfacesAdded(const QDBusObjectPath& objectPath, const DBusObjectManagerInterfaces& interfaces); + void interfacesRemoved(const QDBusObjectPath& objectPath, const QStringList& interfaces); + +private: + void fetchInitialObjects(); + + DBusObjectManagerInterface* mInterface = nullptr; +}; + +} // namespace qs::dbus \ No newline at end of file diff --git a/src/dbus/org.freedesktop.DBus.ObjectManager.xml b/src/dbus/org.freedesktop.DBus.ObjectManager.xml new file mode 100644 index 0000000..24749f2 --- /dev/null +++ b/src/dbus/org.freedesktop.DBus.ObjectManager.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 52f5006..46528d5 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "dbus_properties.h" @@ -326,3 +327,10 @@ void DBusPropertyGroup::onPropertiesChanged( } } // namespace qs::dbus + +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) +QDebug operator<<(QDebug debug, const QDBusObjectPath& path) { + debug.nospace() << "QDBusObjectPath(" << path.path() << ")"; + return debug; +} +#endif diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 5c26a19..9cfaee9 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "../core/util.hpp" @@ -234,6 +235,7 @@ public: void attachProperty(DBusPropertyCore* property); void updateAllDirect(); void updateAllViaGetAll(); + void updatePropertySet(const QVariantMap& properties, bool complainMissing = true); [[nodiscard]] QString toString() const; [[nodiscard]] bool isConnected() const { return this->interface; } @@ -252,7 +254,6 @@ private slots: ); private: - void updatePropertySet(const QVariantMap& properties, bool complainMissing); void tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) const; [[nodiscard]] QString propertyString(const DBusPropertyCore* property) const; @@ -265,6 +266,10 @@ private: } // namespace qs::dbus +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) +QDebug operator<<(QDebug debug, const QDBusObjectPath& path); +#endif + // NOLINTBEGIN #define QS_DBUS_BINDABLE_PROPERTY_GROUP(Class, name) qs::dbus::DBusPropertyGroup name {this}; diff --git a/src/services/status_notifier/dbus_item_types.cpp b/src/services/status_notifier/dbus_item_types.cpp index c751ca2..6678e94 100644 --- a/src/services/status_notifier/dbus_item_types.cpp +++ b/src/services/status_notifier/dbus_item_types.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include bool DBusSniIconPixmap::operator==(const DBusSniIconPixmap& other) const { @@ -122,10 +121,3 @@ QDebug operator<<(QDebug debug, const DBusSniTooltip& tooltip) { return debug; } - -#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) -QDebug operator<<(QDebug debug, const QDBusObjectPath& path) { - debug.nospace() << "QDBusObjectPath(" << path.path() << ")"; - return debug; -} -#endif diff --git a/src/services/status_notifier/dbus_item_types.hpp b/src/services/status_notifier/dbus_item_types.hpp index e81a2ac..cef38f3 100644 --- a/src/services/status_notifier/dbus_item_types.hpp +++ b/src/services/status_notifier/dbus_item_types.hpp @@ -35,7 +35,3 @@ const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniTooltip& t QDebug operator<<(QDebug debug, const DBusSniIconPixmap& pixmap); QDebug operator<<(QDebug debug, const DBusSniTooltip& tooltip); - -#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) -QDebug operator<<(QDebug debug, const QDBusObjectPath& path); -#endif From 86591f122dbb1611d39bee325a857c0a7a961e22 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 2 Jul 2025 20:16:47 -0700 Subject: [PATCH 026/226] io/process: mask the "QProcess destroyed for running process" warn --- src/io/process.cpp | 19 ++++++++++++++- src/io/process.hpp | 10 +++++++- src/io/test/CMakeLists.txt | 3 ++- src/io/test/process.cpp | 47 ++++++++++++++++++++++++++++++++++++++ src/io/test/process.hpp | 12 ++++++++++ 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/io/test/process.cpp create mode 100644 src/io/test/process.hpp diff --git a/src/io/process.cpp b/src/io/process.cpp index 8aa6c24..1abbdb0 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -27,6 +27,21 @@ Process::Process(QObject* parent): QObject(parent) { ); } +Process::~Process() { + if (this->process != nullptr && this->process->processId() != 0) { + // Deleting after the process finishes hides the process destroyed warning in logs + QObject::connect(this->process, &QProcess::finished, [p = this->process] { delete p; }); + + this->process->setParent(nullptr); + this->process->kill(); + } +} + +void Process::onPostReload() { + this->postReload = true; + this->startProcessIfReady(); +} + bool Process::isRunning() const { return this->process != nullptr; } void Process::setRunning(bool running) { @@ -165,7 +180,9 @@ void Process::setStdinEnabled(bool enabled) { } void Process::startProcessIfReady() { - if (this->process != nullptr || !this->targetRunning || this->mCommand.isEmpty()) return; + if (this->process != nullptr || !this->postReload || !this->targetRunning + || this->mCommand.isEmpty()) + return; this->targetRunning = false; auto& cmd = this->mCommand.first(); diff --git a/src/io/process.hpp b/src/io/process.hpp index c9e983e..b115153 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -11,6 +11,7 @@ #include #include "../core/doc.hpp" +#include "../core/reload.hpp" #include "datastream.hpp" #include "processcore.hpp" @@ -30,7 +31,9 @@ /// } /// } /// ``` -class Process: public QObject { +class Process + : public QObject + , public PostReloadHook { Q_OBJECT; // clang-format off /// If the process is currently running. Defaults to false. @@ -136,6 +139,10 @@ class Process: public QObject { public: explicit Process(QObject* parent = nullptr); + ~Process() override; + Q_DISABLE_COPY_MOVE(Process); + + void onPostReload() override; // MUST be before exec(ctx) or the other will be called with a default constructed obj. QSDOC_HIDE Q_INVOKABLE void exec(QList command); @@ -251,4 +258,5 @@ private: bool targetRunning = false; bool mStdinEnabled = false; bool mClearEnvironment = false; + bool postReload = false; }; diff --git a/src/io/test/CMakeLists.txt b/src/io/test/CMakeLists.txt index 8875566..8b3da6a 100644 --- a/src/io/test/CMakeLists.txt +++ b/src/io/test/CMakeLists.txt @@ -1,7 +1,8 @@ function (qs_test name) add_executable(${name} ${ARGN}) - target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test quickshell-io) + target_link_libraries(${name} PRIVATE Qt::Quick Qt::Network Qt::Test quickshell-io quickshell-core quickshell-window quickshell-ui) add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $) endfunction() qs_test(datastream datastream.cpp ../datastream.cpp) +qs_test(process process.cpp ../process.cpp ../datastream.cpp ../processcore.cpp) diff --git a/src/io/test/process.cpp b/src/io/test/process.cpp new file mode 100644 index 0000000..fc7b834 --- /dev/null +++ b/src/io/test/process.cpp @@ -0,0 +1,47 @@ +#include "process.hpp" + +#include +#include +#include +#include + +#include "../process.hpp" + +void TestProcess::startAfterReload() { + auto process = Process(); + auto startedSpy = QSignalSpy(&process, &Process::started); + auto exitedSpy = QSignalSpy(&process, &Process::exited); + + process.setCommand({"true"}); + process.setRunning(true); + + QVERIFY(!process.isRunning()); + QCOMPARE(startedSpy.count(), 0); + + process.onPostReload(); + + QVERIFY(process.isRunning()); + QVERIFY(startedSpy.wait(100)); +} + +void TestProcess::testExec() { + auto process = Process(); + auto startedSpy = QSignalSpy(&process, &Process::started); + auto exitedSpy = QSignalSpy(&process, &Process::exited); + + process.onPostReload(); + + process.setCommand({"sleep", "30"}); + process.setRunning(true); + + QVERIFY(process.isRunning()); + QVERIFY(startedSpy.wait(100)); + + process.exec({"true"}); + + QVERIFY(exitedSpy.wait(100)); + QVERIFY(startedSpy.wait(100)); + QVERIFY(process.isRunning()); +} + +QTEST_MAIN(TestProcess); diff --git a/src/io/test/process.hpp b/src/io/test/process.hpp new file mode 100644 index 0000000..3525be2 --- /dev/null +++ b/src/io/test/process.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +class TestProcess: public QObject { + Q_OBJECT; + +private slots: + static void startAfterReload(); + static void testExec(); +}; From 0e6518a7061b49693e2d8d8b9a2b787c6221ae61 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 2 Jul 2025 22:47:19 -0700 Subject: [PATCH 027/226] core/command: improve dead instance selection Prints dead instances if they exist, as well as allowing dead instance selection for a substring if no live instances exist. --- src/core/instanceinfo.cpp | 4 +- src/core/instanceinfo.hpp | 2 + src/core/paths.cpp | 22 ++++++----- src/core/paths.hpp | 4 +- src/launch/command.cpp | 73 +++++++++++++++++++++++++++++++------ src/launch/launch.cpp | 1 + src/launch/launch_p.hpp | 1 + src/launch/parsecommand.cpp | 3 ++ 8 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp index 96097c7..7f0132b 100644 --- a/src/core/instanceinfo.cpp +++ b/src/core/instanceinfo.cpp @@ -3,12 +3,12 @@ #include QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.instanceId << info.configPath << info.shellId << info.launchTime; + stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid; return stream; } QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime; + stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid; return stream; } diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index f0fc02a..98ce614 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -3,12 +3,14 @@ #include #include #include +#include struct InstanceInfo { QString instanceId; QString configPath; QString shellId; QDateTime launchTime; + pid_t pid = -1; static InstanceInfo CURRENT; // NOLINT }; diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 689d99e..73ce715 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -367,29 +368,30 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD return true; } -QVector QsPaths::collectInstances(const QString& path, bool fallbackDead) { +QPair, QVector> +QsPaths::collectInstances(const QString& path) { qCDebug(logPaths) << "Collecting instances from" << path; - auto instances = QVector(); + auto liveInstances = QVector(); + auto deadInstances = QVector(); auto dir = QDir(path); InstanceLockInfo info; for (auto& entry: dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { auto path = dir.filePath(entry); - if (QsPaths::checkLock(path, &info, fallbackDead)) { - if (fallbackDead && info.pid != -1) { - fallbackDead = false; - instances.clear(); - } - + if (QsPaths::checkLock(path, &info, true)) { qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid " << info.pid << ") at " << path; - instances.push_back(info); + if (info.pid == -1) { + deadInstances.push_back(info); + } else { + liveInstances.push_back(info); + } } else { qCDebug(logPaths) << "Skipped potential instance at" << path; } } - return instances; + return qMakePair(liveInstances, deadInstances); } diff --git a/src/core/paths.hpp b/src/core/paths.hpp index baaf9b2..9646ca4 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include "instanceinfo.hpp" @@ -22,7 +23,8 @@ public: static QString ipcPath(const QString& id); static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false); - static QVector collectInstances(const QString& path, bool fallbackDead = false); + static QPair, QVector> + collectInstances(const QString& path); QDir* baseRunDir(); QDir* shellRunDir(); diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 1704d9d..64eb076 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -177,14 +178,33 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb } } else if (!cmd.instance.id->isEmpty()) { path = basePath->filePath("by-pid"); - auto instances = QsPaths::collectInstances(path, deadFallback); + auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); - instances.removeIf([&](const InstanceLockInfo& info) { + liveInstances.removeIf([&](const InstanceLockInfo& info) { return !info.instance.instanceId.startsWith(*cmd.instance.id); }); + deadInstances.removeIf([&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*cmd.instance.id); + }); + + auto instances = liveInstances.isEmpty() && deadFallback ? deadInstances : liveInstances; + if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; + if (deadFallback) { + qCInfo(logBare) << "No instances start with" << *cmd.instance.id; + } else { + qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; + + if (!deadInstances.isEmpty()) { + qCInfo(logBare) << "Some dead instances match:"; + + for (auto& instance: deadInstances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + } + } + return -1; } else if (instances.length() != 1) { qCInfo(logBare) << "More than one instance starts with" << *cmd.instance.id; @@ -208,14 +228,29 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb path = QDir(basePath->filePath("by-path")).filePath(pathId); - auto instances = QsPaths::collectInstances(path, deadFallback); + auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + + auto instances = liveInstances; + if (instances.isEmpty() && deadFallback) { + instances = deadInstances; + } + sortInstances( instances, cmd.config.newest || (!instances.empty() && instances.first().pid == -1) ); if (instances.isEmpty()) { - qCInfo(logBare) << "No running instances for" << configFilePath; + if (liveInstances.isEmpty() && deadInstances.length() > 1) { + qCInfo(logBare) << "No running instances for" << configFilePath; + qCInfo(logBare) << "Dead instances:"; + sortInstances(deadInstances, cmd.config.newest); + for (auto& instance: deadInstances) { + qCInfo(logBare).noquote() << " -" << instance.instance.instanceId; + } + } else { + qCInfo(logBare) << "No running instances for" << configFilePath; + } return -1; } @@ -276,7 +311,18 @@ int listInstances(CommandState& cmd) { path = QDir(basePath->filePath("by-path")).filePath(pathId); } - auto instances = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + + sortInstances(liveInstances, cmd.config.newest); + + QList instances; + if (cmd.instance.includeDead) { + sortInstances(deadInstances, cmd.config.newest); + instances = std::move(deadInstances); + instances.append(liveInstances); + } else { + instances = std::move(liveInstances); + } if (instances.isEmpty()) { if (cmd.instance.all) { @@ -286,7 +332,6 @@ int listInstances(CommandState& cmd) { qCInfo(logBare) << "Use --all to list all instances."; } } else { - sortInstances(instances, cmd.config.newest); if (cmd.output.json) { auto array = QJsonArray(); @@ -295,7 +340,7 @@ int listInstances(CommandState& cmd) { auto json = QJsonObject(); json["id"] = instance.instance.instanceId; - json["pid"] = instance.pid; + json["pid"] = instance.instance.pid; json["shell_id"] = instance.instance.shellId; json["config_path"] = instance.instance.configPath; json["launch_time"] = instance.instance.launchTime.toString(Qt::ISODate); @@ -319,12 +364,18 @@ int listInstances(CommandState& cmd) { .arg(remMinutes) .arg(remSeconds); + auto isDead = instance.pid == -1; + auto gray = !cmd.log.noColor && isDead; + qCInfo(logBare).noquote().nospace() - << "Instance " << instance.instance.instanceId << ":\n" - << " Process ID: " << instance.pid << '\n' + << (gray ? "\033[90m" : "") << "Instance " << instance.instance.instanceId + << (isDead ? " (dead)" : "") << ":\n" + << " Process ID: " << instance.instance.pid << '\n' << " Shell ID: " << instance.instance.shellId << '\n' << " Config path: " << instance.instance.configPath << '\n' - << " Launch time: " << launchTimeStr << " (running for " << runtimeStr << ")\n"; + << " Launch time: " << launchTimeStr + << (isDead ? "" : " (running for " + runtimeStr + ")") << '\n' + << (gray ? "\033[0m" : ""); } } } diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index fe28a94..8697667 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -128,6 +128,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio .configPath = args.configPath, .shellId = shellId, .launchTime = qs::Common::LAUNCH_TIME, + .pid = getpid(), }; #if CRASH_REPORTER diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index 7780845..7b8fca6 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -61,6 +61,7 @@ struct CommandState { QStringOption id; pid_t pid = -1; // NOLINT (include) bool all = false; + bool includeDead = false; } instance; struct { diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index e49ded7..fc16086 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -163,6 +163,9 @@ int parseCommand(int argc, char** argv, CommandState& state) { sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); + sub->add_flag("--show-dead", state.instance.includeDead) + ->description("Include dead instances in the list."); + addConfigSelection(sub, true)->excludes(all); addLoggingOptions(sub, false, true); From 9708d8212a90ebd1436063099145914330820c7d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 4 Jul 2025 15:58:41 -0700 Subject: [PATCH 028/226] core/reloader: trigger onPostReload if launched post-reload This is similar to the check in Reloadable, and fixes a number of hard to debug issues with Process, IpcHandler, NotificationServer, and GlobalShortcut not working depending on where you put them in a QML file. --- src/core/reload.cpp | 21 ++++++++++++------- src/core/reload.hpp | 17 ++++++++------- src/io/ipchandler.hpp | 6 ++---- src/io/process.cpp | 11 +++++----- src/io/process.hpp | 5 +---- src/io/test/process.cpp | 4 ++-- src/services/notifications/qml.hpp | 4 +--- src/wayland/hyprland/global_shortcuts/qml.hpp | 6 ++---- 8 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 25ab33f..0bdf8fc 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -126,12 +126,17 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId return nullptr; } -void PostReloadHook::postReloadTree(QObject* root) { - for (auto* child: root->children()) { - PostReloadHook::postReloadTree(child); - } - - if (auto* self = dynamic_cast(root)) { - self->onPostReload(); - } +void PostReloadHook::componentComplete() { + auto* engineGeneration = EngineGeneration::findObjectGeneration(this); + if (!engineGeneration || engineGeneration->reloadComplete) this->postReload(); +} + +void PostReloadHook::postReload() { + this->isPostReload = true; + this->onPostReload(); +} + +void PostReloadHook::postReloadTree(QObject* root) { + for (auto* child: root->children()) PostReloadHook::postReloadTree(child); + if (auto* self = dynamic_cast(root)) self->postReload(); } diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 560c8bd..1d4e375 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -119,16 +119,19 @@ private: }; /// Hook that runs after the old widget tree is dropped during a reload. -class PostReloadHook { +class PostReloadHook + : public QObject + , public QQmlParserStatus { public: - PostReloadHook() = default; - virtual ~PostReloadHook() = default; - PostReloadHook(PostReloadHook&&) = default; - PostReloadHook(const PostReloadHook&) = default; - PostReloadHook& operator=(PostReloadHook&&) = default; - PostReloadHook& operator=(const PostReloadHook&) = default; + PostReloadHook(QObject* parent = nullptr): QObject(parent) {} + void classBegin() override {} + void componentComplete() override; + void postReload(); virtual void onPostReload() = 0; static void postReloadTree(QObject* root); + +protected: + bool isPostReload = false; }; diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index e6b24ba..1da3e71 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -154,9 +154,7 @@ class IpcHandlerRegistry; /// #### Properties /// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are /// of an IPC compatible type. See the table above for compatible types. -class IpcHandler - : public QObject - , public PostReloadHook { +class IpcHandler: public PostReloadHook { Q_OBJECT; /// If the handler should be able to receive calls. Defaults to true. Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); @@ -166,7 +164,7 @@ class IpcHandler QML_ELEMENT; public: - explicit IpcHandler(QObject* parent = nullptr): QObject(parent) {}; + explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}; ~IpcHandler() override; Q_DISABLE_COPY_MOVE(IpcHandler); diff --git a/src/io/process.cpp b/src/io/process.cpp index 1abbdb0..c8250c7 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -15,10 +15,11 @@ #include "../core/generation.hpp" #include "../core/qmlglobal.hpp" +#include "../core/reload.hpp" #include "datastream.hpp" #include "processcore.hpp" -Process::Process(QObject* parent): QObject(parent) { +Process::Process(QObject* parent): PostReloadHook(parent) { QObject::connect( QuickshellSettings::instance(), &QuickshellSettings::workingDirectoryChanged, @@ -37,10 +38,7 @@ Process::~Process() { } } -void Process::onPostReload() { - this->postReload = true; - this->startProcessIfReady(); -} +void Process::onPostReload() { this->startProcessIfReady(); } bool Process::isRunning() const { return this->process != nullptr; } @@ -180,9 +178,10 @@ void Process::setStdinEnabled(bool enabled) { } void Process::startProcessIfReady() { - if (this->process != nullptr || !this->postReload || !this->targetRunning + if (this->process != nullptr || !this->isPostReload || !this->targetRunning || this->mCommand.isEmpty()) return; + this->targetRunning = false; auto& cmd = this->mCommand.first(); diff --git a/src/io/process.hpp b/src/io/process.hpp index b115153..ab8763e 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -31,9 +31,7 @@ /// } /// } /// ``` -class Process - : public QObject - , public PostReloadHook { +class Process: public PostReloadHook { Q_OBJECT; // clang-format off /// If the process is currently running. Defaults to false. @@ -258,5 +256,4 @@ private: bool targetRunning = false; bool mStdinEnabled = false; bool mClearEnvironment = false; - bool postReload = false; }; diff --git a/src/io/test/process.cpp b/src/io/test/process.cpp index fc7b834..09fc9f7 100644 --- a/src/io/test/process.cpp +++ b/src/io/test/process.cpp @@ -18,7 +18,7 @@ void TestProcess::startAfterReload() { QVERIFY(!process.isRunning()); QCOMPARE(startedSpy.count(), 0); - process.onPostReload(); + process.postReload(); QVERIFY(process.isRunning()); QVERIFY(startedSpy.wait(100)); @@ -29,7 +29,7 @@ void TestProcess::testExec() { auto startedSpy = QSignalSpy(&process, &Process::started); auto exitedSpy = QSignalSpy(&process, &Process::exited); - process.onPostReload(); + process.postReload(); process.setCommand({"sleep", "30"}); process.setRunning(true); diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index d750210..feb33db 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -21,9 +21,7 @@ namespace qs::service::notifications { /// The server does not advertise most capabilities by default. See the individual properties for details. /// /// [Desktop Notifications Specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html -class NotificationServerQml - : public QObject - , public PostReloadHook { +class NotificationServerQml: public PostReloadHook { Q_OBJECT; // clang-format off /// If notifications should be re-emitted when quickshell reloads. Defaults to true. diff --git a/src/wayland/hyprland/global_shortcuts/qml.hpp b/src/wayland/hyprland/global_shortcuts/qml.hpp index a43d963..f257632 100644 --- a/src/wayland/hyprland/global_shortcuts/qml.hpp +++ b/src/wayland/hyprland/global_shortcuts/qml.hpp @@ -32,9 +32,7 @@ namespace qs::hyprland::global_shortcuts { /// /// [hyprland_global_shortcuts_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml /// [the wiki]: https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts -class GlobalShortcut - : public QObject - , public PostReloadHook { +class GlobalShortcut: public PostReloadHook { using ShortcutImpl = impl::GlobalShortcut; Q_OBJECT; @@ -59,7 +57,7 @@ class GlobalShortcut QML_ELEMENT; public: - explicit GlobalShortcut(QObject* parent = nullptr): QObject(parent) {} + explicit GlobalShortcut(QObject* parent = nullptr): PostReloadHook(parent) {} ~GlobalShortcut() override; Q_DISABLE_COPY_MOVE(GlobalShortcut); From fb37be7611535706025ea1ca1cb742b97777b80f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 4 Jul 2025 16:43:01 -0700 Subject: [PATCH 029/226] core/log: ignore on-disk logging configs for quickshell* rules. Fixes fedora hiding all command output by default. --- src/core/logging.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 6fe80ca..d9f3f57 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -182,17 +182,18 @@ void LogManager::filterCategory(QLoggingCategory* category) { auto categoryName = QLatin1StringView(category->categoryName()); auto isQs = categoryName.startsWith(QLatin1StringView("quickshell.")); - if (instance->lastCategoryFilter) { - instance->lastCategoryFilter(category); - } - - auto filter = CategoryFilter(category); + CategoryFilter filter; + // We don't respect log filters for qs logs because some distros like to ship + // default configs that hide everything. QT_LOGGING_RULES is considered via the filter list. if (isQs) { - filter.debug = filter.debug || instance->mDefaultLevel == QtDebugMsg; - filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg; - filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg; - filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg; + filter.debug = instance->mDefaultLevel == QtDebugMsg; + filter.info = instance->mDefaultLevel == QtInfoMsg; + filter.warn = instance->mDefaultLevel == QtWarningMsg; + filter.critical = instance->mDefaultLevel == QtCriticalMsg; + } else if (instance->lastCategoryFilter) { + instance->lastCategoryFilter(category); + filter = CategoryFilter(category); } for (const auto& rule: *instance->rules) { @@ -235,8 +236,12 @@ void LogManager::init( { QLoggingSettingsParser parser; - parser.setContent(rules); + // Load QT_LOGGING_RULES because we ignore the last category filter for QS messages + // due to disk config files. + parser.setContent(qEnvironmentVariable("QT_LOGGING_RULES")); instance->rules = new QList(parser.rules()); + parser.setContent(rules); + instance->rules->append(parser.rules()); } qInstallMessageHandler(&LogManager::messageHandler); From 3cc7ced3a0eae1905e558372671d3c079d3ad938 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 4 Jul 2025 17:58:55 -0700 Subject: [PATCH 030/226] core/window: fix QsWindow being null for WlrLayershell --- src/window/proxywindow.cpp | 3 ++- src/window/proxywindow.hpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index df9b6b3..81f4334 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -542,7 +542,8 @@ void ProxyWindowAttached::updateWindow() { void ProxyWindowAttached::setWindow(ProxyWindowBase* window) { if (window == this->mWindow) return; this->mWindow = window; - this->mWindowInterface = window ? qobject_cast(window->parent()) : nullptr; + auto* parentInterface = window ? qobject_cast(window->parent()) : nullptr; + this->mWindowInterface = parentInterface ? static_cast(parentInterface) : window; emit this->windowChanged(); } diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 4203941..1d07516 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -220,7 +220,7 @@ protected: private: ProxyWindowBase* mWindow = nullptr; - WindowInterface* mWindowInterface = nullptr; + QObject* mWindowInterface = nullptr; void setWindow(ProxyWindowBase* window); }; From 7eff415b252c8c8deb7aff5030020c54ba7ce651 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 4 Jul 2025 20:06:22 -0700 Subject: [PATCH 031/226] core/qmlglobal: re-add shellRoot as a deprecated property --- src/core/qmlglobal.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 82442ce..d05b96d 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -109,6 +109,8 @@ class QuickshellGlobal: public QObject { /// The root directory is the folder containing the entrypoint to your shell, often referred /// to as `shell.qml`. Q_PROPERTY(QString configDir READ configDir CONSTANT); + /// > [!WARNING] Deprecated: Returns @@configDir. + Q_PROPERTY(QString shellRoot READ configDir CONSTANT); /// Quickshell's working directory. Defaults to whereever quickshell was launched from. Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); /// If true then the configuration will be reloaded whenever any files change. From 87d99b866f9866db10b55c8b03632f0eecae52d8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 4 Jul 2025 20:29:50 -0700 Subject: [PATCH 032/226] services/pipewire: destroy bound audio object when node is destroyed Fixes a leak and prevents a UAF via device volume signals derefing the freed node. Fixes #91 --- src/services/pipewire/node.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index ed65fca..3c7af1d 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -252,7 +252,7 @@ void PwNode::onParam( } } -PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): node(node) { +PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): QObject(node), node(node) { if (node->device) { QObject::connect(node->device, &PwDevice::deviceReady, this, &PwNodeBoundAudio::onDeviceReady); From 5d7e07508ae3e5487edb1ac5a152120434f091d5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 7 Jul 2025 02:21:50 -0700 Subject: [PATCH 033/226] bluetooth: fix defaultAdapter reactivity Fixes #100 --- src/bluetooth/bluez.cpp | 12 +++++++++--- src/bluetooth/bluez.hpp | 20 +++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/bluetooth/bluez.cpp b/src/bluetooth/bluez.cpp index 3c8bf94..6ff2e30 100644 --- a/src/bluetooth/bluez.cpp +++ b/src/bluetooth/bluez.cpp @@ -26,6 +26,11 @@ Bluez* Bluez::instance() { Bluez::Bluez() { this->init(); } +void Bluez::updateDefaultAdapter() { + const auto& adapters = this->mAdapters.valueList(); + this->bDefaultAdapter = adapters.empty() ? nullptr : adapters.first(); +} + void Bluez::init() { qCDebug(logBluetooth) << "Connecting to BlueZ"; @@ -95,6 +100,7 @@ void Bluez::onInterfacesAdded( this->mAdapterMap.insert(path.path(), adapter); this->mAdapters.insertObject(adapter); + this->updateDefaultAdapter(); } else if (interfaces.contains("org.bluez.Device1")) { auto* device = new BluetoothDevice(path.path(), this); @@ -127,6 +133,7 @@ void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& this->mAdapterMap.remove(path.path()); this->mAdapters.removeObject(adapter); + this->updateDefaultAdapter(); delete adapter; } } else if (auto* device = this->mDeviceMap.value(path.path())) { @@ -148,9 +155,8 @@ void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& } } -BluetoothAdapter* Bluez::defaultAdapter() const { - const auto& adapters = this->mAdapters.valueList(); - return adapters.isEmpty() ? nullptr : adapters.first(); +BluezQml::BluezQml() { + QObject::connect(Bluez::instance(), &Bluez::defaultAdapterChanged, this, &BluezQml::defaultAdapterChanged); } } // namespace qs::bluetooth diff --git a/src/bluetooth/bluez.hpp b/src/bluetooth/bluez.hpp index d888e8f..a0c1267 100644 --- a/src/bluetooth/bluez.hpp +++ b/src/bluetooth/bluez.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -22,7 +23,6 @@ class Bluez: public QObject { public: [[nodiscard]] ObjectModel* adapters() { return &this->mAdapters; } [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } - [[nodiscard]] BluetoothAdapter* defaultAdapter() const; [[nodiscard]] BluetoothAdapter* adapter(const QString& path) { return this->mAdapterMap.value(path); @@ -30,10 +30,14 @@ public: static Bluez* instance(); +signals: + void defaultAdapterChanged(); + private slots: void onInterfacesAdded(const QDBusObjectPath& path, const DBusObjectManagerInterfaces& interfaces); void onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& interfaces); + void updateDefaultAdapter(); private: explicit Bluez(); @@ -44,6 +48,9 @@ private: QHash mDeviceMap; ObjectModel mAdapters {this}; ObjectModel mDevices {this}; + +public: + Q_OBJECT_BINDABLE_PROPERTY(Bluez, BluetoothAdapter*, bDefaultAdapter, &Bluez::defaultAdapterChanged); }; ///! Bluetooth manager @@ -51,7 +58,7 @@ private: class BluezQml: public QObject { Q_OBJECT; /// The default bluetooth adapter. Usually there is only one. - Q_PROPERTY(BluetoothAdapter* defaultAdapter READ defaultAdapter CONSTANT); + Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter); QSDOC_TYPE_OVERRIDE(ObjectModel*); /// A list of all bluetooth adapters. See @@defaultAdapter for the default. Q_PROPERTY(UntypedObjectModel* adapters READ adapters CONSTANT); @@ -62,8 +69,11 @@ class BluezQml: public QObject { QML_NAMED_ELEMENT(Bluetooth); QML_SINGLETON; +signals: + void defaultAdapterChanged(); + public: - explicit BluezQml(QObject* parent = nullptr): QObject(parent) {} + explicit BluezQml(); [[nodiscard]] static ObjectModel* adapters() { return Bluez::instance()->adapters(); @@ -73,8 +83,8 @@ public: return Bluez::instance()->devices(); } - [[nodiscard]] static BluetoothAdapter* defaultAdapter() { - return Bluez::instance()->defaultAdapter(); + [[nodiscard]] static QBindable bindableDefaultAdapter() { + return &Bluez::instance()->bDefaultAdapter; } }; From 3d594e16dd3850973336c70014a948dc97837d39 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 8 Jul 2025 13:39:34 -0700 Subject: [PATCH 034/226] core/log: track default logging categories Fixes a bug in fb37be7 which ignored default logging categories due to skipping QLoggingRegistry's filter. --- src/bluetooth/adapter.cpp | 3 +- src/bluetooth/bluez.cpp | 10 +++++-- src/bluetooth/bluez.hpp | 13 +++++++-- src/bluetooth/device.cpp | 3 +- src/core/colorquantizer.cpp | 4 ++- src/core/desktopentry.cpp | 3 +- src/core/generation.cpp | 3 +- src/core/incubator.cpp | 5 ++-- src/core/incubator.hpp | 5 ++-- src/core/logcat.hpp | 28 +++++++++++++++++++ src/core/logging.cpp | 23 +++++++++++---- src/core/logging.hpp | 8 +++++- src/core/logging_qtprivate.cpp | 3 +- src/core/logging_qtprivate.hpp | 4 ++- src/core/paths.cpp | 3 +- src/core/qsintercept.cpp | 4 ++- src/core/qsintercept.hpp | 4 ++- src/core/scan.cpp | 4 ++- src/core/scan.hpp | 4 ++- src/crash/handler.cpp | 3 +- src/crash/main.cpp | 3 +- src/dbus/bus.cpp | 4 ++- src/dbus/dbusmenu/dbusmenu.cpp | 3 +- src/dbus/dbusmenu/dbusmenu.hpp | 2 +- src/dbus/objectmanager.cpp | 3 +- src/dbus/properties.cpp | 3 +- src/dbus/properties.hpp | 3 +- src/debug/lint.cpp | 4 ++- src/io/fileview.cpp | 3 +- src/io/socket.cpp | 3 +- src/io/socket.hpp | 3 +- src/ipc/ipc.cpp | 3 +- src/ipc/ipc.hpp | 4 ++- src/services/greetd/connection.cpp | 3 +- src/services/mpris/player.cpp | 3 +- src/services/mpris/watcher.cpp | 3 +- src/services/notifications/dbusimage.cpp | 4 ++- src/services/notifications/notification.cpp | 3 +- src/services/notifications/server.cpp | 3 +- src/services/pam/conversation.cpp | 3 +- src/services/pam/conversation.hpp | 3 +- src/services/pipewire/core.cpp | 4 ++- src/services/pipewire/defaults.cpp | 3 +- src/services/pipewire/device.cpp | 3 +- src/services/pipewire/link.cpp | 3 +- src/services/pipewire/metadata.cpp | 3 +- src/services/pipewire/node.cpp | 3 +- src/services/pipewire/registry.cpp | 3 +- src/services/pipewire/registry.hpp | 3 +- src/services/status_notifier/host.cpp | 3 +- src/services/status_notifier/host.hpp | 3 +- src/services/status_notifier/item.cpp | 3 +- src/services/status_notifier/item.hpp | 3 +- src/services/status_notifier/watcher.cpp | 4 ++- src/services/status_notifier/watcher.hpp | 4 ++- src/services/upower/core.cpp | 3 +- src/services/upower/device.cpp | 3 +- src/services/upower/powerprofiles.cpp | 3 +- src/wayland/buffer/dmabuf.cpp | 3 +- src/wayland/buffer/manager.cpp | 3 +- src/wayland/buffer/shm.cpp | 3 +- src/wayland/hyprland/ipc/connection.cpp | 5 ++-- .../hyprland_screencopy.cpp | 3 +- .../image_copy_capture/image_copy_capture.cpp | 3 +- .../wlr_screencopy/wlr_screencopy.cpp | 3 +- src/wayland/toplevel_management/manager.cpp | 3 +- src/wayland/toplevel_management/manager.hpp | 3 +- src/x11/i3/ipc/connection.cpp | 5 ++-- 68 files changed, 212 insertions(+), 79 deletions(-) create mode 100644 src/core/logcat.hpp diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp index e24b13a..92ab837 100644 --- a/src/bluetooth/adapter.cpp +++ b/src/bluetooth/adapter.cpp @@ -12,13 +12,14 @@ #include #include +#include "../core/logcat.hpp" #include "../dbus/properties.hpp" #include "dbus_adapter.h" namespace qs::bluetooth { namespace { -Q_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg); +QS_LOGGING_CATEGORY(logAdapter, "quickshell.bluetooth.adapter", QtWarningMsg); } QString BluetoothAdapterState::toString(BluetoothAdapterState::Enum state) { diff --git a/src/bluetooth/bluez.cpp b/src/bluetooth/bluez.cpp index 6ff2e30..f2c4300 100644 --- a/src/bluetooth/bluez.cpp +++ b/src/bluetooth/bluez.cpp @@ -8,6 +8,7 @@ #include #include +#include "../core/logcat.hpp" #include "../dbus/dbus_objectmanager_types.hpp" #include "../dbus/objectmanager.hpp" #include "adapter.hpp" @@ -16,7 +17,7 @@ namespace qs::bluetooth { namespace { -Q_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg); +QS_LOGGING_CATEGORY(logBluetooth, "quickshell.bluetooth", QtWarningMsg); } Bluez* Bluez::instance() { @@ -156,7 +157,12 @@ void Bluez::onInterfacesRemoved(const QDBusObjectPath& path, const QStringList& } BluezQml::BluezQml() { - QObject::connect(Bluez::instance(), &Bluez::defaultAdapterChanged, this, &BluezQml::defaultAdapterChanged); + QObject::connect( + Bluez::instance(), + &Bluez::defaultAdapterChanged, + this, + &BluezQml::defaultAdapterChanged + ); } } // namespace qs::bluetooth diff --git a/src/bluetooth/bluez.hpp b/src/bluetooth/bluez.hpp index a0c1267..9d7c93c 100644 --- a/src/bluetooth/bluez.hpp +++ b/src/bluetooth/bluez.hpp @@ -50,13 +50,21 @@ private: ObjectModel mDevices {this}; public: - Q_OBJECT_BINDABLE_PROPERTY(Bluez, BluetoothAdapter*, bDefaultAdapter, &Bluez::defaultAdapterChanged); + Q_OBJECT_BINDABLE_PROPERTY( + Bluez, + BluetoothAdapter*, + bDefaultAdapter, + &Bluez::defaultAdapterChanged + ); }; ///! Bluetooth manager /// Provides access to bluetooth devices and adapters. class BluezQml: public QObject { Q_OBJECT; + QML_NAMED_ELEMENT(Bluetooth); + QML_SINGLETON; + // clang-format off /// The default bluetooth adapter. Usually there is only one. Q_PROPERTY(BluetoothAdapter* defaultAdapter READ default NOTIFY defaultAdapterChanged BINDABLE bindableDefaultAdapter); QSDOC_TYPE_OVERRIDE(ObjectModel*); @@ -66,8 +74,7 @@ class BluezQml: public QObject { /// A list of all connected bluetooth devices across all adapters. /// See @@BluetoothAdapter.devices for the devices connected to a single adapter. Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); - QML_NAMED_ELEMENT(Bluetooth); - QML_SINGLETON; + // clang-format on signals: void defaultAdapterChanged(); diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp index 30008a3..7265b24 100644 --- a/src/bluetooth/device.cpp +++ b/src/bluetooth/device.cpp @@ -12,6 +12,7 @@ #include #include +#include "../core/logcat.hpp" #include "../dbus/properties.hpp" #include "adapter.hpp" #include "bluez.hpp" @@ -20,7 +21,7 @@ namespace qs::bluetooth { namespace { -Q_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg); +QS_LOGGING_CATEGORY(logDevice, "quickshell.bluetooth.device", QtWarningMsg); } QString BluetoothDeviceState::toString(BluetoothDeviceState::Enum state) { diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp index 667942c..9f443b8 100644 --- a/src/core/colorquantizer.cpp +++ b/src/core/colorquantizer.cpp @@ -18,8 +18,10 @@ #include #include +#include "logcat.hpp" + namespace { -Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg); +QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg); } ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 541207e..3c4b6f2 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -17,10 +17,11 @@ #include #include "common.hpp" +#include "logcat.hpp" #include "model.hpp" namespace { -Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); +QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); } struct Locale { diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 1189ab7..d99409b 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -21,13 +21,14 @@ #include "iconimageprovider.hpp" #include "imageprovider.hpp" #include "incubator.hpp" +#include "logcat.hpp" #include "plugin.hpp" #include "qsintercept.hpp" #include "reload.hpp" #include "scan.hpp" namespace { -Q_LOGGING_CATEGORY(logScene, "scene"); +QS_LOGGING_CATEGORY(logScene, "scene"); } static QHash g_generations; // NOLINT diff --git a/src/core/incubator.cpp b/src/core/incubator.cpp index c43703a..c9d149a 100644 --- a/src/core/incubator.cpp +++ b/src/core/incubator.cpp @@ -1,11 +1,12 @@ #include "incubator.hpp" #include -#include #include #include -Q_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg); +#include "logcat.hpp" + +QS_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg); void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { switch (status) { diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp index 5928ffe..5ebb9a0 100644 --- a/src/core/incubator.hpp +++ b/src/core/incubator.hpp @@ -1,11 +1,12 @@ #pragma once -#include #include #include #include -Q_DECLARE_LOGGING_CATEGORY(logIncubator); +#include "logcat.hpp" + +QS_DECLARE_LOGGING_CATEGORY(logIncubator); class QsQmlIncubator : public QObject diff --git a/src/core/logcat.hpp b/src/core/logcat.hpp new file mode 100644 index 0000000..9650ddb --- /dev/null +++ b/src/core/logcat.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace qs::log { +void initLogCategoryLevel(const char* name, QtMsgType defaultLevel = QtDebugMsg); +} + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define QS_DECLARE_LOGGING_CATEGORY(name) \ + namespace qslogcat { \ + Q_DECLARE_LOGGING_CATEGORY(name); \ + } \ + const QLoggingCategory& name() + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define QS_LOGGING_CATEGORY(name, category, ...) \ + namespace qslogcat { \ + Q_LOGGING_CATEGORY(name, category __VA_OPT__(, __VA_ARGS__)); \ + } \ + const QLoggingCategory& name() { \ + static auto* init = []() { \ + qs::log::initLogCategoryLevel(category __VA_OPT__(, __VA_ARGS__)); \ + return &qslogcat::name; \ + }(); \ + return (init) (); \ + } diff --git a/src/core/logging.cpp b/src/core/logging.cpp index d9f3f57..7f95e46 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -30,17 +30,18 @@ #include #include "instanceinfo.hpp" +#include "logcat.hpp" #include "logging_p.hpp" #include "logging_qtprivate.cpp" // NOLINT #include "paths.hpp" #include "ringbuf.hpp" -Q_LOGGING_CATEGORY(logBare, "quickshell.bare"); +QS_LOGGING_CATEGORY(logBare, "quickshell.bare"); namespace qs::log { using namespace qt_logging_registry; -Q_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); +QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); bool LogMessage::operator==(const LogMessage& other) const { // note: not including time @@ -187,10 +188,16 @@ void LogManager::filterCategory(QLoggingCategory* category) { // We don't respect log filters for qs logs because some distros like to ship // default configs that hide everything. QT_LOGGING_RULES is considered via the filter list. if (isQs) { - filter.debug = instance->mDefaultLevel == QtDebugMsg; - filter.info = instance->mDefaultLevel == QtInfoMsg; - filter.warn = instance->mDefaultLevel == QtWarningMsg; - filter.critical = instance->mDefaultLevel == QtCriticalMsg; + // QtDebugMsg == 0, so default + auto defaultLevel = instance->defaultLevels.value(categoryName); + + filter = CategoryFilter(); + // clang-format off + filter.debug = instance->mDefaultLevel == QtDebugMsg || defaultLevel == QtDebugMsg; + filter.info = filter.debug || instance->mDefaultLevel == QtInfoMsg || defaultLevel == QtInfoMsg; + filter.warn = filter.info || instance->mDefaultLevel == QtWarningMsg || defaultLevel == QtWarningMsg; + filter.critical = filter.warn || instance->mDefaultLevel == QtCriticalMsg || defaultLevel == QtCriticalMsg; + // clang-format on } else if (instance->lastCategoryFilter) { instance->lastCategoryFilter(category); filter = CategoryFilter(category); @@ -262,6 +269,10 @@ void LogManager::init( qCDebug(logLogging) << "Logger initialized."; } +void initLogCategoryLevel(const char* name, QtMsgType defaultLevel) { + LogManager::instance()->defaultLevels.insert(QLatin1StringView(name), defaultLevel); +} + void LogManager::initFs() { QMetaObject::invokeMethod( &LogManager::instance()->threadProxy, diff --git a/src/core/logging.hpp b/src/core/logging.hpp index 7ff1b5e..bf81133 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -12,7 +13,9 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(logBare); +#include "logcat.hpp" + +QS_DECLARE_LOGGING_CATEGORY(logBare); namespace qs::log { @@ -127,11 +130,14 @@ private: QString mRulesString; QList* rules = nullptr; QtMsgType mDefaultLevel = QtWarningMsg; + QHash defaultLevels; QHash sparseFilters; QHash allFilters; QTextStream stdoutStream; LoggingThreadProxy threadProxy; + + friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel); }; bool readEncodedLogs( diff --git a/src/core/logging_qtprivate.cpp b/src/core/logging_qtprivate.cpp index 5078eeb..48f74de 100644 --- a/src/core/logging_qtprivate.cpp +++ b/src/core/logging_qtprivate.cpp @@ -16,10 +16,11 @@ #include #include +#include "logcat.hpp" #include "logging_qtprivate.hpp" namespace qs::log { -Q_DECLARE_LOGGING_CATEGORY(logLogging); +QS_DECLARE_LOGGING_CATEGORY(logLogging); namespace qt_logging_registry { diff --git a/src/core/logging_qtprivate.hpp b/src/core/logging_qtprivate.hpp index 83c8258..61d3a7c 100644 --- a/src/core/logging_qtprivate.hpp +++ b/src/core/logging_qtprivate.hpp @@ -12,8 +12,10 @@ #include #include +#include "logcat.hpp" + namespace qs::log { -Q_DECLARE_LOGGING_CATEGORY(logLogging); +QS_DECLARE_LOGGING_CATEGORY(logLogging); namespace qt_logging_registry { diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 73ce715..2c341bb 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -16,9 +16,10 @@ #include #include "instanceinfo.hpp" +#include "logcat.hpp" namespace { -Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); +QS_LOGGING_CATEGORY(logPaths, "quickshell.paths"); } QsPaths* QsPaths::instance() { diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index 12ca118..560331d 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -14,7 +14,9 @@ #include #include -Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); +#include "logcat.hpp" + +QS_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); QUrl QsUrlInterceptor::intercept( const QUrl& originalUrl, diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index 8113749..f0e1098 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -10,7 +10,9 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(logQsIntercept); +#include "logcat.hpp" + +QS_DECLARE_LOGGING_CATEGORY(logQsIntercept); class QsUrlInterceptor: public QQmlAbstractUrlInterceptor { public: diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 8d6362e..90b19b5 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -15,7 +15,9 @@ #include #include -Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); +#include "logcat.hpp" + +QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); void QmlScanner::scanDir(const QString& path) { if (this->scannedDirs.contains(path)) return; diff --git a/src/core/scan.hpp b/src/core/scan.hpp index d8fb500..80b44ca 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -6,7 +6,9 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(logQmlScanner); +#include "logcat.hpp" + +QS_DECLARE_LOGGING_CATEGORY(logQmlScanner); // expects canonical paths class QmlScanner { diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 8d9a8a7..1433a87 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -15,6 +15,7 @@ #include #include "../core/instanceinfo.hpp" +#include "../core/logcat.hpp" extern char** environ; // NOLINT @@ -23,7 +24,7 @@ using namespace google_breakpad; namespace qs::crash { namespace { -Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); +QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); } struct CrashHandlerPrivate { diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 7c3bad7..fb53a56 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -17,6 +17,7 @@ #include #include "../core/instanceinfo.hpp" +#include "../core/logcat.hpp" #include "../core/logging.hpp" #include "../core/paths.hpp" #include "build.hpp" @@ -24,7 +25,7 @@ namespace { -Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); +QS_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); int tryDup(int fd, const QString& path) { QFile sourceFile; diff --git a/src/dbus/bus.cpp b/src/dbus/bus.cpp index dc6d21b..d53c4c6 100644 --- a/src/dbus/bus.cpp +++ b/src/dbus/bus.cpp @@ -12,10 +12,12 @@ #include #include +#include "../core/logcat.hpp" + namespace qs::dbus { namespace { -Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); +QS_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); } void tryLaunchService( diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 2b633b7..c0b4386 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -21,13 +21,14 @@ #include #include "../../core/iconimageprovider.hpp" +#include "../../core/logcat.hpp" #include "../../core/model.hpp" #include "../../core/qsmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_menu.h" #include "dbus_menu_types.hpp" -Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg); +QS_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg); using namespace qs::menu; diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 1a8b399..1192baa 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -20,7 +20,7 @@ #include "../properties.hpp" #include "dbus_menu_types.hpp" -Q_DECLARE_LOGGING_CATEGORY(logDbusMenu); +QS_DECLARE_LOGGING_CATEGORY(logDbusMenu); class DBusMenuInterface; diff --git a/src/dbus/objectmanager.cpp b/src/dbus/objectmanager.cpp index d7acb74..258f6fe 100644 --- a/src/dbus/objectmanager.cpp +++ b/src/dbus/objectmanager.cpp @@ -9,11 +9,12 @@ #include #include +#include "../core/logcat.hpp" #include "dbus_objectmanager.h" #include "dbus_objectmanager_types.hpp" namespace { -Q_LOGGING_CATEGORY(logDbusObjectManager, "quickshell.dbus.objectmanager", QtWarningMsg); +QS_LOGGING_CATEGORY(logDbusObjectManager, "quickshell.dbus.objectmanager", QtWarningMsg); } namespace qs::dbus { diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 46528d5..81f26d2 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -20,9 +20,10 @@ #include #include +#include "../core/logcat.hpp" #include "dbus_properties.h" -Q_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg); +QS_LOGGING_CATEGORY(logDbusProperties, "quickshell.dbus.properties", QtWarningMsg); namespace qs::dbus { diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 9cfaee9..a5fce98 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -23,11 +23,12 @@ #include #include +#include "../core/logcat.hpp" #include "../core/util.hpp" class DBusPropertiesInterface; -Q_DECLARE_LOGGING_CATEGORY(logDbusProperties); +QS_DECLARE_LOGGING_CATEGORY(logDbusProperties); namespace qs::dbus { diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp index eb0450f..dd65a28 100644 --- a/src/debug/lint.cpp +++ b/src/debug/lint.cpp @@ -11,10 +11,12 @@ #include #include +#include "../core/logcat.hpp" + namespace qs::debug { namespace { -Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg); +QS_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg); void lintZeroSized(QQuickItem* item); bool isRenderable(QQuickItem* item); diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index cbe8417..1585f26 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -20,12 +20,13 @@ #include #include +#include "../core/logcat.hpp" #include "../core/util.hpp" namespace qs::io { namespace { -Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg); +QS_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg); } QString FileViewError::toString(FileViewError::Enum value) { diff --git a/src/io/socket.cpp b/src/io/socket.cpp index 2cf9b62..371f687 100644 --- a/src/io/socket.cpp +++ b/src/io/socket.cpp @@ -11,9 +11,10 @@ #include #include +#include "../core/logcat.hpp" #include "datastream.hpp" -Q_LOGGING_CATEGORY(logSocket, "quickshell.io.socket", QtWarningMsg); +QS_LOGGING_CATEGORY(logSocket, "quickshell.io.socket", QtWarningMsg); void Socket::setSocket(QLocalSocket* socket) { if (this->socket != nullptr) this->socket->deleteLater(); diff --git a/src/io/socket.hpp b/src/io/socket.hpp index 64605f8..3fd230e 100644 --- a/src/io/socket.hpp +++ b/src/io/socket.hpp @@ -10,10 +10,11 @@ #include #include +#include "../core/logcat.hpp" #include "../core/reload.hpp" #include "datastream.hpp" -Q_DECLARE_LOGGING_CATEGORY(logSocket); +QS_DECLARE_LOGGING_CATEGORY(logSocket); ///! Unix socket listener. class Socket: public DataStream { diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 3580e2b..bf66801 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -10,12 +10,13 @@ #include #include "../core/generation.hpp" +#include "../core/logcat.hpp" #include "../core/paths.hpp" #include "ipccommand.hpp" namespace qs::ipc { -Q_LOGGING_CATEGORY(logIpc, "quickshell.ipc", QtWarningMsg); +QS_LOGGING_CATEGORY(logIpc, "quickshell.ipc", QtWarningMsg); IpcServer::IpcServer(const QString& path) { QObject::connect(&this->server, &QLocalServer::newConnection, this, &IpcServer::onNewConnection); diff --git a/src/ipc/ipc.hpp b/src/ipc/ipc.hpp index 77bff91..8ad4c42 100644 --- a/src/ipc/ipc.hpp +++ b/src/ipc/ipc.hpp @@ -15,6 +15,8 @@ #include #include +#include "../core/logcat.hpp" + template constexpr void assertSerializable() { // monostate being zero ensures transactional reads wont break @@ -109,7 +111,7 @@ DEFINE_SIMPLE_DATASTREAM_OPS(std::monostate); namespace qs::ipc { -Q_DECLARE_LOGGING_CATEGORY(logIpc); +QS_DECLARE_LOGGING_CATEGORY(logIpc); template class MessageStream { diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index ecfd9a5..bf0d1fd 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -14,9 +14,10 @@ #include #include "../../core/generation.hpp" +#include "../../core/logcat.hpp" namespace { -Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd"); +QS_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd"); } QString GreetdState::toString(GreetdState::Enum value) { diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 6730572..67c562d 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -14,6 +14,7 @@ #include #include +#include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" #include "dbus_player_app.h" @@ -23,7 +24,7 @@ using namespace qs::dbus; namespace qs::service::mpris { namespace { -Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); +QS_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); } QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 9461907..fdfe97a 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -9,13 +9,14 @@ #include #include +#include "../../core/logcat.hpp" #include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { namespace { -Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); +QS_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); } MprisWatcher::MprisWatcher() { diff --git a/src/services/notifications/dbusimage.cpp b/src/services/notifications/dbusimage.cpp index 9c10e22..e6c091b 100644 --- a/src/services/notifications/dbusimage.cpp +++ b/src/services/notifications/dbusimage.cpp @@ -7,10 +7,12 @@ #include #include +#include "../../core/logcat.hpp" + namespace qs::service::notifications { // NOLINTNEXTLINE(misc-use-internal-linkage) -Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp +QS_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp QImage DBusNotificationImage::createImage() const { auto format = this->hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888; diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 742d607..96a2ff0 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -12,13 +12,14 @@ #include "../../core/desktopentry.hpp" #include "../../core/iconimageprovider.hpp" +#include "../../core/logcat.hpp" #include "dbusimage.hpp" #include "server.hpp" namespace qs::service::notifications { // NOLINTNEXTLINE(misc-use-internal-linkage) -Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp +QS_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp QString NotificationUrgency::toString(NotificationUrgency::Enum value) { switch (value) { diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 0c41fb8..18a898a 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -12,6 +12,7 @@ #include #include +#include "../../core/logcat.hpp" #include "../../core/model.hpp" #include "dbus_notifications.h" #include "dbusimage.hpp" @@ -20,7 +21,7 @@ namespace qs::service::notifications { // NOLINTNEXTLINE(misc-use-internal-linkage) -Q_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); +QS_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); NotificationServer::NotificationServer() { qDBusRegisterMetaType(); diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index 07dbd59..6d27978 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -8,9 +8,10 @@ #include #include +#include "../../core/logcat.hpp" #include "ipc.hpp" -Q_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg); +QS_LOGGING_CATEGORY(logPam, "quickshell.service.pam", QtWarningMsg); QString PamError::toString(PamError::Enum value) { switch (value) { diff --git a/src/services/pam/conversation.hpp b/src/services/pam/conversation.hpp index d0f2d97..779e6f4 100644 --- a/src/services/pam/conversation.hpp +++ b/src/services/pam/conversation.hpp @@ -8,9 +8,10 @@ #include #include +#include "../../core/logcat.hpp" #include "ipc.hpp" -Q_DECLARE_LOGGING_CATEGORY(logPam); +QS_DECLARE_LOGGING_CATEGORY(logPam); ///! The result of an authentication. /// See @@PamContext.completed(s). diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index 9c2a3db..22445aa 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -14,10 +14,12 @@ #include #include +#include "../../core/logcat.hpp" + namespace qs::service::pipewire { namespace { -Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); +QS_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); } const pw_core_events PwCore::EVENTS = { diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 23252e7..b3d8bfc 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -11,6 +11,7 @@ #include #include +#include "../../core/logcat.hpp" #include "../../core/util.hpp" #include "metadata.hpp" #include "node.hpp" @@ -22,7 +23,7 @@ struct spa_json; namespace qs::service::pipewire { namespace { -Q_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg); +QS_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg); } PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) { diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 649b9c6..616e7d0 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -20,13 +20,14 @@ #include #include +#include "../../core/logcat.hpp" #include "core.hpp" #include "node.hpp" namespace qs::service::pipewire { namespace { -Q_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg); +QS_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg); } // https://github.com/PipeWire/wireplumber/blob/895c1c7286e8809fad869059179e53ab39c807e9/modules/module-mixer-api.c#L397 diff --git a/src/services/pipewire/link.cpp b/src/services/pipewire/link.cpp index c6421af..b2549f6 100644 --- a/src/services/pipewire/link.cpp +++ b/src/services/pipewire/link.cpp @@ -10,12 +10,13 @@ #include #include +#include "../../core/logcat.hpp" #include "registry.hpp" namespace qs::service::pipewire { namespace { -Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg); +QS_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg); } QString PwLinkState::toString(Enum value) { diff --git a/src/services/pipewire/metadata.cpp b/src/services/pipewire/metadata.cpp index ea79611..f2f4ec8 100644 --- a/src/services/pipewire/metadata.cpp +++ b/src/services/pipewire/metadata.cpp @@ -11,12 +11,13 @@ #include #include +#include "../../core/logcat.hpp" #include "registry.hpp" namespace qs::service::pipewire { namespace { -Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg); +QS_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg); } void PwMetadata::bindHooks() { diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 3c7af1d..3e68149 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -25,6 +25,7 @@ #include #include +#include "../../core/logcat.hpp" #include "connection.hpp" #include "core.hpp" #include "device.hpp" @@ -32,7 +33,7 @@ namespace qs::service::pipewire { namespace { -Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg); +QS_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg); } QString PwAudioChannel::toString(Enum value) { diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index d2967d0..c08fc1d 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -15,6 +15,7 @@ #include #include +#include "../../core/logcat.hpp" #include "core.hpp" #include "device.hpp" #include "link.hpp" @@ -23,7 +24,7 @@ namespace qs::service::pipewire { -Q_LOGGING_CATEGORY(logRegistry, "quickshell.service.pipewire.registry", QtWarningMsg); +QS_LOGGING_CATEGORY(logRegistry, "quickshell.service.pipewire.registry", QtWarningMsg); PwBindableObject::~PwBindableObject() { if (this->id != 0) { diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index f1ba961..14ea405 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -12,12 +12,13 @@ #include #include +#include "../../core/logcat.hpp" #include "../../core/util.hpp" #include "core.hpp" namespace qs::service::pipewire { -Q_DECLARE_LOGGING_CATEGORY(logRegistry); +QS_DECLARE_LOGGING_CATEGORY(logRegistry); class PwRegistry; class PwMetadata; diff --git a/src/services/status_notifier/host.cpp b/src/services/status_notifier/host.cpp index 5fa9af0..0e4530a 100644 --- a/src/services/status_notifier/host.cpp +++ b/src/services/status_notifier/host.cpp @@ -12,12 +12,13 @@ #include #include "../../core/common.hpp" +#include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" #include "dbus_watcher_interface.h" #include "item.hpp" #include "watcher.hpp" -Q_LOGGING_CATEGORY(logStatusNotifierHost, "quickshell.service.sni.host", QtWarningMsg); +QS_LOGGING_CATEGORY(logStatusNotifierHost, "quickshell.service.sni.host", QtWarningMsg); namespace qs::service::sni { diff --git a/src/services/status_notifier/host.hpp b/src/services/status_notifier/host.hpp index 9d823e5..87b3137 100644 --- a/src/services/status_notifier/host.hpp +++ b/src/services/status_notifier/host.hpp @@ -8,10 +8,11 @@ #include #include +#include "../../core/logcat.hpp" #include "dbus_watcher_interface.h" #include "item.hpp" -Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierHost); +QS_DECLARE_LOGGING_CATEGORY(logStatusNotifierHost); namespace qs::service::sni { diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index c84dfe6..4632995 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -22,6 +22,7 @@ #include "../../core/iconimageprovider.hpp" #include "../../core/imageprovider.hpp" +#include "../../core/logcat.hpp" #include "../../core/platformmenu.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" @@ -34,7 +35,7 @@ using namespace qs::dbus; using namespace qs::dbus::dbusmenu; using namespace qs::menu::platform; -Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); +QS_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); namespace qs::service::sni { diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 321b73d..60f3a98 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -11,12 +11,13 @@ #include #include "../../core/imageprovider.hpp" +#include "../../core/logcat.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_item.h" #include "dbus_item_types.hpp" -Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierItem); +QS_DECLARE_LOGGING_CATEGORY(logStatusNotifierItem); namespace qs::service::sni { diff --git a/src/services/status_notifier/watcher.cpp b/src/services/status_notifier/watcher.cpp index 4917077..e5b841d 100644 --- a/src/services/status_notifier/watcher.cpp +++ b/src/services/status_notifier/watcher.cpp @@ -10,7 +10,9 @@ #include #include -Q_LOGGING_CATEGORY(logStatusNotifierWatcher, "quickshell.service.sni.watcher", QtWarningMsg); +#include "../../core/logcat.hpp" + +QS_LOGGING_CATEGORY(logStatusNotifierWatcher, "quickshell.service.sni.watcher", QtWarningMsg); namespace qs::service::sni { diff --git a/src/services/status_notifier/watcher.hpp b/src/services/status_notifier/watcher.hpp index 4a04225..4f3c68f 100644 --- a/src/services/status_notifier/watcher.hpp +++ b/src/services/status_notifier/watcher.hpp @@ -9,7 +9,9 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierWatcher); +#include "../../core/logcat.hpp" + +QS_DECLARE_LOGGING_CATEGORY(logStatusNotifierWatcher); namespace qs::service::sni { diff --git a/src/services/upower/core.cpp b/src/services/upower/core.cpp index 9fe0e60..0f9d9da 100644 --- a/src/services/upower/core.cpp +++ b/src/services/upower/core.cpp @@ -12,6 +12,7 @@ #include #include +#include "../../core/logcat.hpp" #include "../../core/model.hpp" #include "../../dbus/bus.hpp" #include "../../dbus/properties.hpp" @@ -21,7 +22,7 @@ namespace qs::service::upower { namespace { -Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg); +QS_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg); } UPower::UPower() { diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index b7c61e1..2492b1f 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" #include "dbus_device.h" @@ -16,7 +17,7 @@ using namespace qs::dbus; namespace qs::service::upower { namespace { -Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg); +QS_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg); } QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) { diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp index 4e9ea92..4c40798 100644 --- a/src/services/upower/powerprofiles.cpp +++ b/src/services/upower/powerprofiles.cpp @@ -12,13 +12,14 @@ #include #include +#include "../../core/logcat.hpp" #include "../../dbus/bus.hpp" #include "../../dbus/properties.hpp" namespace qs::service::upower { namespace { -Q_LOGGING_CATEGORY(logPowerProfiles, "quickshell.service.powerprofiles", QtWarningMsg); +QS_LOGGING_CATEGORY(logPowerProfiles, "quickshell.service.powerprofiles", QtWarningMsg); } QString PowerProfile::toString(PowerProfile::Enum profile) { diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index d233da5..4593389 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -36,6 +36,7 @@ #include #include +#include "../../core/logcat.hpp" #include "../../core/stacklist.hpp" #include "manager.hpp" #include "manager_p.hpp" @@ -44,7 +45,7 @@ namespace qs::wayland::buffer::dmabuf { namespace { -Q_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg); +QS_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg); LinuxDmabufManager* MANAGER = nullptr; // NOLINT diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp index 4c2e267..c7448df 100644 --- a/src/wayland/buffer/manager.cpp +++ b/src/wayland/buffer/manager.cpp @@ -10,6 +10,7 @@ #include #include +#include "../../core/logcat.hpp" #include "dmabuf.hpp" #include "manager_p.hpp" #include "qsg.hpp" @@ -18,7 +19,7 @@ namespace qs::wayland::buffer { namespace { -Q_LOGGING_CATEGORY(logBuffer, "quickshell.wayland.buffer", QtWarningMsg); +QS_LOGGING_CATEGORY(logBuffer, "quickshell.wayland.buffer", QtWarningMsg); } WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) { diff --git a/src/wayland/buffer/shm.cpp b/src/wayland/buffer/shm.cpp index 59a8e91..6a8c642 100644 --- a/src/wayland/buffer/shm.cpp +++ b/src/wayland/buffer/shm.cpp @@ -13,12 +13,13 @@ #include #include +#include "../../core/logcat.hpp" #include "manager.hpp" namespace qs::wayland::buffer::shm { namespace { -Q_LOGGING_CATEGORY(logShm, "quickshell.wayland.buffer.shm", QtWarningMsg); +QS_LOGGING_CATEGORY(logShm, "quickshell.wayland.buffer.shm", QtWarningMsg); } bool WlShmBuffer::isCompatible(const WlBufferRequest& request) const { diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 90cb8a2..067b922 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -21,6 +21,7 @@ #include #include +#include "../../../core/logcat.hpp" #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" #include "../../toplevel_management/handle.hpp" @@ -32,8 +33,8 @@ namespace qs::hyprland::ipc { namespace { -Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); -Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); +QS_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); +QS_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); } // namespace HyprlandIpc::HyprlandIpc() { diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp index 457f105..b8aef96 100644 --- a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp @@ -8,6 +8,7 @@ #include #include +#include "../../../core/logcat.hpp" #include "../../toplevel_management/handle.hpp" #include "../manager.hpp" #include "hyprland_screencopy_p.hpp" @@ -15,7 +16,7 @@ namespace qs::wayland::screencopy::hyprland { namespace { -Q_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.hyprland", QtWarningMsg); +QS_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.hyprland", QtWarningMsg); } HyprlandScreencopyManager::HyprlandScreencopyManager(): QWaylandClientExtensionTemplate(2) { diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp index 649b111..a307d1e 100644 --- a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp @@ -13,13 +13,14 @@ #include #include +#include "../../../core/logcat.hpp" #include "../manager.hpp" #include "image_copy_capture_p.hpp" namespace qs::wayland::screencopy::icc { namespace { -Q_LOGGING_CATEGORY(logIcc, "quickshell.wayland.screencopy.icc", QtWarningMsg); +QS_LOGGING_CATEGORY(logIcc, "quickshell.wayland.screencopy.icc", QtWarningMsg); } using IccCaptureSession = QtWayland::ext_image_copy_capture_session_v1; diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index aa21266..f4d8c48 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -12,6 +12,7 @@ #include #include +#include "../../../core/logcat.hpp" #include "../../buffer/manager.hpp" #include "../manager.hpp" #include "wlr_screencopy_p.hpp" @@ -19,7 +20,7 @@ namespace qs::wayland::screencopy::wlr { namespace { -Q_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.wlr", QtWarningMsg); +QS_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.wlr", QtWarningMsg); } WlrScreencopyManager::WlrScreencopyManager(): QWaylandClientExtensionTemplate(3) { diff --git a/src/wayland/toplevel_management/manager.cpp b/src/wayland/toplevel_management/manager.cpp index bd477b4..471d7e2 100644 --- a/src/wayland/toplevel_management/manager.cpp +++ b/src/wayland/toplevel_management/manager.cpp @@ -7,12 +7,13 @@ #include #include +#include "../../core/logcat.hpp" #include "handle.hpp" #include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" namespace qs::wayland::toplevel_management::impl { -Q_LOGGING_CATEGORY(logToplevelManagement, "quickshell.wayland.toplevelManagement", QtWarningMsg); +QS_LOGGING_CATEGORY(logToplevelManagement, "quickshell.wayland.toplevelManagement", QtWarningMsg); ToplevelManager::ToplevelManager(): QWaylandClientExtensionTemplate(3) { this->initialize(); } diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp index 41848de..4b906a5 100644 --- a/src/wayland/toplevel_management/manager.hpp +++ b/src/wayland/toplevel_management/manager.hpp @@ -6,13 +6,14 @@ #include #include +#include "../../core/logcat.hpp" #include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" namespace qs::wayland::toplevel_management::impl { class ToplevelHandle; -Q_DECLARE_LOGGING_CATEGORY(logToplevelManagement); +QS_DECLARE_LOGGING_CATEGORY(logToplevelManagement); class ToplevelManager : public QWaylandClientExtensionTemplate diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index cce9ba0..ba010ed 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -22,6 +22,7 @@ #include #include +#include "../../../core/logcat.hpp" #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" @@ -31,8 +32,8 @@ namespace qs::i3::ipc { namespace { -Q_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); -Q_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); +QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); +QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); } // namespace void I3Ipc::makeRequest(const QByteArray& request) { From 4b35d7b51b61f16a5f3d862419ba173783a21079 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 00:48:15 -0700 Subject: [PATCH 035/226] core: support qs. imports --- src/core/generation.cpp | 10 +++++-- src/core/generation.hpp | 2 +- src/core/qsintercept.cpp | 61 +++++++++++++++++++++++++++++----------- src/core/qsintercept.hpp | 11 ++++++-- src/core/rootwrapper.cpp | 13 +++++---- src/core/scan.cpp | 60 ++++++++++++++++++++++++++++++++++----- src/core/scan.hpp | 1 + 7 files changed, 123 insertions(+), 35 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index d99409b..332b7d2 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -37,7 +37,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) : rootPath(rootPath) , scanner(std::move(scanner)) , urlInterceptor(this->rootPath) - , interceptNetFactory(this->scanner.fileIntercepts) + , interceptNetFactory(this->rootPath, this->scanner.fileIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); @@ -45,6 +45,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings); this->engine->addUrlInterceptor(&this->urlInterceptor); + this->engine->addImportPath("qs:@/"); + this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); this->engine->setIncubationController(&this->delayedIncubationController); @@ -322,9 +324,11 @@ void EngineGeneration::incubationControllerDestroyed() { } } -void EngineGeneration::onEngineWarnings(const QList& warnings) const { +void EngineGeneration::onEngineWarnings(const QList& warnings) { for (const auto& error: warnings) { - auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path()); + const auto& url = error.url(); + auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5) + : url.toString(); QString objectName; auto desc = error.description(); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index df2c85a..9889e3c 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -84,7 +84,7 @@ private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); void incubationControllerDestroyed(); - void onEngineWarnings(const QList& warnings) const; + static void onEngineWarnings(const QList& warnings); private: void postReload(); diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index 560331d..6687681 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -1,6 +1,7 @@ #include "qsintercept.hpp" #include +#include #include #include #include @@ -25,27 +26,44 @@ QUrl QsUrlInterceptor::intercept( auto url = originalUrl; if (url.scheme() == "root") { - url.setScheme("qsintercept"); + url.setScheme("qs"); auto path = url.path(); if (path.startsWith('/')) path = path.sliced(1); - url.setPath(this->configRoot.filePath(path)); + url.setPath("@/qs/" % path); qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; } - // Some types such as Image take into account where they are loading from, and force - // asynchronous loading over a network. qsintercept is considered to be over a network. - if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") { - // Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which - // case we want to keep the intercept, otherwise objects created from those paths - // will not be able to use singletons. - if (url.path().endsWith(".qml")) return url; + if (url.scheme() == "qs") { + auto path = url.path(); - auto newUrl = url; - newUrl.setScheme("file"); - qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl; - return newUrl; + // Our import path is on "qs:@/". + // We want to blackhole any import resolution outside of the config folder as it breaks Qt + // but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix. + if (path.startsWith("@/qs/")) { + path = this->configRoot.filePath(path.sliced(5)); + } else if (!path.startsWith("/")) { + qCDebug(logQsIntercept) << "Blackholed import URL" << url; + return QUrl("qrc:/qs-blackhole"); + } + + // Some types such as Image take into account where they are loading from, and force + // asynchronous loading over a network. qs: is considered to be over a network. + // In those cases we want to return a file:// url so asynchronous loading is not forced. + if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) { + // Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which + // case we want to keep the intercept, otherwise objects created from those paths + // will not be able to use singletons. + if (path.endsWith(".qml")) return url; + + auto newUrl = url; + newUrl.setScheme("file"); + // above check asserts path starts with /qs/ + newUrl.setPath(path); + qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl; + return newUrl; + } } return url; @@ -67,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) { } QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( + const QDir& configRoot, const QHash& fileIntercepts, QObject* parent ) : QNetworkAccessManager(parent) + , configRoot(configRoot) , fileIntercepts(fileIntercepts) {} QNetworkReply* QsInterceptNetworkAccessManager::createRequest( @@ -79,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest( QIODevice* outgoingData ) { auto url = req.url(); - if (url.scheme() == "qsintercept") { + + if (url.scheme() == "qs") { auto path = url.path(); + + if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5)); + // otherwise pass through to fs + qCDebug(logQsIntercept) << "Got intercept for" << path << "contains" << this->fileIntercepts.value(path); - auto data = this->fileIntercepts.value(path); - if (data != nullptr) { + + if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) { return new QsInterceptDataReply(data, this); } auto fileReq = req; auto fileUrl = req.url(); fileUrl.setScheme("file"); + fileUrl.setPath(path); qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl; + fileReq.setUrl(fileUrl); return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData); } @@ -100,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest( } QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { - return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent); + return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent); } diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index f0e1098..c3d8b55 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -45,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager { public: QsInterceptNetworkAccessManager( + const QDir& configRoot, const QHash& fileIntercepts, QObject* parent = nullptr ); @@ -57,15 +58,21 @@ protected: ) override; private: + QDir configRoot; const QHash& fileIntercepts; }; class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { public: - QsInterceptNetworkAccessManagerFactory(const QHash& fileIntercepts) - : fileIntercepts(fileIntercepts) {} + QsInterceptNetworkAccessManagerFactory( + const QDir& configRoot, + const QHash& fileIntercepts + ) + : configRoot(configRoot) + , fileIntercepts(fileIntercepts) {} QNetworkAccessManager* create(QObject* parent) override; private: + QDir configRoot; const QHash& fileIntercepts; }; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index b51b403..2968402 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -43,7 +43,8 @@ RootWrapper::~RootWrapper() { } void RootWrapper::reloadGraph(bool hard) { - auto rootPath = QFileInfo(this->rootPath).dir(); + auto rootFile = QFileInfo(this->rootPath); + auto rootPath = rootFile.dir(); auto scanner = QmlScanner(rootPath); scanner.scanQmlFile(this->rootPath); @@ -58,9 +59,9 @@ void RootWrapper::reloadGraph(bool hard) { QDir::setCurrent(this->originalWorkingDirectory); - auto url = QUrl::fromLocalFile(this->rootPath); - // unless the original file comes from the qsintercept scheme - url.setScheme("qsintercept"); + QUrl url; + url.setScheme("qs"); + url.setPath("@/qs/" % rootFile.fileName()); auto component = QQmlComponent(generation->engine, url); if (!component.isReady()) { @@ -69,7 +70,9 @@ void RootWrapper::reloadGraph(bool hard) { auto errors = component.errors(); for (auto& error: errors) { - auto rel = "**/" % rootPath.relativeFilePath(error.url().path()); + const auto& url = error.url(); + auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5) + : url.toString(); auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':' % QString::number(error.column()) % "]: " % error.description(); errorString += '\n' % msg; diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 90b19b5..b84b3d8 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -47,7 +47,6 @@ void QmlScanner::scanDir(const QString& path) { } } - // Due to the qsintercept:// protocol a qmldir is always required, even without singletons. if (!seenQmldir) { qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" << singletons; @@ -55,6 +54,29 @@ void QmlScanner::scanDir(const QString& path) { QString qmldir; auto stream = QTextStream(&qmldir); + // cant derive a module name if not in shell path + if (path.startsWith(this->rootPath.path())) { + auto end = path.sliced(this->rootPath.path().length()); + + // verify we have a valid module name. + for (auto& c: end) { + if (c == '/') c = '.'; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) + << "Module path contains invalid characters for a module name: " << end; + goto skipadd; + } + } + + stream << "module qs" << end << '\n'; + skipadd:; + } else { + qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; + } + for (auto& singleton: singletons) { stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton << "\n"; @@ -92,15 +114,39 @@ bool QmlScanner::scanQmlFile(const QString& path) { qCDebug(logQmlScanner) << "Discovered singleton" << path; singleton = true; } else if (line.startsWith("import")) { + // we dont care about "import qs" as we always load the root folder + if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { + importCursor += 4; + QString path; - auto startQuot = line.indexOf('"'); - if (startQuot == -1 || line.length() < startQuot + 3) continue; - auto endQuot = line.indexOf('"', startQuot + 1); - if (endQuot == -1) continue; + while (importCursor != line.length()) { + auto c = line.at(importCursor); + if (c == '.') c = '/'; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; + goto next; + } - auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); - imports.push_back(name); + path.append(c); + importCursor += 1; + } + + imports.append(this->rootPath.filePath(path)); + } else if (auto startQuot = line.indexOf('"'); + startQuot != -1 && line.length() >= startQuot + 3) + { + auto endQuot = line.indexOf('"', startQuot + 1); + if (endQuot == -1) continue; + + auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); + imports.push_back(name); + } } else if (line.contains('{')) break; + + next:; } file.close(); diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 80b44ca..6220bae 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -16,6 +16,7 @@ public: QmlScanner() = default; QmlScanner(const QDir& rootPath): rootPath(rootPath) {} + // path must be canonical void scanDir(const QString& path); // returns if the file has a singleton bool scanQmlFile(const QString& path); From 1af08c0c52300477a573820f07f924dbd91ae09c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 03:12:43 -0700 Subject: [PATCH 036/226] core: only call QmlScanner::scanDir() on directories Removes a bogus warning message. --- src/core/scan.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index b84b3d8..3f5e092 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -65,8 +65,8 @@ void QmlScanner::scanDir(const QString& path) { || c == '_') { } else { - qCWarning(logQmlScanner) - << "Module path contains invalid characters for a module name: " << end; + qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: " + << path.sliced(this->rootPath.path().length()); goto skipadd; } } @@ -170,13 +170,19 @@ bool QmlScanner::scanQmlFile(const QString& path) { ipath = currentdir.filePath(import); } - auto cpath = QFileInfo(ipath).canonicalFilePath(); + auto pathInfo = QFileInfo(ipath); + auto cpath = pathInfo.canonicalFilePath(); if (cpath.isEmpty()) { qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; continue; } + if (!pathInfo.isDir()) { + qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path; + continue; + } + if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); else this->scanDir(cpath); } From b4c62b8ff99460e2c6f9d2eea5ba913a87e5557b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 03:40:30 -0700 Subject: [PATCH 037/226] core: only log warn+ from quickshell.paths --- src/core/paths.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 2c341bb..1f3c494 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -19,7 +19,7 @@ #include "logcat.hpp" namespace { -QS_LOGGING_CATEGORY(logPaths, "quickshell.paths"); +QS_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); } QsPaths* QsPaths::instance() { From 2629e211fa1e5084548b3ac11dab95d096dd356d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 03:40:55 -0700 Subject: [PATCH 038/226] crash: initialize QApplication after logging to run cat filter --- src/crash/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/crash/main.cpp b/src/crash/main.cpp index fb53a56..770f961 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -152,7 +152,6 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { void qsCheckCrash(int argc, char** argv) { auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); if (fd.isEmpty()) return; - auto app = QApplication(argc, argv); RelaunchInfo info; @@ -177,6 +176,8 @@ void qsCheckCrash(int argc, char** argv) { info.logRules ); + auto app = QApplication(argc, argv); + auto crashDir = QsPaths::crashDir(info.instance.instanceId); qCInfo(logCrashReporter) << "Starting crash reporter..."; From 07ea4de248c779ccdd26921cb57a1362bedafbac Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 03:50:11 -0700 Subject: [PATCH 039/226] io/ipchandler: add registry logs --- src/io/ipchandler.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 517e450..5ffa0ad 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,10 +15,15 @@ #include #include "../core/generation.hpp" +#include "../core/logcat.hpp" #include "ipc.hpp" namespace qs::io::ipc { +namespace { +QS_LOGGING_CATEGORY(logIpcHandler, "quickshell.ipchandler", QtWarningMsg) +} + bool IpcFunction::resolve(QString& error) { if (this->method.parameterCount() > 10) { error = "Due to technical limitations, IPC functions can only have 10 arguments."; @@ -210,6 +216,7 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati if (!ext) { ext = new IpcHandlerRegistry(); generation->registerExtension(&key, ext); + qCDebug(logIpcHandler) << "Created new IPC handler registry" << ext << "for" << generation; } return dynamic_cast(ext); @@ -232,10 +239,12 @@ void IpcHandler::updateRegistration(bool destroying) { if (this->registeredState.enabled) { registry->deregisterHandler(this); + qCDebug(logIpcHandler) << "Deregistered" << this << "from registry" << registry; } if (this->targetState.enabled && !this->targetState.target.isEmpty()) { registry->registerHandler(this); + qCDebug(logIpcHandler) << "Registered" << this << "to registry" << registry; } } From 5703fbae21924c7f65a0d6759a31f51a352f39d2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 04:01:00 -0700 Subject: [PATCH 040/226] wayland/lock: handle null window in configure() Has caused a crash. --- src/wayland/session_lock/surface.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index a2608dd..bc0e75d 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -81,6 +81,8 @@ void QSWaylandSessionLockSurface::ext_session_lock_surface_v1_configure( quint32 width, quint32 height ) { + if (!this->window()) return; + this->ack_configure(serial); this->size = QSize(static_cast(width), static_cast(height)); From 6f774af11e0316a3d57f1403f96874dfd5576574 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 04:05:16 -0700 Subject: [PATCH 041/226] core/colorquant: print image source url vs pointer on err --- src/core/colorquantizer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp index 9f443b8..6cfb05d 100644 --- a/src/core/colorquantizer.cpp +++ b/src/core/colorquantizer.cpp @@ -47,7 +47,7 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCa } if (image.isNull()) { - qCWarning(logColorQuantizer) << "Failed to load image from" << source; + qCWarning(logColorQuantizer) << "Failed to load image from" << source->toString(); return; } From d7079b75241c6e2b67f2429996fa7679ffc052e2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 04:28:05 -0700 Subject: [PATCH 042/226] core: allow qml scanner to detect namespaced and versioned imports --- src/core/scan.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 3f5e092..a29ee59 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -122,6 +122,7 @@ bool QmlScanner::scanQmlFile(const QString& path) { while (importCursor != line.length()) { auto c = line.at(importCursor); if (c == '.') c = '/'; + else if (c == ' ') break; else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') { From 026aac375617a99ea0b5746fd6df2fc8aa526761 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 10 Jul 2025 21:57:10 -0700 Subject: [PATCH 043/226] build: add icon and desktop file --- CMakeLists.txt | 11 +++++++++++ assets/org.quickshell.desktop | 7 +++++++ assets/quickshell.svg | 1 + src/crash/main.cpp | 1 + src/launch/launch.cpp | 2 ++ 5 files changed, 22 insertions(+) create mode 100644 assets/org.quickshell.desktop create mode 100644 assets/quickshell.svg diff --git a/CMakeLists.txt b/CMakeLists.txt index 7161c4e..9ef5b98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,3 +146,14 @@ install(CODE " ${CMAKE_INSTALL_FULL_BINDIR}/quickshell \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/qs ) ") + +install( + FILES ${CMAKE_SOURCE_DIR}/assets/org.quickshell.desktop + DESTINATION ${CMAKE_INSTALL_DATADIR}/applications +) + +install( + FILES ${CMAKE_SOURCE_DIR}/assets/quickshell.svg + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps + RENAME org.quickshell.svg +) diff --git a/assets/org.quickshell.desktop b/assets/org.quickshell.desktop new file mode 100644 index 0000000..63f65fd --- /dev/null +++ b/assets/org.quickshell.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Version=1.5 +Type=Application +NoDisplay=true + +Name=Quickshell +Icon=org.quickshell diff --git a/assets/quickshell.svg b/assets/quickshell.svg new file mode 100644 index 0000000..7d0f948 --- /dev/null +++ b/assets/quickshell.svg @@ -0,0 +1 @@ + diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 770f961..b9f0eab 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -177,6 +177,7 @@ void qsCheckCrash(int argc, char** argv) { ); auto app = QApplication(argc, argv); + QApplication::setDesktopFileName("org.quickshell"); auto crashDir = QsPaths::crashDir(info.instance.instanceId); diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 8697667..91e2e24 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -219,6 +219,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio app = new QGuiApplication(qArgC, argv); } + QGuiApplication::setDesktopFileName("org.quickshell"); + if (args.debugPort != -1) { QQmlDebuggingEnabler::enableDebugging(true); auto wait = args.waitForDebug ? QQmlDebuggingEnabler::WaitForClient From 49a3752b9d79bf9f56d8372de594d54312315470 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 11 Jul 2025 00:38:58 -0700 Subject: [PATCH 044/226] core: correctly deregister QML incubators on destruction Previously we'd try to cast the QObject* sender from QObject::destroyed to a QQmlIncubationController*. This will always return nullptr because C++ destructors change the type of the object and the QQmlIncubationController destructor has already run at this point. We now store controllers as QObject*s. Fixes #108 --- src/core/generation.cpp | 62 +++++++++++++++++++---------------------- src/core/generation.hpp | 2 +- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 332b7d2..90a2939 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -238,23 +239,24 @@ 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. - if (auto* obj = dynamic_cast(controller)) { - QObject::connect( - obj, - &QObject::destroyed, - this, - &EngineGeneration::incubationControllerDestroyed - ); - } else { + auto* obj = dynamic_cast(controller); + if (!obj) { qCWarning(logIncubator) << "Could not register incubation controller as it is not a QObject" << controller; return; } - this->incubationControllers.push_back(controller); - qCDebug(logIncubator) << "Registered incubation controller" << controller << "to generation" - << this; + 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; @@ -264,21 +266,25 @@ void EngineGeneration::registerIncubationController(QQmlIncubationController* co } } +// 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) { - if (auto* obj = dynamic_cast(controller)) { - QObject::disconnect(obj, nullptr, this, nullptr); - } else { + auto* obj = dynamic_cast(controller); + if (!obj) { qCCritical(logIncubator) << "Deregistering incubation controller which is not a QObject, " "however only QObject controllers should be registered."; } - if (!this->incubationControllers.removeOne(controller)) { - qCCritical(logIncubator) << "Failed to deregister incubation controller" << controller << "from" + 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; - } else { - qCDebug(logIncubator) << "Deregistered incubation controller" << controller << "from" << this; } // This function can run during destruction. @@ -293,22 +299,12 @@ void EngineGeneration::deregisterIncubationController(QQmlIncubationController* void EngineGeneration::incubationControllerDestroyed() { auto* sender = this->sender(); - auto* controller = dynamic_cast(sender); - if (controller == nullptr) { - qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "is not known to" - << this << ", this may cause memory corruption"; - qCCritical(logIncubator) << "Current registered incuabation controllers" - << this->incubationControllers; - - return; - } - - if (this->incubationControllers.removeOne(controller)) { - qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered from" + if (this->incubationControllers.removeAll(sender) != 0) { + qCDebug(logIncubator) << "Destroyed incubation controller" << sender << "deregistered from" << this; } else { - qCCritical(logIncubator) << "Destroyed incubation controller" << controller + qCCritical(logIncubator) << "Destroyed incubation controller" << sender << "was not registered, but its destruction was observed by" << this; return; @@ -317,7 +313,7 @@ void EngineGeneration::incubationControllerDestroyed() { // This function can run during destruction. if (this->engine == nullptr) return; - if (this->engine->incubationController() == controller) { + if (dynamic_cast(this->engine->incubationController()) == sender) { qCDebug(logIncubator ) << "Destroyed incubation controller was currently active, reassigning from pool"; this->assignIncubationController(); @@ -371,7 +367,7 @@ void EngineGeneration::assignIncubationController() { if (this->incubationControllersLocked || this->incubationControllers.isEmpty()) { controller = &this->delayedIncubationController; } else { - controller = this->incubationControllers.first(); + controller = dynamic_cast(this->incubationControllers.first()); } qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 9889e3c..5d3c5c6 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -89,7 +89,7 @@ private slots: private: void postReload(); void assignIncubationController(); - QVector incubationControllers; + QVector incubationControllers; bool incubationControllersLocked = false; QHash extensions; From 0c9c5be8dd856b8ed3c1d37be24d96f9b4171c20 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 12 Jul 2025 20:00:13 -0700 Subject: [PATCH 045/226] core/region: use QList over QQmlListProperty for child regions --- src/core/region.cpp | 98 ++++++++++++++------------------------------- src/core/region.hpp | 14 ++----- 2 files changed, 33 insertions(+), 79 deletions(-) diff --git a/src/core/region.cpp b/src/core/region.cpp index 439cfbd..e36ed7d 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -1,13 +1,13 @@ #include "region.hpp" #include +#include #include #include #include #include #include #include -#include #include PendingRegion::PendingRegion(QObject* parent): QObject(parent) { @@ -19,9 +19,12 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::regionsChanged, this, &PendingRegion::childrenChanged); } void PendingRegion::setItem(QQuickItem* item) { + if (item == this->mItem) return; + if (this->mItem != nullptr) { QObject::disconnect(this->mItem, nullptr, this, nullptr); } @@ -39,21 +42,33 @@ void PendingRegion::setItem(QQuickItem* item) { emit this->itemChanged(); } -void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } +void PendingRegion::onItemDestroyed() { + this->mItem = nullptr; + emit this->itemChanged(); +} -void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } +void PendingRegion::onChildDestroyed() { + this->mRegions.removeAll(this->sender()); + emit this->regionsChanged(); +} -QQmlListProperty PendingRegion::regions() { - return QQmlListProperty( - this, - nullptr, - &PendingRegion::regionsAppend, - &PendingRegion::regionsCount, - &PendingRegion::regionAt, - &PendingRegion::regionsClear, - &PendingRegion::regionsReplace, - &PendingRegion::regionsRemoveLast - ); +const QList& PendingRegion::regions() const { return this->mRegions; } + +void PendingRegion::setRegions(const QList& regions) { + if (regions == this->mRegions) return; + + for (auto* region: this->mRegions) { + QObject::disconnect(region, nullptr, this, nullptr); + } + + this->mRegions = regions; + + for (auto* region: regions) { + QObject::connect(region, &QObject::destroyed, this, &PendingRegion::onChildDestroyed); + QObject::connect(region, &PendingRegion::changed, this, &PendingRegion::childrenChanged); + } + + emit this->regionsChanged(); } bool PendingRegion::empty() const { @@ -115,58 +130,3 @@ QRegion PendingRegion::applyTo(const QRect& rect) const { return this->applyTo(baseRegion); } } - -void PendingRegion::regionsAppend(QQmlListProperty* prop, PendingRegion* region) { - auto* self = static_cast(prop->object); // NOLINT - if (!region) return; - - QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed); - QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); - - self->mRegions.append(region); - - emit self->childrenChanged(); -} - -PendingRegion* PendingRegion::regionAt(QQmlListProperty* prop, qsizetype i) { - return static_cast(prop->object)->mRegions.at(i); // NOLINT -} - -void PendingRegion::regionsClear(QQmlListProperty* prop) { - auto* self = static_cast(prop->object); // NOLINT - - for (auto* region: self->mRegions) { - QObject::disconnect(region, nullptr, self, nullptr); - } - - self->mRegions.clear(); // NOLINT - emit self->childrenChanged(); -} - -qsizetype PendingRegion::regionsCount(QQmlListProperty* prop) { - return static_cast(prop->object)->mRegions.length(); // NOLINT -} - -void PendingRegion::regionsRemoveLast(QQmlListProperty* prop) { - auto* self = static_cast(prop->object); // NOLINT - - auto* last = self->mRegions.last(); - if (last != nullptr) QObject::disconnect(last, nullptr, self, nullptr); - - self->mRegions.removeLast(); - emit self->childrenChanged(); -} - -void PendingRegion::regionsReplace( - QQmlListProperty* prop, - qsizetype i, - PendingRegion* region -) { - auto* self = static_cast(prop->object); // NOLINT - - auto* old = self->mRegions.at(i); - if (old != nullptr) QObject::disconnect(old, nullptr, self, nullptr); - - self->mRegions.replace(i, region); - emit self->childrenChanged(); -} diff --git a/src/core/region.hpp b/src/core/region.hpp index 6637d7b..0335abb 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -82,7 +82,7 @@ class PendingRegion: public QObject { /// } /// } /// ``` - Q_PROPERTY(QQmlListProperty regions READ regions); + Q_PROPERTY(QList regions READ regions WRITE setRegions NOTIFY regionsChanged); Q_CLASSINFO("DefaultProperty", "regions"); QML_NAMED_ELEMENT(Region); @@ -91,7 +91,8 @@ public: void setItem(QQuickItem* item); - QQmlListProperty regions(); + [[nodiscard]] const QList& regions() const; + void setRegions(const QList& regions); [[nodiscard]] bool empty() const; [[nodiscard]] QRegion build() const; @@ -109,6 +110,7 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); + void regionsChanged(); void childrenChanged(); /// Triggered when the region's geometry changes. @@ -122,14 +124,6 @@ private slots: void onChildDestroyed(); private: - static void regionsAppend(QQmlListProperty* prop, PendingRegion* region); - static PendingRegion* regionAt(QQmlListProperty* prop, qsizetype i); - static void regionsClear(QQmlListProperty* prop); - static qsizetype regionsCount(QQmlListProperty* prop); - static void regionsRemoveLast(QQmlListProperty* prop); - static void - regionsReplace(QQmlListProperty* prop, qsizetype i, PendingRegion* region); - QQuickItem* mItem = nullptr; qint32 mX = 0; From bb206e3a19b48af987977d86ad77822439f37360 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 12 Jul 2025 20:36:34 -0700 Subject: [PATCH 046/226] core/window: run window-level polish along with item polish Fixes input masks not updating after a reload. --- src/window/proxywindow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 81f4334..468820b 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -283,6 +283,7 @@ void ProxyWindowBase::polishItems() { // This hack manually polishes the item tree right before showing the window so it will // always be created with the correct size. QQuickWindowPrivate::get(this->window)->polishItems(); + this->onPolished(); } void ProxyWindowBase::runLints() { From 3b4ebc5f1669f2d7811faa38e0af3ce98f64e95a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 17:02:18 -0700 Subject: [PATCH 047/226] wayland/layershell: support auto exclusive zone without constraint --- src/wayland/wlr_layershell/wlr_layershell.cpp | 9 ++++++--- src/wayland/wlr_layershell/wlr_layershell.hpp | 2 ++ src/window/panelinterface.hpp | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/wayland/wlr_layershell/wlr_layershell.cpp b/src/wayland/wlr_layershell/wlr_layershell.cpp index 4e61530..0960ca5 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell/wlr_layershell.cpp @@ -1,6 +1,7 @@ #include "wlr_layershell.hpp" #include +#include #include #include #include @@ -20,13 +21,15 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) { case ExclusionMode::Ignore: return -1; case ExclusionMode::Normal: return this->bExclusiveZone; case ExclusionMode::Auto: - const auto anchors = this->bAnchors.value(); + const auto edge = this->bcExclusionEdge.value(); - if (anchors.horizontalConstraint()) return this->bImplicitHeight; - else if (anchors.verticalConstraint()) return this->bImplicitWidth; + if (edge == Qt::TopEdge || edge == Qt::BottomEdge) return this->bImplicitHeight; + else if (edge == Qt::LeftEdge || edge == Qt::RightEdge) return this->bImplicitWidth; else return 0; } }); + + this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); } ProxiedWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { diff --git a/src/wayland/wlr_layershell/wlr_layershell.hpp b/src/wayland/wlr_layershell/wlr_layershell.hpp index 739f5ff..457a1f5 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell/wlr_layershell.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -196,6 +197,7 @@ private: Q_OBJECT_BINDABLE_PROPERTY(WlrLayershell, WlrKeyboardFocus::Enum, bKeyboardFocus, &WlrLayershell::keyboardFocusChanged); Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(WlrLayershell, ExclusionMode::Enum, bExclusionMode, ExclusionMode::Auto, &WlrLayershell::exclusionModeChanged); Q_OBJECT_BINDABLE_PROPERTY(WlrLayershell, qint32, bcExclusiveZone); + Q_OBJECT_BINDABLE_PROPERTY(WlrLayershell, Qt::Edge, bcExclusionEdge); QS_BINDING_SUBSCRIBE_METHOD(WlrLayershell, bLayer, onStateChanged, onValueChanged); QS_BINDING_SUBSCRIBE_METHOD(WlrLayershell, bAnchors, onStateChanged, onValueChanged); diff --git a/src/window/panelinterface.hpp b/src/window/panelinterface.hpp index 22c47c9..64dff50 100644 --- a/src/window/panelinterface.hpp +++ b/src/window/panelinterface.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -21,6 +22,21 @@ public: [[nodiscard]] bool horizontalConstraint() const noexcept { return this->mLeft && this->mRight; } [[nodiscard]] bool verticalConstraint() const noexcept { return this->mTop && this->mBottom; } + [[nodiscard]] Qt::Edge exclusionEdge() const noexcept { + auto hasHEdge = this->mLeft ^ this->mRight; + auto hasVEdge = this->mTop ^ this->mBottom; + + if (hasVEdge && !hasHEdge) { + if (this->mTop) return Qt::TopEdge; + if (this->mBottom) return Qt::BottomEdge; + } else if (hasHEdge && !hasVEdge) { + if (this->mLeft) return Qt::LeftEdge; + if (this->mRight) return Qt::RightEdge; + } + + return static_cast(0); + } + [[nodiscard]] bool operator==(const Anchors& other) const noexcept { // clang-format off return this->mLeft == other.mLeft From 479ff58f84876cb3f4d7d8e8e2d0fa2c73e4484d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 17:08:12 -0700 Subject: [PATCH 048/226] wayland/layershell: support opposite-to-exclusion edge margins --- src/wayland/wlr_layershell/wlr_layershell.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/wayland/wlr_layershell/wlr_layershell.cpp b/src/wayland/wlr_layershell/wlr_layershell.cpp index 0960ca5..e4726b5 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell/wlr_layershell.cpp @@ -21,11 +21,16 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) { case ExclusionMode::Ignore: return -1; case ExclusionMode::Normal: return this->bExclusiveZone; case ExclusionMode::Auto: - const auto edge = this->bcExclusionEdge.value(); + const auto margins = this->bMargins.value(); - if (edge == Qt::TopEdge || edge == Qt::BottomEdge) return this->bImplicitHeight; - else if (edge == Qt::LeftEdge || edge == Qt::RightEdge) return this->bImplicitWidth; - else return 0; + // add reverse edge margins which are ignored by wlr-layer-shell + switch (this->bcExclusionEdge.value()) { + case Qt::TopEdge: return this->bImplicitHeight + margins.bottom; + case Qt::BottomEdge: return this->bImplicitHeight + margins.top; + case Qt::LeftEdge: return this->bImplicitHeight + margins.right; + case Qt::RightEdge: return this->bImplicitHeight + margins.left; + default: return 0; + } } }); From 96043024159d0216d7a175a563302cce74c2b44c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 17:50:17 -0700 Subject: [PATCH 049/226] x11/panelwindow: convert to bindable properties --- src/x11/panel_window.cpp | 204 +++++++++++++-------------------------- src/x11/panel_window.hpp | 58 +++++++---- 2 files changed, 108 insertions(+), 154 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index ef271b5..5700364 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -98,6 +98,26 @@ XPanelWindow::XPanelWindow(QObject* parent): ProxyWindowBase(parent) { this, &XPanelWindow::xInit ); + + this->bcExclusiveZone.setBinding([this]() -> qint32 { + switch (this->bExclusionMode.value()) { + case ExclusionMode::Ignore: return 0; + case ExclusionMode::Normal: return this->bExclusiveZone; + case ExclusionMode::Auto: + auto edge = this->bcExclusionEdge.value(); + auto margins = this->bMargins.value(); + + if (edge == Qt::TopEdge || edge == Qt::BottomEdge) { + return this->bImplicitHeight + margins.top + margins.bottom; + } else if (edge == Qt::LeftEdge || edge == Qt::RightEdge) { + return this->bImplicitWidth + margins.left + margins.right; + } else { + return 0; + } + } + }); + + this->bcExclusionEdge.setBinding([this] { return this->bAnchors.value().exclusionEdge(); }); } XPanelWindow::~XPanelWindow() { XPanelStack::instance()->removePanel(this); } @@ -133,7 +153,7 @@ void XPanelWindow::connectWindow() { void XPanelWindow::trySetWidth(qint32 implicitWidth) { // only update the actual size if not blocked by anchors - if (!this->mAnchors.horizontalConstraint()) { + if (!this->bAnchors.value().horizontalConstraint()) { this->ProxyWindowBase::trySetWidth(implicitWidth); this->updateDimensions(); } @@ -141,7 +161,7 @@ void XPanelWindow::trySetWidth(qint32 implicitWidth) { void XPanelWindow::trySetHeight(qint32 implicitHeight) { // only update the actual size if not blocked by anchors - if (!this->mAnchors.verticalConstraint()) { + if (!this->bAnchors.value().verticalConstraint()) { this->ProxyWindowBase::trySetHeight(implicitHeight); this->updateDimensions(); } @@ -152,61 +172,6 @@ void XPanelWindow::setScreen(QuickshellScreenInfo* screen) { this->connectScreen(); } -Anchors XPanelWindow::anchors() const { return this->mAnchors; } - -void XPanelWindow::setAnchors(Anchors anchors) { - if (this->mAnchors == anchors) return; - this->mAnchors = anchors; - this->updateDimensions(); - emit this->anchorsChanged(); -} - -qint32 XPanelWindow::exclusiveZone() const { return this->mExclusiveZone; } - -void XPanelWindow::setExclusiveZone(qint32 exclusiveZone) { - if (this->mExclusiveZone == exclusiveZone) return; - this->mExclusiveZone = exclusiveZone; - this->setExclusionMode(ExclusionMode::Normal); - this->updateStrut(); - emit this->exclusiveZoneChanged(); -} - -ExclusionMode::Enum XPanelWindow::exclusionMode() const { return this->mExclusionMode; } - -void XPanelWindow::setExclusionMode(ExclusionMode::Enum exclusionMode) { - if (this->mExclusionMode == exclusionMode) return; - this->mExclusionMode = exclusionMode; - this->updateStrut(); - emit this->exclusionModeChanged(); -} - -Margins XPanelWindow::margins() const { return this->mMargins; } - -void XPanelWindow::setMargins(Margins margins) { - if (this->mMargins == margins) return; - this->mMargins = margins; - this->updateDimensions(); - emit this->marginsChanged(); -} - -bool XPanelWindow::aboveWindows() const { return this->mAboveWindows; } - -void XPanelWindow::setAboveWindows(bool aboveWindows) { - if (this->mAboveWindows == aboveWindows) return; - this->mAboveWindows = aboveWindows; - this->updateAboveWindows(); - emit this->aboveWindowsChanged(); -} - -bool XPanelWindow::focusable() const { return this->mFocusable; } - -void XPanelWindow::setFocusable(bool focusable) { - if (this->mFocusable == focusable) return; - this->mFocusable = focusable; - this->updateFocusable(); - emit this->focusableChanged(); -} - void XPanelWindow::xInit() { if (this->window == nullptr || this->window->handle() == nullptr) return; this->updateDimensions(); @@ -271,44 +236,42 @@ void XPanelWindow::updateDimensions(bool propagate) { auto screenGeometry = this->mScreen->geometry(); - if (this->mExclusionMode != ExclusionMode::Ignore) { + if (this->bExclusionMode != ExclusionMode::Ignore) { for (auto* panel: XPanelStack::instance()->panels(this)) { // we only care about windows below us if (panel == this) break; // we only care about windows in the same layer - if (panel->mAboveWindows != this->mAboveWindows) continue; + if (panel->bAboveWindows != this->bAboveWindows) continue; if (panel->mScreen != this->mScreen) continue; - int side = -1; - quint32 exclusiveZone = 0; - panel->getExclusion(side, exclusiveZone); - - if (exclusiveZone == 0) continue; - - auto zone = static_cast(exclusiveZone); + auto edge = this->bcExclusionEdge.value(); + auto exclusiveZone = this->bcExclusiveZone.value(); screenGeometry.adjust( - side == 0 ? zone : 0, - side == 2 ? zone : 0, - side == 1 ? -zone : 0, - side == 3 ? -zone : 0 + edge == Qt::LeftEdge ? exclusiveZone : 0, + edge == Qt::TopEdge ? exclusiveZone : 0, + edge == Qt::RightEdge ? -exclusiveZone : 0, + edge == Qt::BottomEdge ? -exclusiveZone : 0 ); } } auto geometry = QRect(); - if (this->mAnchors.horizontalConstraint()) { - geometry.setX(screenGeometry.x() + this->mMargins.left); - geometry.setWidth(screenGeometry.width() - this->mMargins.left - this->mMargins.right); + auto anchors = this->bAnchors.value(); + auto margins = this->bMargins.value(); + + if (anchors.horizontalConstraint()) { + geometry.setX(screenGeometry.x() + margins.left); + geometry.setWidth(screenGeometry.width() - margins.left - margins.right); } else { - if (this->mAnchors.mLeft) { - geometry.setX(screenGeometry.x() + this->mMargins.left); - } else if (this->mAnchors.mRight) { + if (anchors.mLeft) { + geometry.setX(screenGeometry.x() + margins.left); + } else if (anchors.mRight) { geometry.setX( - screenGeometry.x() + screenGeometry.width() - this->implicitWidth() - this->mMargins.right + screenGeometry.x() + screenGeometry.width() - this->implicitWidth() - margins.right ); } else { geometry.setX(screenGeometry.x() + screenGeometry.width() / 2 - this->implicitWidth() / 2); @@ -317,16 +280,15 @@ void XPanelWindow::updateDimensions(bool propagate) { geometry.setWidth(this->implicitWidth()); } - if (this->mAnchors.verticalConstraint()) { - geometry.setY(screenGeometry.y() + this->mMargins.top); - geometry.setHeight(screenGeometry.height() - this->mMargins.top - this->mMargins.bottom); + if (anchors.verticalConstraint()) { + geometry.setY(screenGeometry.y() + margins.top); + geometry.setHeight(screenGeometry.height() - margins.top - margins.bottom); } else { - if (this->mAnchors.mTop) { - geometry.setY(screenGeometry.y() + this->mMargins.top); - } else if (this->mAnchors.mBottom) { + if (anchors.mTop) { + geometry.setY(screenGeometry.y() + margins.top); + } else if (anchors.mBottom) { geometry.setY( - screenGeometry.y() + screenGeometry.height() - this->implicitHeight() - - this->mMargins.bottom + screenGeometry.y() + screenGeometry.height() - this->implicitHeight() - margins.bottom ); } else { geometry.setY(screenGeometry.y() + screenGeometry.height() / 2 - this->implicitHeight() / 2); @@ -355,42 +317,6 @@ void XPanelWindow::updatePanelStack() { } } -void XPanelWindow::getExclusion(int& side, quint32& exclusiveZone) { - if (this->mExclusionMode == ExclusionMode::Ignore) { - exclusiveZone = 0; - return; - } - - auto& anchors = this->mAnchors; - if (anchors.mLeft || anchors.mRight || anchors.mTop || anchors.mBottom) { - if (!anchors.horizontalConstraint() - && (anchors.verticalConstraint() || (!anchors.mTop && !anchors.mBottom))) - { - side = anchors.mLeft ? 0 : anchors.mRight ? 1 : -1; - } else if (!anchors.verticalConstraint() - && (anchors.horizontalConstraint() || (!anchors.mLeft && !anchors.mRight))) - { - side = anchors.mTop ? 2 : anchors.mBottom ? 3 : -1; - } - } - - if (side == -1) return; - - auto autoExclude = this->mExclusionMode == ExclusionMode::Auto; - - if (autoExclude) { - if (side == 0 || side == 1) { - exclusiveZone = - this->implicitWidth() + (side == 0 ? this->mMargins.left : this->mMargins.right); - } else { - exclusiveZone = - this->implicitHeight() + (side == 2 ? this->mMargins.top : this->mMargins.bottom); - } - } else { - exclusiveZone = this->mExclusiveZone; - } -} - // Disable xinerama structs to break multi monitor configurations with bad WMs less. // Usually this results in one monitor at the top left corner of the root window working // perfectly and all others being broken semi randomly. @@ -400,12 +326,10 @@ void XPanelWindow::updateStrut(bool propagate) { if (this->window == nullptr || this->window->handle() == nullptr) return; auto* conn = x11Connection(); - int side = -1; - quint32 exclusiveZone = 0; + auto edge = this->bcExclusionEdge.value(); + auto exclusiveZone = this->bcExclusiveZone.value(); - this->getExclusion(side, exclusiveZone); - - if (side == -1 || this->mExclusionMode == ExclusionMode::Ignore) { + if (edge == 0 || this->bExclusionMode == ExclusionMode::Ignore) { xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT.atom()); xcb_delete_property(conn, this->window->winId(), XAtom::_NET_WM_STRUT_PARTIAL.atom()); return; @@ -413,18 +337,27 @@ void XPanelWindow::updateStrut(bool propagate) { auto rootGeometry = this->window->screen()->virtualGeometry(); auto screenGeometry = this->window->screen()->geometry(); - auto horizontal = side == 0 || side == 1; + auto horizontal = edge == Qt::LeftEdge || edge == Qt::RightEdge; if (XINERAMA_STRUTS) { - switch (side) { - case 0: exclusiveZone += screenGeometry.left(); break; - case 1: exclusiveZone += rootGeometry.right() - screenGeometry.right(); break; - case 2: exclusiveZone += screenGeometry.top(); break; - case 3: exclusiveZone += rootGeometry.bottom() - screenGeometry.bottom(); break; + switch (edge) { + case Qt::LeftEdge: exclusiveZone += screenGeometry.left(); break; + case Qt::RightEdge: exclusiveZone += rootGeometry.right() - screenGeometry.right(); break; + case Qt::TopEdge: exclusiveZone += screenGeometry.top(); break; + case Qt::BottomEdge: exclusiveZone += rootGeometry.bottom() - screenGeometry.bottom(); break; default: break; } } + quint32 side = -1; + + switch (edge) { + case Qt::LeftEdge: side = 0; break; + case Qt::RightEdge: side = 1; break; + case Qt::TopEdge: side = 2; break; + case Qt::BottomEdge: side = 3; break; + } + auto data = std::array(); data[side] = exclusiveZone; @@ -461,13 +394,14 @@ void XPanelWindow::updateStrut(bool propagate) { void XPanelWindow::updateAboveWindows() { if (this->window == nullptr) return; - this->window->setFlag(Qt::WindowStaysOnBottomHint, !this->mAboveWindows); - this->window->setFlag(Qt::WindowStaysOnTopHint, this->mAboveWindows); + auto above = this->bAboveWindows.value(); + this->window->setFlag(Qt::WindowStaysOnBottomHint, !above); + this->window->setFlag(Qt::WindowStaysOnTopHint, above); } void XPanelWindow::updateFocusable() { if (this->window == nullptr) return; - this->window->setFlag(Qt::WindowDoesNotAcceptFocus, !this->mFocusable); + this->window->setFlag(Qt::WindowDoesNotAcceptFocus, !this->bFocusable); } // XPanelInterface diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index c6c1de7..b809dc1 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include "../core/doc.hpp" +#include "../core/util.hpp" #include "../window/panelinterface.hpp" #include "../window/proxywindow.hpp" @@ -51,23 +54,28 @@ public: void setScreen(QuickshellScreenInfo* screen) override; - [[nodiscard]] Anchors anchors() const; - void setAnchors(Anchors anchors); + [[nodiscard]] bool aboveWindows() const { return this->bAboveWindows; } + void setAboveWindows(bool aboveWindows) { this->bAboveWindows = aboveWindows; } - [[nodiscard]] qint32 exclusiveZone() const; - void setExclusiveZone(qint32 exclusiveZone); + [[nodiscard]] Anchors anchors() const { return this->bAnchors; } + void setAnchors(Anchors anchors) { this->bAnchors = anchors; } - [[nodiscard]] ExclusionMode::Enum exclusionMode() const; - void setExclusionMode(ExclusionMode::Enum exclusionMode); + [[nodiscard]] qint32 exclusiveZone() const { return this->bExclusiveZone; } + void setExclusiveZone(qint32 exclusiveZone) { + Qt::beginPropertyUpdateGroup(); + this->bExclusiveZone = exclusiveZone; + this->bExclusionMode = ExclusionMode::Normal; + Qt::endPropertyUpdateGroup(); + } - [[nodiscard]] Margins margins() const; - void setMargins(Margins margins); + [[nodiscard]] ExclusionMode::Enum exclusionMode() const { return this->bExclusionMode; } + void setExclusionMode(ExclusionMode::Enum exclusionMode) { this->bExclusionMode = exclusionMode; } - [[nodiscard]] bool aboveWindows() const; - void setAboveWindows(bool aboveWindows); + [[nodiscard]] Margins margins() const { return this->bMargins; } + void setMargins(Margins margins) { this->bMargins = margins; } - [[nodiscard]] bool focusable() const; - void setFocusable(bool focusable); + [[nodiscard]] bool focusable() const { return this->bFocusable; } + void setFocusable(bool focusable) { this->bFocusable = focusable; } signals: QSDOC_HIDE void anchorsChanged(); @@ -85,24 +93,36 @@ private slots: private: void connectScreen(); - void getExclusion(int& side, quint32& exclusiveZone); void updateStrut(bool propagate = true); + void updateStrutCb() { this->updateStrut(); } void updateAboveWindows(); void updateFocusable(); void updateDimensions(bool propagate = true); + void updateDimensionsCb() { this->updateDimensions(); } QPointer mTrackedScreen = nullptr; - bool mAboveWindows = true; - bool mFocusable = false; - Anchors mAnchors; - Margins mMargins; - qint32 mExclusiveZone = 0; - ExclusionMode::Enum mExclusionMode = ExclusionMode::Auto; EngineGeneration* knownGeneration = nullptr; QRect lastScreenVirtualGeometry; XPanelEventFilter eventFilter; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(XPanelWindow, bool, bAboveWindows, true, &XPanelWindow::aboveWindowsChanged); + Q_OBJECT_BINDABLE_PROPERTY(XPanelWindow, bool, bFocusable, &XPanelWindow::focusableChanged); + Q_OBJECT_BINDABLE_PROPERTY(XPanelWindow, Anchors, bAnchors, &XPanelWindow::anchorsChanged); + Q_OBJECT_BINDABLE_PROPERTY(XPanelWindow, Margins, bMargins, &XPanelWindow::marginsChanged); + Q_OBJECT_BINDABLE_PROPERTY(XPanelWindow, qint32, bExclusiveZone, &XPanelWindow::exclusiveZoneChanged); + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(XPanelWindow, ExclusionMode::Enum, bExclusionMode, ExclusionMode::Auto, &XPanelWindow::exclusionModeChanged); + Q_OBJECT_BINDABLE_PROPERTY(XPanelWindow, qint32, bcExclusiveZone); + Q_OBJECT_BINDABLE_PROPERTY(XPanelWindow, Qt::Edge, bcExclusionEdge); + + QS_BINDING_SUBSCRIBE_METHOD(XPanelWindow, bAboveWindows, updateAboveWindows, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(XPanelWindow, bAnchors, updateDimensionsCb, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(XPanelWindow, bMargins, updateDimensionsCb, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(XPanelWindow, bcExclusiveZone, updateStrutCb, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(XPanelWindow, bFocusable, updateFocusable, onValueChanged); + // clang-format on + friend class XPanelStack; }; From 59d29bb254455ba26eb0f7b7fda76e77669d7eb7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 18:30:48 -0700 Subject: [PATCH 050/226] x11/panelwindow: use Qt window default screen if none is provided Fixes panels not updating geometry or attachments under X if a screen was not explicitly provided by the user. --- src/x11/panel_window.cpp | 21 +++++++++++++-------- src/x11/panel_window.hpp | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 5700364..97d4645 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -126,7 +126,7 @@ void XPanelWindow::connectWindow() { this->ProxyWindowBase::connectWindow(); this->window->installEventFilter(&this->eventFilter); - this->connectScreen(); + this->updateScreen(); QObject::connect( this->window, @@ -169,7 +169,7 @@ void XPanelWindow::trySetHeight(qint32 implicitHeight) { void XPanelWindow::setScreen(QuickshellScreenInfo* screen) { this->ProxyWindowBase::setScreen(screen); - this->connectScreen(); + this->updateScreen(); } void XPanelWindow::xInit() { @@ -192,12 +192,17 @@ void XPanelWindow::xInit() { ); } -void XPanelWindow::connectScreen() { +void XPanelWindow::updateScreen() { + auto* newScreen = + this->mScreen ? this->mScreen : (this->window ? this->window->screen() : nullptr); + + if (newScreen == this->mTrackedScreen) return; + if (this->mTrackedScreen != nullptr) { QObject::disconnect(this->mTrackedScreen, nullptr, this, nullptr); } - this->mTrackedScreen = this->mScreen; + this->mTrackedScreen = newScreen; if (this->mTrackedScreen != nullptr) { QObject::connect( @@ -212,7 +217,6 @@ void XPanelWindow::connectScreen() { &QScreen::virtualGeometryChanged, this, &XPanelWindow::onScreenVirtualGeometryChanged - ); } @@ -231,10 +235,11 @@ void XPanelWindow::onScreenVirtualGeometryChanged() { void XPanelWindow::updateDimensionsSlot() { this->updateDimensions(); } void XPanelWindow::updateDimensions(bool propagate) { - if (this->window == nullptr || this->window->handle() == nullptr || this->mScreen == nullptr) + if (this->window == nullptr || this->window->handle() == nullptr + || this->mTrackedScreen == nullptr) return; - auto screenGeometry = this->mScreen->geometry(); + auto screenGeometry = this->mTrackedScreen->geometry(); if (this->bExclusionMode != ExclusionMode::Ignore) { for (auto* panel: XPanelStack::instance()->panels(this)) { @@ -244,7 +249,7 @@ void XPanelWindow::updateDimensions(bool propagate) { // we only care about windows in the same layer if (panel->bAboveWindows != this->bAboveWindows) continue; - if (panel->mScreen != this->mScreen) continue; + if (panel->mTrackedScreen != this->mTrackedScreen) continue; auto edge = this->bcExclusionEdge.value(); auto exclusiveZone = this->bcExclusiveZone.value(); diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index b809dc1..02c05b1 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -92,7 +92,7 @@ private slots: void onScreenVirtualGeometryChanged(); private: - void connectScreen(); + void updateScreen(); void updateStrut(bool propagate = true); void updateStrutCb() { this->updateStrut(); } void updateAboveWindows(); From 1e1ba93713c4d557ff8b0b44a882b67b91f01034 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 18:35:41 -0700 Subject: [PATCH 051/226] core/window: add manual PanelWindow tester --- src/window/test/manual/panel.qml | 151 +++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/window/test/manual/panel.qml diff --git a/src/window/test/manual/panel.qml b/src/window/test/manual/panel.qml new file mode 100644 index 0000000..5c4868c --- /dev/null +++ b/src/window/test/manual/panel.qml @@ -0,0 +1,151 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell + +Scope { + FloatingWindow { + color: contentItem.palette.window + minimumSize.width: layout.implicitWidth + minimumSize.height: layout.implicitHeight + + ColumnLayout { + id: layout + + RowLayout { + CheckBox { + id: visibleCb + text: "Visible" + checked: true + } + + CheckBox { + id: aboveCb + text: "Above Windows" + checked: true + } + } + + RowLayout { + ColumnLayout { + CheckBox { + id: leftAnchorCb + text: "Left" + } + + SpinBox { + id: leftMarginSb + editable: true + value: 0 + to: 1000 + } + } + + ColumnLayout { + CheckBox { + id: rightAnchorCb + text: "Right" + } + + SpinBox { + id: rightMarginSb + editable: true + value: 0 + to: 1000 + } + } + + ColumnLayout { + CheckBox { + id: topAnchorCb + text: "Top" + } + + SpinBox { + id: topMarginSb + editable: true + value: 0 + to: 1000 + } + } + + ColumnLayout { + CheckBox { + id: bottomAnchorCb + text: "Bottom" + } + + SpinBox { + id: bottomMarginSb + editable: true + value: 0 + to: 1000 + } + } + } + + RowLayout { + ComboBox { + id: exclusiveModeCb + model: [ "Normal", "Ignore", "Auto" ] + currentIndex: w.exclusionMode + } + + SpinBox { + id: exclusiveZoneSb + editable: true + value: 100 + to: 1000 + } + } + + RowLayout { + Label { text: "Width" } + + SpinBox { + id: widthSb + editable: true + value: 100 + to: 1000 + } + } + + RowLayout { + Label { text: "Height" } + + SpinBox { + id: heightSb + editable: true + value: 100 + to: 1000 + } + } + } + } + + PanelWindow { + id: w + visible: visibleCb.checked + aboveWindows: aboveCb.checked + + anchors { + left: leftAnchorCb.checked + right: rightAnchorCb.checked + top: topAnchorCb.checked + bottom: bottomAnchorCb.checked + } + + margins { + left: leftMarginSb.value + right: rightMarginSb.value + top: topMarginSb.value + bottom: bottomMarginSb.value + } + + exclusionMode: exclusiveModeCb.currentIndex + exclusiveZone: exclusiveZoneSb.value + + implicitWidth: widthSb.value + implicitHeight: heightSb.value + } +} From b011cd9d33332f9e34df3cdb4da02fb8bf30fddd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 18:46:30 -0700 Subject: [PATCH 052/226] core/window: set FloatingWindow default max size to QWINDOWSIZE_MAX Was previously zero, which will shrink the window to 1px depending on the display server. --- src/window/floatingwindow.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index a98e9f4..48b7c13 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -8,6 +8,9 @@ #include "proxywindow.hpp" #include "windowinterface.hpp" +// see #include +static const int QWINDOWSIZE_MAX = ((1 << 24) - 1); + class ProxyFloatingWindow: public ProxyWindowBase { Q_OBJECT; @@ -46,10 +49,11 @@ public: &ProxyFloatingWindow::onMinimumSizeChanged ); - Q_OBJECT_BINDABLE_PROPERTY( + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS( ProxyFloatingWindow, QSize, bMaximumSize, + QSize(QWINDOWSIZE_MAX, QWINDOWSIZE_MAX), &ProxyFloatingWindow::onMaximumSizeChanged ); }; From de2578745109a32b4f6fee5dcec37184ea97bc8b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 19:48:08 -0700 Subject: [PATCH 053/226] io/process: null stdio channels in detached processes --- src/core/qmlglobal.cpp | 9 ++++++++- src/io/process.cpp | 5 +++++ src/io/processcore.hpp | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 0aaf06c..0aba306 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -263,7 +263,6 @@ void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& conte auto args = context.command.sliced(1); QProcess process; - qs::io::process::setupProcessEnvironment(&process, context.clearEnvironment, context.environment); if (!context.workingDirectory.isEmpty()) { @@ -272,6 +271,14 @@ void QuickshellGlobal::execDetached(const qs::io::process::ProcessContext& conte process.setProgram(cmd); process.setArguments(args); + + process.setStandardInputFile(QProcess::nullDevice()); + + if (context.unbindStdout) { + process.setStandardOutputFile(QProcess::nullDevice()); + process.setStandardErrorFile(QProcess::nullDevice()); + } + process.startDetached(); } diff --git a/src/io/process.cpp b/src/io/process.cpp index c8250c7..6055e2c 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -249,6 +249,11 @@ void Process::startDetached() { this->setupEnvironment(&process); process.setProgram(cmd); process.setArguments(args); + + process.setStandardInputFile(QProcess::nullDevice()); + process.setStandardOutputFile(QProcess::nullDevice()); + process.setStandardErrorFile(QProcess::nullDevice()); + process.startDetached(); } diff --git a/src/io/processcore.hpp b/src/io/processcore.hpp index c74f6fb..37ec409 100644 --- a/src/io/processcore.hpp +++ b/src/io/processcore.hpp @@ -16,6 +16,7 @@ class ProcessContext { Q_PROPERTY(QHash environment MEMBER environment WRITE setEnvironment); Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment); Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory); + Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout); Q_GADGET; QML_STRUCTURED_VALUE; QML_VALUE_TYPE(processContext); @@ -45,6 +46,8 @@ public: this->workingDirectorySet = true; } + void setUnbindStdout(bool unbindStdout) { this->unbindStdout = unbindStdout; } + QList command; QHash environment; bool clearEnvironment = false; @@ -54,6 +57,7 @@ public: bool environmentSet : 1 = false; bool clearEnvironmentSet : 1 = false; bool workingDirectorySet : 1 = false; + bool unbindStdout : 1 = true; }; void setupProcessEnvironment( From 71334bfcaf243a175bb7a96dfaf29863c8c217a0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 19:55:09 -0700 Subject: [PATCH 054/226] core/desktopentry: expose exec command and use execDetached on call --- src/core/common.cpp | 2 -- src/core/common.hpp | 2 +- src/core/desktopentry.cpp | 37 ++++++++++++++++----------------- src/core/desktopentry.hpp | 43 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/core/common.cpp b/src/core/common.cpp index 5928e47..080019a 100644 --- a/src/core/common.cpp +++ b/src/core/common.cpp @@ -1,11 +1,9 @@ #include "common.hpp" #include -#include namespace qs { const QDateTime Common::LAUNCH_TIME = QDateTime::currentDateTime(); -QProcessEnvironment Common::INITIAL_ENVIRONMENT = {}; // NOLINT } // namespace qs diff --git a/src/core/common.hpp b/src/core/common.hpp index f2a01bc..ab8edb8 100644 --- a/src/core/common.hpp +++ b/src/core/common.hpp @@ -7,7 +7,7 @@ namespace qs { struct Common { static const QDateTime LAUNCH_TIME; - static QProcessEnvironment INITIAL_ENVIRONMENT; // NOLINT + static inline QProcessEnvironment INITIAL_ENVIRONMENT = {}; // NOLINT }; } // namespace qs diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 3c4b6f2..4673881 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -11,14 +11,14 @@ #include #include #include -#include #include #include #include -#include "common.hpp" +#include "../io/processcore.hpp" #include "logcat.hpp" #include "model.hpp" +#include "qmlglobal.hpp" namespace { QS_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); @@ -111,8 +111,10 @@ void DesktopEntry::parseEntry(const QString& text) { else if (key == "NoDisplay") this->mNoDisplay = value == "true"; else if (key == "Comment") this->mComment = value; else if (key == "Icon") this->mIcon = value; - else if (key == "Exec") this->mExecString = value; - else if (key == "Path") this->mWorkingDirectory = 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); @@ -127,7 +129,10 @@ void DesktopEntry::parseEntry(const QString& text) { if (key == "Name") action->mName = value; else if (key == "Icon") action->mIcon = value; - else if (key == "Exec") action->mExecString = value; + else if (key == "Exec") { + action->mExecString = value; + action->mCommand = DesktopEntry::parseExecString(value); + } } this->mActions.insert(actionName, action); @@ -179,7 +184,7 @@ void DesktopEntry::parseEntry(const QString& text) { } void DesktopEntry::execute() const { - DesktopEntry::doExec(this->mExecString, this->mWorkingDirectory); + DesktopEntry::doExec(this->mCommand, this->mWorkingDirectory); } bool DesktopEntry::isValid() const { return !this->mName.isEmpty(); } @@ -251,23 +256,15 @@ QVector DesktopEntry::parseExecString(const QString& execString) { return arguments; } -void DesktopEntry::doExec(const QString& execString, const QString& workingDirectory) { - auto args = DesktopEntry::parseExecString(execString); - if (args.isEmpty()) { - qCWarning(logDesktopEntry) << "Tried to exec string" << execString << "which parsed as empty."; - return; - } - - auto process = QProcess(); - process.setProgram(args.at(0)); - process.setArguments(args.sliced(1)); - if (!workingDirectory.isEmpty()) process.setWorkingDirectory(workingDirectory); - process.setProcessEnvironment(qs::Common::INITIAL_ENVIRONMENT); - process.startDetached(); +void DesktopEntry::doExec(const QList& execString, const QString& workingDirectory) { + qs::io::process::ProcessContext ctx; + ctx.setCommand(execString); + ctx.setWorkingDirectory(workingDirectory); + QuickshellGlobal::execDetached(ctx); } void DesktopAction::execute() const { - DesktopEntry::doExec(this->mExecString, this->entry->mWorkingDirectory); + DesktopEntry::doExec(this->mCommand, this->entry->mWorkingDirectory); } DesktopEntryManager::DesktopEntryManager() { diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 3871181..ee8f511 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -28,8 +28,19 @@ class DesktopEntry: public QObject { Q_PROPERTY(QString comment MEMBER mComment CONSTANT); /// Name of the icon associated with this application. May be empty. Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); - /// The raw `Exec` string from the desktop entry. You probably want @@execute(). + /// 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); + /// The parsed `Exec` command in the desktop entry. + /// + /// The entry can be run with @@execute(), or by using this command in + /// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process. + /// If used in `execDetached` or a `Process`, @@workingDirectory should also be passed to + /// 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); /// The working directory to execute from. Q_PROPERTY(QString workingDirectory MEMBER mWorkingDirectory CONSTANT); /// If the application should run in a terminal. @@ -46,6 +57,16 @@ public: void parseEntry(const QString& text); /// Run the application. Currently ignores @@runInTerminal and field codes. + /// + /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command + /// and @@DesktopEntry.workingDirectory as shown below: + /// + /// ```qml + /// Quickshell.execDetached({ + /// command: desktopEntry.command, + /// workingDirectory: desktopEntry.workingDirectory, + /// }); + /// ``` Q_INVOKABLE void execute() const; [[nodiscard]] bool isValid() const; @@ -54,7 +75,7 @@ public: // currently ignores all field codes. static QVector parseExecString(const QString& execString); - static void doExec(const QString& execString, const QString& workingDirectory); + static void doExec(const QList& execString, const QString& workingDirectory); public: QString mId; @@ -64,6 +85,7 @@ public: QString mComment; QString mIcon; QString mExecString; + QVector mCommand; QString mWorkingDirectory; bool mTerminal = false; QVector mCategories; @@ -82,8 +104,19 @@ class DesktopAction: public QObject { Q_PROPERTY(QString id MEMBER mId CONSTANT); Q_PROPERTY(QString name MEMBER mName CONSTANT); Q_PROPERTY(QString icon MEMBER mIcon CONSTANT); - /// The raw `Exec` string from the desktop entry. You probably want @@execute(). + /// 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); + /// The parsed `Exec` command in the action. + /// + /// The entry can be run with @@execute(), or by using this command in + /// @@Quickshell.Quickshell.execDetached() or @@Quickshell.Io.Process. + /// If used in `execDetached` or a `Process`, @@DesktopEntry.workingDirectory should also be passed to + /// the invoked process. + /// + /// > [!NOTE] The provided command does not invoke a terminal even if @@runInTerminal is true. + Q_PROPERTY(QVector command MEMBER mCommand CONSTANT); QML_ELEMENT; QML_UNCREATABLE("DesktopAction instances must be retrieved from a DesktopEntry"); @@ -94,6 +127,9 @@ public: , mId(std::move(id)) {} /// Run the application. Currently ignores @@DesktopEntry.runInTerminal and field codes. + /// + /// This is equivalent to calling @@Quickshell.Quickshell.execDetached() with @@command + /// and @@DesktopEntry.workingDirectory. Q_INVOKABLE void execute() const; private: @@ -102,6 +138,7 @@ private: QString mName; QString mIcon; QString mExecString; + QVector mCommand; QHash mEntries; friend class DesktopEntry; From cee1f5837ee3716129adc865af0b5e646933d3aa Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 20:32:51 -0700 Subject: [PATCH 055/226] service/mpris: make lengthSupported bindable and notify for changes Fixes #109 --- src/services/mpris/player.cpp | 8 ++++---- src/services/mpris/player.hpp | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 67c562d..45d5cd4 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -99,6 +99,8 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren } else return static_cast(-1); }); + this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; }); + this->bPlaybackState.setBinding([this]() { const auto& status = this->bpPlaybackStatus.value(); @@ -258,21 +260,19 @@ void MprisPlayer::setPosition(qlonglong position) { } void MprisPlayer::onExportedPositionChanged() { - if (!this->lengthSupported()) emit this->lengthChanged(); + if (!this->bLengthSupported) emit this->lengthChanged(); } void MprisPlayer::onSeek(qlonglong time) { this->setPosition(time); } qreal MprisPlayer::length() const { - if (this->bInternalLength == -1) { + if (!this->bLengthSupported) { return this->position(); // unsupported } else { return static_cast(this->bInternalLength / 1000) / 1000; // NOLINT } } -bool MprisPlayer::lengthSupported() const { return this->bInternalLength != -1; } - bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); } void MprisPlayer::setVolume(qreal volume) { diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index a2ea59b..89bc27a 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -117,7 +117,7 @@ class MprisPlayer: public QObject { /// The length of the playing track, as seconds, with millisecond precision, /// or the value of @@position if @@lengthSupported is false. Q_PROPERTY(qreal length READ length NOTIFY lengthChanged); - Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged); + Q_PROPERTY(bool lengthSupported READ default NOTIFY lengthSupportedChanged BINDABLE bindableLengthSupported); /// The volume of the playing track from 0.0 to 1.0, or 1.0 if @@volumeSupported is false. /// /// May only be written to if @@canControl and @@volumeSupported are true. @@ -274,7 +274,7 @@ public: void setPosition(qreal position); [[nodiscard]] qreal length() const; - [[nodiscard]] bool lengthSupported() const; + [[nodiscard]] QBindable bindableLengthSupported() const { return &this->bLengthSupported; } [[nodiscard]] qreal volume() const { return this->bVolume; }; [[nodiscard]] bool volumeSupported() const; @@ -447,6 +447,7 @@ private: Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackAlbumArtist, &MprisPlayer::trackAlbumArtistChanged); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackArtUrl, &MprisPlayer::trackArtUrlChanged); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, qlonglong, bInternalLength, &MprisPlayer::lengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bLengthSupported, &MprisPlayer::lengthSupportedChanged); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bShuffle, &MprisPlayer::shuffleChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(MprisPlayer, playerProperties); From 478aa2bda1aa3f451232a998bc3731057e0aef09 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 22:27:44 -0700 Subject: [PATCH 056/226] core/window: run polish in onExposed instead of polishItems Fixes hyprland visible regions created before window expose. --- src/window/proxywindow.cpp | 9 +++++---- src/window/proxywindow.hpp | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 468820b..56d250c 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -85,7 +85,7 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { if (wasVisible && this->isVisibleDirect()) { emit this->backerVisibilityChanged(); - this->runLints(); + this->onExposed(); } } @@ -195,7 +195,7 @@ void ProxyWindowBase::connectWindow() { QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged); QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); - QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::runLints); + QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::onExposed); QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged); // clang-format on } @@ -283,10 +283,11 @@ void ProxyWindowBase::polishItems() { // This hack manually polishes the item tree right before showing the window so it will // always be created with the correct size. QQuickWindowPrivate::get(this->window)->polishItems(); - this->onPolished(); } -void ProxyWindowBase::runLints() { +void ProxyWindowBase::onExposed() { + this->onPolished(); + if (!this->ranLints) { qs::debug::lintItemTree(this->mContentItem); this->ranLints = true; diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 1d07516..3fbc08e 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -166,7 +166,7 @@ protected slots: void onMaskDestroyed(); void onScreenDestroyed(); virtual void onPolished(); - void runLints(); + void onExposed(); protected: bool mVisible = true; From 05fbead660f8df1ea7274f3cb7b97132ac698705 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 13 Jul 2025 22:54:20 -0700 Subject: [PATCH 057/226] x11/panelwindow: calc screen geom with exclusions of other panels Fixes a typo in 9604302 which calculated panel stack offsets from the current panel instead of others in the stack. --- src/x11/panel_window.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index 97d4645..adba0ab 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -251,8 +251,8 @@ void XPanelWindow::updateDimensions(bool propagate) { if (panel->mTrackedScreen != this->mTrackedScreen) continue; - auto edge = this->bcExclusionEdge.value(); - auto exclusiveZone = this->bcExclusiveZone.value(); + auto edge = panel->bcExclusionEdge.value(); + auto exclusiveZone = panel->bcExclusiveZone.value(); screenGeometry.adjust( edge == Qt::LeftEdge ? exclusiveZone : 0, From 5ac9096c1c63f6940c6b95f1118b540dfe029278 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 14 Jul 2025 02:54:46 -0700 Subject: [PATCH 058/226] Revert "core/region: use QList over QQmlListProperty for child regions" This reverts commit 0c9c5be8dd856b8ed3c1d37be24d96f9b4171c20. Using QList breaks the default property usage. --- src/core/region.cpp | 96 ++++++++++++++++++++++++++++++++------------- src/core/region.hpp | 14 +++++-- 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/src/core/region.cpp b/src/core/region.cpp index e36ed7d..11892d6 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -1,13 +1,13 @@ #include "region.hpp" #include -#include #include #include #include #include #include #include +#include #include PendingRegion::PendingRegion(QObject* parent): QObject(parent) { @@ -19,7 +19,6 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); - QObject::connect(this, &PendingRegion::regionsChanged, this, &PendingRegion::childrenChanged); } void PendingRegion::setItem(QQuickItem* item) { @@ -42,33 +41,21 @@ void PendingRegion::setItem(QQuickItem* item) { emit this->itemChanged(); } -void PendingRegion::onItemDestroyed() { - this->mItem = nullptr; - emit this->itemChanged(); -} +void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } -void PendingRegion::onChildDestroyed() { - this->mRegions.removeAll(this->sender()); - emit this->regionsChanged(); -} +void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } -const QList& PendingRegion::regions() const { return this->mRegions; } - -void PendingRegion::setRegions(const QList& regions) { - if (regions == this->mRegions) return; - - for (auto* region: this->mRegions) { - QObject::disconnect(region, nullptr, this, nullptr); - } - - this->mRegions = regions; - - for (auto* region: regions) { - QObject::connect(region, &QObject::destroyed, this, &PendingRegion::onChildDestroyed); - QObject::connect(region, &PendingRegion::changed, this, &PendingRegion::childrenChanged); - } - - emit this->regionsChanged(); +QQmlListProperty PendingRegion::regions() { + return QQmlListProperty( + this, + nullptr, + &PendingRegion::regionsAppend, + &PendingRegion::regionsCount, + &PendingRegion::regionAt, + &PendingRegion::regionsClear, + &PendingRegion::regionsReplace, + &PendingRegion::regionsRemoveLast + ); } bool PendingRegion::empty() const { @@ -130,3 +117,58 @@ QRegion PendingRegion::applyTo(const QRect& rect) const { return this->applyTo(baseRegion); } } + +void PendingRegion::regionsAppend(QQmlListProperty* prop, PendingRegion* region) { + auto* self = static_cast(prop->object); // NOLINT + if (!region) return; + + QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed); + QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); + + self->mRegions.append(region); + + emit self->childrenChanged(); +} + +PendingRegion* PendingRegion::regionAt(QQmlListProperty* prop, qsizetype i) { + return static_cast(prop->object)->mRegions.at(i); // NOLINT +} + +void PendingRegion::regionsClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); // NOLINT + + for (auto* region: self->mRegions) { + QObject::disconnect(region, nullptr, self, nullptr); + } + + self->mRegions.clear(); // NOLINT + emit self->childrenChanged(); +} + +qsizetype PendingRegion::regionsCount(QQmlListProperty* prop) { + return static_cast(prop->object)->mRegions.length(); // NOLINT +} + +void PendingRegion::regionsRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); // NOLINT + + auto* last = self->mRegions.last(); + if (last != nullptr) QObject::disconnect(last, nullptr, self, nullptr); + + self->mRegions.removeLast(); + emit self->childrenChanged(); +} + +void PendingRegion::regionsReplace( + QQmlListProperty* prop, + qsizetype i, + PendingRegion* region +) { + auto* self = static_cast(prop->object); // NOLINT + + auto* old = self->mRegions.at(i); + if (old != nullptr) QObject::disconnect(old, nullptr, self, nullptr); + + self->mRegions.replace(i, region); + emit self->childrenChanged(); +} diff --git a/src/core/region.hpp b/src/core/region.hpp index 0335abb..6637d7b 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -82,7 +82,7 @@ class PendingRegion: public QObject { /// } /// } /// ``` - Q_PROPERTY(QList regions READ regions WRITE setRegions NOTIFY regionsChanged); + Q_PROPERTY(QQmlListProperty regions READ regions); Q_CLASSINFO("DefaultProperty", "regions"); QML_NAMED_ELEMENT(Region); @@ -91,8 +91,7 @@ public: void setItem(QQuickItem* item); - [[nodiscard]] const QList& regions() const; - void setRegions(const QList& regions); + QQmlListProperty regions(); [[nodiscard]] bool empty() const; [[nodiscard]] QRegion build() const; @@ -110,7 +109,6 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); - void regionsChanged(); void childrenChanged(); /// Triggered when the region's geometry changes. @@ -124,6 +122,14 @@ private slots: void onChildDestroyed(); private: + static void regionsAppend(QQmlListProperty* prop, PendingRegion* region); + static PendingRegion* regionAt(QQmlListProperty* prop, qsizetype i); + static void regionsClear(QQmlListProperty* prop); + static qsizetype regionsCount(QQmlListProperty* prop); + static void regionsRemoveLast(QQmlListProperty* prop); + static void + regionsReplace(QQmlListProperty* prop, qsizetype i, PendingRegion* region); + QQuickItem* mItem = nullptr; qint32 mX = 0; From 5706c09e6fb6681396f345658f1b6751b6435a47 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 15 Jul 2025 14:06:26 -0700 Subject: [PATCH 059/226] core/window: clean up window interface property proxies --- src/wayland/wlr_layershell/wlr_layershell.cpp | 36 +----------- src/wayland/wlr_layershell/wlr_layershell.hpp | 35 ----------- src/window/floatingwindow.cpp | 43 +------------- src/window/floatingwindow.hpp | 35 ----------- src/window/windowinterface.cpp | 58 +++++++++++++++++++ src/window/windowinterface.hpp | 47 ++++++++------- src/x11/panel_window.cpp | 31 +--------- src/x11/panel_window.hpp | 35 ----------- 8 files changed, 87 insertions(+), 233 deletions(-) diff --git a/src/wayland/wlr_layershell/wlr_layershell.cpp b/src/wayland/wlr_layershell/wlr_layershell.cpp index e4726b5..d30740d 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell/wlr_layershell.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include "../../core/qmlscreen.hpp" @@ -173,23 +172,9 @@ WlrLayershell* WlrLayershell::qmlAttachedProperties(QObject* object) { WaylandPanelInterface::WaylandPanelInterface(QObject* parent) : PanelWindowInterface(parent) , layer(new WlrLayershell(this)) { + this->connectSignals(); // clang-format off - QObject::connect(this->layer, &ProxyWindowBase::windowConnected, this, &WaylandPanelInterface::windowConnected); - QObject::connect(this->layer, &ProxyWindowBase::visibleChanged, this, &WaylandPanelInterface::visibleChanged); - QObject::connect(this->layer, &ProxyWindowBase::backerVisibilityChanged, this, &WaylandPanelInterface::backingWindowVisibleChanged); - QObject::connect(this->layer, &ProxyWindowBase::implicitHeightChanged, this, &WaylandPanelInterface::implicitHeightChanged); - QObject::connect(this->layer, &ProxyWindowBase::implicitWidthChanged, this, &WaylandPanelInterface::implicitWidthChanged); - QObject::connect(this->layer, &ProxyWindowBase::heightChanged, this, &WaylandPanelInterface::heightChanged); - QObject::connect(this->layer, &ProxyWindowBase::widthChanged, this, &WaylandPanelInterface::widthChanged); - QObject::connect(this->layer, &ProxyWindowBase::devicePixelRatioChanged, this, &WaylandPanelInterface::devicePixelRatioChanged); - QObject::connect(this->layer, &ProxyWindowBase::screenChanged, this, &WaylandPanelInterface::screenChanged); - QObject::connect(this->layer, &ProxyWindowBase::windowTransformChanged, this, &WaylandPanelInterface::windowTransformChanged); - QObject::connect(this->layer, &ProxyWindowBase::colorChanged, this, &WaylandPanelInterface::colorChanged); - QObject::connect(this->layer, &ProxyWindowBase::maskChanged, this, &WaylandPanelInterface::maskChanged); - QObject::connect(this->layer, &ProxyWindowBase::surfaceFormatChanged, this, &WaylandPanelInterface::surfaceFormatChanged); - - // panel specific QObject::connect(this->layer, &WlrLayershell::anchorsChanged, this, &WaylandPanelInterface::anchorsChanged); QObject::connect(this->layer, &WlrLayershell::marginsChanged, this, &WaylandPanelInterface::marginsChanged); QObject::connect(this->layer, &WlrLayershell::exclusiveZoneChanged, this, &WaylandPanelInterface::exclusiveZoneChanged); @@ -206,32 +191,13 @@ void WaylandPanelInterface::onReload(QObject* oldInstance) { this->layer->reload(old != nullptr ? old->layer : nullptr); } -QQmlListProperty WaylandPanelInterface::data() { return this->layer->data(); } ProxyWindowBase* WaylandPanelInterface::proxyWindow() const { return this->layer; } -QQuickItem* WaylandPanelInterface::contentItem() const { return this->layer->contentItem(); } - -bool WaylandPanelInterface::isBackingWindowVisible() const { - return this->layer->isVisibleDirect(); -} - -qreal WaylandPanelInterface::devicePixelRatio() const { return this->layer->devicePixelRatio(); } // NOLINTBEGIN #define proxyPair(type, get, set) \ type WaylandPanelInterface::get() const { return this->layer->get(); } \ void WaylandPanelInterface::set(type value) { this->layer->set(value); } -proxyPair(bool, isVisible, setVisible); -proxyPair(qint32, implicitWidth, setImplicitWidth); -proxyPair(qint32, implicitHeight, setImplicitHeight); -proxyPair(qint32, width, setWidth); -proxyPair(qint32, height, setHeight); -proxyPair(QuickshellScreenInfo*, screen, setScreen); -proxyPair(QColor, color, setColor); -proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); - -// panel specific proxyPair(Anchors, anchors, setAnchors); proxyPair(Margins, margins, setMargins); proxyPair(qint32, exclusiveZone, setExclusiveZone); diff --git a/src/wayland/wlr_layershell/wlr_layershell.hpp b/src/wayland/wlr_layershell/wlr_layershell.hpp index 457a1f5..0f5617f 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell/wlr_layershell.hpp @@ -216,43 +216,8 @@ public: void onReload(QObject* oldInstance) override; [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - [[nodiscard]] QQuickItem* contentItem() const override; // NOLINTBEGIN - [[nodiscard]] bool isVisible() const override; - [[nodiscard]] bool isBackingWindowVisible() const override; - void setVisible(bool visible) override; - - [[nodiscard]] qint32 implicitWidth() const override; - void setImplicitWidth(qint32 implicitWidth) override; - - [[nodiscard]] qint32 implicitHeight() const override; - void setImplicitHeight(qint32 implicitHeight) override; - - [[nodiscard]] qint32 width() const override; - void setWidth(qint32 width) override; - - [[nodiscard]] qint32 height() const override; - void setHeight(qint32 height) override; - - [[nodiscard]] virtual qreal devicePixelRatio() const override; - - [[nodiscard]] QuickshellScreenInfo* screen() const override; - void setScreen(QuickshellScreenInfo* screen) override; - - [[nodiscard]] QColor color() const override; - void setColor(QColor color) override; - - [[nodiscard]] PendingRegion* mask() const override; - void setMask(PendingRegion* mask) override; - - [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; - void setSurfaceFormat(QsSurfaceFormat mask) override; - - [[nodiscard]] QQmlListProperty data() override; - - // panel specific - [[nodiscard]] Anchors anchors() const override; void setAnchors(Anchors anchors) override; diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index 2f196fc..0b9e9b1 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -50,21 +49,9 @@ void ProxyFloatingWindow::onMaximumSizeChanged() { FloatingWindowInterface::FloatingWindowInterface(QObject* parent) : WindowInterface(parent) , window(new ProxyFloatingWindow(this)) { - // clang-format off - QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::windowConnected); - QObject::connect(this->window, &ProxyWindowBase::visibleChanged, this, &FloatingWindowInterface::visibleChanged); - QObject::connect(this->window, &ProxyWindowBase::backerVisibilityChanged, this, &FloatingWindowInterface::backingWindowVisibleChanged); - QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged); - QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged); - QObject::connect(this->window, &ProxyWindowBase::implicitHeightChanged, this, &FloatingWindowInterface::implicitHeightChanged); - QObject::connect(this->window, &ProxyWindowBase::implicitWidthChanged, this, &FloatingWindowInterface::implicitWidthChanged); - QObject::connect(this->window, &ProxyWindowBase::devicePixelRatioChanged, this, &FloatingWindowInterface::devicePixelRatioChanged); - QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged); - QObject::connect(this->window, &ProxyWindowBase::windowTransformChanged, this, &FloatingWindowInterface::windowTransformChanged); - QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged); - QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged); - QObject::connect(this->window, &ProxyWindowBase::surfaceFormatChanged, this, &FloatingWindowInterface::surfaceFormatChanged); + this->connectSignals(); + // clang-format off QObject::connect(this->window, &ProxyFloatingWindow::titleChanged, this, &FloatingWindowInterface::titleChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); @@ -78,30 +65,4 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) { this->window->reload(old != nullptr ? old->window : nullptr); } -QQmlListProperty FloatingWindowInterface::data() { return this->window->data(); } ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; } -QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); } - -bool FloatingWindowInterface::isBackingWindowVisible() const { - return this->window->isVisibleDirect(); -} - -qreal FloatingWindowInterface::devicePixelRatio() const { return this->window->devicePixelRatio(); } - -// NOLINTBEGIN -#define proxyPair(type, get, set) \ - type FloatingWindowInterface::get() const { return this->window->get(); } \ - void FloatingWindowInterface::set(type value) { this->window->set(value); } - -proxyPair(bool, isVisible, setVisible); -proxyPair(qint32, implicitWidth, setImplicitWidth); -proxyPair(qint32, implicitHeight, setImplicitHeight); -proxyPair(qint32, width, setWidth); -proxyPair(qint32, height, setHeight); -proxyPair(QuickshellScreenInfo*, screen, setScreen); -proxyPair(QColor, color, setColor); -proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); - -#undef proxyPair -// NOLINTEND diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index 48b7c13..f9cd5ce 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -77,41 +77,6 @@ public: void onReload(QObject* oldInstance) override; [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - [[nodiscard]] QQuickItem* contentItem() const override; - - // NOLINTBEGIN - [[nodiscard]] bool isVisible() const override; - [[nodiscard]] bool isBackingWindowVisible() const override; - void setVisible(bool visible) override; - - [[nodiscard]] qint32 implicitWidth() const override; - void setImplicitWidth(qint32 implicitWidth) override; - - [[nodiscard]] qint32 implicitHeight() const override; - void setImplicitHeight(qint32 implicitHeight) override; - - [[nodiscard]] qint32 width() const override; - void setWidth(qint32 width) override; - - [[nodiscard]] qint32 height() const override; - void setHeight(qint32 height) override; - - [[nodiscard]] virtual qreal devicePixelRatio() const override; - - [[nodiscard]] QuickshellScreenInfo* screen() const override; - void setScreen(QuickshellScreenInfo* screen) override; - - [[nodiscard]] QColor color() const override; - void setColor(QColor color) override; - - [[nodiscard]] PendingRegion* mask() const override; - void setMask(PendingRegion* mask) override; - - [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; - void setSurfaceFormat(QsSurfaceFormat mask) override; - - [[nodiscard]] QQmlListProperty data() override; - // NOLINTEND QBindable bindableMinimumSize() { return &this->window->bMinimumSize; } QBindable bindableMaximumSize() { return &this->window->bMaximumSize; } diff --git a/src/window/windowinterface.cpp b/src/window/windowinterface.cpp index 20057d6..d808c80 100644 --- a/src/window/windowinterface.cpp +++ b/src/window/windowinterface.cpp @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include "../core/qmlscreen.hpp" +#include "../core/region.hpp" #include "proxywindow.hpp" QPointF WindowInterface::itemPosition(QQuickItem* item) const { @@ -91,6 +94,61 @@ QsWindowAttached::mapFromItem(QQuickItem* item, qreal x, qreal y, qreal width, q } } +// clang-format off +QQuickItem* WindowInterface::contentItem() const { return this->proxyWindow()->contentItem(); } + +bool WindowInterface::isVisible() const { return this->proxyWindow()->isVisible(); }; +bool WindowInterface::isBackingWindowVisible() const { return this->proxyWindow()->isVisibleDirect(); }; +void WindowInterface::setVisible(bool visible) const { this->proxyWindow()->setVisible(visible); }; + +qint32 WindowInterface::implicitWidth() const { return this->proxyWindow()->implicitWidth(); }; +void WindowInterface::setImplicitWidth(qint32 implicitWidth) const { this->proxyWindow()->setImplicitWidth(implicitWidth); }; + +qint32 WindowInterface::implicitHeight() const { return this->proxyWindow()->implicitHeight(); }; +void WindowInterface::setImplicitHeight(qint32 implicitHeight) const { this->proxyWindow()->setImplicitHeight(implicitHeight); }; + +qint32 WindowInterface::width() const { return this->proxyWindow()->width(); }; +void WindowInterface::setWidth(qint32 width) const { this->proxyWindow()->setWidth(width); }; + +qint32 WindowInterface::height() const { return this->proxyWindow()->height(); }; +void WindowInterface::setHeight(qint32 height) const { this->proxyWindow()->setHeight(height); }; + +qreal WindowInterface::devicePixelRatio() const { return this->proxyWindow()->devicePixelRatio(); }; + +QuickshellScreenInfo* WindowInterface::screen() const { return this->proxyWindow()->screen(); }; +void WindowInterface::setScreen(QuickshellScreenInfo* screen) const { this->proxyWindow()->setScreen(screen); }; + +QColor WindowInterface::color() const { return this->proxyWindow()->color(); }; +void WindowInterface::setColor(QColor color) const { this->proxyWindow()->setColor(color); }; + +PendingRegion* WindowInterface::mask() const { return this->proxyWindow()->mask(); }; +void WindowInterface::setMask(PendingRegion* mask) const { this->proxyWindow()->setMask(mask); }; + +QsSurfaceFormat WindowInterface::surfaceFormat() const { return this->proxyWindow()->surfaceFormat(); }; +void WindowInterface::setSurfaceFormat(QsSurfaceFormat format) const { this->proxyWindow()->setSurfaceFormat(format); }; + +QQmlListProperty WindowInterface::data() const { return this->proxyWindow()->data(); }; +// clang-format on + +void WindowInterface::connectSignals() const { + auto* window = this->proxyWindow(); + // clang-format off + QObject::connect(window, &ProxyWindowBase::windowConnected, this, &WindowInterface::windowConnected); + QObject::connect(window, &ProxyWindowBase::visibleChanged, this, &WindowInterface::visibleChanged); + QObject::connect(window, &ProxyWindowBase::backerVisibilityChanged, this, &WindowInterface::backingWindowVisibleChanged); + QObject::connect(window, &ProxyWindowBase::implicitHeightChanged, this, &WindowInterface::implicitHeightChanged); + QObject::connect(window, &ProxyWindowBase::implicitWidthChanged, this, &WindowInterface::implicitWidthChanged); + QObject::connect(window, &ProxyWindowBase::heightChanged, this, &WindowInterface::heightChanged); + QObject::connect(window, &ProxyWindowBase::widthChanged, this, &WindowInterface::widthChanged); + QObject::connect(window, &ProxyWindowBase::devicePixelRatioChanged, this, &WindowInterface::devicePixelRatioChanged); + QObject::connect(window, &ProxyWindowBase::screenChanged, this, &WindowInterface::screenChanged); + QObject::connect(window, &ProxyWindowBase::windowTransformChanged, this, &WindowInterface::windowTransformChanged); + QObject::connect(window, &ProxyWindowBase::colorChanged, this, &WindowInterface::colorChanged); + QObject::connect(window, &ProxyWindowBase::maskChanged, this, &WindowInterface::maskChanged); + QObject::connect(window, &ProxyWindowBase::surfaceFormatChanged, this, &WindowInterface::surfaceFormatChanged); + // clang-format on +} + QsWindowAttached* WindowInterface::qmlAttachedProperties(QObject* object) { while (object && !qobject_cast(object)) { object = object->parent(); diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index b8edff2..5021ae8 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -197,41 +197,41 @@ public: // clang-format on [[nodiscard]] virtual ProxyWindowBase* proxyWindow() const = 0; - [[nodiscard]] virtual QQuickItem* contentItem() const = 0; + [[nodiscard]] QQuickItem* contentItem() const; - [[nodiscard]] virtual bool isVisible() const = 0; - [[nodiscard]] virtual bool isBackingWindowVisible() const = 0; - virtual void setVisible(bool visible) = 0; + [[nodiscard]] bool isVisible() const; + [[nodiscard]] bool isBackingWindowVisible() const; + void setVisible(bool visible) const; - [[nodiscard]] virtual qint32 implicitWidth() const = 0; - virtual void setImplicitWidth(qint32 implicitWidth) = 0; + [[nodiscard]] qint32 implicitWidth() const; + void setImplicitWidth(qint32 implicitWidth) const; - [[nodiscard]] virtual qint32 implicitHeight() const = 0; - virtual void setImplicitHeight(qint32 implicitHeight) = 0; + [[nodiscard]] qint32 implicitHeight() const; + void setImplicitHeight(qint32 implicitHeight) const; - [[nodiscard]] virtual qint32 width() const = 0; - virtual void setWidth(qint32 width) = 0; + [[nodiscard]] qint32 width() const; + void setWidth(qint32 width) const; - [[nodiscard]] virtual qint32 height() const = 0; - virtual void setHeight(qint32 height) = 0; + [[nodiscard]] qint32 height() const; + void setHeight(qint32 height) const; - [[nodiscard]] virtual qreal devicePixelRatio() const = 0; + [[nodiscard]] qreal devicePixelRatio() const; - [[nodiscard]] virtual QuickshellScreenInfo* screen() const = 0; - virtual void setScreen(QuickshellScreenInfo* screen) = 0; + [[nodiscard]] QuickshellScreenInfo* screen() const; + void setScreen(QuickshellScreenInfo* screen) const; [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT - [[nodiscard]] virtual QColor color() const = 0; - virtual void setColor(QColor color) = 0; + [[nodiscard]] QColor color() const; + void setColor(QColor color) const; - [[nodiscard]] virtual PendingRegion* mask() const = 0; - virtual void setMask(PendingRegion* mask) = 0; + [[nodiscard]] PendingRegion* mask() const; + void setMask(PendingRegion* mask) const; - [[nodiscard]] virtual QsSurfaceFormat surfaceFormat() const = 0; - virtual void setSurfaceFormat(QsSurfaceFormat format) = 0; + [[nodiscard]] QsSurfaceFormat surfaceFormat() const; + void setSurfaceFormat(QsSurfaceFormat format) const; - [[nodiscard]] virtual QQmlListProperty data() = 0; + [[nodiscard]] QQmlListProperty data() const; static QsWindowAttached* qmlAttachedProperties(QObject* object); @@ -249,6 +249,9 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + +protected: + void connectSignals() const; }; class QsWindowAttached: public QObject { diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index adba0ab..5d53fdd 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -414,23 +414,9 @@ void XPanelWindow::updateFocusable() { XPanelInterface::XPanelInterface(QObject* parent) : PanelWindowInterface(parent) , panel(new XPanelWindow(this)) { + this->connectSignals(); // clang-format off - QObject::connect(this->panel, &ProxyWindowBase::windowConnected, this, &XPanelInterface::windowConnected); - QObject::connect(this->panel, &ProxyWindowBase::visibleChanged, this, &XPanelInterface::visibleChanged); - QObject::connect(this->panel, &ProxyWindowBase::backerVisibilityChanged, this, &XPanelInterface::backingWindowVisibleChanged); - QObject::connect(this->panel, &ProxyWindowBase::implicitHeightChanged, this, &XPanelInterface::implicitHeightChanged); - QObject::connect(this->panel, &ProxyWindowBase::implicitWidthChanged, this, &XPanelInterface::implicitWidthChanged); - QObject::connect(this->panel, &ProxyWindowBase::heightChanged, this, &XPanelInterface::heightChanged); - QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged); - QObject::connect(this->panel, &ProxyWindowBase::devicePixelRatioChanged, this, &XPanelInterface::devicePixelRatioChanged); - QObject::connect(this->panel, &ProxyWindowBase::screenChanged, this, &XPanelInterface::screenChanged); - QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged); - QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged); - QObject::connect(this->panel, &ProxyWindowBase::maskChanged, this, &XPanelInterface::maskChanged); - QObject::connect(this->panel, &ProxyWindowBase::surfaceFormatChanged, this, &XPanelInterface::surfaceFormatChanged); - - // panel specific QObject::connect(this->panel, &XPanelWindow::anchorsChanged, this, &XPanelInterface::anchorsChanged); QObject::connect(this->panel, &XPanelWindow::marginsChanged, this, &XPanelInterface::marginsChanged); QObject::connect(this->panel, &XPanelWindow::exclusiveZoneChanged, this, &XPanelInterface::exclusiveZoneChanged); @@ -447,28 +433,13 @@ void XPanelInterface::onReload(QObject* oldInstance) { this->panel->reload(old != nullptr ? old->panel : nullptr); } -QQmlListProperty XPanelInterface::data() { return this->panel->data(); } ProxyWindowBase* XPanelInterface::proxyWindow() const { return this->panel; } -QQuickItem* XPanelInterface::contentItem() const { return this->panel->contentItem(); } -bool XPanelInterface::isBackingWindowVisible() const { return this->panel->isVisibleDirect(); } -qreal XPanelInterface::devicePixelRatio() const { return this->panel->devicePixelRatio(); } // NOLINTBEGIN #define proxyPair(type, get, set) \ type XPanelInterface::get() const { return this->panel->get(); } \ void XPanelInterface::set(type value) { this->panel->set(value); } -proxyPair(bool, isVisible, setVisible); -proxyPair(qint32, implicitWidth, setImplicitWidth); -proxyPair(qint32, implicitHeight, setImplicitHeight); -proxyPair(qint32, width, setWidth); -proxyPair(qint32, height, setHeight); -proxyPair(QuickshellScreenInfo*, screen, setScreen); -proxyPair(QColor, color, setColor); -proxyPair(PendingRegion*, mask, setMask); -proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); - -// panel specific proxyPair(Anchors, anchors, setAnchors); proxyPair(Margins, margins, setMargins); proxyPair(qint32, exclusiveZone, setExclusiveZone); diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 02c05b1..ab36826 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -135,43 +135,8 @@ public: void onReload(QObject* oldInstance) override; [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - [[nodiscard]] QQuickItem* contentItem() const override; // NOLINTBEGIN - [[nodiscard]] bool isVisible() const override; - [[nodiscard]] bool isBackingWindowVisible() const override; - void setVisible(bool visible) override; - - [[nodiscard]] qint32 implicitWidth() const override; - void setImplicitWidth(qint32 implicitWidth) override; - - [[nodiscard]] qint32 implicitHeight() const override; - void setImplicitHeight(qint32 implicitHeight) override; - - [[nodiscard]] qint32 width() const override; - void setWidth(qint32 width) override; - - [[nodiscard]] qint32 height() const override; - void setHeight(qint32 height) override; - - [[nodiscard]] virtual qreal devicePixelRatio() const override; - - [[nodiscard]] QuickshellScreenInfo* screen() const override; - void setScreen(QuickshellScreenInfo* screen) override; - - [[nodiscard]] QColor color() const override; - void setColor(QColor color) override; - - [[nodiscard]] PendingRegion* mask() const override; - void setMask(PendingRegion* mask) override; - - [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; - void setSurfaceFormat(QsSurfaceFormat mask) override; - - [[nodiscard]] QQmlListProperty data() override; - - // panel specific - [[nodiscard]] Anchors anchors() const override; void setAnchors(Anchors anchors) override; From a2146f6394ec91b60b718b44051dcd9b6d53dd7b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 15 Jul 2025 15:35:48 -0700 Subject: [PATCH 060/226] core/window: add closed() signal to all window types --- src/window/proxywindow.cpp | 12 +++++++++++- src/window/proxywindow.hpp | 2 ++ src/window/windowinterface.cpp | 1 + src/window/windowinterface.hpp | 3 +++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 56d250c..ac5127b 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -188,7 +188,7 @@ void ProxyWindowBase::connectWindow() { this->window->setProxy(this); // clang-format off - QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); + QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::onVisibleChanged); QObject::connect(this->window, &QWindow::xChanged, this, &ProxyWindowBase::xChanged); QObject::connect(this->window, &QWindow::yChanged, this, &ProxyWindowBase::yChanged); QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged); @@ -226,6 +226,16 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } +void ProxyWindowBase::onVisibleChanged() { + if (this->mVisible && !this->window->isVisible()) { + this->mVisible = false; + this->setVisibleDirect(false); + emit this->closed(); + } + + emit this->visibleChanged(); +} + bool ProxyWindowBase::deleteOnInvisible() const { return false; } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 3fbc08e..ba35d59 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -141,6 +141,7 @@ public: [[nodiscard]] QQmlListProperty data(); signals: + void closed(); void windowConnected(); void windowDestroyed(); void visibleChanged(); @@ -160,6 +161,7 @@ signals: void polished(); protected slots: + void onVisibleChanged(); virtual void onWidthChanged(); virtual void onHeightChanged(); void onMaskChanged(); diff --git a/src/window/windowinterface.cpp b/src/window/windowinterface.cpp index d808c80..aac0df3 100644 --- a/src/window/windowinterface.cpp +++ b/src/window/windowinterface.cpp @@ -133,6 +133,7 @@ QQmlListProperty WindowInterface::data() const { return this->proxyWind void WindowInterface::connectSignals() const { auto* window = this->proxyWindow(); // clang-format off + QObject::connect(window, &ProxyWindowBase::closed, this, &WindowInterface::closed); QObject::connect(window, &ProxyWindowBase::windowConnected, this, &WindowInterface::windowConnected); QObject::connect(window, &ProxyWindowBase::visibleChanged, this, &WindowInterface::visibleChanged); QObject::connect(window, &ProxyWindowBase::backerVisibilityChanged, this, &WindowInterface::backingWindowVisibleChanged); diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index 5021ae8..e3ed84a 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -236,6 +236,9 @@ public: static QsWindowAttached* qmlAttachedProperties(QObject* object); signals: + /// This signal is emitted when the window is closed by the user, the display server, + /// or an error. It is not emitted when @@visible is set to false. + void closed(); void windowConnected(); void visibleChanged(); void backingWindowVisibleChanged(); From 3dfb7d8827acd34fc17617deab68c9f0990782e1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 15 Jul 2025 15:36:28 -0700 Subject: [PATCH 061/226] core/window: handle graphics context loss --- src/window/proxywindow.cpp | 20 ++++++++++++++++++++ src/window/proxywindow.hpp | 8 ++++++-- src/window/windowinterface.cpp | 1 + src/window/windowinterface.hpp | 6 ++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index ac5127b..618751a 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -1,6 +1,7 @@ #include "proxywindow.hpp" #include +#include #include #include #include @@ -112,6 +113,8 @@ void ProxyWindowBase::ensureQWindow() { auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque : this->mColor.alpha() >= 255; + format.setOption(QSurfaceFormat::ResetNotification); + if (opaque) format.setAlphaBufferSize(0); else format.setAlphaBufferSize(8); @@ -195,6 +198,7 @@ void ProxyWindowBase::connectWindow() { QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged); QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); + QObject::connect(this->window, &QQuickWindow::sceneGraphError, this, &ProxyWindowBase::onSceneGraphError); QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::onExposed); QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged); // clang-format on @@ -226,6 +230,22 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } +void ProxyWindowBase::onSceneGraphError( + QQuickWindow::SceneGraphError error, + const QString& message +) { + if (error == QQuickWindow::ContextNotAvailable) { + qCritical().nospace() << "Failed to create graphics context for " << this << ": " << message; + } else { + qCritical().nospace() << "Scene graph error " << error << " occurred for " << this << ": " + << message; + } + + emit this->resourcesLost(); + this->mVisible = false; + this->setVisibleDirect(false); +} + void ProxyWindowBase::onVisibleChanged() { if (this->mVisible && !this->window->isVisible()) { this->mVisible = false; diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index ba35d59..025b970 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -142,6 +142,7 @@ public: signals: void closed(); + void resourcesLost(); void windowConnected(); void windowDestroyed(); void visibleChanged(); @@ -161,13 +162,16 @@ signals: void polished(); protected slots: - void onVisibleChanged(); virtual void onWidthChanged(); virtual void onHeightChanged(); + virtual void onPolished(); + +private slots: + void onSceneGraphError(QQuickWindow::SceneGraphError error, const QString& message); + void onVisibleChanged(); void onMaskChanged(); void onMaskDestroyed(); void onScreenDestroyed(); - virtual void onPolished(); void onExposed(); protected: diff --git a/src/window/windowinterface.cpp b/src/window/windowinterface.cpp index aac0df3..8917f12 100644 --- a/src/window/windowinterface.cpp +++ b/src/window/windowinterface.cpp @@ -134,6 +134,7 @@ void WindowInterface::connectSignals() const { auto* window = this->proxyWindow(); // clang-format off QObject::connect(window, &ProxyWindowBase::closed, this, &WindowInterface::closed); + QObject::connect(window, &ProxyWindowBase::resourcesLost, this, &WindowInterface::resourcesLost); QObject::connect(window, &ProxyWindowBase::windowConnected, this, &WindowInterface::windowConnected); QObject::connect(window, &ProxyWindowBase::visibleChanged, this, &WindowInterface::visibleChanged); QObject::connect(window, &ProxyWindowBase::backerVisibilityChanged, this, &WindowInterface::backingWindowVisibleChanged); diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index e3ed84a..9e917b9 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -239,6 +239,12 @@ signals: /// This signal is emitted when the window is closed by the user, the display server, /// or an error. It is not emitted when @@visible is set to false. void closed(); + /// This signal is emitted when resources a window depends on to display are lost, + /// or could not be acquired during window creation. The most common trigger for + /// this signal is a lack of VRAM when creating or resizing a window. + /// + /// Following this signal, @@closed(s) will be sent. + void resourcesLost(); void windowConnected(); void visibleChanged(); void backingWindowVisibleChanged(); From c40074dd5684df0efa1eda7aa0820d7be2a3e43e Mon Sep 17 00:00:00 2001 From: ipg0 Date: Tue, 15 Jul 2025 00:15:55 +0300 Subject: [PATCH 062/226] service/notifications: add inline-reply action support Signed-off-by: ipg0 --- src/services/notifications/notification.cpp | 41 +++++++++++++++++-- src/services/notifications/notification.hpp | 18 ++++++++ .../org.freedesktop.Notifications.xml | 5 +++ src/services/notifications/qml.cpp | 9 ++++ src/services/notifications/qml.hpp | 6 +++ src/services/notifications/server.cpp | 1 + src/services/notifications/server.hpp | 2 + 7 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 96a2ff0..c5269f3 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -78,6 +78,29 @@ void Notification::close(NotificationCloseReason::Enum reason) { } } +void Notification::sendInlineReply(const QString& replyText) { + if (!NotificationServer::instance()->support.inlineReply) { + qCritical() << "Inline reply support disabled on server"; + return; + } + + if (!this->bHasInlineReply) { + qCritical() << "Cannot send reply to notification without inline-reply action"; + return; + } + + if (this->isRetained()) { + qCritical() << "Cannot send reply to destroyed notification" << this; + return; + } + + NotificationServer::instance()->NotificationReplied(this->id(), replyText); + + if (!this->bindableResident().value()) { + this->close(NotificationCloseReason::Dismissed); + } +} + void Notification::updateProperties( const QString& appName, QString appIcon, @@ -147,17 +170,27 @@ void Notification::updateProperties( this->bImage = imagePath; this->bHints = hints; - Qt::endPropertyUpdateGroup(); - bool actionsChanged = false; auto deletedActions = QVector(); if (actions.length() % 2 == 0) { int ai = 0; for (auto i = 0; i != actions.length(); i += 2) { - ai = i / 2; const auto& identifier = actions.at(i); const auto& text = actions.at(i + 1); + + if (identifier == "inline-reply" && NotificationServer::instance()->support.inlineReply) { + if (this->bHasInlineReply) { + qCWarning(logNotifications) << this << '(' << appName << ')' + << "sent an action set with duplicate inline-reply actions."; + } else { + this->bHasInlineReply = true; + this->bInlineReplyPlaceholder = text; + } + // skip inserting this action into action list + continue; + } + auto* action = ai < this->mActions.length() ? this->mActions.at(ai) : nullptr; if (action && identifier == action->identifier()) { @@ -188,6 +221,8 @@ void Notification::updateProperties( << "sent an action set of an invalid length."; } + Qt::endPropertyUpdateGroup(); + if (actionsChanged) emit this->actionsChanged(); for (auto* action: deletedActions) { diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index f0c65bb..06c871b 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -107,6 +107,12 @@ class Notification /// /// This image is often something like a profile picture in instant messaging applications. Q_PROPERTY(QString image READ default NOTIFY imageChanged BINDABLE bindableImage); + /// If true, the notification has an inline reply action. + /// + /// A quick reply text field should be displayed and the reply can be sent using @@sendInlineReply(). + Q_PROPERTY(bool hasInlineReply READ default NOTIFY hasInlineReplyChanged BINDABLE bindableHasInlineReply); + /// The placeholder text/button caption for the inline reply. + Q_PROPERTY(QString inlineReplyPlaceholder READ default NOTIFY inlineReplyPlaceholderChanged BINDABLE bindableInlineReplyPlaceholder); /// All hints sent by the client application as a javascript object. /// Many common hints are exposed via other properties. Q_PROPERTY(QVariantMap hints READ default NOTIFY hintsChanged BINDABLE bindableHints); @@ -124,6 +130,12 @@ public: /// explicitly closed by the user. Q_INVOKABLE void dismiss(); + /// Send an inline reply to the notification with an inline reply action. + /// > [!WARNING] This method can only be called if + /// > @@hasInlineReply is true + /// > and the server has @@NotificationServer.inlineReplySupported set to true. + Q_INVOKABLE void sendInlineReply(const QString& replyText); + void updateProperties( const QString& appName, QString appIcon, @@ -158,6 +170,8 @@ public: [[nodiscard]] QBindable bindableTransient() const { return &this->bTransient; }; [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; [[nodiscard]] QBindable bindableImage() const { return &this->bImage; }; + [[nodiscard]] QBindable bindableHasInlineReply() const { return &this->bHasInlineReply; }; + [[nodiscard]] QBindable bindableInlineReplyPlaceholder() const { return &this->bInlineReplyPlaceholder; }; [[nodiscard]] QBindable bindableHints() const { return &this->bHints; }; [[nodiscard]] NotificationCloseReason::Enum closeReason() const; @@ -182,6 +196,8 @@ signals: void transientChanged(); void desktopEntryChanged(); void imageChanged(); + void hasInlineReplyChanged(); + void inlineReplyPlaceholderChanged(); void hintsChanged(); private: @@ -202,6 +218,8 @@ private: Q_OBJECT_BINDABLE_PROPERTY(Notification, bool, bTransient, &Notification::transientChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bDesktopEntry, &Notification::desktopEntryChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bImage, &Notification::imageChanged); + Q_OBJECT_BINDABLE_PROPERTY(Notification, bool, bHasInlineReply, &Notification::hasInlineReplyChanged); + Q_OBJECT_BINDABLE_PROPERTY(Notification, QString, bInlineReplyPlaceholder, &Notification::inlineReplyPlaceholderChanged); Q_OBJECT_BINDABLE_PROPERTY(Notification, QVariantMap, bHints, &Notification::hintsChanged); // clang-format on diff --git a/src/services/notifications/org.freedesktop.Notifications.xml b/src/services/notifications/org.freedesktop.Notifications.xml index 1a2001f..3d99db0 100644 --- a/src/services/notifications/org.freedesktop.Notifications.xml +++ b/src/services/notifications/org.freedesktop.Notifications.xml @@ -38,6 +38,11 @@ + + + + + diff --git a/src/services/notifications/qml.cpp b/src/services/notifications/qml.cpp index 9981821..42bb23a 100644 --- a/src/services/notifications/qml.cpp +++ b/src/services/notifications/qml.cpp @@ -115,6 +115,15 @@ void NotificationServerQml::setImageSupported(bool imageSupported) { emit this->imageSupportedChanged(); } +bool NotificationServerQml::inlineReplySupported() const { return this->support.inlineReply; } + +void NotificationServerQml::setInlineReplySupported(bool inlineReplySupported) { + if (inlineReplySupported == this->support.inlineReply) return; + this->support.inlineReply = inlineReplySupported; + this->updateSupported(); + emit this->inlineReplySupportedChanged(); +} + QVector NotificationServerQml::extraHints() const { return this->support.extraHints; } void NotificationServerQml::setExtraHints(QVector extraHints) { diff --git a/src/services/notifications/qml.hpp b/src/services/notifications/qml.hpp index feb33db..88132c7 100644 --- a/src/services/notifications/qml.hpp +++ b/src/services/notifications/qml.hpp @@ -65,6 +65,8 @@ class NotificationServerQml: public PostReloadHook { Q_PROPERTY(bool actionIconsSupported READ actionIconsSupported WRITE setActionIconsSupported NOTIFY actionIconsSupportedChanged); /// If the notification server should advertise that it supports images. Defaults to false. Q_PROPERTY(bool imageSupported READ imageSupported WRITE setImageSupported NOTIFY imageSupportedChanged); + /// If the notification server should advertise that it supports inline replies. Defaults to false. + Q_PROPERTY(bool inlineReplySupported READ inlineReplySupported WRITE setInlineReplySupported NOTIFY inlineReplySupportedChanged); /// All notifications currently tracked by the server. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* trackedNotifications READ trackedNotifications NOTIFY trackedNotificationsChanged); @@ -103,6 +105,9 @@ public: [[nodiscard]] bool imageSupported() const; void setImageSupported(bool imageSupported); + [[nodiscard]] bool inlineReplySupported() const; + void setInlineReplySupported(bool inlineReplySupported); + [[nodiscard]] QVector extraHints() const; void setExtraHints(QVector extraHints); @@ -123,6 +128,7 @@ signals: void actionsSupportedChanged(); void actionIconsSupportedChanged(); void imageSupportedChanged(); + void inlineReplySupportedChanged(); void extraHintsChanged(); void trackedNotificationsChanged(); diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 18a898a..ac1e905 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -155,6 +155,7 @@ QStringList NotificationServer::GetCapabilities() const { } if (this->support.image) capabilities += "icon-static"; + if (this->support.inlineReply) capabilities += "inline-reply"; capabilities += this->support.extraHints; diff --git a/src/services/notifications/server.hpp b/src/services/notifications/server.hpp index 8c20943..8bd92a3 100644 --- a/src/services/notifications/server.hpp +++ b/src/services/notifications/server.hpp @@ -23,6 +23,7 @@ struct NotificationServerSupport { bool actions = false; bool actionIcons = false; bool image = false; + bool inlineReply = false; QVector extraHints; }; @@ -60,6 +61,7 @@ signals: // NOLINTBEGIN void NotificationClosed(quint32 id, quint32 reason); void ActionInvoked(quint32 id, QString action); + void NotificationReplied(quint32 id, QString replyText); // NOLINTEND private slots: From a45fc03c7dc60acc3fbbb9fce46519267ca23510 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 15 Jul 2025 15:58:03 -0700 Subject: [PATCH 063/226] service/tray: fix missing documentation for invokables '};' prior to invokables caused the docgen regex to miss them --- src/services/status_notifier/item.hpp | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 60f3a98..5ce5a7f 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -126,13 +126,6 @@ class StatusNotifierItem: public QObject { public: explicit StatusNotifierItem(const QString& address, QObject* parent = nullptr); - [[nodiscard]] bool isValid() const; - [[nodiscard]] bool isReady() const; - [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; }; - [[nodiscard]] QPixmap createPixmap(const QSize& size) const; - - [[nodiscard]] dbus::dbusmenu::DBusMenuHandle* menuHandle(); - /// Primary activation action, generally triggered via a left click. Q_INVOKABLE void activate(); /// Secondary activation action, generally triggered via a middle click. @@ -142,14 +135,21 @@ public: /// Display a platform menu at the given location relative to the parent window. Q_INVOKABLE void display(QObject* parentWindow, qint32 relativeX, qint32 relativeY); - [[nodiscard]] QBindable bindableId() const { return &this->bId; }; - [[nodiscard]] QBindable bindableTitle() const { return &this->bTitle; }; - [[nodiscard]] QBindable bindableStatus() const { return &this->bStatus; }; - [[nodiscard]] QBindable bindableCategory() const { return &this->bCategory; }; - [[nodiscard]] QString tooltipTitle() const { return this->bTooltip.value().title; }; - [[nodiscard]] QString tooltipDescription() const { return this->bTooltip.value().description; }; - [[nodiscard]] QBindable bindableHasMenu() const { return &this->bHasMenu; }; - [[nodiscard]] QBindable bindableOnlyMenu() const { return &this->bIsMenu; }; + [[nodiscard]] bool isValid() const; + [[nodiscard]] bool isReady() const; + [[nodiscard]] QBindable bindableIcon() const { return &this->bIcon; } + [[nodiscard]] QPixmap createPixmap(const QSize& size) const; + + [[nodiscard]] dbus::dbusmenu::DBusMenuHandle* menuHandle(); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableTitle() const { return &this->bTitle; } + [[nodiscard]] QBindable bindableStatus() const { return &this->bStatus; } + [[nodiscard]] QBindable bindableCategory() const { return &this->bCategory; } + [[nodiscard]] QString tooltipTitle() const { return this->bTooltip.value().title; } + [[nodiscard]] QString tooltipDescription() const { return this->bTooltip.value().description; } + [[nodiscard]] QBindable bindableHasMenu() const { return &this->bHasMenu; } + [[nodiscard]] QBindable bindableOnlyMenu() const { return &this->bIsMenu; } signals: void ready(); From 4d8055f1cd9924bcace59405894b8879633eb83d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 15 Jul 2025 19:03:27 -0700 Subject: [PATCH 064/226] build: fix PostReloadHook resolution in LSP --- src/core/reload.hpp | 4 ++++ src/io/CMakeLists.txt | 1 + src/io/jsonadapter.hpp | 1 + src/services/notifications/CMakeLists.txt | 1 - src/services/notifications/notification.hpp | 6 +++++- src/services/pam/qml.hpp | 4 +++- src/wayland/hyprland/CMakeLists.txt | 1 + src/wayland/hyprland/focus_grab/qml.hpp | 3 ++- src/widgets/wrapper.hpp | 4 +++- 9 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 1d4e375..1117eb8 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -122,6 +122,10 @@ private: class PostReloadHook : public QObject , public QQmlParserStatus { + Q_OBJECT; + QML_ANONYMOUS; + Q_INTERFACES(QQmlParserStatus); + public: PostReloadHook(QObject* parent = nullptr): QObject(parent) {} void classBegin() override {} diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 8b5c20a..17628d3 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -21,6 +21,7 @@ qt_add_qml_module(quickshell-io FileView.qml ) +qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-io) target_link_libraries(quickshell-io PRIVATE Qt::Quick) diff --git a/src/io/jsonadapter.hpp b/src/io/jsonadapter.hpp index a447c41..276d6a7 100644 --- a/src/io/jsonadapter.hpp +++ b/src/io/jsonadapter.hpp @@ -91,6 +91,7 @@ class JsonAdapter , public QQmlParserStatus { Q_OBJECT; QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); public: void classBegin() override {} diff --git a/src/services/notifications/CMakeLists.txt b/src/services/notifications/CMakeLists.txt index 0cbb42e..58b6648 100644 --- a/src/services/notifications/CMakeLists.txt +++ b/src/services/notifications/CMakeLists.txt @@ -23,7 +23,6 @@ qt_add_qml_module(quickshell-service-notifications ) qs_add_module_deps_light(quickshell-service-notifications Quickshell) - install_qml_module(quickshell-service-notifications) target_link_libraries(quickshell-service-notifications PRIVATE Qt::Quick Qt::DBus) diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index 06c871b..fc3f30b 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -171,7 +171,11 @@ public: [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; [[nodiscard]] QBindable bindableImage() const { return &this->bImage; }; [[nodiscard]] QBindable bindableHasInlineReply() const { return &this->bHasInlineReply; }; - [[nodiscard]] QBindable bindableInlineReplyPlaceholder() const { return &this->bInlineReplyPlaceholder; }; + + [[nodiscard]] QBindable bindableInlineReplyPlaceholder() const { + return &this->bInlineReplyPlaceholder; + }; + [[nodiscard]] QBindable bindableHints() const { return &this->bHints; }; [[nodiscard]] NotificationCloseReason::Enum closeReason() const; diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp index 805e04c..a8ffcc3 100644 --- a/src/services/pam/qml.hpp +++ b/src/services/pam/qml.hpp @@ -17,6 +17,9 @@ class PamContext : public QObject , public QQmlParserStatus { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); + // clang-format off /// If the pam context is actively performing an authentication. /// @@ -49,7 +52,6 @@ class PamContext /// If the user's response should be visible. Only valid when @@responseRequired is true. Q_PROPERTY(bool responseVisible READ isResponseVisible NOTIFY responseVisibleChanged); // clang-format on - QML_ELEMENT; public: explicit PamContext(QObject* parent = nullptr): QObject(parent) {} diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 570cbe5..66b32b6 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -30,6 +30,7 @@ qt_add_qml_module(quickshell-hyprland IMPORTS ${HYPRLAND_MODULES} ) +qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-hyprland) # intentionally no pch as the module is empty diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp index 4ba7227..705b0d3 100644 --- a/src/wayland/hyprland/focus_grab/qml.hpp +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -56,6 +56,8 @@ class HyprlandFocusGrab : public QObject , public QQmlParserStatus { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); /// If the focus grab is active. Defaults to false. /// /// When set to true, an input grab will be created for the listed windows. @@ -66,7 +68,6 @@ class HyprlandFocusGrab Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); /// The list of windows to whitelist for input. Q_PROPERTY(QList windows READ windows WRITE setWindows NOTIFY windowsChanged); - QML_ELEMENT; public: explicit HyprlandFocusGrab(QObject* parent = nullptr): QObject(parent) {} diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp index d506750..f0a2a13 100644 --- a/src/widgets/wrapper.hpp +++ b/src/widgets/wrapper.hpp @@ -85,6 +85,9 @@ class WrapperManager : public QObject , public QQmlParserStatus { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); + // clang-format off /// The wrapper component's selected child. /// @@ -102,7 +105,6 @@ class WrapperManager /// This property may not be changed after Component.onCompleted. Q_PROPERTY(QQuickItem* wrapper READ wrapper WRITE setWrapper NOTIFY wrapperChanged FINAL); // clang-format on - QML_ELEMENT; public: explicit WrapperManager(QObject* parent = nullptr): QObject(parent) {} From 986749cdb9ca9078b66297d60bbf21d48e33a6cf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 16 Jul 2025 14:35:46 -0700 Subject: [PATCH 065/226] tooling: add automatic QMLLS support for new imports and singletons --- src/core/CMakeLists.txt | 1 + src/core/paths.cpp | 27 ++++++ src/core/paths.hpp | 3 + src/core/rootwrapper.cpp | 27 +++++- src/core/rootwrapper.hpp | 3 + src/core/toolsupport.cpp | 205 +++++++++++++++++++++++++++++++++++++++ src/core/toolsupport.hpp | 20 ++++ 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 src/core/toolsupport.cpp create mode 100644 src/core/toolsupport.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eca7270..7cef987 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -38,6 +38,7 @@ qt_add_library(quickshell-core STATIC iconprovider.cpp scriptmodel.cpp colorquantizer.cpp + toolsupport.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 1f3c494..e17c3bc 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -135,6 +135,33 @@ QDir* QsPaths::instanceRunDir() { else return &this->mInstanceRunDir; } +QDir* QsPaths::shellVfsDir() { + if (this->shellVfsState == DirState::Unknown) { + if (auto* baseRunDir = this->baseRunDir()) { + this->mShellVfsDir = QDir(baseRunDir->filePath("vfs")); + this->mShellVfsDir = QDir(this->mShellVfsDir.filePath(this->shellId)); + + qCDebug(logPaths) << "Initialized runtime vfs path:" << this->mShellVfsDir.path(); + + if (!this->mShellVfsDir.mkpath(".")) { + qCCritical(logPaths) << "Could not create runtime vfs directory at" + << this->mShellVfsDir.path(); + this->shellVfsState = DirState::Failed; + } else { + this->shellVfsState = DirState::Ready; + } + } else { + qCCritical(logPaths) << "Could not create shell runtime vfs path as it was not possible to " + "create the base runtime path."; + + this->shellVfsState = DirState::Failed; + } + } + + if (this->shellVfsState == DirState::Failed) return nullptr; + else return &this->mShellVfsDir; +} + void QsPaths::linkRunDir() { if (auto* runDir = this->instanceRunDir()) { auto pidDir = QDir(this->baseRunDir()->filePath("by-pid")); diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 9646ca4..178bcda 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -28,6 +28,7 @@ public: QDir* baseRunDir(); QDir* shellRunDir(); + QDir* shellVfsDir(); QDir* instanceRunDir(); void linkRunDir(); void linkPathDir(); @@ -48,9 +49,11 @@ private: QString pathId; QDir mBaseRunDir; QDir mShellRunDir; + QDir mShellVfsDir; QDir mInstanceRunDir; DirState baseRunState = DirState::Unknown; DirState shellRunState = DirState::Unknown; + DirState shellVfsState = DirState::Unknown; DirState instanceRunState = DirState::Unknown; QDir mShellDataDir; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 2968402..7dc1068 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -18,15 +19,26 @@ #include "instanceinfo.hpp" #include "qmlglobal.hpp" #include "scan.hpp" +#include "toolsupport.hpp" RootWrapper::RootWrapper(QString rootPath, QString shellId) : QObject(nullptr) , rootPath(std::move(rootPath)) , shellId(std::move(shellId)) , originalWorkingDirectory(QDir::current().absolutePath()) { - // clang-format off - QObject::connect(QuickshellSettings::instance(), &QuickshellSettings::watchFilesChanged, this, &RootWrapper::onWatchFilesChanged); - // clang-format on + QObject::connect( + QuickshellSettings::instance(), + &QuickshellSettings::watchFilesChanged, + this, + &RootWrapper::onWatchFilesChanged + ); + + QObject::connect( + &this->configDirWatcher, + &QFileSystemWatcher::directoryChanged, + this, + &RootWrapper::updateTooling + ); this->reloadGraph(true); @@ -48,6 +60,9 @@ void RootWrapper::reloadGraph(bool hard) { auto scanner = QmlScanner(rootPath); scanner.scanQmlFile(this->rootPath); + qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); + this->configDirWatcher.addPath(rootPath.path()); + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); generation->wrapper = this; @@ -168,3 +183,9 @@ void RootWrapper::onWatchFilesChanged() { } void RootWrapper::onWatchedFilesChanged() { this->reloadGraph(false); } + +void RootWrapper::updateTooling() { + if (!this->generation) return; + auto configDir = QFileInfo(this->rootPath).dir(); + qs::core::QmlToolingSupport::updateTooling(configDir, this->generation->scanner); +} diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 02d7a14..1425d17 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -22,10 +23,12 @@ private slots: void generationDestroyed(); void onWatchFilesChanged(); void onWatchedFilesChanged(); + void updateTooling(); private: QString rootPath; QString shellId; EngineGeneration* generation = nullptr; QString originalWorkingDirectory; + QFileSystemWatcher configDirWatcher; }; diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp new file mode 100644 index 0000000..febb97f --- /dev/null +++ b/src/core/toolsupport.cpp @@ -0,0 +1,205 @@ +#include "toolsupport.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "logcat.hpp" +#include "paths.hpp" +#include "scan.hpp" + +namespace qs::core { + +namespace { +QS_LOGGING_CATEGORY(logTooling, "quickshell.tooling", QtWarningMsg); +} + +bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanner) { + auto* vfs = QsPaths::instance()->shellVfsDir(); + + if (!vfs) { + qCCritical(logTooling) << "Tooling dir could not be created"; + return false; + } + + if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) { + QDir(vfs->filePath("qs")).removeRecursively(); + return false; + } + + QmlToolingSupport::updateToolingFs(scanner, configRoot, vfs->filePath("qs")); + return true; +} + +QString QmlToolingSupport::getQmllsConfig() { + static auto config = []() { + QList importPaths; + + auto addPaths = [&](const QList& paths) { + for (const auto& path: paths) { + if (!importPaths.contains(path)) importPaths.append(path); + } + }; + + addPaths(qEnvironmentVariable("QML_IMPORT_PATH").split(u':', Qt::SkipEmptyParts)); + addPaths(qEnvironmentVariable("QML2_IMPORT_PATH").split(u':', Qt::SkipEmptyParts)); + + auto vfsPath = QsPaths::instance()->shellVfsDir()->path(); + auto importPathsStr = importPaths.join(u':'); + + QString qmllsConfig; + auto print = QDebug(&qmllsConfig).nospace(); + print << "[General]\nno-cmake-calls=true\nbuildDir=" << vfsPath + << "\nimportPaths=" << importPathsStr << '\n'; + + return qmllsConfig; + }(); + + return config; +} + +bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) { + auto shellConfigPath = configRoot.filePath(".qmlls.ini"); + auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini"); + + auto shellFileInfo = QFileInfo(shellConfigPath); + if (!create && !shellFileInfo.exists()) { + if (QmlToolingSupport::toolingEnabled) { + qInfo() << "QML tooling support disabled"; + QmlToolingSupport::toolingEnabled = false; + } + + QFile::remove(vfsConfigPath); + return false; + } + + auto vfsFile = QFile(vfsConfigPath); + + if (!vfsFile.open(QFile::ReadWrite | QFile::Text)) { + qCCritical(logTooling) << "Failed to create qmlls config in vfs"; + return false; + } + + auto config = QmlToolingSupport::getQmllsConfig(); + + if (vfsFile.readAll() != config) { + if (!vfsFile.resize(0) || !vfsFile.write(config.toUtf8())) { + qCCritical(logTooling) << "Failed to write qmlls config in vfs"; + return false; + } + + qCDebug(logTooling) << "Wrote qmlls config in vfs"; + } + + if (!shellFileInfo.isSymLink() || shellFileInfo.symLinkTarget() != vfsConfigPath) { + QFile::remove(shellConfigPath); + + if (!QFile::link(vfsConfigPath, shellConfigPath)) { + qCCritical(logTooling) << "Failed to create qmlls config symlink"; + return false; + } + + qCDebug(logTooling) << "Created qmlls config symlink"; + } + + if (!QmlToolingSupport::toolingEnabled) { + qInfo() << "QML tooling support enabled"; + QmlToolingSupport::toolingEnabled = true; + } + + return true; +} + +void QmlToolingSupport::updateToolingFs( + QmlScanner& scanner, + const QDir& scanDir, + const QDir& linkDir +) { + QList files; + QSet subdirs; + + auto scanPath = scanDir.path(); + + linkDir.mkpath("."); + + for (auto& path: scanner.scannedFiles) { + if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue; + auto name = path.sliced(scanPath.length() + 1); + + if (name.contains('/')) { + auto dirname = name.first(name.indexOf('/')); + subdirs.insert(dirname); + continue; + } + + auto fileInfo = QFileInfo(path); + if (!fileInfo.isFile()) continue; + + auto spath = linkDir.filePath(name); + auto sFileInfo = QFileInfo(spath); + + if (!sFileInfo.isSymLink() || sFileInfo.symLinkTarget() != path) { + QFile::remove(spath); + + if (QFile::link(path, spath)) { + qCDebug(logTooling) << "Created symlink to" << path << "at" << spath; + files.append(spath); + } else { + qCCritical(logTooling) << "Could not create symlink to" << path << "at" << spath; + } + } else { + files.append(spath); + } + } + + for (auto [path, text]: scanner.fileIntercepts.asKeyValueRange()) { + if (path.length() < scanPath.length() + 1 || !path.startsWith(scanPath)) continue; + auto name = path.sliced(scanPath.length() + 1); + + if (name.contains('/')) { + auto dirname = name.first(name.indexOf('/')); + subdirs.insert(dirname); + continue; + } + + auto spath = linkDir.filePath(name); + auto file = QFile(spath); + if (!file.open(QFile::ReadWrite | QFile::Text)) { + qCCritical(logTooling) << "Failed to open injected file" << spath; + continue; + } + + if (file.readAll() == text) { + files.append(spath); + continue; + } + + if (file.resize(0) && file.write(text.toUtf8())) { + files.append(spath); + qCDebug(logTooling) << "Wrote injected file" << spath; + } else { + qCCritical(logTooling) << "Failed to write injected file" << spath; + } + } + + for (auto& name: linkDir.entryList(QDir::Files | QDir::System)) { // System = broken symlinks + auto path = linkDir.filePath(name); + + if (!files.contains(path)) { + if (QFile::remove(path)) qCDebug(logTooling) << "Removed old file at" << path; + else qCWarning(logTooling) << "Failed to remove old file at" << path; + } + } + + for (const auto& subdir: subdirs) { + QmlToolingSupport::updateToolingFs(scanner, scanDir.filePath(subdir), linkDir.filePath(subdir)); + } +} + +} // namespace qs::core diff --git a/src/core/toolsupport.hpp b/src/core/toolsupport.hpp new file mode 100644 index 0000000..0aee9c5 --- /dev/null +++ b/src/core/toolsupport.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "scan.hpp" + +namespace qs::core { + +class QmlToolingSupport { +public: + static bool updateTooling(const QDir& configRoot, QmlScanner& scanner); + +private: + static QString getQmllsConfig(); + static bool updateQmllsConfig(const QDir& configRoot, bool create); + static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir); + static inline bool toolingEnabled = false; +}; + +} // namespace qs::core From 78e3874ac67a570abf9c800bdac3250b44dd3844 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 16 Jul 2025 17:46:53 -0700 Subject: [PATCH 066/226] tooling: add per-shell tooling lock to prevent races --- src/core/toolsupport.cpp | 39 +++++++++++++++++++++++++++++++++++++++ src/core/toolsupport.hpp | 2 ++ 2 files changed, 41 insertions(+) diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp index febb97f..5622d92 100644 --- a/src/core/toolsupport.cpp +++ b/src/core/toolsupport.cpp @@ -1,5 +1,7 @@ #include "toolsupport.hpp" +#include +#include #include #include #include @@ -28,6 +30,10 @@ bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanne return false; } + if (!QmlToolingSupport::lockTooling()) { + return false; + } + if (!QmlToolingSupport::updateQmllsConfig(configRoot, false)) { QDir(vfs->filePath("qs")).removeRecursively(); return false; @@ -37,6 +43,39 @@ bool QmlToolingSupport::updateTooling(const QDir& configRoot, QmlScanner& scanne return true; } +bool QmlToolingSupport::lockTooling() { + if (QmlToolingSupport::toolingLock) return true; + + auto lockPath = QsPaths::instance()->shellVfsDir()->filePath("tooling.lock"); + auto* file = new QFile(lockPath); + + if (!file->open(QFile::WriteOnly)) { + qCCritical(logTooling) << "Could not open tooling lock for write"; + return false; + } + + auto lock = flock { + .l_type = F_WRLCK, + .l_whence = SEEK_SET, // NOLINT (fcntl.h??) + .l_start = 0, + .l_len = 0, + .l_pid = 0, + }; + + if (fcntl(file->handle(), F_SETLK, &lock) == 0) { + qCInfo(logTooling) << "Acquired tooling support lock"; + QmlToolingSupport::toolingLock = file; + return true; + } else if (errno == EACCES || errno == EAGAIN) { + qCInfo(logTooling) << "Tooling support locked by another instance"; + return false; + } else { + qCCritical(logTooling).nospace() << "Could not create tooling lock at " << lockPath + << " with error code " << errno << ": " << qt_error_string(); + return false; + } +} + QString QmlToolingSupport::getQmllsConfig() { static auto config = []() { QList importPaths; diff --git a/src/core/toolsupport.hpp b/src/core/toolsupport.hpp index 0aee9c5..9fb7921 100644 --- a/src/core/toolsupport.hpp +++ b/src/core/toolsupport.hpp @@ -12,9 +12,11 @@ public: private: static QString getQmllsConfig(); + static bool lockTooling(); static bool updateQmllsConfig(const QDir& configRoot, bool create); static void updateToolingFs(QmlScanner& scanner, const QDir& scanDir, const QDir& linkDir); static inline bool toolingEnabled = false; + static inline QFile* toolingLock = nullptr; }; } // namespace qs::core From 201c559dcdc1244515332a88b5145ead531787ed Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 16 Jul 2025 20:13:59 -0700 Subject: [PATCH 067/226] core: add Internal pragma --- src/core/rootwrapper.cpp | 2 +- src/core/scan.cpp | 70 ++++++++++++++++++++++++---------------- src/core/scan.hpp | 7 ++-- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 7dc1068..25c46cc 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -58,7 +58,7 @@ void RootWrapper::reloadGraph(bool hard) { auto rootFile = QFileInfo(this->rootPath); auto rootPath = rootFile.dir(); auto scanner = QmlScanner(rootPath); - scanner.scanQmlFile(this->rootPath); + scanner.scanQmlRoot(this->rootPath); qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); this->configDirWatcher.addPath(rootPath.path()); diff --git a/src/core/scan.cpp b/src/core/scan.cpp index a29ee59..4306de7 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -26,30 +26,41 @@ void QmlScanner::scanDir(const QString& path) { qCDebug(logQmlScanner) << "Scanning directory" << path; auto dir = QDir(path); + struct Entry { + QString name; + bool singleton = false; + bool internal = false; + }; + bool seenQmldir = false; - auto singletons = QVector(); - auto entries = QVector(); - for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { - if (entry == "qmldir") { + auto entries = QVector(); + + for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { + if (name == "qmldir") { qCDebug(logQmlScanner ) << "Found qmldir file, qmldir synthesization will be disabled for directory" << path; seenQmldir = true; - } else if (entry.at(0).isUpper() && entry.endsWith(".qml")) { - if (this->scanQmlFile(dir.filePath(entry))) { - singletons.push_back(entry); + } else if (name.at(0).isUpper() && name.endsWith(".qml")) { + auto& entry = entries.emplaceBack(); + + if (this->scanQmlFile(dir.filePath(name), entry.singleton, entry.internal)) { + entry.name = name; } else { - entries.push_back(entry); + entries.pop_back(); + } + } else if (name.at(0).isUpper() && name.endsWith(".qml.json")) { + if (this->scanQmlJson(dir.filePath(name))) { + entries.push_back({ + .name = name.first(name.length() - 5), + .singleton = true, + }); } - } else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) { - this->scanQmlJson(dir.filePath(entry)); - singletons.push_back(entry.first(entry.length() - 5)); } } if (!seenQmldir) { - qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" - << singletons; + qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path; QString qmldir; auto stream = QTextStream(&qmldir); @@ -77,13 +88,10 @@ void QmlScanner::scanDir(const QString& path) { qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; } - for (auto& singleton: singletons) { - stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton - << "\n"; - } - - for (auto& entry: entries) { - stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n"; + for (const auto& entry: entries) { + if (entry.internal) stream << "internal "; + if (entry.singleton) stream << "singleton "; + stream << entry.name.sliced(0, entry.name.length() - 4) << " 1.0 " << entry.name << '\n'; } qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); @@ -91,7 +99,7 @@ void QmlScanner::scanDir(const QString& path) { } } -bool QmlScanner::scanQmlFile(const QString& path) { +bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& internal) { if (this->scannedFiles.contains(path)) return false; this->scannedFiles.push_back(path); @@ -106,13 +114,12 @@ bool QmlScanner::scanQmlFile(const QString& path) { auto stream = QTextStream(&file); auto imports = QVector(); - bool singleton = false; - while (!stream.atEnd()) { auto line = stream.readLine().trimmed(); if (!singleton && line == "pragma Singleton") { - qCDebug(logQmlScanner) << "Discovered singleton" << path; singleton = true; + } else if (!internal && line == "//@ pragma Internal") { + internal = true; } else if (line.startsWith("import")) { // we dont care about "import qs" as we always load the root folder if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { @@ -188,16 +195,22 @@ bool QmlScanner::scanQmlFile(const QString& path) { else this->scanDir(cpath); } - return singleton; + return true; } -void QmlScanner::scanQmlJson(const QString& path) { +void QmlScanner::scanQmlRoot(const QString& path) { + bool singleton = false; + bool internal = false; + this->scanQmlFile(path, singleton, internal); +} + +bool QmlScanner::scanQmlJson(const QString& path) { qCDebug(logQmlScanner) << "Scanning qml.json file" << path; auto file = QFile(path); if (!file.open(QFile::ReadOnly | QFile::Text)) { qCWarning(logQmlScanner) << "Failed to open file" << path; - return; + return false; } auto data = file.readAll(); @@ -209,7 +222,7 @@ void QmlScanner::scanQmlJson(const QString& path) { if (error.error != QJsonParseError::NoError) { qCCritical(logQmlScanner).nospace() << "Failed to parse qml.json file at " << path << ": " << error.errorString(); - return; + return false; } const QString body = @@ -219,6 +232,7 @@ void QmlScanner::scanQmlJson(const QString& path) { this->fileIntercepts.insert(path.first(path.length() - 5), body); this->scannedFiles.push_back(path); + return true; } QPair QmlScanner::jsonToQml(const QJsonValue& value, int indent) { diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 6220bae..1d3be85 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -18,8 +18,8 @@ public: // path must be canonical void scanDir(const QString& path); - // returns if the file has a singleton - bool scanQmlFile(const QString& path); + + void scanQmlRoot(const QString& path); QVector scannedDirs; QVector scannedFiles; @@ -28,6 +28,7 @@ public: private: QDir rootPath; - void scanQmlJson(const QString& path); + bool scanQmlFile(const QString& path, bool& singleton, bool& internal); + bool scanQmlJson(const QString& path); [[nodiscard]] static QPair jsonToQml(const QJsonValue& value, int indent = 0); }; From 91dcb41d2216be6b11955c59b54637bff6c2f296 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 17 Jul 2025 00:06:32 -0700 Subject: [PATCH 068/226] services/pipewire: destroy qml ifaces early to avoid user callbacks Consumers of defaultAudio*Changed signals can run code between safeDestroy being called and PwObjectIface destruction due to signal connection order. This change destroys ifaces earlier so they are nulled by the time a changed signal is fired from destruction, preventing access between ~PwNode() and ~QObject() completion. Fixes #116 #122 #124 --- src/services/pipewire/qml.cpp | 10 ++++++++++ src/services/pipewire/qml.hpp | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index 5d8c45e..9efb17e 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -18,6 +18,16 @@ namespace qs::service::pipewire { +PwObjectIface::PwObjectIface(PwBindableObject* object): QObject(object), object(object) { + // We want to destroy the interface before QObject::destroyed is fired, as handlers + // connected before PwObjectIface will run first and emit signals that hit user code, + // which can then try to reference the iface again after ~PwNode() has been called but + // before ~QObject() has finished. + QObject::connect(object, &PwBindableObject::destroying, this, &PwObjectIface::onObjectDestroying); +} + +void PwObjectIface::onObjectDestroying() { delete this; } + void PwObjectIface::ref() { this->refcount++; diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 5bcc70d..e3489a1 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -36,7 +36,7 @@ class PwObjectIface Q_OBJECT; public: - explicit PwObjectIface(PwBindableObject* object): QObject(object), object(object) {}; + explicit PwObjectIface(PwBindableObject* object); // destructor should ONLY be called by the pw object destructor, making an unref unnecessary ~PwObjectIface() override = default; Q_DISABLE_COPY_MOVE(PwObjectIface); @@ -44,6 +44,9 @@ public: void ref() override; void unref() override; +private slots: + void onObjectDestroying(); + private: quint32 refcount = 0; PwBindableObject* object; From 115d6717a85d1b2246f82479d1aacca181893014 Mon Sep 17 00:00:00 2001 From: ipg0 Date: Thu, 17 Jul 2025 22:27:46 +0300 Subject: [PATCH 069/226] services/tray: use normal icon as fallback for attention custom icon Signed-off-by: ipg0 --- src/services/status_notifier/item.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 4632995..0b9700f 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -163,6 +163,10 @@ QPixmap StatusNotifierItem::createPixmap(const QSize& size) const { } else { const auto* icon = closestPixmap(size, this->bAttentionIconPixmaps.value()); + if (icon == nullptr) { + icon = closestPixmap(size, this->bIconPixmaps.value()); + } + if (icon != nullptr) { const auto image = icon->createImage().scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); From e885f4aec10c641b907bda57ce4c252c404708f4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 18 Jul 2025 00:07:25 -0700 Subject: [PATCH 070/226] tooling: check if .qmlls.ini is a symlink in addition to exists QFileInfo::exists() returns false on broken symlinks. --- src/core/toolsupport.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp index 5622d92..ad19335 100644 --- a/src/core/toolsupport.cpp +++ b/src/core/toolsupport.cpp @@ -108,10 +108,13 @@ bool QmlToolingSupport::updateQmllsConfig(const QDir& configRoot, bool create) { auto vfsConfigPath = QsPaths::instance()->shellVfsDir()->filePath(".qmlls.ini"); auto shellFileInfo = QFileInfo(shellConfigPath); - if (!create && !shellFileInfo.exists()) { + if (!create && !shellFileInfo.exists() && !shellFileInfo.isSymLink()) { if (QmlToolingSupport::toolingEnabled) { qInfo() << "QML tooling support disabled"; QmlToolingSupport::toolingEnabled = false; + } else { + qCInfo(logTooling) << "Not enabling QML tooling support, qmlls.ini is missing at path" + << shellConfigPath; } QFile::remove(vfsConfigPath); From 6572a7f61df30c3b26e324e5af000086612f2c8b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 18 Jul 2025 00:33:58 -0700 Subject: [PATCH 071/226] tooling: derive import paths from QML engine import paths Due to distro patches and default locations, we can't correctly derive it without calling the QQmlEngine function. --- src/core/toolsupport.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp index ad19335..afce008 100644 --- a/src/core/toolsupport.cpp +++ b/src/core/toolsupport.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include "logcat.hpp" @@ -78,16 +78,10 @@ bool QmlToolingSupport::lockTooling() { QString QmlToolingSupport::getQmllsConfig() { static auto config = []() { - QList importPaths; - - auto addPaths = [&](const QList& paths) { - for (const auto& path: paths) { - if (!importPaths.contains(path)) importPaths.append(path); - } - }; - - addPaths(qEnvironmentVariable("QML_IMPORT_PATH").split(u':', Qt::SkipEmptyParts)); - addPaths(qEnvironmentVariable("QML2_IMPORT_PATH").split(u':', Qt::SkipEmptyParts)); + // We can't replicate the algorithm used to create the import path list as it can have distro + // specific patches, e.g. nixos. + auto importPaths = QQmlEngine().importPathList(); + importPaths.removeIf([](const QString& path) { return path.startsWith("qrc:"); }); auto vfsPath = QsPaths::instance()->shellVfsDir()->path(); auto importPathsStr = importPaths.join(u':'); From ecc4a1249da85a736042a6ff084809dbd5ab63c4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 18 Jul 2025 04:14:58 -0700 Subject: [PATCH 072/226] all: mask various useless dbus errors --- src/dbus/dbusmenu/dbusmenu.cpp | 4 +- src/dbus/properties.cpp | 9 ++- src/services/mpris/player.cpp | 87 +++++++++++++++------------ src/services/mpris/player.hpp | 40 +++++++++--- src/services/notifications/server.cpp | 2 +- src/services/status_notifier/item.cpp | 25 +++++--- 6 files changed, 106 insertions(+), 61 deletions(-) diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index c0b4386..186b133 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -312,8 +312,8 @@ void DBusMenu::prepareToShow(qint32 item, qint32 depth) { auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) { const QDBusPendingReply reply = *call; if (reply.isError()) { - qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" - << this << reply.error(); + qCDebug(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" + << this << reply.error(); } this->updateLayout(item, depth); diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 81f26d2..d0f65d9 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -246,8 +246,13 @@ void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) { const QDBusPendingReply reply = *call; if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; - qCWarning(logDbusProperties) << reply.error(); + if (!property->isRequired() && reply.error().type() == QDBusError::InvalidArgs) { + qCDebug(logDbusProperties) << "Error updating non-required property" << propStr; + qCDebug(logDbusProperties) << reply.error(); + } else { + qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } } else { this->tryUpdateProperty(property, reply.value().variant()); } diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 45d5cd4..116a6e9 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -101,41 +102,10 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren this->bLengthSupported.setBinding([this]() { return this->bInternalLength != -1; }); - this->bPlaybackState.setBinding([this]() { - const auto& status = this->bpPlaybackStatus.value(); - - if (status == "Playing") { - return MprisPlaybackState::Playing; - } else if (status == "Paused") { - this->pausedTime = QDateTime::currentDateTimeUtc(); - return MprisPlaybackState::Paused; - } else if (status == "Stopped") { - return MprisPlaybackState::Stopped; - } else { - qWarning() << "Received unexpected PlaybackStatus for" << this << status; - return MprisPlaybackState::Stopped; - } - }); - this->bIsPlaying.setBinding([this]() { return this->bPlaybackState == MprisPlaybackState::Playing; }); - this->bLoopState.setBinding([this]() { - const auto& status = this->bpLoopStatus.value(); - - if (status == "None") { - return MprisLoopState::None; - } else if (status == "Track") { - return MprisLoopState::Track; - } else if (status == "Playlist") { - return MprisLoopState::Playlist; - } else { - qWarning() << "Received unexpected LoopStatus for" << this << status; - return MprisLoopState::None; - } - }); - // clang-format off QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); @@ -432,18 +402,11 @@ void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) { } if (loopState == this->bLoopState) return; - - QString loopStatusStr; - switch (loopState) { - case MprisLoopState::None: loopStatusStr = "None"; break; - case MprisLoopState::Track: loopStatusStr = "Track"; break; - case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break; - default: + if (loopState < MprisLoopState::None || loopState > MprisLoopState::Playlist) { qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState; - return; } - this->bpLoopStatus = loopStatusStr; + this->bLoopState = loopState; this->pLoopStatus.write(); } @@ -496,3 +459,47 @@ void MprisPlayer::onGetAllFinished() { } } // namespace qs::service::mpris + +namespace qs::dbus { + +using namespace qs::service::mpris; + +DBusResult +DBusDataTransform::fromWire(const QString& wire) { + if (wire == "Playing") return MprisPlaybackState::Playing; + if (wire == "Paused") return MprisPlaybackState::Paused; + if (wire == "Stopped") return MprisPlaybackState::Stopped; + return QDBusError(QDBusError::InvalidArgs, QString("Invalid MprisPlaybackState: %1").arg(wire)); +} + +QString DBusDataTransform::toWire(MprisPlaybackState::Enum data) { + switch (data) { + case MprisPlaybackState::Playing: return "Playing"; + case MprisPlaybackState::Paused: return "Paused"; + case MprisPlaybackState::Stopped: return "Stopped"; + default: + qFatal() << "Tried to convert an invalid MprisPlaybackState to String"; + return QString(); + } +} + +DBusResult +DBusDataTransform::fromWire(const QString& wire) { + if (wire == "None") return MprisLoopState::None; + if (wire == "Track") return MprisLoopState::Track; + if (wire == "Playlist") return MprisLoopState::Playlist; + return QDBusError(QDBusError::InvalidArgs, QString("Invalid MprisLoopState: %1").arg(wire)); +} + +QString DBusDataTransform::toWire(MprisLoopState::Enum data) { + switch (data) { + case MprisLoopState::None: return "None"; + case MprisLoopState::Track: return "Track"; + case MprisLoopState::Playlist: return "Playlist"; + default: + qFatal() << "Tried to convert an invalid MprisLoopState to String"; + return QString(); + } +} + +} // namespace qs::dbus diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 89bc27a..93837c6 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -51,6 +51,30 @@ public: Q_INVOKABLE static QString toString(qs::service::mpris::MprisLoopState::Enum status); }; +}; // namespace qs::service::mpris + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::mpris::MprisPlaybackState::Enum; + static DBusResult fromWire(const QString& wire); + static QString toWire(Data data); +}; + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::mpris::MprisLoopState::Enum; + static DBusResult fromWire(const QString& wire); + static QString toWire(Data data); +}; + +}; // namespace qs::dbus + +namespace qs::service::mpris { + ///! A media player exposed over MPRIS. /// A media player exposed over MPRIS. /// @@ -404,13 +428,13 @@ private: QS_DBUS_BINDABLE_PROPERTY_GROUP(MprisPlayer, appProperties); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pIdentity, bIdentity, appProperties, "Identity"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pDesktopEntry, bDesktopEntry, appProperties, "DesktopEntry"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanQuit, bCanQuit, appProperties, "CanQuit"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanRaise, bCanRaise, appProperties, "CanRaise"); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pDesktopEntry, bDesktopEntry, appProperties, "DesktopEntry", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanQuit, bCanQuit, appProperties, "CanQuit", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanRaise, bCanRaise, appProperties, "CanRaise", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pFullscreen, bFullscreen, appProperties, "Fullscreen", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSetFullscreen, bCanSetFullscreen, appProperties, "CanSetFullscreen", false); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedUriSchemes, bSupportedUriSchemes, appProperties, "SupportedUriSchemes"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedMimeTypes, bSupportedMimeTypes, appProperties, "SupportedMimeTypes"); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedUriSchemes, bSupportedUriSchemes, appProperties, "SupportedUriSchemes", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pSupportedMimeTypes, bSupportedMimeTypes, appProperties, "SupportedMimeTypes", false); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bpCanPlay); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bpCanPause); @@ -420,8 +444,6 @@ private: Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QVariantMap, bpMetadata); QS_BINDING_SUBSCRIBE_METHOD(MprisPlayer, bpMetadata, onMetadataChanged, onValueChanged); Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(MprisPlayer, qlonglong, bpPosition, -1, &MprisPlayer::positionChanged); - Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bpPlaybackStatus); - Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bpLoopStatus); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bCanControl, &MprisPlayer::canControlChanged); Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, bool, bCanPlay, &MprisPlayer::canPlayChanged); @@ -460,8 +482,8 @@ private: QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, onPositionUpdated, playerProperties, "Position", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pVolume, bVolume, playerProperties, "Volume", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMetadata, bpMetadata, playerProperties, "Metadata"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, void, pPlaybackStatus, bpPlaybackStatus, onPlaybackStatusUpdated, playerProperties, "PlaybackStatus", true); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, pLoopStatus, bpLoopStatus, playerProperties, "LoopStatus", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, void, pPlaybackStatus, bPlaybackState, onPlaybackStatusUpdated, playerProperties, "PlaybackStatus", true); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, pLoopStatus, bLoopState, playerProperties, "LoopStatus", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pRate, bRate, playerProperties, "Rate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMinRate, bMinRate, playerProperties, "MinimumRate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false); diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index ac1e905..3f2469d 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -21,7 +21,7 @@ namespace qs::service::notifications { // NOLINTNEXTLINE(misc-use-internal-linkage) -QS_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); +QS_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications", QtWarningMsg); NotificationServer::NotificationServer() { qDBusRegisterMetaType(); diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 0b9700f..650c812 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -222,9 +222,14 @@ void StatusNotifierItem::activate() { const QDBusPendingReply<> reply = *call; if (reply.isError()) { - qCWarning(logStatusNotifierItem).noquote() - << "Error calling Activate method of StatusNotifierItem" << this->properties.toString(); - qCWarning(logStatusNotifierItem) << reply.error(); + if (reply.error().type() == QDBusError::UnknownMethod) { + qCDebug(logStatusNotifierItem) << "Tried to call Activate method of StatusNotifierItem" + << this->properties.toString() << "but it does not exist."; + } else { + qCWarning(logStatusNotifierItem).noquote() + << "Error calling Activate method of StatusNotifierItem" << this->properties.toString(); + qCWarning(logStatusNotifierItem) << reply.error(); + } } delete call; @@ -241,10 +246,16 @@ void StatusNotifierItem::secondaryActivate() { const QDBusPendingReply<> reply = *call; if (reply.isError()) { - qCWarning(logStatusNotifierItem).noquote() - << "Error calling SecondaryActivate method of StatusNotifierItem" - << this->properties.toString(); - qCWarning(logStatusNotifierItem) << reply.error(); + if (reply.error().type() == QDBusError::UnknownMethod) { + qCDebug(logStatusNotifierItem) + << "Tried to call SecondaryActivate method of StatusNotifierItem" + << this->properties.toString() << "but it does not exist."; + } else { + qCWarning(logStatusNotifierItem).noquote() + << "Error calling SecondaryActivate method of StatusNotifierItem" + << this->properties.toString(); + qCWarning(logStatusNotifierItem) << reply.error(); + } } delete call; From e55d519c280192d8d97695b6c5905a0d7a46f8fe Mon Sep 17 00:00:00 2001 From: Rexiel Scarlet <37258415+Rexcrazy804@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:43:09 +0400 Subject: [PATCH 073/226] build: split derivation for extensible wrapper --- default.nix | 137 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 83 insertions(+), 54 deletions(-) diff --git a/default.nix b/default.nix index 73cd8d1..4d43cb7 100644 --- a/default.nix +++ b/default.nix @@ -43,64 +43,93 @@ withPam ? true, withHyprland ? true, withI3 ? true, -}: buildStdenv.mkDerivation { - pname = "quickshell${lib.optionalString debug "-debug"}"; - version = "0.1.0"; - src = nix-gitignore.gitignoreSource [] ./.; +}: let + unwrapped = buildStdenv.mkDerivation { + pname = "quickshell${lib.optionalString debug "-debug"}"; + version = "0.1.0"; + src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; - nativeBuildInputs = [ - cmake - ninja - qt6.qtshadertools - spirv-tools - qt6.wrapQtAppsHook - pkg-config - ] - ++ lib.optional withWayland wayland-scanner; + dontWrapQtApps = true; # see wrappers - buildInputs = [ - qt6.qtbase - qt6.qtdeclarative - cli11 - ] - ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optional withCrashReporter breakpad - ++ lib.optional withJemalloc jemalloc - ++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] - ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] - ++ lib.optional withX11 xorg.libxcb - ++ lib.optional withPam pam - ++ lib.optional withPipewire pipewire; + nativeBuildInputs = [ + cmake + ninja + qt6.qtshadertools + spirv-tools + pkg-config + ] + ++ lib.optional withWayland wayland-scanner; - cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; + buildInputs = [ + qt6.qtbase + qt6.qtdeclarative + cli11 + ] + ++ lib.optional withQtSvg qt6.qtsvg + ++ lib.optional withCrashReporter breakpad + ++ lib.optional withJemalloc jemalloc + ++ lib.optionals withWayland [ qt6.qtwayland wayland wayland-protocols ] + ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] + ++ lib.optional withX11 xorg.libxcb + ++ lib.optional withPam pam + ++ lib.optional withPipewire pipewire; - cmakeFlags = [ - (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake") - (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) - (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) - (lib.cmakeFeature "GIT_REVISION" gitRev) - (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) - (lib.cmakeBool "USE_JEMALLOC" withJemalloc) - (lib.cmakeBool "WAYLAND" withWayland) - (lib.cmakeBool "SCREENCOPY" (libgbm != null)) - (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) - (lib.cmakeBool "SERVICE_PAM" withPam) - (lib.cmakeBool "HYPRLAND" withHyprland) - (lib.cmakeBool "I3" withI3) - ]; + cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; - # How to get debuginfo in gdb from a release build: - # 1. build `quickshell.debug` - # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug" - # 3. launch gdb / coredumpctl and debuginfo will work - separateDebugInfo = !debug; - dontStrip = debug; + cmakeFlags = [ + (lib.cmakeFeature "DISTRIBUTOR" "Official-Nix-Flake") + (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) + (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) + (lib.cmakeFeature "GIT_REVISION" gitRev) + (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) + (lib.cmakeBool "USE_JEMALLOC" withJemalloc) + (lib.cmakeBool "WAYLAND" withWayland) + (lib.cmakeBool "SCREENCOPY" (libgbm != null)) + (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) + (lib.cmakeBool "SERVICE_PAM" withPam) + (lib.cmakeBool "HYPRLAND" withHyprland) + (lib.cmakeBool "I3" withI3) + ]; - meta = with lib; { - homepage = "https://quickshell.outfoxxed.me"; - description = "Flexbile QtQuick based desktop shell toolkit"; - license = licenses.lgpl3Only; - platforms = platforms.linux; - mainProgram = "quickshell"; + # How to get debuginfo in gdb from a release build: + # 1. build `quickshell.debug` + # 2. set NIX_DEBUG_INFO_DIRS="/lib/debug" + # 3. launch gdb / coredumpctl and debuginfo will work + separateDebugInfo = !debug; + dontStrip = debug; + + meta = with lib; { + homepage = "https://quickshell.org"; + description = "Flexbile QtQuick based desktop shell toolkit"; + license = licenses.lgpl3Only; + platforms = platforms.linux; + mainProgram = "quickshell"; + }; }; -} + + wrapper = unwrapped.stdenv.mkDerivation { + inherit (unwrapped) version meta buildInputs; + pname = "${unwrapped.pname}-wrapped"; + + nativeBuildInputs = unwrapped.nativeBuildInputs ++ [ qt6.wrapQtAppsHook ]; + + dontUnpack = true; + dontConfigure = true; + dontBuild = true; + + installPhase = '' + mkdir -p $out/bin + # cp will create .quickshell-wrapped in path, ln will not. It is occasionally useful. + cp -r ${unwrapped}/bin/* $out/bin + ln -s ${unwrapped}/share $out/share + # not /lib + ''; + + passthru = { + unwrapped = unwrapped; + withModules = modules: wrapper.overrideAttrs (prev: { + buildInputs = prev.buildInputs ++ modules; + }); + }; + }; +in wrapper From 7b417bb80811d3d036df97d7149352b01ca6fb72 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 18 Jul 2025 17:58:20 -0700 Subject: [PATCH 074/226] build: add /lib/qt-6 to wrapped nix package Fixes #130 --- default.nix | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/default.nix b/default.nix index 4d43cb7..7dc68e2 100644 --- a/default.nix +++ b/default.nix @@ -118,11 +118,8 @@ dontBuild = true; installPhase = '' - mkdir -p $out/bin - # cp will create .quickshell-wrapped in path, ln will not. It is occasionally useful. - cp -r ${unwrapped}/bin/* $out/bin - ln -s ${unwrapped}/share $out/share - # not /lib + mkdir -p $out + cp -r ${unwrapped}/* $out ''; passthru = { From 77de23bb71231c2ed146c00f3218d13f054e7650 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 18 Jul 2025 22:32:48 -0700 Subject: [PATCH 075/226] core/desktopentry: add StartupWMClass and heuristicLookup --- src/core/desktopentry.cpp | 25 +++++++++++++++++++++++++ src/core/desktopentry.hpp | 12 ++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 4673881..3582431 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -1,4 +1,5 @@ #include "desktopentry.hpp" +#include #include #include @@ -108,6 +109,7 @@ void DesktopEntry::parseEntry(const QString& text) { 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; @@ -384,6 +386,25 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { } } +DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { + if (auto* entry = DesktopEntryManager::byId(name)) return entry; + + auto& list = this->mApplications.valueList(); + + auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { + return name == entry->mStartupClass; + }); + + if (iter != list.end()) return *iter; + + iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { + return name.toLower() == entry->mStartupClass.toLower(); + }); + + if (iter != list.end()) return *iter; + return nullptr; +} + ObjectModel* DesktopEntryManager::applications() { return &this->mApplications; } DesktopEntries::DesktopEntries() { DesktopEntryManager::instance(); } @@ -392,6 +413,10 @@ DesktopEntry* DesktopEntries::byId(const QString& id) { return DesktopEntryManager::instance()->byId(id); } +DesktopEntry* DesktopEntries::heuristicLookup(const QString& name) { + return DesktopEntryManager::instance()->heuristicLookup(name); +} + ObjectModel* DesktopEntries::applications() { return DesktopEntryManager::instance()->applications(); } diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index ee8f511..3413772 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -22,6 +22,9 @@ class DesktopEntry: public QObject { Q_PROPERTY(QString name MEMBER mName CONSTANT); /// Short description of the application, such as "Web Browser". May be empty. Q_PROPERTY(QString genericName MEMBER mGenericName CONSTANT); + /// 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); /// If true, this application should not be displayed in menus and launchers. Q_PROPERTY(bool noDisplay MEMBER mNoDisplay CONSTANT); /// Long description of the application, such as "View websites on the internet". May be empty. @@ -81,6 +84,7 @@ public: QString mId; QString mName; QString mGenericName; + QString mStartupClass; bool mNoDisplay = false; QString mComment; QString mIcon; @@ -151,6 +155,7 @@ public: void scanDesktopEntries(); [[nodiscard]] DesktopEntry* byId(const QString& id); + [[nodiscard]] DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] ObjectModel* applications(); @@ -186,7 +191,14 @@ public: explicit DesktopEntries(); /// Look up a desktop entry by name. Includes NoDisplay entries. May return null. + /// + /// While this function requires an exact match, @@heuristicLookup() will correctly + /// find an entry more often and is generally more useful. Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); + /// Look up a desktop entry by name using heuristics. Unline @@byId(), + /// if no exact matches are found this function will try to guess - potentially incorrectly. + /// May return null. + Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); [[nodiscard]] static ObjectModel* applications(); }; From 63a6d272136bd5585724fd0d4d724c4afcdb6541 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 19 Jul 2025 02:58:55 -0700 Subject: [PATCH 076/226] core/qmlglobal: configDir, configPath() -> shellDir, shellPath() --- src/core/qmlglobal.cpp | 22 ++++++++++++++++++++-- src/core/qmlglobal.hpp | 10 ++++++++-- src/services/mpris/player.cpp | 8 ++------ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 0aba306..07238f6 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -210,10 +210,22 @@ void QuickshellGlobal::onClipboardChanged(QClipboard::Mode mode) { if (mode == QClipboard::Clipboard) emit this->clipboardTextChanged(); } -QString QuickshellGlobal::configDir() const { +QString QuickshellGlobal::shellDir() const { return EngineGeneration::findObjectGeneration(this)->rootPath.path(); } +QString QuickshellGlobal::configDir() const { + qWarning() << "Quickshell.configDir is deprecated and may be removed in a future release. Use " + "Quickshell.shellDir."; + return this->shellDir(); +} + +QString QuickshellGlobal::shellRoot() const { + qWarning() << "Quickshell.shellRoot is deprecated and may be removed in a future release. Use " + "Quickshell.shellDir."; + return this->shellDir(); +} + QString QuickshellGlobal::dataDir() const { // NOLINT return QsPaths::instance()->shellDataDir().path(); } @@ -226,8 +238,14 @@ QString QuickshellGlobal::cacheDir() const { // NOLINT return QsPaths::instance()->shellCacheDir().path(); } +QString QuickshellGlobal::shellPath(const QString& path) const { + return this->shellDir() % '/' % path; +} + QString QuickshellGlobal::configPath(const QString& path) const { - return this->configDir() % '/' % path; + qWarning() << "Quickshell.configPath() is deprecated and may be removed in a future release. Use " + "Quickshell.shellPath()."; + return this->shellPath(path); } QString QuickshellGlobal::dataPath(const QString& path) const { diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index d05b96d..9d88591 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -108,9 +108,11 @@ class QuickshellGlobal: public QObject { /// /// The root directory is the folder containing the entrypoint to your shell, often referred /// to as `shell.qml`. + Q_PROPERTY(QString shellDir READ shellDir CONSTANT); + /// > [!WARNING] Deprecated: Renamed to @@shellDir for clarity. Q_PROPERTY(QString configDir READ configDir CONSTANT); - /// > [!WARNING] Deprecated: Returns @@configDir. - Q_PROPERTY(QString shellRoot READ configDir CONSTANT); + /// > [!WARNING] Deprecated: Renamed to @@shellDir for consistency. + Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); /// Quickshell's working directory. Defaults to whereever quickshell was launched from. Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); /// If true then the configuration will be reloaded whenever any files change. @@ -198,6 +200,8 @@ public: /// icon if the requested one could not be loaded. Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); /// Equivalent to `${Quickshell.configDir}/${path}` + Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const; + /// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity. Q_INVOKABLE [[nodiscard]] QString configPath(const QString& path) const; /// Equivalent to `${Quickshell.dataDir}/${path}` Q_INVOKABLE [[nodiscard]] QString dataPath(const QString& path) const; @@ -214,7 +218,9 @@ public: void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } + [[nodiscard]] QString shellDir() const; [[nodiscard]] QString configDir() const; + [[nodiscard]] QString shellRoot() const; [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 116a6e9..751a4e7 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -477,9 +477,7 @@ QString DBusDataTransform::toWire(MprisPlaybackState:: case MprisPlaybackState::Playing: return "Playing"; case MprisPlaybackState::Paused: return "Paused"; case MprisPlaybackState::Stopped: return "Stopped"; - default: - qFatal() << "Tried to convert an invalid MprisPlaybackState to String"; - return QString(); + default: qFatal() << "Tried to convert an invalid MprisPlaybackState to String"; return QString(); } } @@ -496,9 +494,7 @@ QString DBusDataTransform::toWire(MprisLoopState::Enum dat case MprisLoopState::None: return "None"; case MprisLoopState::Track: return "Track"; case MprisLoopState::Playlist: return "Playlist"; - default: - qFatal() << "Tried to convert an invalid MprisLoopState to String"; - return QString(); + default: qFatal() << "Tried to convert an invalid MprisLoopState to String"; return QString(); } } From 759bd721dfd38e2ce02048f378ee025bcb175f93 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 19 Jul 2025 03:41:24 -0700 Subject: [PATCH 077/226] core/log: stop trying to store detailed logs after write fail Not stopping will cause the logger's write buffer to fill until OOM if writing fails. --- src/core/logging.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 7f95e46..cb3a214 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -457,10 +457,14 @@ void ThreadLogging::onMessage(const LogMessage& msg, bool showInSparse) { this->fileStream << Qt::endl; } - if (this->detailedWriter.write(msg)) { - this->detailedFile->flush(); - } else if (this->detailedFile != nullptr) { - qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; + if (!this->detailedWriter.write(msg) || (this->detailedFile && !this->detailedFile->flush())) { + if (this->detailedFile) { + qCCritical(logLogging) << "Detailed logger failed to write. Ending detailed logs."; + } + + this->detailedWriter.setDevice(nullptr); + this->detailedFile->close(); + this->detailedFile = nullptr; } } From fcffbbced889717e09115e22fd181341746bf6e6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 19 Jul 2025 14:26:18 -0700 Subject: [PATCH 078/226] core/desktopentry: lookup wm class in nodisplay entries --- src/core/desktopentry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 3582431..bb0d2c5 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -389,7 +389,7 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { if (auto* entry = DesktopEntryManager::byId(name)) return entry; - auto& list = this->mApplications.valueList(); + auto list = this->desktopEntries.values(); auto iter = std::ranges::find_if(list, [&](const DesktopEntry* entry) { return name == entry->mStartupClass; From db77c71c216530159c2dcf5b269ebb4706b2e2dd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 21 Jul 2025 02:32:50 -0700 Subject: [PATCH 079/226] wayland/layershell: use width over height in horizontal auto exclude Fixes #135 --- src/wayland/wlr_layershell/wlr_layershell.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wayland/wlr_layershell/wlr_layershell.cpp b/src/wayland/wlr_layershell/wlr_layershell.cpp index d30740d..2b77690 100644 --- a/src/wayland/wlr_layershell/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell/wlr_layershell.cpp @@ -26,8 +26,8 @@ WlrLayershell::WlrLayershell(QObject* parent): ProxyWindowBase(parent) { switch (this->bcExclusionEdge.value()) { case Qt::TopEdge: return this->bImplicitHeight + margins.bottom; case Qt::BottomEdge: return this->bImplicitHeight + margins.top; - case Qt::LeftEdge: return this->bImplicitHeight + margins.right; - case Qt::RightEdge: return this->bImplicitHeight + margins.left; + case Qt::LeftEdge: return this->bImplicitWidth + margins.right; + case Qt::RightEdge: return this->bImplicitWidth + margins.left; default: return 0; } } From f90bef2d994c88f075dbc2fcd81140e160351328 Mon Sep 17 00:00:00 2001 From: cameron Date: Thu, 24 Jul 2025 15:40:54 +1000 Subject: [PATCH 080/226] hyprland/workspace: Use name instead of id for activate --- src/wayland/hyprland/ipc/workspace.cpp | 2 +- src/wayland/hyprland/ipc/workspace.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp index d16c821..c5d63b0 100644 --- a/src/wayland/hyprland/ipc/workspace.cpp +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -151,7 +151,7 @@ void HyprlandWorkspace::clearUrgent() { } void HyprlandWorkspace::activate() { - this->ipc->dispatch(QString("workspace %1").arg(this->bId.value())); + this->ipc->dispatch(QString("workspace %1").arg(this->bName.value())); } } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp index 957639a..a0b09cf 100644 --- a/src/wayland/hyprland/ipc/workspace.hpp +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -54,7 +54,7 @@ public: /// /// > [!NOTE] This is equivalent to running /// > ```qml - /// > HyprlandIpc.dispatch(`workspace ${workspace.id}`); + /// > HyprlandIpc.dispatch(`workspace ${workspace.name}`); /// > ``` Q_INVOKABLE void activate(); From 3bbf39c67e3108b12cc4eac689050bc5d8d71d12 Mon Sep 17 00:00:00 2001 From: Karboggy Date: Thu, 24 Jul 2025 10:41:57 +0200 Subject: [PATCH 081/226] core/reloader: fix file watcher compatibility with vscode --- src/core/generation.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 90a2939..54a1b86 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -222,6 +222,11 @@ void EngineGeneration::onFileChanged(const QString& name) { if (!this->watcher->files().contains(name)) { this->deletedWatchedFiles.push_back(name); } else { + // some editors (e.g vscode) perform file saving in two steps: truncate + write + // ignore the first event (truncate) with size 0 to prevent incorrect live reloading + auto fileInfo = QFileInfo(name); + if (fileInfo.isFile() && fileInfo.size() == 0) return; + emit this->filesChanged(); } } From 4dad44757085a42423f758bf0177cebcd07b4a4a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 24 Jul 2025 16:44:10 -0700 Subject: [PATCH 082/226] docs: remove }; in headers + typo fixes }; breaks the docgen regex --- src/core/desktopentry.hpp | 2 +- src/core/model.hpp | 2 +- src/core/qsmenu.hpp | 4 +- src/core/reload.hpp | 2 +- src/dbus/properties.hpp | 6 +- src/io/datastream.hpp | 4 +- src/io/ipchandler.hpp | 2 +- src/services/mpris/player.hpp | 62 ++++++++++----------- src/services/mpris/watcher.hpp | 2 +- src/services/notifications/notification.hpp | 28 +++++----- src/services/pipewire/registry.hpp | 4 +- src/services/upower/core.hpp | 2 +- src/services/upower/device.hpp | 34 +++++------ src/widgets/wrapper.hpp | 4 +- 14 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 3413772..827a618 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -195,7 +195,7 @@ public: /// While this function requires an exact match, @@heuristicLookup() will correctly /// find an entry more often and is generally more useful. Q_INVOKABLE [[nodiscard]] static DesktopEntry* byId(const QString& id); - /// Look up a desktop entry by name using heuristics. Unline @@byId(), + /// Look up a desktop entry by name using heuristics. Unlike @@byId(), /// if no exact matches are found this function will try to guess - potentially incorrectly. /// May return null. Q_INVOKABLE [[nodiscard]] static DesktopEntry* heuristicLookup(const QString& name); diff --git a/src/core/model.hpp b/src/core/model.hpp index 6346c96..3c5822a 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -53,7 +53,7 @@ public: [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override; [[nodiscard]] QHash roleNames() const override; - [[nodiscard]] QList values() const { return this->valuesList; }; + [[nodiscard]] QList values() const { return this->valuesList; } void removeAt(qsizetype index); Q_INVOKABLE qsizetype indexOf(QObject* object); diff --git a/src/core/qsmenu.hpp b/src/core/qsmenu.hpp index 6684c68..90df8b9 100644 --- a/src/core/qsmenu.hpp +++ b/src/core/qsmenu.hpp @@ -46,8 +46,8 @@ class QsMenuHandle: public QObject { public: explicit QsMenuHandle(QObject* parent): QObject(parent) {} - virtual void refHandle() {}; - virtual void unrefHandle() {}; + virtual void refHandle() {} + virtual void unrefHandle() {} [[nodiscard]] virtual QsMenuEntry* menu() = 0; diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 1117eb8..0ed34ee 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -57,7 +57,7 @@ public: void reload(QObject* oldInstance = nullptr); - void classBegin() override {}; + void classBegin() override {} void componentComplete() override; // Reload objects in the parent->child graph recursively. diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index a5fce98..f6a6330 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -168,9 +168,9 @@ class DBusBindableProperty: public DBusPropertyCore { public: explicit DBusBindableProperty() { this->group()->attachProperty(this); } - [[nodiscard]] QString name() const override { return Name; }; - [[nodiscard]] QStringView nameRef() const override { return Name; }; - [[nodiscard]] bool isRequired() const override { return required; }; + [[nodiscard]] QString name() const override { return Name; } + [[nodiscard]] QStringView nameRef() const override { return Name; } + [[nodiscard]] bool isRequired() const override { return required; } [[nodiscard]] QString valueString() override { QString str; diff --git a/src/io/datastream.hpp b/src/io/datastream.hpp index d83e571..b91ec04 100644 --- a/src/io/datastream.hpp +++ b/src/io/datastream.hpp @@ -55,7 +55,7 @@ public: // the buffer will be sent in both slots if there is data remaining from a previous parser virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0; - virtual void streamEnded(QByteArray& /*buffer*/) {}; + virtual void streamEnded(QByteArray& /*buffer*/) {} signals: /// Emitted when data is read from the stream. @@ -63,7 +63,7 @@ signals: }; ///! DataStreamParser for delimited data streams. -/// DataStreamParser for delimited data streams. @@read() is emitted once per delimited chunk of the stream. +/// DataStreamParser for delimited data streams. @@DataStreamParser.read(s) is emitted once per delimited chunk of the stream. class SplitParser: public DataStreamParser { Q_OBJECT; /// The delimiter for parsed data. May be multiple characters. Defaults to `\n`. diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 1da3e71..4c5d9bc 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -164,7 +164,7 @@ class IpcHandler: public PostReloadHook { QML_ELEMENT; public: - explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {}; + explicit IpcHandler(QObject* parent = nullptr): PostReloadHook(parent) {} ~IpcHandler() override; Q_DISABLE_COPY_MOVE(IpcHandler); diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 93837c6..423453d 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -274,23 +274,23 @@ public: [[nodiscard]] bool isValid() const; [[nodiscard]] QString address() const; - [[nodiscard]] QBindable bindableCanControl() const { return &this->bCanControl; }; - [[nodiscard]] QBindable bindableCanSeek() const { return &this->bCanSeek; }; - [[nodiscard]] QBindable bindableCanGoNext() const { return &this->bCanGoNext; }; - [[nodiscard]] QBindable bindableCanGoPrevious() const { return &this->bCanGoPrevious; }; - [[nodiscard]] QBindable bindableCanPlay() const { return &this->bCanPlay; }; - [[nodiscard]] QBindable bindableCanPause() const { return &this->bCanPause; }; + [[nodiscard]] QBindable bindableCanControl() const { return &this->bCanControl; } + [[nodiscard]] QBindable bindableCanSeek() const { return &this->bCanSeek; } + [[nodiscard]] QBindable bindableCanGoNext() const { return &this->bCanGoNext; } + [[nodiscard]] QBindable bindableCanGoPrevious() const { return &this->bCanGoPrevious; } + [[nodiscard]] QBindable bindableCanPlay() const { return &this->bCanPlay; } + [[nodiscard]] QBindable bindableCanPause() const { return &this->bCanPause; } [[nodiscard]] QBindable bindableCanTogglePlaying() const { return &this->bCanTogglePlaying; - }; - [[nodiscard]] QBindable bindableCanQuit() const { return &this->bCanQuit; }; - [[nodiscard]] QBindable bindableCanRaise() const { return &this->bCanRaise; }; + } + [[nodiscard]] QBindable bindableCanQuit() const { return &this->bCanQuit; } + [[nodiscard]] QBindable bindableCanRaise() const { return &this->bCanRaise; } [[nodiscard]] QBindable bindableCanSetFullscreen() const { return &this->bCanSetFullscreen; - }; + } - [[nodiscard]] QBindable bindableIdentity() const { return &this->bIdentity; }; - [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; + [[nodiscard]] QBindable bindableIdentity() const { return &this->bIdentity; } + [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; } [[nodiscard]] qlonglong positionMs() const; [[nodiscard]] qreal position() const; @@ -300,49 +300,49 @@ public: [[nodiscard]] qreal length() const; [[nodiscard]] QBindable bindableLengthSupported() const { return &this->bLengthSupported; } - [[nodiscard]] qreal volume() const { return this->bVolume; }; + [[nodiscard]] qreal volume() const { return this->bVolume; } [[nodiscard]] bool volumeSupported() const; void setVolume(qreal volume); - [[nodiscard]] QBindable bindableUniqueId() const { return &this->bUniqueId; }; - [[nodiscard]] QBindable bindableMetadata() const { return &this->bMetadata; }; - [[nodiscard]] QBindable bindableTrackTitle() const { return &this->bTrackTitle; }; - [[nodiscard]] QBindable bindableTrackAlbum() const { return &this->bTrackAlbum; }; + [[nodiscard]] QBindable bindableUniqueId() const { return &this->bUniqueId; } + [[nodiscard]] QBindable bindableMetadata() const { return &this->bMetadata; } + [[nodiscard]] QBindable bindableTrackTitle() const { return &this->bTrackTitle; } + [[nodiscard]] QBindable bindableTrackAlbum() const { return &this->bTrackAlbum; } [[nodiscard]] QBindable bindableTrackAlbumArtist() const { return &this->bTrackAlbumArtist; - }; - [[nodiscard]] QBindable bindableTrackArtist() const { return &this->bTrackArtist; }; - [[nodiscard]] QBindable bindableTrackArtUrl() const { return &this->bTrackArtUrl; }; + } + [[nodiscard]] QBindable bindableTrackArtist() const { return &this->bTrackArtist; } + [[nodiscard]] QBindable bindableTrackArtUrl() const { return &this->bTrackArtUrl; } - [[nodiscard]] MprisPlaybackState::Enum playbackState() const { return this->bPlaybackState; }; + [[nodiscard]] MprisPlaybackState::Enum playbackState() const { return this->bPlaybackState; } void setPlaybackState(MprisPlaybackState::Enum playbackState); - [[nodiscard]] bool isPlaying() const { return this->bIsPlaying; }; + [[nodiscard]] bool isPlaying() const { return this->bIsPlaying; } void setPlaying(bool playing); - [[nodiscard]] MprisLoopState::Enum loopState() const { return this->bLoopState; }; + [[nodiscard]] MprisLoopState::Enum loopState() const { return this->bLoopState; } [[nodiscard]] bool loopSupported() const; void setLoopState(MprisLoopState::Enum loopState); - [[nodiscard]] qreal rate() const { return this->bRate; }; - [[nodiscard]] QBindable bindableMinRate() const { return &this->bRate; }; - [[nodiscard]] QBindable bindableMaxRate() const { return &this->bRate; }; + [[nodiscard]] qreal rate() const { return this->bRate; } + [[nodiscard]] QBindable bindableMinRate() const { return &this->bRate; } + [[nodiscard]] QBindable bindableMaxRate() const { return &this->bRate; } void setRate(qreal rate); - [[nodiscard]] bool shuffle() const { return this->bShuffle; }; + [[nodiscard]] bool shuffle() const { return this->bShuffle; } [[nodiscard]] bool shuffleSupported() const; void setShuffle(bool shuffle); - [[nodiscard]] bool fullscreen() const { return this->bFullscreen; }; + [[nodiscard]] bool fullscreen() const { return this->bFullscreen; } void setFullscreen(bool fullscreen); [[nodiscard]] QBindable> bindableSupportedUriSchemes() const { return &this->bSupportedUriSchemes; - }; + } [[nodiscard]] QBindable> bindableSupportedMimeTypes() const { return &this->bSupportedMimeTypes; - }; + } signals: /// The track has changed. @@ -414,7 +414,7 @@ private: void onPlaybackStatusUpdated(); // call instead of setting bpPosition void setPosition(qlonglong position); - void requestPositionUpdate() { this->pPosition.requestUpdate(); }; + void requestPositionUpdate() { this->pPosition.requestUpdate(); } // clang-format off Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bIdentity, &MprisPlayer::identityChanged); diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index bd922cd..cbe9669 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -51,7 +51,7 @@ class MprisQml: public QObject { Q_PROPERTY(UntypedObjectModel* players READ players CONSTANT); public: - explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; + explicit MprisQml(QObject* parent = nullptr): QObject(parent) {} [[nodiscard]] ObjectModel* players(); }; diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index fc3f30b..7f5246c 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -154,29 +154,29 @@ public: [[nodiscard]] bool isLastGeneration() const; void setLastGeneration(); - [[nodiscard]] QBindable bindableExpireTimeout() const { return &this->bExpireTimeout; }; - [[nodiscard]] QBindable bindableAppName() const { return &this->bAppName; }; - [[nodiscard]] QBindable bindableAppIcon() const { return &this->bAppIcon; }; - [[nodiscard]] QBindable bindableSummary() const { return &this->bSummary; }; - [[nodiscard]] QBindable bindableBody() const { return &this->bBody; }; + [[nodiscard]] QBindable bindableExpireTimeout() const { return &this->bExpireTimeout; } + [[nodiscard]] QBindable bindableAppName() const { return &this->bAppName; } + [[nodiscard]] QBindable bindableAppIcon() const { return &this->bAppIcon; } + [[nodiscard]] QBindable bindableSummary() const { return &this->bSummary; } + [[nodiscard]] QBindable bindableBody() const { return &this->bBody; } [[nodiscard]] QBindable bindableUrgency() const { return &this->bUrgency; - }; + } [[nodiscard]] QList actions() const; - [[nodiscard]] QBindable bindableHasActionIcons() const { return &this->bHasActionIcons; }; - [[nodiscard]] QBindable bindableResident() const { return &this->bResident; }; - [[nodiscard]] QBindable bindableTransient() const { return &this->bTransient; }; - [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; }; - [[nodiscard]] QBindable bindableImage() const { return &this->bImage; }; - [[nodiscard]] QBindable bindableHasInlineReply() const { return &this->bHasInlineReply; }; + [[nodiscard]] QBindable bindableHasActionIcons() const { return &this->bHasActionIcons; } + [[nodiscard]] QBindable bindableResident() const { return &this->bResident; } + [[nodiscard]] QBindable bindableTransient() const { return &this->bTransient; } + [[nodiscard]] QBindable bindableDesktopEntry() const { return &this->bDesktopEntry; } + [[nodiscard]] QBindable bindableImage() const { return &this->bImage; } + [[nodiscard]] QBindable bindableHasInlineReply() const { return &this->bHasInlineReply; } [[nodiscard]] QBindable bindableInlineReplyPlaceholder() const { return &this->bInlineReplyPlaceholder; - }; + } - [[nodiscard]] QBindable bindableHints() const { return &this->bHints; }; + [[nodiscard]] QBindable bindableHints() const { return &this->bHints; } [[nodiscard]] NotificationCloseReason::Enum closeReason() const; void setTracked(bool tracked); diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index 14ea405..8473f04 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -55,8 +55,8 @@ protected: void registryBind(const char* interface, quint32 version); virtual void bind(); void unbind(); - virtual void bindHooks() {}; - virtual void unbindHooks() {}; + virtual void bindHooks() {} + virtual void unbindHooks() {} quint32 refcount = 0; pw_proxy* object = nullptr; diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index e2ed4f7..62fca1d 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -22,7 +22,7 @@ class UPower: public QObject { public: [[nodiscard]] UPowerDevice* displayDevice(); [[nodiscard]] ObjectModel* devices(); - [[nodiscard]] QBindable bindableOnBattery() const { return &this->bOnBattery; }; + [[nodiscard]] QBindable bindableOnBattery() const { return &this->bOnBattery; } static UPower* instance(); diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp index b2b5f02..a4fbe83 100644 --- a/src/services/upower/device.hpp +++ b/src/services/upower/device.hpp @@ -173,25 +173,25 @@ public: [[nodiscard]] QString address() const; [[nodiscard]] QString path() const; - [[nodiscard]] QBindable bindableType() const { return &this->bType; }; - [[nodiscard]] QBindable bindablePowerSupply() const { return &this->bPowerSupply; }; - [[nodiscard]] QBindable bindableEnergy() const { return &this->bEnergy; }; - [[nodiscard]] QBindable bindableEnergyCapacity() const { return &this->bEnergyCapacity; }; - [[nodiscard]] QBindable bindableChangeRate() const { return &this->bChangeRate; }; - [[nodiscard]] QBindable bindableTimeToEmpty() const { return &this->bTimeToEmpty; }; - [[nodiscard]] QBindable bindableTimeToFull() const { return &this->bTimeToFull; }; - [[nodiscard]] QBindable bindablePercentage() const { return &this->bPercentage; }; - [[nodiscard]] QBindable bindableIsPresent() const { return &this->bIsPresent; }; - [[nodiscard]] QBindable bindableState() const { return &this->bState; }; + [[nodiscard]] QBindable bindableType() const { return &this->bType; } + [[nodiscard]] QBindable bindablePowerSupply() const { return &this->bPowerSupply; } + [[nodiscard]] QBindable bindableEnergy() const { return &this->bEnergy; } + [[nodiscard]] QBindable bindableEnergyCapacity() const { return &this->bEnergyCapacity; } + [[nodiscard]] QBindable bindableChangeRate() const { return &this->bChangeRate; } + [[nodiscard]] QBindable bindableTimeToEmpty() const { return &this->bTimeToEmpty; } + [[nodiscard]] QBindable bindableTimeToFull() const { return &this->bTimeToFull; } + [[nodiscard]] QBindable bindablePercentage() const { return &this->bPercentage; } + [[nodiscard]] QBindable bindableIsPresent() const { return &this->bIsPresent; } + [[nodiscard]] QBindable bindableState() const { return &this->bState; } [[nodiscard]] QBindable bindableHealthPercentage() const { return &this->bHealthPercentage; - }; - [[nodiscard]] QBindable bindableHealthSupported() const { return &this->bHealthSupported; }; - [[nodiscard]] QBindable bindableIconName() const { return &this->bIconName; }; - [[nodiscard]] QBindable bindableIsLaptopBattery() const { return &this->bIsLaptopBattery; }; - [[nodiscard]] QBindable bindableNativePath() const { return &this->bNativePath; }; - [[nodiscard]] QBindable bindableModel() const { return &this->bModel; }; - [[nodiscard]] QBindable bindableReady() const { return &this->bReady; }; + } + [[nodiscard]] QBindable bindableHealthSupported() const { return &this->bHealthSupported; } + [[nodiscard]] QBindable bindableIconName() const { return &this->bIconName; } + [[nodiscard]] QBindable bindableIsLaptopBattery() const { return &this->bIsLaptopBattery; } + [[nodiscard]] QBindable bindableNativePath() const { return &this->bNativePath; } + [[nodiscard]] QBindable bindableModel() const { return &this->bModel; } + [[nodiscard]] QBindable bindableReady() const { return &this->bReady; } signals: QSDOC_HIDE void readyChanged(); diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp index f0a2a13..aca4172 100644 --- a/src/widgets/wrapper.hpp +++ b/src/widgets/wrapper.hpp @@ -139,8 +139,8 @@ protected: void printChildCountWarning() const; void updateGeometry(); - virtual void disconnectChild() {}; - virtual void connectChild() {}; + virtual void disconnectChild() {} + virtual void connectChild() {} QQuickItem* mWrapper = nullptr; QQuickItem* mAssignedWrapper = nullptr; From dfededc901d4d103d5e238724a871965c6fe3b56 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 25 Jul 2025 18:24:43 -0700 Subject: [PATCH 083/226] launch: ignore QT_STYLE_OVERRIDE and QT_QUICK_CONTROLS_STYLE QT_STYLE_OVERRIDE often results in unexpected QML dependencies that don't exist being required. QT_QUICK_CONTROLS_STYLE can vary across systems and produce unexpected results. --- src/launch/launch.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 91e2e24..fd6a0af 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -73,6 +73,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio bool useQApplication = false; bool nativeTextRendering = false; bool desktopSettingsAware = true; + bool useSystemStyle = false; QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QHash envOverrides; QString dataDir; @@ -88,6 +89,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio if (pragma == "UseQApplication") pragmas.useQApplication = true; else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; + else if (pragma == "RespectSystemStyle") pragmas.useSystemStyle = true; else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); else if (pragma.startsWith("Env ")) { auto envPragma = pragma.sliced(4); @@ -155,6 +157,11 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment(); + if (!pragmas.useSystemStyle) { + qunsetenv("QT_STYLE_OVERRIDE"); + qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion"); + } + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { qputenv(var.toUtf8(), val.toUtf8()); } From 448623de5a7cff8a878373b54d3675b5840a54f9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 25 Jul 2025 22:08:15 -0700 Subject: [PATCH 084/226] service/notifications: use bytes over bits in pixmap rowstride check Fixes incorrect rowstride warnings. --- src/services/notifications/dbusimage.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/services/notifications/dbusimage.cpp b/src/services/notifications/dbusimage.cpp index e6c091b..469d08c 100644 --- a/src/services/notifications/dbusimage.cpp +++ b/src/services/notifications/dbusimage.cpp @@ -42,10 +42,9 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationI } else if (channels != (pixmap.hasAlpha ? 4 : 3)) { qCWarning(logNotifications) << "Unable to parse pixmap as channel count is incorrect." << "Got " << channels << "expected" << (pixmap.hasAlpha ? 4 : 3); - } else if (rowstride != pixmap.width * sampleBits * channels) { + } else if (rowstride != pixmap.width * channels) { qCWarning(logNotifications) << "Unable to parse pixmap as rowstride is incorrect. Got" - << rowstride << "expected" - << (pixmap.width * sampleBits * channels); + << rowstride << "expected" << (pixmap.width * channels); } return argument; @@ -55,7 +54,7 @@ const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationI argument.beginStructure(); argument << pixmap.width; argument << pixmap.height; - argument << pixmap.width * (pixmap.hasAlpha ? 4 : 3) * 8; + argument << pixmap.width * (pixmap.hasAlpha ? 4 : 3); argument << pixmap.hasAlpha; argument << 8; argument << (pixmap.hasAlpha ? 4 : 3); From ab096b7e784a84015633b0ca1d5c63095444cbe1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 00:09:28 -0700 Subject: [PATCH 085/226] wayland/screencopy: reset buffer requests between frames Prevents buffer requests from collecting a huge set of duplicate dmabuf and shm formats. --- src/wayland/buffer/manager.cpp | 5 ++++- src/wayland/buffer/manager.hpp | 2 ++ .../screencopy/hyprland_screencopy/hyprland_screencopy.cpp | 2 ++ src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp index c7448df..6bbdf29 100644 --- a/src/wayland/buffer/manager.cpp +++ b/src/wayland/buffer/manager.cpp @@ -22,6 +22,8 @@ namespace { QS_LOGGING_CATEGORY(logBuffer, "quickshell.wayland.buffer", QtWarningMsg); } +void WlBufferRequest::reset() { *this = WlBufferRequest(); } + WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) { auto& buffer = this->presentSecondBuffer ? this->buffer1 : this->buffer2; @@ -53,7 +55,8 @@ bool WlBufferManager::isReady() const { return this->p->mReady; } << " (disabled: " << dmabufDisabled << ')'; for (const auto& [format, modifiers]: request.dmabuf.formats) { - qCDebug(logBuffer) << " Format" << dmabuf::FourCCStr(format); + qCDebug(logBuffer).nospace() << " Format " << dmabuf::FourCCStr(format) + << (modifiers.length() == 0 ? " (No modifiers specified)" : ""); for (const auto& modifier: modifiers) { qCDebug(logBuffer) << " Explicit Modifier" << dmabuf::FourCCModStr(modifier); diff --git a/src/wayland/buffer/manager.hpp b/src/wayland/buffer/manager.hpp index b521e89..8abc218 100644 --- a/src/wayland/buffer/manager.hpp +++ b/src/wayland/buffer/manager.hpp @@ -68,6 +68,8 @@ struct WlBufferRequest { dev_t device = 0; StackList formats; } dmabuf; + + void reset(); }; class WlBuffer { diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp index b8aef96..5268f66 100644 --- a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp @@ -64,6 +64,8 @@ void HyprlandScreencopyContext::onToplevelDestroyed() { void HyprlandScreencopyContext::captureFrame() { if (this->object()) return; + this->request.reset(); + this->init(this->manager->capture_toplevel_with_wlr_toplevel_handle( this->paintCursors ? 1 : 0, this->handle->object() diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index f4d8c48..c7a11a7 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -65,6 +65,8 @@ void WlrScreencopyContext::onScreenDestroyed() { void WlrScreencopyContext::captureFrame() { if (this->object()) return; + this->request.reset(); + if (this->region.isEmpty()) { this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output())); } else { From 91c9db581e4a5ecb21dcfe9fa81bacd582745b49 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 00:48:21 -0700 Subject: [PATCH 086/226] wayland/screencopy: handle buffer creation failures --- src/wayland/buffer/manager.cpp | 5 +++++ .../hyprland_screencopy/hyprland_screencopy.cpp | 10 ++++++++++ .../image_copy_capture/image_copy_capture.cpp | 6 ++++++ .../screencopy/wlr_screencopy/wlr_screencopy.cpp | 9 +++++++++ 4 files changed, 30 insertions(+) diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp index 6bbdf29..713752a 100644 --- a/src/wayland/buffer/manager.cpp +++ b/src/wayland/buffer/manager.cpp @@ -69,6 +69,11 @@ bool WlBufferManager::isReady() const { return this->p->mReady; } qCDebug(logBuffer) << " Format" << format; } + if (request.width == 0 || request.height == 0) { + qCWarning(logBuffer) << "Cannot create zero-sized buffer."; + return nullptr; + } + if (!dmabufDisabled) { if (auto* buf = this->p->dmabuf.createDmabuf(request)) return buf; qCWarning(logBuffer) << "DMA buffer creation failed, falling back to SHM."; diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp index 5268f66..6fc2955 100644 --- a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp @@ -103,6 +103,16 @@ void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_flags(uint32_t void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_buffer_done() { auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + + if (!backbuffer || !backbuffer->buffer()) { + qCWarning(logScreencopy) << "Backbuffer creation failed for screencopy. Skipping frame."; + + // Try again. This will be spammy if the compositor continuously sends bad frames. + this->destroy(); + this->captureFrame(); + return; + } + this->copy(backbuffer->buffer(), this->copiedFirstFrame ? 0 : 1); } diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp index a307d1e..13d1bc6 100644 --- a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp @@ -117,6 +117,12 @@ void IccScreencopyContext::doCapture() { auto newBuffer = false; auto* backbuffer = this->mSwapchain.createBackbuffer(this->request, &newBuffer); + if (!backbuffer || !backbuffer->buffer()) { + qCWarning(logIcc) << "Backbuffer creation failed for screencopy. Waiting for updated buffer " + "creation parameters before trying again."; + return; + } + this->IccCaptureFrame::init(this->IccCaptureSession::create_frame()); this->IccCaptureFrame::attach_buffer(backbuffer->buffer()); diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index c7a11a7..e1553f5 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -111,6 +111,15 @@ void WlrScreencopyContext::zwlr_screencopy_frame_v1_flags(uint32_t flags) { void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer_done() { auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + if (!backbuffer || !backbuffer->buffer()) { + qCWarning(logScreencopy) << "Backbuffer creation failed for screencopy. Skipping frame."; + + // Try again. This will be spammy if the compositor continuously sends bad frames. + this->destroy(); + this->captureFrame(); + return; + } + if (this->copiedFirstFrame) { this->copy_with_damage(backbuffer->buffer()); } else { From 1644ed5e195bcee0ce32d14574480283af6c3d36 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 17:02:35 -0700 Subject: [PATCH 087/226] bluetooth: do not try to enable rfkilled devices Bluez will not do this and reports a property change failure. --- src/bluetooth/adapter.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp index 92ab837..0d8a319 100644 --- a/src/bluetooth/adapter.cpp +++ b/src/bluetooth/adapter.cpp @@ -53,6 +53,12 @@ QString BluetoothAdapter::adapterId() const { void BluetoothAdapter::setEnabled(bool enabled) { if (enabled == this->bEnabled) return; + + if (enabled && this->bState == BluetoothAdapterState::Blocked) { + qCCritical(logAdapter) << "Cannot enable adapter because it is blocked by rfkill."; + return; + } + this->bEnabled = enabled; this->pEnabled.write(); } From 0416032a7c2f2fdab2abdd262a4e4f8a5c6dcea5 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 17:28:03 -0700 Subject: [PATCH 088/226] core/reloader: trigger postReload with a signal A signal is now used over the previous tree-searching method as some QML components such as Repeater fail to reparent created children to themselves, which breaks the tree. --- src/core/generation.cpp | 5 +++-- src/core/generation.hpp | 1 + src/core/reload.cpp | 14 +++++++++----- src/core/reload.hpp | 4 ++-- src/core/singleton.cpp | 6 ------ src/core/singleton.hpp | 1 - 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 54a1b86..fee9441 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -161,8 +161,9 @@ void EngineGeneration::postReload() { if (this->engine == nullptr || this->root == nullptr) return; QsEnginePlugin::runOnReload(); - PostReloadHook::postReloadTree(this->root); - this->singletonRegistry.onPostReload(); + + emit this->firePostReload(); + QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr); } void EngineGeneration::setWatchingFiles(bool watching) { diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 5d3c5c6..3c0c4ae 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -75,6 +75,7 @@ public: signals: void filesChanged(); void reloadFinished(); + void firePostReload(); public slots: void quit(); diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 0bdf8fc..ea2abbf 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -129,14 +129,18 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId void PostReloadHook::componentComplete() { auto* engineGeneration = EngineGeneration::findObjectGeneration(this); if (!engineGeneration || engineGeneration->reloadComplete) this->postReload(); + else { + // disconnected by EngineGeneration::postReload + QObject::connect( + engineGeneration, + &EngineGeneration::firePostReload, + this, + &PostReloadHook::postReload + ); + } } void PostReloadHook::postReload() { this->isPostReload = true; this->onPostReload(); } - -void PostReloadHook::postReloadTree(QObject* root) { - for (auto* child: root->children()) PostReloadHook::postReloadTree(child); - if (auto* self = dynamic_cast(root)) self->postReload(); -} diff --git a/src/core/reload.hpp b/src/core/reload.hpp index 0ed34ee..ae5d7c9 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -131,10 +131,10 @@ public: void classBegin() override {} void componentComplete() override; - void postReload(); virtual void onPostReload() = 0; - static void postReloadTree(QObject* root); +public slots: + void postReload(); protected: bool isPostReload = false; diff --git a/src/core/singleton.cpp b/src/core/singleton.cpp index 61ac992..15668c9 100644 --- a/src/core/singleton.cpp +++ b/src/core/singleton.cpp @@ -51,9 +51,3 @@ void SingletonRegistry::onReload(SingletonRegistry* old) { singleton->reload(old == nullptr ? nullptr : old->registry.value(url)); } } - -void SingletonRegistry::onPostReload() { - for (auto* singleton: this->registry.values()) { - PostReloadHook::postReloadTree(singleton); - } -} diff --git a/src/core/singleton.hpp b/src/core/singleton.hpp index e63ab12..200c97f 100644 --- a/src/core/singleton.hpp +++ b/src/core/singleton.hpp @@ -26,7 +26,6 @@ public: void registerSingleton(const QUrl& url, Singleton* singleton); void onReload(SingletonRegistry* old); - void onPostReload(); private: QHash registry; From 1c026545e9f45ad7b252e31643d2725beca653af Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 22:50:17 -0700 Subject: [PATCH 089/226] core/desktopentry: use this-> in heuristicLookup --- src/core/desktopentry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index bb0d2c5..95fcb89 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -387,7 +387,7 @@ DesktopEntry* DesktopEntryManager::byId(const QString& id) { } DesktopEntry* DesktopEntryManager::heuristicLookup(const QString& name) { - if (auto* entry = DesktopEntryManager::byId(name)) return entry; + if (auto* entry = this->byId(name)) return entry; auto list = this->desktopEntries.values(); From f0d5f48a8232c638bc852c9ea4219494592251a6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 22:50:32 -0700 Subject: [PATCH 090/226] docs: add changelogs --- changelog/v0.1.0.md | 1 + changelog/v0.2.0.md | 84 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 changelog/v0.1.0.md create mode 100644 changelog/v0.2.0.md diff --git a/changelog/v0.1.0.md b/changelog/v0.1.0.md new file mode 100644 index 0000000..f8a032f --- /dev/null +++ b/changelog/v0.1.0.md @@ -0,0 +1 @@ +Initial release diff --git a/changelog/v0.2.0.md b/changelog/v0.2.0.md new file mode 100644 index 0000000..2fbf74d --- /dev/null +++ b/changelog/v0.2.0.md @@ -0,0 +1,84 @@ +## Breaking Changes + +- Files outside of the shell directory can no longer be referenced with relative paths, e.g. '../../foo.png'. +- PanelWindow's Automatic exclusion mode now adds an exclusion zone for panels with a single anchor. +- `QT_QUICK_CONTROLS_STYLE` and `QT_STYLE_OVERRIDE` are ignored unless `//@ pragma RespectSystemStyle` is set. + +## New Features + +### Root-Relative Imports + +Quickshell 0.2 comes with a new method to import QML modules which is supported by QMLLS. +This replaces "root:/" imports for QML modules. + +The new syntax is `import qs.path.to.module`, where `path/to/module` is the path to +a module/subdirectory relative to the config root (`qs`). + +### Better LSP support + +LSP support for Singletons and Root-Relative imports can be enabled by creating a file named +`.qmlls.ini` in the shell root directory. Quickshell will detect this file and automatically +populate it with an LSP configuration. This file should be gitignored in your configuration, +as it is system dependent. + +The generated configuration also includes QML import paths available to Quickshell, meaning +QMLLS no longer requires the `-E` flag. + +### Bluetooth Module + +Quickshell can now manage your bluetooth devices through BlueZ. While authenticated pairing +has not landed in 0.2, support for connecting and disconnecting devices, basic device information, +and non-authenticated pairing are now supported. + +### Other Features + +- Added `HyprlandToplevel` and related toplevel/window management APIs in the Hyprland module. +- Added `Quickshell.execDetached()`, which spawns a detached process without a `Process` object. +- Added `Process.exec()` for easier reconfiguration of process commands when starting them. +- Added `FloatingWindow.title`, which allows changing the title of a floating window. +- Added `signal QsWindow.closed()`, fired when a window is closed externally. +- Added support for inline replies in notifications, when supported by applications. +- Added `DesktopEntry.startupWmClass` and `DesktopEntry.heuristicLookup()` to better identify toplevels. +- Added `DesktopEntry.command` which can be run as an alternative to `DesktopEntry.execute()`. +- Added `//@ pragma Internal`, which makes a QML component impossible to import outside of its module. +- Added dead instance selection for some subcommands, such as `qs log` and `qs list`. + +## Other Changes + +- `Quickshell.shellRoot` has been renamed to `Quickshell.shellDir`. +- PanelWindow margins opposite the window's anchorpoint are now added to exclusion zone. +- stdout/stderr or detached processes and executed desktop entries are now hidden by default. +- Various warnings caused by other applications Quickshell communicates with over D-BUS have been hidden in logs. +- Quickshell's new logo is now shown in any floating windows. + +## Bug Fixes + +- Fixed pipewire device volume and mute states not updating before the device has been used. +- Fixed a crash when changing the volume of any pipewire device on a sound card another removed device was using. +- Fixed a crash when accessing a removed previous default pipewire node from the default sink/source changed signals. +- Fixed session locks crashing if all monitors are disconnected. +- Fixed session locks crashing if unsupported by the compositor. +- Fixed a crash when creating a session lock and destroying it before acknowledged by the compositor. +- Fixed window input masks not updating after a reload. +- Fixed PanelWindows being unconfigurable unless `screen` was set under X11. +- Fixed a crash when anchoring a popup to a zero sized `Item`. +- Fixed `FileView` crashing if `watchChanges` was used. +- Fixed `SocketServer` sockets disappearing after a reload. +- Fixed `ScreencopyView` having incorrect rotation when displaying a rotated monitor. +- Fixed `MarginWrapperManager` breaking pixel alignment of child items when centering. +- Fixed `IpcHandler`, `NotificationServer` and `GlobalShortcut` not activating with certain QML structures. +- Fixed tracking of QML incubator destruction and deregistration, which occasionally caused crashes. +- Fixed FloatingWindows being constrained to the smallest window manager supported size unless max size was set. +- Fixed `MprisPlayer.lengthSupported` not updating reactively. +- Fixed normal tray icon being ignored when status is `NeedsAttention` and no attention icon is provided. +- Fixed `HyprlandWorkspace.activate()` sending invalid commands to Hyprland for named or special workspaces. +- Fixed file watcher occasionally breaking when using VSCode to edit QML files. +- Fixed crashes when screencopy buffer creation fails. +- Fixed a crash when wayland layer surfaces are recreated for the same window. +- Fixed the `QsWindow` attached object not working when using `WlrLayershell` directly. +- Fixed a crash when attempting to create a window without available VRAM. +- Fixed OOM crash when failing to write to detailed log file. +- Prevented distro logging configurations for Qt from interfering with Quickshell commands. +- Removed the "QProcess destroyed for running process" warning when destroying `Process` objects. +- Fixed `ColorQuantizer` printing a pointer to an error message instead of an error message. +- Fixed notification pixmap rowstride warning showing for correct rowstrides. From a5431dd02dc23d9ef1680e67777fed00fe5f7cda Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 26 Jul 2025 22:50:52 -0700 Subject: [PATCH 091/226] version: bump to 0.2.0 --- CMakeLists.txt | 2 +- default.nix | 2 +- src/launch/command.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ef5b98..55b5e5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(quickshell VERSION "0.1.0" LANGUAGES CXX C) +project(quickshell VERSION "0.2.0" LANGUAGES CXX C) set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) diff --git a/default.nix b/default.nix index 7dc68e2..71c949e 100644 --- a/default.nix +++ b/default.nix @@ -46,7 +46,7 @@ }: let unwrapped = buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; - version = "0.1.0"; + version = "0.2.0"; src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; dontWrapQtApps = true; # see wrappers diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 64eb076..8a9c6de 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.1.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; + << "quickshell 0.2.0, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; if (state.log.verbosity > 1) { qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; From b8625aa0987cbe1bb3d94e21ba5ac701d9aaf993 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 27 Aug 2025 02:30:16 -0700 Subject: [PATCH 092/226] wayland/idle-inhibit: add idle inhibitor --- src/wayland/CMakeLists.txt | 3 + src/wayland/idle_inhibit/CMakeLists.txt | 25 ++++ .../idle_inhibit/idle-inhibit-unstable-v1.xml | 83 +++++++++++ src/wayland/idle_inhibit/inhibitor.cpp | 136 ++++++++++++++++++ src/wayland/idle_inhibit/inhibitor.hpp | 72 ++++++++++ src/wayland/idle_inhibit/proto.cpp | 23 +++ src/wayland/idle_inhibit/proto.hpp | 34 +++++ src/wayland/module.md | 1 + 8 files changed, 377 insertions(+) create mode 100644 src/wayland/idle_inhibit/CMakeLists.txt create mode 100644 src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml create mode 100644 src/wayland/idle_inhibit/inhibitor.cpp create mode 100644 src/wayland/idle_inhibit/inhibitor.hpp create mode 100644 src/wayland/idle_inhibit/proto.cpp create mode 100644 src/wayland/idle_inhibit/proto.hpp diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 1d6543e..ea2a5d8 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -114,6 +114,9 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +add_subdirectory(idle_inhibit) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/idle_inhibit/CMakeLists.txt b/src/wayland/idle_inhibit/CMakeLists.txt new file mode 100644 index 0000000..040e10f --- /dev/null +++ b/src/wayland/idle_inhibit/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-idle-inhibit STATIC + proto.cpp + inhibitor.cpp +) + +qt_add_qml_module(quickshell-wayland-idle-inhibit + URI Quickshell.Wayland._IdleInhibitor + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-idle-inhibit) + +qs_add_module_deps_light(quickshell-wayland-idle-inhibit Quickshell) + +wl_proto(wlp-idle-inhibit idle-inhibit-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(quickshell-wayland-idle-inhibit PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-idle-inhibit +) + +qs_module_pch(quickshell-wayland-idle-inhibit SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-idle-inhibitplugin) diff --git a/src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml b/src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml new file mode 100644 index 0000000..9c06cdc --- /dev/null +++ b/src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml @@ -0,0 +1,83 @@ + + + + + Copyright © 2015 Samsung Electronics Co., Ltd + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This interface permits inhibiting the idle behavior such as screen + blanking, locking, and screensaving. The client binds the idle manager + globally, then creates idle-inhibitor objects for each surface. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + Destroy the inhibit manager. + + + + + + Create a new inhibitor object associated with the given surface. + + + + + + + + + + An idle inhibitor prevents the output that the associated surface is + visible on from being set to a state where it is not visually usable due + to lack of user interaction (e.g. blanked, dimmed, locked, set to power + save, etc.) Any screensaver processes are also blocked from displaying. + + If the surface is destroyed, unmapped, becomes occluded, loses + visibility, or otherwise becomes not visually relevant for the user, the + idle inhibitor will not be honored by the compositor; if the surface + subsequently regains visibility the inhibitor takes effect once again. + Likewise, the inhibitor isn't honored if the system was already idled at + the time the inhibitor was established, although if the system later + de-idles and re-idles the inhibitor will take effect. + + + + + Remove the inhibitor effect from the associated wl_surface. + + + + + diff --git a/src/wayland/idle_inhibit/inhibitor.cpp b/src/wayland/idle_inhibit/inhibitor.cpp new file mode 100644 index 0000000..f576722 --- /dev/null +++ b/src/wayland/idle_inhibit/inhibitor.cpp @@ -0,0 +1,136 @@ +#include "inhibitor.hpp" + +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_inhibit { +using QtWaylandClient::QWaylandWindow; + +IdleInhibitor::IdleInhibitor() { + this->bBoundWindow.setBinding([this] { return this->bEnabled ? this->bWindowObject : nullptr; }); +} + +QObject* IdleInhibitor::window() const { return this->bWindowObject; } + +void IdleInhibitor::setWindow(QObject* window) { + if (window == this->bWindowObject) return; + + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + this->bWindowObject = proxyWindow ? window : nullptr; +} + +void IdleInhibitor::boundWindowChanged() { + auto* window = this->bBoundWindow.value(); + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow == this->proxyWindow) return; + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + this->mWaylandWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + } + + if (this->proxyWindow) { + QObject::disconnect(this->proxyWindow, nullptr, this, nullptr); + this->proxyWindow = nullptr; + } + + if (proxyWindow) { + this->proxyWindow = proxyWindow; + + QObject::connect(proxyWindow, &QObject::destroyed, this, &IdleInhibitor::onWindowDestroyed); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &IdleInhibitor::onWindowVisibilityChanged + ); + + this->onWindowVisibilityChanged(); + } + + emit this->windowChanged(); +} + +void IdleInhibitor::onWindowDestroyed() { + this->proxyWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + this->bWindowObject = nullptr; +} + +void IdleInhibitor::onWindowVisibilityChanged() { + if (!this->proxyWindow->isVisibleDirect()) return; + + auto* window = this->proxyWindow->backingWindow(); + if (!window->handle()) window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (waylandWindow == this->mWaylandWindow) return; + this->mWaylandWindow = waylandWindow; + + QObject::connect( + waylandWindow, + &QObject::destroyed, + this, + &IdleInhibitor::onWaylandWindowDestroyed + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &IdleInhibitor::onWaylandSurfaceCreated + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &IdleInhibitor::onWaylandSurfaceDestroyed + ); + + if (waylandWindow->surface()) this->onWaylandSurfaceCreated(); +} + +void IdleInhibitor::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void IdleInhibitor::onWaylandSurfaceCreated() { + auto* manager = impl::IdleInhibitManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable idle inhibitor as idle-inhibit-unstable-v1 is not supported by " + "the current compositor."; + return; + } + + delete this->inhibitor; + this->inhibitor = manager->createIdleInhibitor(this->mWaylandWindow); +} + +void IdleInhibitor::onWaylandSurfaceDestroyed() { + delete this->inhibitor; + this->inhibitor = nullptr; +} + +} // namespace qs::wayland::idle_inhibit diff --git a/src/wayland/idle_inhibit/inhibitor.hpp b/src/wayland/idle_inhibit/inhibitor.hpp new file mode 100644 index 0000000..3d16bd4 --- /dev/null +++ b/src/wayland/idle_inhibit/inhibitor.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_inhibit { + +///! Prevents a wayland session from idling +/// If an idle daemon is running, it may perform actions such as locking the screen +/// or putting the computer to sleep. +/// +/// An idle inhibitor prevents a wayland session from being marked as idle, if compositor +/// defined heuristics determine the window the inhibitor is attached to is important. +/// +/// A compositor will usually consider a @@Quickshell.PanelWindow or +/// a focused @@Quickshell.FloatingWindow to be important. +/// +/// > [!NOTE] Using an idle inhibitor requires the compositor support the [idle-inhibit-unstable-v1] protocol. +/// +/// [idle-inhibit-unstable-v1]: https://wayland.app/protocols/idle-inhibit-unstable-v1 +class IdleInhibitor: public QObject { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the idle inhibitor should be enabled. Defaults to false. + Q_PROPERTY(bool enabled READ default WRITE default NOTIFY enabledChanged BINDABLE bindableEnabled); + /// The window to associate the idle inhibitor with. This may be used by the compositor + /// to determine if the inhibitor should be respected. + /// + /// Must be set to a non null value to enable the inhibitor. + Q_PROPERTY(QObject* window READ window WRITE setWindow NOTIFY windowChanged); + // clang-format on + +public: + IdleInhibitor(); + + [[nodiscard]] QObject* window() const; + void setWindow(QObject* window); + + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + +signals: + void enabledChanged(); + void windowChanged(); + +private slots: + void onWindowDestroyed(); + void onWindowVisibilityChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + +private: + void boundWindowChanged(); + ProxyWindowBase* proxyWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + impl::IdleInhibitor* inhibitor = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(IdleInhibitor, bool, bEnabled, &IdleInhibitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleInhibitor, QObject*, bWindowObject, &IdleInhibitor::windowChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleInhibitor, QObject*, bBoundWindow, &IdleInhibitor::boundWindowChanged); + // clang-format on +}; + +} // namespace qs::wayland::idle_inhibit diff --git a/src/wayland/idle_inhibit/proto.cpp b/src/wayland/idle_inhibit/proto.cpp new file mode 100644 index 0000000..0caab91 --- /dev/null +++ b/src/wayland/idle_inhibit/proto.cpp @@ -0,0 +1,23 @@ +#include "proto.hpp" + +#include +#include + +namespace qs::wayland::idle_inhibit::impl { + +IdleInhibitManager::IdleInhibitManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +IdleInhibitManager* IdleInhibitManager::instance() { + static auto* instance = new IdleInhibitManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +IdleInhibitor* IdleInhibitManager::createIdleInhibitor(QtWaylandClient::QWaylandWindow* surface) { + return new IdleInhibitor(this->create_inhibitor(surface->surface())); +} + +IdleInhibitor::~IdleInhibitor() { + if (this->isInitialized()) this->destroy(); +} + +} // namespace qs::wayland::idle_inhibit::impl diff --git a/src/wayland/idle_inhibit/proto.hpp b/src/wayland/idle_inhibit/proto.hpp new file mode 100644 index 0000000..c797c33 --- /dev/null +++ b/src/wayland/idle_inhibit/proto.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +#include "wayland-idle-inhibit-unstable-v1-client-protocol.h" + +namespace qs::wayland::idle_inhibit::impl { + +class IdleInhibitor; + +class IdleInhibitManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_idle_inhibit_manager_v1 { +public: + explicit IdleInhibitManager(); + + IdleInhibitor* createIdleInhibitor(QtWaylandClient::QWaylandWindow* surface); + + static IdleInhibitManager* instance(); +}; + +class IdleInhibitor: public QtWayland::zwp_idle_inhibitor_v1 { +public: + explicit IdleInhibitor(::zwp_idle_inhibitor_v1* inhibitor) + : QtWayland::zwp_idle_inhibitor_v1(inhibitor) {} + + ~IdleInhibitor() override; + Q_DISABLE_COPY_MOVE(IdleInhibitor); +}; + +} // namespace qs::wayland::idle_inhibit::impl diff --git a/src/wayland/module.md b/src/wayland/module.md index b9f8f59..622346a 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -5,5 +5,6 @@ headers = [ "session_lock.hpp", "toplevel_management/qml.hpp", "screencopy/view.hpp", + "idle_inhibit/inhibitor.hpp", ] ----- From 42420ea26dfda71e026d0428e86e5a0064914024 Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 27 Aug 2025 19:41:02 -0400 Subject: [PATCH 093/226] wayland/idle-inhibit: use bindable .value() instead of implicit cast Fixes compilation on some targets. --- src/wayland/idle_inhibit/inhibitor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wayland/idle_inhibit/inhibitor.cpp b/src/wayland/idle_inhibit/inhibitor.cpp index f576722..697f127 100644 --- a/src/wayland/idle_inhibit/inhibitor.cpp +++ b/src/wayland/idle_inhibit/inhibitor.cpp @@ -13,7 +13,7 @@ namespace qs::wayland::idle_inhibit { using QtWaylandClient::QWaylandWindow; IdleInhibitor::IdleInhibitor() { - this->bBoundWindow.setBinding([this] { return this->bEnabled ? this->bWindowObject : nullptr; }); + this->bBoundWindow.setBinding([this] { return this->bEnabled ? this->bWindowObject.value() : nullptr; }); } QObject* IdleInhibitor::window() const { return this->bWindowObject; } From f7597cdae2d537c5b12843599955856090dc49d5 Mon Sep 17 00:00:00 2001 From: Derock Date: Wed, 13 Aug 2025 12:52:16 -0400 Subject: [PATCH 094/226] 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 f592793873f3cab387fafdad1d08a696a0edcede Mon Sep 17 00:00:00 2001 From: kossLAN Date: Tue, 2 Sep 2025 12:38:34 -0400 Subject: [PATCH 095/226] 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 2c2983462c4cfbab846bd4718c0cfd53d57d46a9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 4 Sep 2025 00:51:56 -0700 Subject: [PATCH 096/226] wayland/idle-inhibit: stop vendoring protocol Idle-inhibit is included in wayland-protocols and this was vendored by mistake. --- src/wayland/idle_inhibit/CMakeLists.txt | 2 +- .../idle_inhibit/idle-inhibit-unstable-v1.xml | 83 ------------------- 2 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml diff --git a/src/wayland/idle_inhibit/CMakeLists.txt b/src/wayland/idle_inhibit/CMakeLists.txt index 040e10f..eb346d6 100644 --- a/src/wayland/idle_inhibit/CMakeLists.txt +++ b/src/wayland/idle_inhibit/CMakeLists.txt @@ -13,7 +13,7 @@ install_qml_module(quickshell-wayland-idle-inhibit) qs_add_module_deps_light(quickshell-wayland-idle-inhibit Quickshell) -wl_proto(wlp-idle-inhibit idle-inhibit-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") +wl_proto(wlp-idle-inhibit idle-inhibit-unstable-v1 "${WAYLAND_PROTOCOLS}/unstable/idle-inhibit") target_link_libraries(quickshell-wayland-idle-inhibit PRIVATE Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client diff --git a/src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml b/src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml deleted file mode 100644 index 9c06cdc..0000000 --- a/src/wayland/idle_inhibit/idle-inhibit-unstable-v1.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - Copyright © 2015 Samsung Electronics Co., Ltd - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice (including the next - paragraph) shall be included in all copies or substantial portions of the - Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. - - - - - This interface permits inhibiting the idle behavior such as screen - blanking, locking, and screensaving. The client binds the idle manager - globally, then creates idle-inhibitor objects for each surface. - - Warning! The protocol described in this file is experimental and - backward incompatible changes may be made. Backward compatible changes - may be added together with the corresponding interface version bump. - Backward incompatible changes are done by bumping the version number in - the protocol and interface names and resetting the interface version. - Once the protocol is to be declared stable, the 'z' prefix and the - version number in the protocol and interface names are removed and the - interface version number is reset. - - - - - Destroy the inhibit manager. - - - - - - Create a new inhibitor object associated with the given surface. - - - - - - - - - - An idle inhibitor prevents the output that the associated surface is - visible on from being set to a state where it is not visually usable due - to lack of user interaction (e.g. blanked, dimmed, locked, set to power - save, etc.) Any screensaver processes are also blocked from displaying. - - If the surface is destroyed, unmapped, becomes occluded, loses - visibility, or otherwise becomes not visually relevant for the user, the - idle inhibitor will not be honored by the compositor; if the surface - subsequently regains visibility the inhibitor takes effect once again. - Likewise, the inhibitor isn't honored if the system was already idled at - the time the inhibitor was established, although if the system later - de-idles and re-idles the inhibitor will take effect. - - - - - Remove the inhibitor effect from the associated wl_surface. - - - - - From b8fa424f85b681772eab1948ff8b02808832748c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 4 Sep 2025 02:51:50 -0700 Subject: [PATCH 097/226] wayland/idle-inhibit: fix formatting + lints, destructor, add logs --- src/wayland/idle_inhibit/inhibitor.cpp | 6 +++++- src/wayland/idle_inhibit/inhibitor.hpp | 3 +++ src/wayland/idle_inhibit/proto.cpp | 13 +++++++++++- .../idle_inhibit/test/manual/idle_inhibit.qml | 20 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/wayland/idle_inhibit/test/manual/idle_inhibit.qml diff --git a/src/wayland/idle_inhibit/inhibitor.cpp b/src/wayland/idle_inhibit/inhibitor.cpp index 697f127..efeeae1 100644 --- a/src/wayland/idle_inhibit/inhibitor.cpp +++ b/src/wayland/idle_inhibit/inhibitor.cpp @@ -13,9 +13,13 @@ namespace qs::wayland::idle_inhibit { using QtWaylandClient::QWaylandWindow; IdleInhibitor::IdleInhibitor() { - this->bBoundWindow.setBinding([this] { return this->bEnabled ? this->bWindowObject.value() : nullptr; }); + this->bBoundWindow.setBinding([this] { + return this->bEnabled ? this->bWindowObject.value() : nullptr; + }); } +IdleInhibitor::~IdleInhibitor() { delete this->inhibitor; } + QObject* IdleInhibitor::window() const { return this->bWindowObject; } void IdleInhibitor::setWindow(QObject* window) { diff --git a/src/wayland/idle_inhibit/inhibitor.hpp b/src/wayland/idle_inhibit/inhibitor.hpp index 3d16bd4..c1a3e95 100644 --- a/src/wayland/idle_inhibit/inhibitor.hpp +++ b/src/wayland/idle_inhibit/inhibitor.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "../../window/proxywindow.hpp" @@ -38,6 +39,8 @@ class IdleInhibitor: public QObject { public: IdleInhibitor(); + ~IdleInhibitor() override; + Q_DISABLE_COPY_MOVE(IdleInhibitor); [[nodiscard]] QObject* window() const; void setWindow(QObject* window); diff --git a/src/wayland/idle_inhibit/proto.cpp b/src/wayland/idle_inhibit/proto.cpp index 0caab91..25701a7 100644 --- a/src/wayland/idle_inhibit/proto.cpp +++ b/src/wayland/idle_inhibit/proto.cpp @@ -1,10 +1,18 @@ #include "proto.hpp" #include +#include +#include #include +#include "../../core/logcat.hpp" + namespace qs::wayland::idle_inhibit::impl { +namespace { +QS_LOGGING_CATEGORY(logIdleInhibit, "quickshell.wayland.idle_inhibit", QtWarningMsg); +} + IdleInhibitManager::IdleInhibitManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } IdleInhibitManager* IdleInhibitManager::instance() { @@ -13,10 +21,13 @@ IdleInhibitManager* IdleInhibitManager::instance() { } IdleInhibitor* IdleInhibitManager::createIdleInhibitor(QtWaylandClient::QWaylandWindow* surface) { - return new IdleInhibitor(this->create_inhibitor(surface->surface())); + auto* inhibitor = new IdleInhibitor(this->create_inhibitor(surface->surface())); + qCDebug(logIdleInhibit) << "Created inhibitor" << inhibitor; + return inhibitor; } IdleInhibitor::~IdleInhibitor() { + qCDebug(logIdleInhibit) << "Destroyed inhibitor" << this; if (this->isInitialized()) this->destroy(); } diff --git a/src/wayland/idle_inhibit/test/manual/idle_inhibit.qml b/src/wayland/idle_inhibit/test/manual/idle_inhibit.qml new file mode 100644 index 0000000..f80e647 --- /dev/null +++ b/src/wayland/idle_inhibit/test/manual/idle_inhibit.qml @@ -0,0 +1,20 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + id: root + color: contentItem.palette.window + + CheckBox { + id: enableCb + anchors.centerIn: parent + text: "Enable Inhibitor" + } + + IdleInhibitor { + window: root + enabled: enableCb.checked + } +} From 6eb12551baf924f8fdecdd04113863a754259c34 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 4 Sep 2025 02:44:22 -0700 Subject: [PATCH 098/226] wayland/idle-notify: add idle notify --- src/wayland/CMakeLists.txt | 3 + src/wayland/idle_notify/CMakeLists.txt | 25 ++++++ src/wayland/idle_notify/monitor.cpp | 52 +++++++++++++ src/wayland/idle_notify/monitor.hpp | 78 +++++++++++++++++++ src/wayland/idle_notify/proto.cpp | 75 ++++++++++++++++++ src/wayland/idle_notify/proto.hpp | 51 ++++++++++++ .../idle_notify/test/manual/idle_notify.qml | 44 +++++++++++ src/wayland/module.md | 1 + 8 files changed, 329 insertions(+) create mode 100644 src/wayland/idle_notify/CMakeLists.txt create mode 100644 src/wayland/idle_notify/monitor.cpp create mode 100644 src/wayland/idle_notify/monitor.hpp create mode 100644 src/wayland/idle_notify/proto.cpp create mode 100644 src/wayland/idle_notify/proto.hpp create mode 100644 src/wayland/idle_notify/test/manual/idle_notify.qml diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ea2a5d8..cf4ebbc 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -117,6 +117,9 @@ endif() add_subdirectory(idle_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) +add_subdirectory(idle_notify) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/idle_notify/CMakeLists.txt b/src/wayland/idle_notify/CMakeLists.txt new file mode 100644 index 0000000..889c7ce --- /dev/null +++ b/src/wayland/idle_notify/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-idle-notify STATIC + proto.cpp + monitor.cpp +) + +qt_add_qml_module(quickshell-wayland-idle-notify + URI Quickshell.Wayland._IdleNotify + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-idle-notify) + +qs_add_module_deps_light(quickshell-wayland-idle-notify Quickshell) + +wl_proto(wlp-idle-notify ext-idle-notify-v1 "${WAYLAND_PROTOCOLS}/staging/ext-idle-notify") + +target_link_libraries(quickshell-wayland-idle-notify PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-idle-notify +) + +qs_module_pch(quickshell-wayland-idle-notify SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-idle-notifyplugin) diff --git a/src/wayland/idle_notify/monitor.cpp b/src/wayland/idle_notify/monitor.cpp new file mode 100644 index 0000000..e830d4b --- /dev/null +++ b/src/wayland/idle_notify/monitor.cpp @@ -0,0 +1,52 @@ +#include "monitor.hpp" +#include + +#include +#include +#include + +#include "proto.hpp" + +namespace qs::wayland::idle_notify { + +IdleMonitor::~IdleMonitor() { delete this->bNotification.value(); } + +void IdleMonitor::onPostReload() { + this->bParams.setBinding([this] { + return Params { + .enabled = this->bEnabled.value(), + .timeout = this->bTimeout.value(), + .respectInhibitors = this->bRespectInhibitors.value() + }; + }); + + this->bIsIdle.setBinding([this] { + auto* notification = this->bNotification.value(); + return notification ? notification->bIsIdle.value() : false; + }); +} + +void IdleMonitor::updateNotification() { + auto* notification = this->bNotification.value(); + delete notification; + notification = nullptr; + + auto guard = qScopeGuard([&] { this->bNotification = notification; }); + + auto params = this->bParams.value(); + + if (params.enabled) { + auto* manager = impl::IdleNotificationManager::instance(); + + if (!manager) { + qWarning() << "Cannot create idle monitor as ext-idle-notify-v1 is not supported by the " + "current compositor."; + return; + } + + auto timeout = static_cast(std::max(0, static_cast(params.timeout * 1000))); + notification = manager->createIdleNotification(timeout, params.respectInhibitors); + } +} + +} // namespace qs::wayland::idle_notify diff --git a/src/wayland/idle_notify/monitor.hpp b/src/wayland/idle_notify/monitor.hpp new file mode 100644 index 0000000..25bd5a6 --- /dev/null +++ b/src/wayland/idle_notify/monitor.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/reload.hpp" +#include "proto.hpp" + +namespace qs::wayland::idle_notify { + +///! Provides a notification when a wayland session is makred idle +/// An idle monitor detects when the user stops providing input for a period of time. +/// +/// > [!NOTE] Using an idle monitor requires the compositor support the [ext-idle-notify-v1] protocol. +/// +/// [ext-idle-notify-v1]: https://wayland.app/protocols/ext-idle-notify-v1 +class IdleMonitor: public PostReloadHook { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the idle monitor should be enabled. Defaults to true. + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged); + /// The amount of time in seconds the idle monitor should wait before reporting an idle state. + /// + /// Defaults to zero, which reports idle status immediately. + Q_PROPERTY(qreal timeout READ default WRITE default NOTIFY timeoutChanged BINDABLE bindableTimeout); + /// When set to true, @@isIdle will depend on both user interaction and active idle inhibitors. + /// When false, the value will depend solely on user interaction. Defaults to true. + Q_PROPERTY(bool respectInhibitors READ default WRITE default NOTIFY respectInhibitorsChanged BINDABLE bindableRespectInhibitors); + /// This property is true if the user has been idle for at least @@timeout. + /// What is considered to be idle is influenced by @@respectInhibitors. + Q_PROPERTY(bool isIdle READ default NOTIFY isIdleChanged BINDABLE bindableIsIdle); + // clang-format on + +public: + IdleMonitor() = default; + ~IdleMonitor() override; + Q_DISABLE_COPY_MOVE(IdleMonitor); + + void onPostReload() override; + + [[nodiscard]] bool isEnabled() const { return this->bNotification.value(); } + void setEnabled(bool enabled) { this->bEnabled = enabled; } + + [[nodiscard]] QBindable bindableTimeout() { return &this->bTimeout; } + [[nodiscard]] QBindable bindableRespectInhibitors() { return &this->bRespectInhibitors; } + [[nodiscard]] QBindable bindableIsIdle() const { return &this->bIsIdle; } + +signals: + void enabledChanged(); + void timeoutChanged(); + void respectInhibitorsChanged(); + void isIdleChanged(); + +private: + void updateNotification(); + + struct Params { + bool enabled; + qreal timeout; + bool respectInhibitors; + }; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(IdleMonitor, bool, bEnabled, true, &IdleMonitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, qreal, bTimeout, &IdleMonitor::timeoutChanged); + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(IdleMonitor, bool, bRespectInhibitors, true, &IdleMonitor::respectInhibitorsChanged); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, Params, bParams, &IdleMonitor::updateNotification); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, impl::IdleNotification*, bNotification); + Q_OBJECT_BINDABLE_PROPERTY(IdleMonitor, bool, bIsIdle, &IdleMonitor::isIdleChanged); + // clang-format on +}; + +} // namespace qs::wayland::idle_notify diff --git a/src/wayland/idle_notify/proto.cpp b/src/wayland/idle_notify/proto.cpp new file mode 100644 index 0000000..9b3fa81 --- /dev/null +++ b/src/wayland/idle_notify/proto.cpp @@ -0,0 +1,75 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_notify { +QS_LOGGING_CATEGORY(logIdleNotify, "quickshell.wayland.idle_notify", QtWarningMsg); +} + +namespace qs::wayland::idle_notify::impl { + +IdleNotificationManager::IdleNotificationManager(): QWaylandClientExtensionTemplate(2) { + this->initialize(); +} + +IdleNotificationManager* IdleNotificationManager::instance() { + static auto* instance = new IdleNotificationManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +IdleNotification* +IdleNotificationManager::createIdleNotification(quint32 timeout, bool respectInhibitors) { + if (!respectInhibitors + && this->QtWayland::ext_idle_notifier_v1::version() + < EXT_IDLE_NOTIFIER_V1_GET_INPUT_IDLE_NOTIFICATION_SINCE_VERSION) + { + qCWarning(logIdleNotify) << "Cannot ignore inhibitors for new idle notifier: Compositor does " + "not support protocol version 2."; + + respectInhibitors = true; + } + + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) inputDevice = display->defaultInputDevice(); + if (inputDevice == nullptr) { + qCCritical(logIdleNotify) << "Could not create idle notifier: No seat."; + return nullptr; + } + + ::ext_idle_notification_v1* notification = nullptr; + if (respectInhibitors) notification = this->get_idle_notification(timeout, inputDevice->object()); + else notification = this->get_input_idle_notification(timeout, inputDevice->object()); + + auto* wrapper = new IdleNotification(notification); + qCDebug(logIdleNotify) << "Created" << wrapper << "with timeout:" << timeout + << "respects inhibitors:" << respectInhibitors; + return wrapper; +} + +IdleNotification::~IdleNotification() { + qCDebug(logIdleNotify) << "Destroyed" << this; + if (this->isInitialized()) this->destroy(); +} + +void IdleNotification::ext_idle_notification_v1_idled() { + qCDebug(logIdleNotify) << this << "has been marked idle"; + this->bIsIdle = true; +} + +void IdleNotification::ext_idle_notification_v1_resumed() { + qCDebug(logIdleNotify) << this << "has been marked resumed"; + this->bIsIdle = false; +} + +} // namespace qs::wayland::idle_notify::impl diff --git a/src/wayland/idle_notify/proto.hpp b/src/wayland/idle_notify/proto.hpp new file mode 100644 index 0000000..11dbf29 --- /dev/null +++ b/src/wayland/idle_notify/proto.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::idle_notify { +QS_DECLARE_LOGGING_CATEGORY(logIdleNotify); +} + +namespace qs::wayland::idle_notify::impl { + +class IdleNotification; + +class IdleNotificationManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_idle_notifier_v1 { +public: + explicit IdleNotificationManager(); + IdleNotification* createIdleNotification(quint32 timeout, bool respectInhibitors); + + static IdleNotificationManager* instance(); +}; + +class IdleNotification + : public QObject + , public QtWayland::ext_idle_notification_v1 { + Q_OBJECT; + +public: + explicit IdleNotification(::ext_idle_notification_v1* notification) + : QtWayland::ext_idle_notification_v1(notification) {} + + ~IdleNotification() override; + Q_DISABLE_COPY_MOVE(IdleNotification); + + Q_OBJECT_BINDABLE_PROPERTY(IdleNotification, bool, bIsIdle); + +protected: + void ext_idle_notification_v1_idled() override; + void ext_idle_notification_v1_resumed() override; +}; + +} // namespace qs::wayland::idle_notify::impl diff --git a/src/wayland/idle_notify/test/manual/idle_notify.qml b/src/wayland/idle_notify/test/manual/idle_notify.qml new file mode 100644 index 0000000..3bf6cbd --- /dev/null +++ b/src/wayland/idle_notify/test/manual/idle_notify.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + color: contentItem.palette.window + + IdleMonitor { + id: monitor + enabled: enabledCb.checked + timeout: timeoutSb.value + respectInhibitors: respectInhibitorsCb.checked + } + + ColumnLayout { + Label { text: `Is idle? ${monitor.isIdle}` } + + CheckBox { + id: enabledCb + text: "Enabled" + checked: true + } + + CheckBox { + id: respectInhibitorsCb + text: "Respect Inhibitors" + checked: true + } + + RowLayout { + Label { text: "Timeout" } + + SpinBox { + id: timeoutSb + editable: true + from: 0 + to: 1000 + value: 5 + } + } + } +} diff --git a/src/wayland/module.md b/src/wayland/module.md index 622346a..0216e6d 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -6,5 +6,6 @@ headers = [ "toplevel_management/qml.hpp", "screencopy/view.hpp", "idle_inhibit/inhibitor.hpp", + "idle_notify/monitor.hpp", ] ----- From 49646e4407fce5925920b178872ddd9f8e495218 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 16 Sep 2025 00:15:13 -0700 Subject: [PATCH 099/226] 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 59f5744f307606435c52e7356ec67e3a483ddff0 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 14 Jul 2025 11:09:34 -0400 Subject: [PATCH 100/226] 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 e9a574d919a89602d2868621576b2ccae54a5cb0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 19 Sep 2025 00:16:26 -0700 Subject: [PATCH 101/226] 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 2119eb2205d7bb3de3fec814135eda2073328f50 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 18:55:45 -0700 Subject: [PATCH 102/226] 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 cf4ebbc..a96fe6b 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 b9905ef8244ef70c87d51fd5cdc2c48d9ff0276a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 20:24:43 -0700 Subject: [PATCH 103/226] 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 afada1eb6c78485dc9735c04d86be4a460b3b438 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 22:27:23 -0700 Subject: [PATCH 104/226] 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 a922694a7d3cba911ea90822c5b50ab0bc36ffa0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 22:27:31 -0700 Subject: [PATCH 105/226] 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 eeb8181cb13887cd761092cb2b3a099cdb9d94e2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 22:38:03 -0700 Subject: [PATCH 106/226] 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 f78078dfafaaf271d4a99c600ecf5bb4ad335707 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 20:25:13 -0700 Subject: [PATCH 107/226] 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 1d94144976252a922e03e4c0fbb921ee8b8bb079 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 28 Sep 2025 23:51:06 -0700 Subject: [PATCH 108/226] 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 +- src/wayland/idle_notify/monitor.cpp | 2 +- .../wlr_screencopy/wlr_screencopy.cpp | 6 +- 15 files changed, 61 insertions(+), 57 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/idle_notify/monitor.cpp b/src/wayland/idle_notify/monitor.cpp index e830d4b..3f496e4 100644 --- a/src/wayland/idle_notify/monitor.cpp +++ b/src/wayland/idle_notify/monitor.cpp @@ -31,7 +31,7 @@ void IdleMonitor::updateNotification() { delete notification; notification = nullptr; - auto guard = qScopeGuard([&] { this->bNotification = notification; }); + auto guard = qScopeGuard([&, this] { this->bNotification = notification; }); auto params = this->bParams.value(); 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 6092b37c56de4a88fb927a8ed2fb02cfe44c7006 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 29 Sep 2025 21:19:36 -0700 Subject: [PATCH 109/226] 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 482744cfa95cdb76a6175d08f29f35005b8e0887 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 29 Sep 2025 21:49:34 -0700 Subject: [PATCH 110/226] 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 475856b767b223887c671a5a62f72e981ef82d01 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 30 Sep 2025 23:28:05 -0700 Subject: [PATCH 111/226] docs: start tracking qs-next changelog --- changelog/next.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 changelog/next.md diff --git a/changelog/next.md b/changelog/next.md new file mode 100644 index 0000000..e2ba257 --- /dev/null +++ b/changelog/next.md @@ -0,0 +1,11 @@ +## New Features + +- Added support for creating wayland idle inhibitors. +- Added support for wayland idle timeouts. +- 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 9662234759eb57f2a1057f2a1c667da1bf128c1c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 1 Oct 2025 00:29:45 -0700 Subject: [PATCH 112/226] 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 e2ba257..3b5c9c3 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -6,6 +6,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 3bcc1993f46a7a09348edd02be23d46cee397cb9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Oct 2025 12:36:28 -0700 Subject: [PATCH 113/226] 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 3b5c9c3..10acd9a 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -4,6 +4,9 @@ - Added support for wayland idle timeouts. - 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 9bb2c043ae303acc281b9d8b08e5b756563ed0ac Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Oct 2025 12:49:36 -0700 Subject: [PATCH 114/226] 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 c5c438f1cd1a76660a8658ef929a3d19e968e2ce Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Oct 2025 13:22:17 -0700 Subject: [PATCH 115/226] 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 2eacb713b95bb7a9f0ab8e4a96ee43f47e8a8e36 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 18 Sep 2025 09:33:55 -0400 Subject: [PATCH 116/226] 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 10acd9a..4e24c67 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -13,3 +13,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 3e32ae595f97bd2d2e5ed4512fb4bb25edb4eae6 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 30 Sep 2025 08:17:03 -0400 Subject: [PATCH 117/226] 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 4e24c67..38f2f7a 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -14,3 +14,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 f12f0e7c7d883f737ac45b88c5993090b3c87cce 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 118/226] 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 38f2f7a..6aa800c 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -15,3 +15,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 c9d3ffb6043c5bf3f3009202bad7e0e5132c4a25 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 11 Oct 2025 17:14:14 -0700 Subject: [PATCH 119/226] version: bump to 0.2.1 --- BUILD.md | 4 ++-- CMakeLists.txt | 2 +- changelog/next.md | 14 -------------- changelog/v0.2.1.md | 17 +++++++++++++++++ src/launch/command.cpp | 2 +- 5 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 changelog/v0.2.1.md 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/next.md index 6aa800c..62a730f 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,17 +2,3 @@ - Added support for creating wayland idle inhibitors. - Added support for wayland idle timeouts. -- 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. -- 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. -- Fixed desktop entry keys with mismatched modifier or country not being discarded. -- Fixed greetd hanging when authenticating with a fingerprint. diff --git a/changelog/v0.2.1.md b/changelog/v0.2.1.md new file mode 100644 index 0000000..596b82f --- /dev/null +++ b/changelog/v0.2.1.md @@ -0,0 +1,17 @@ +## New Features + +- 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. +- 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. +- Fixed desktop entry keys with mismatched modifier or country not being discarded. +- Fixed greetd hanging when authenticating with a fingerprint. 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; From ea79eaceb0375a8a2ebdd55c60bf89c4167b264d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 11 Oct 2025 21:42:58 -0700 Subject: [PATCH 120/226] services/pipewire: do not use device for pro audio node controls Note that the device object is currently still bound. This should not be necessary. Fixes #178 --- changelog/next.md | 4 ++++ src/services/pipewire/node.cpp | 19 +++++++++++++++---- src/services/pipewire/node.hpp | 3 +++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 62a730f..13123b8 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,3 +2,7 @@ - Added support for creating wayland idle inhibitors. - Added support for wayland idle timeouts. + +## Bug Fixes + +- Fixed volume control breaking with pipewire pro audio mode. diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 031a68f..f336558 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "../../core/logcat.hpp" @@ -195,6 +196,16 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) != 0) { auto properties = QMap(); + bool proAudio = false; + if (const auto* proAudioStr = spa_dict_lookup(info->props, "device.profile.pro")) { + proAudio = spa_atob(proAudioStr); + } + + if (proAudio != self->proAudio) { + qCDebug(logNode) << self << "pro audio state changed:" << proAudio; + self->proAudio = proAudio; + } + if (self->device) { if (const auto* routeDevice = spa_dict_lookup(info->props, "card.profile.device")) { auto ok = false; @@ -286,7 +297,7 @@ void PwNodeBoundAudio::onInfo(const pw_node_info* info) { void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) { if (id == SPA_PARAM_Props && index == 0) { - if (this->node->device) { + if (this->node->shouldUseDevice()) { qCDebug(logNode) << "Skipping node volume props update for" << this->node << "in favor of device updates."; return; @@ -358,7 +369,7 @@ void PwNodeBoundAudio::setMuted(bool muted) { if (muted == this->mMuted) return; - if (this->node->device) { + if (this->node->shouldUseDevice()) { qCInfo(logNode) << "Changing muted state of" << this->node << "to" << muted << "via device"; if (!this->node->device->setMuted(this->node->routeDevice, muted)) { return; @@ -431,7 +442,7 @@ void PwNodeBoundAudio::setVolumes(const QVector& volumes) { return; } - if (this->node->device) { + if (this->node->shouldUseDevice()) { if (this->node->device->waitingForDevice()) { qCInfo(logNode) << "Waiting to change volumes of" << this->node << "to" << realVolumes << "via device"; @@ -511,7 +522,7 @@ void PwNodeBoundAudio::onDeviceVolumesChanged( qint32 routeDevice, const PwVolumeProps& volumeProps ) { - if (this->node->device && this->node->routeDevice == routeDevice) { + if (this->node->shouldUseDevice() && this->node->routeDevice == routeDevice) { qCDebug(logNode) << "Got updated device volume props for" << this->node << "via" << this->node->device; diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index b53015f..359c0f3 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -240,6 +240,9 @@ public: PwDevice* device = nullptr; qint32 routeDevice = -1; + bool proAudio = false; + + [[nodiscard]] bool shouldUseDevice() const { return this->device && !this->proAudio; } signals: void propertiesChanged(); From 1e8cc2e78da0cdfa98aafb02d9c1b22e71e07dff Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 12 Oct 2025 00:14:36 -0700 Subject: [PATCH 121/226] core: add CacheDir pragma Closes #293 --- changelog/next.md | 1 + src/core/paths.cpp | 22 ++++++++++++++++++---- src/core/paths.hpp | 9 ++++++++- src/core/qmlglobal.hpp | 7 +++++-- src/launch/launch.cpp | 5 ++++- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 13123b8..7b7ee40 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -2,6 +2,7 @@ - Added support for creating wayland idle inhibitors. - Added support for wayland idle timeouts. +- Added the ability to override Quickshell.cacheDir with a custom path. ## Bug Fixes diff --git a/src/core/paths.cpp b/src/core/paths.cpp index e17c3bc..1424d2b 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -27,12 +27,19 @@ QsPaths* QsPaths::instance() { return instance; } -void QsPaths::init(QString shellId, QString pathId, QString dataOverride, QString stateOverride) { +void QsPaths::init( + QString shellId, + QString pathId, + QString dataOverride, + QString stateOverride, + QString cacheOverride +) { auto* instance = QsPaths::instance(); instance->shellId = std::move(shellId); instance->pathId = std::move(pathId); instance->shellDataOverride = std::move(dataOverride); instance->shellStateOverride = std::move(stateOverride); + instance->shellCacheOverride = std::move(cacheOverride); } QDir QsPaths::crashDir(const QString& id) { @@ -316,9 +323,16 @@ QDir QsPaths::shellStateDir() { QDir QsPaths::shellCacheDir() { if (this->shellCacheState == DirState::Unknown) { - auto dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - dir = QDir(dir.filePath("by-shell")); - dir = QDir(dir.filePath(this->shellId)); + QDir dir; + if (this->shellCacheOverride.isEmpty()) { + dir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + dir = QDir(dir.filePath("by-shell")); + dir = QDir(dir.filePath(this->shellId)); + } else { + auto basedir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); + dir = QDir(this->shellCacheOverride.replace("$BASE", basedir)); + } + this->mShellCacheDir = dir; qCDebug(logPaths) << "Initialized cache path:" << dir.path(); diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 178bcda..1c10758 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -17,7 +17,13 @@ QDataStream& operator>>(QDataStream& stream, InstanceLockInfo& info); class QsPaths { public: static QsPaths* instance(); - static void init(QString shellId, QString pathId, QString dataOverride, QString stateOverride); + static void init( + QString shellId, + QString pathId, + QString dataOverride, + QString stateOverride, + QString cacheOverride + ); static QDir crashDir(const QString& id); static QString basePath(const QString& id); static QString ipcPath(const QString& id); @@ -65,4 +71,5 @@ private: QString shellDataOverride; QString shellStateOverride; + QString shellCacheOverride; }; diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 9d88591..1fc363b 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -127,18 +127,21 @@ class QuickshellGlobal: public QObject { /// Usually `~/.local/share/quickshell/by-shell/` /// /// Can be overridden using `//@ pragma DataDir $BASE/path` in the root qml file, where `$BASE` - /// corrosponds to `$XDG_DATA_HOME` (usually `~/.local/share`). + /// corresponds to `$XDG_DATA_HOME` (usually `~/.local/share`). Q_PROPERTY(QString dataDir READ dataDir CONSTANT); /// The per-shell state directory. /// /// Usually `~/.local/state/quickshell/by-shell/` /// /// Can be overridden using `//@ pragma StateDir $BASE/path` in the root qml file, where `$BASE` - /// corrosponds to `$XDG_STATE_HOME` (usually `~/.local/state`). + /// corresponds to `$XDG_STATE_HOME` (usually `~/.local/state`). Q_PROPERTY(QString stateDir READ stateDir CONSTANT); /// The per-shell cache directory. /// /// Usually `~/.cache/quickshell/by-shell/` + /// + /// Can be overridden using `//@ pragma CacheDir $BASE/path` in the root qml file, where `$BASE` + /// corresponds to `$XDG_CACHE_HOME` (usually `~/.cache`). Q_PROPERTY(QString cacheDir READ cacheDir CONSTANT); // clang-format on QML_SINGLETON; diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index fd6a0af..101820e 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -78,6 +78,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio QHash envOverrides; QString dataDir; QString stateDir; + QString cacheDir; } pragmas; auto stream = QTextStream(&file); @@ -109,6 +110,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio pragmas.dataDir = pragma.sliced(8).trimmed(); } else if (pragma.startsWith("StateDir ")) { pragmas.stateDir = pragma.sliced(9).trimmed(); + } else if (pragma.startsWith("CacheDir ")) { + pragmas.cacheDir = pragma.sliced(9).trimmed(); } else { qCritical() << "Unrecognized pragma" << pragma; return -1; @@ -150,7 +153,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio } #endif - QsPaths::init(shellId, pathId, pragmas.dataDir, pragmas.stateDir); + QsPaths::init(shellId, pathId, pragmas.dataDir, pragmas.stateDir, pragmas.cacheDir); QsPaths::instance()->linkRunDir(); QsPaths::instance()->linkPathDir(); LogManager::initFs(); From 00858812f25b748d08b075a0d284093685fa3ffd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 12 Oct 2025 17:33:21 -0700 Subject: [PATCH 122/226] core/command: filter instance selection by current display --- changelog/next.md | 4 ++++ src/core/instanceinfo.cpp | 6 ++++-- src/core/instanceinfo.hpp | 1 + src/core/paths.cpp | 7 ++++++- src/core/paths.hpp | 2 +- src/launch/command.cpp | 27 ++++++++++++++++++++++++--- src/launch/launch.cpp | 1 + src/launch/launch_p.hpp | 3 +++ src/launch/parsecommand.cpp | 8 ++++++-- 9 files changed, 50 insertions(+), 9 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 7b7ee40..53b50c8 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -4,6 +4,10 @@ - Added support for wayland idle timeouts. - Added the ability to override Quickshell.cacheDir with a custom path. +## Other Changes + +- IPC operations filter available instances to the current display connection by default. + ## Bug Fixes - Fixed volume control breaking with pipewire pro audio mode. diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp index 7f0132b..1f71b8a 100644 --- a/src/core/instanceinfo.cpp +++ b/src/core/instanceinfo.cpp @@ -3,12 +3,14 @@ #include QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid; + stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid + << info.display; return stream; } QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid; + stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid + >> info.display; return stream; } diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index 98ce614..d462f6e 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -11,6 +11,7 @@ struct InstanceInfo { QString shellId; QDateTime launchTime; pid_t pid = -1; + QString display; static InstanceInfo CURRENT; // NOLINT }; diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 1424d2b..70e1bd1 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -411,7 +411,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD } QPair, QVector> -QsPaths::collectInstances(const QString& path) { +QsPaths::collectInstances(const QString& path, const QString& display) { qCDebug(logPaths) << "Collecting instances from" << path; auto liveInstances = QVector(); auto deadInstances = QVector(); @@ -425,6 +425,11 @@ QsPaths::collectInstances(const QString& path) { qCDebug(logPaths).nospace() << "Found instance " << info.instance.instanceId << " (pid " << info.pid << ") at " << path; + if (!display.isEmpty() && info.instance.display != display) { + qCDebug(logPaths) << "Skipped instance with mismatched display at" << path; + continue; + } + if (info.pid == -1) { deadInstances.push_back(info); } else { diff --git a/src/core/paths.hpp b/src/core/paths.hpp index 1c10758..c2500ed 100644 --- a/src/core/paths.hpp +++ b/src/core/paths.hpp @@ -30,7 +30,7 @@ public: static bool checkLock(const QString& path, InstanceLockInfo* info = nullptr, bool allowDead = false); static QPair, QVector> - collectInstances(const QString& path); + collectInstances(const QString& path, const QString& display); QDir* baseRunDir(); QDir* shellRunDir(); diff --git a/src/launch/command.cpp b/src/launch/command.cpp index e63498a..18dcc43 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -178,7 +179,8 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb } } else if (!cmd.instance.id->isEmpty()) { path = basePath->filePath("by-pid"); - auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = + QsPaths::collectInstances(path, cmd.config.anyDisplay ? "" : getDisplayConnection()); liveInstances.removeIf([&](const InstanceLockInfo& info) { return !info.instance.instanceId.startsWith(*cmd.instance.id); @@ -228,7 +230,8 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance, bool deadFallb path = QDir(basePath->filePath("by-path")).filePath(pathId); - auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = + QsPaths::collectInstances(path, cmd.config.anyDisplay ? "" : getDisplayConnection()); auto instances = liveInstances; if (instances.isEmpty() && deadFallback) { @@ -311,7 +314,10 @@ int listInstances(CommandState& cmd) { path = QDir(basePath->filePath("by-path")).filePath(pathId); } - auto [liveInstances, deadInstances] = QsPaths::collectInstances(path); + auto [liveInstances, deadInstances] = QsPaths::collectInstances( + path, + cmd.config.anyDisplay || cmd.instance.all ? "" : getDisplayConnection() + ); sortInstances(liveInstances, cmd.config.newest); @@ -373,6 +379,7 @@ int listInstances(CommandState& cmd) { << " Process ID: " << instance.instance.pid << '\n' << " Shell ID: " << instance.instance.shellId << '\n' << " Config path: " << instance.instance.configPath << '\n' + << " Display connection: " << instance.instance.display << '\n' << " Launch time: " << launchTimeStr << (isDead ? "" : " (running for " + runtimeStr + ")") << '\n' << (gray ? "\033[0m" : ""); @@ -545,4 +552,18 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { return 0; } +QString getDisplayConnection() { + auto platform = qEnvironmentVariable("QT_QPA_PLATFORM"); + auto wlDisplay = qEnvironmentVariable("WAYLAND_DISPLAY"); + auto xDisplay = qEnvironmentVariable("DISPLAY"); + + if (platform == "wayland" || (platform.isEmpty() && !wlDisplay.isEmpty())) { + return "wayland," + wlDisplay; + } else if (platform == "xcb" || (platform.isEmpty() && !xDisplay.isEmpty())) { + return "x11," + xDisplay; + } else { + return "unk," + QGuiApplication::platformName(); + } +} + } // namespace qs::launch diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 101820e..f269f61 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -134,6 +134,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio .shellId = shellId, .launchTime = qs::Common::LAUNCH_TIME, .pid = getpid(), + .display = getDisplayConnection(), }; #if CRASH_REPORTER diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index 7b8fca6..a186ddb 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -50,6 +50,7 @@ struct CommandState { QStringOption manifest; QStringOption name; bool newest = false; + bool anyDisplay = false; } config; struct { @@ -106,6 +107,8 @@ void exitDaemon(int code); int parseCommand(int argc, char** argv, CommandState& state); int runCommand(int argc, char** argv, QCoreApplication* coreApplication); +QString getDisplayConnection(); + int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication); } // namespace qs::launch diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index fc16086..c12d9b9 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -16,7 +16,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { .argv = argv, }; - auto addConfigSelection = [&](CLI::App* cmd, bool withNewestOption = false) { + auto addConfigSelection = [&](CLI::App* cmd, bool filtering = false) { auto* group = cmd->add_option_group("Config Selection") ->description( @@ -49,9 +49,13 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->envname("QS_MANIFEST") ->excludes(path); - if (withNewestOption) { + if (filtering) { group->add_flag("-n,--newest", state.config.newest) ->description("Operate on the most recently launched instance instead of the oldest"); + + group->add_flag("--any-display", state.config.anyDisplay) + ->description("If passed, instances will not be filtered by the display connection they " + "were launched on."); } return group; From 3e2ce40b18af943f9ba370ed73565e9f487663ef Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 18 Oct 2025 14:09:03 -0700 Subject: [PATCH 123/226] core: reference configs by absolute instead of canonical paths --- changelog/next.md | 11 +++++++++++ src/core/scan.cpp | 6 +++--- src/core/scan.hpp | 2 -- src/launch/command.cpp | 8 ++++---- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 53b50c8..93d1f2f 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -1,3 +1,14 @@ +## Breaking Changes + +### Config paths are no longer canonicalized + +This fixes nix configs changing shell-ids on rebuild as the shell id is now derived from +the symlink path. Configs with a symlink in their path will have a different shell id. + +Shell ids are used to derive the default config / state / cache folders, so those files +will need to be manually moved if using a config behind a symlinked path without an explicitly +set shell id. + ## New Features - Added support for creating wayland idle inhibitors. diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 4306de7..45413fb 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -163,7 +163,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna qCDebug(logQmlScanner) << "Found imports" << imports; } - auto currentdir = QDir(QFileInfo(path).canonicalPath()); + auto currentdir = QDir(QFileInfo(path).absolutePath()); // the root can never be a singleton so it dosent matter if we skip it this->scanDir(currentdir.path()); @@ -179,9 +179,9 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna } auto pathInfo = QFileInfo(ipath); - auto cpath = pathInfo.canonicalFilePath(); + auto cpath = pathInfo.absoluteFilePath(); - if (cpath.isEmpty()) { + if (!pathInfo.exists()) { qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; continue; } diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 1d3be85..9d88f07 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -16,9 +16,7 @@ public: QmlScanner() = default; QmlScanner(const QDir& rootPath): rootPath(rootPath) {} - // path must be canonical void scanDir(const QString& path); - void scanQmlRoot(const QString& path); QVector scannedDirs; diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 18dcc43..81a9243 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -110,7 +110,7 @@ int locateConfigFile(CommandState& cmd, QString& path) { } if (split[0].trimmed() == *cmd.config.name) { - path = QDir(QFileInfo(file).canonicalPath()).filePath(split[1].trimmed()); + path = QDir(QFileInfo(file).absolutePath()).filePath(split[1].trimmed()); break; } } @@ -140,8 +140,7 @@ int locateConfigFile(CommandState& cmd, QString& path) { return -1; } - path = QFileInfo(path).canonicalFilePath(); - return 0; + goto rpath; } } @@ -154,7 +153,8 @@ int locateConfigFile(CommandState& cmd, QString& path) { return -1; } - path = QFileInfo(path).canonicalFilePath(); +rpath: + path = QFileInfo(path).absoluteFilePath(); return 0; } From 1b147a2c78983877909f9e531fc8ce17c35a297a Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 23 Oct 2025 10:21:01 -0400 Subject: [PATCH 124/226] core/desktopentry: handle string escape sequences --- changelog/next.md | 1 + src/core/desktopentry.cpp | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 93d1f2f..543f9e2 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -22,3 +22,4 @@ set shell id. ## Bug Fixes - Fixed volume control breaking with pipewire pro audio mode. +- Fixed escape sequence handling in desktop entries. diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 941a405..2dbafea 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -269,16 +269,22 @@ QVector DesktopEntry::parseExecString(const QString& execString) { currentArgument += '\\'; escape = 0; } + } else if (escape == 2) { + currentArgument += c; + escape = 0; } else if (escape != 0) { - if (escape != 2) { - // Technically this is an illegal state, but the spec has a terrible double escape - // rule in strings for no discernable reason. Assuming someone might understandably - // misunderstand it, treat it as a normal escape and log it. + switch (c.unicode()) { + case 's': currentArgument += u' '; break; + case 'n': currentArgument += u'\n'; break; + case 't': currentArgument += u'\t'; break; + case 'r': currentArgument += u'\r'; break; + case '\\': currentArgument += u'\\'; break; + default: qCWarning(logDesktopEntry).noquote() << "Illegal escape sequence in desktop entry exec string:" << execString; + currentArgument += c; + break; } - - currentArgument += c; escape = 0; } else if (c == u'"' || c == u'\'') { parsingString = false; From db1777c20b936a86528c1095cbcb1ebd92801402 Mon Sep 17 00:00:00 2001 From: Cu3PO42 Date: Thu, 9 Oct 2025 23:50:08 +0200 Subject: [PATCH 125/226] service/polkit: add service module to write Polkit agents --- .github/workflows/build.yml | 1 + BUILD.md | 7 + CMakeLists.txt | 1 + changelog/next.md | 5 + default.nix | 7 +- quickshell.scm | 1 + src/services/CMakeLists.txt | 4 + src/services/polkit/CMakeLists.txt | 35 ++++ src/services/polkit/agentimpl.cpp | 179 +++++++++++++++++ src/services/polkit/agentimpl.hpp | 66 ++++++ src/services/polkit/flow.cpp | 163 +++++++++++++++ src/services/polkit/flow.hpp | 179 +++++++++++++++++ src/services/polkit/gobjectref.hpp | 65 ++++++ src/services/polkit/identity.cpp | 84 ++++++++ src/services/polkit/identity.hpp | 64 ++++++ src/services/polkit/listener.cpp | 234 ++++++++++++++++++++++ src/services/polkit/listener.hpp | 75 +++++++ src/services/polkit/module.md | 52 +++++ src/services/polkit/qml.cpp | 35 ++++ src/services/polkit/qml.hpp | 84 ++++++++ src/services/polkit/session.cpp | 68 +++++++ src/services/polkit/session.hpp | 52 +++++ src/services/polkit/test/manual/agent.qml | 97 +++++++++ 23 files changed, 1557 insertions(+), 1 deletion(-) create mode 100644 src/services/polkit/CMakeLists.txt create mode 100644 src/services/polkit/agentimpl.cpp create mode 100644 src/services/polkit/agentimpl.hpp create mode 100644 src/services/polkit/flow.cpp create mode 100644 src/services/polkit/flow.hpp create mode 100644 src/services/polkit/gobjectref.hpp create mode 100644 src/services/polkit/identity.cpp create mode 100644 src/services/polkit/identity.hpp create mode 100644 src/services/polkit/listener.cpp create mode 100644 src/services/polkit/listener.hpp create mode 100644 src/services/polkit/module.md create mode 100644 src/services/polkit/qml.cpp create mode 100644 src/services/polkit/qml.hpp create mode 100644 src/services/polkit/session.cpp create mode 100644 src/services/polkit/session.hpp create mode 100644 src/services/polkit/test/manual/agent.qml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83957dc..9a3d097 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,7 @@ jobs: libxcb \ libpipewire \ cli11 \ + polkit \ jemalloc - name: Build diff --git a/BUILD.md b/BUILD.md index 742baa7..fdea27e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -192,6 +192,13 @@ To disable: `-DSERVICE_PAM=OFF` Dependencies: `pam` +### Polkit +This feature enables creating Polkit agents that can prompt user for authentication. + +To disable: `-DSERVICE_POLKIT=OFF` + +Dependencies: `polkit`, `glib` + ### Hyprland This feature enables hyprland specific integrations. It requires wayland support but has no extra dependencies. diff --git a/CMakeLists.txt b/CMakeLists.txt index 880b9ca..c867001 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_MPRIS "Mpris" ON) boption(SERVICE_PAM "Pam" ON) +boption(SERVICE_POLKIT "Polkit" ON) boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) diff --git a/changelog/next.md b/changelog/next.md index 543f9e2..5f5aa34 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -11,6 +11,7 @@ set shell id. ## New Features +- Added support for creating Polkit agents. - Added support for creating wayland idle inhibitors. - Added support for wayland idle timeouts. - Added the ability to override Quickshell.cacheDir with a custom path. @@ -23,3 +24,7 @@ set shell id. - Fixed volume control breaking with pipewire pro audio mode. - Fixed escape sequence handling in desktop entries. + +## Packaging Changes + +`glib` and `polkit` have been added as dependencies when compiling with polkit agent support. diff --git a/default.nix b/default.nix index adb978b..a00f0f1 100644 --- a/default.nix +++ b/default.nix @@ -21,6 +21,8 @@ libgbm ? null, pipewire, pam, + polkit, + glib, gitRev ? (let headExists = builtins.pathExists ./.git/HEAD; @@ -43,6 +45,7 @@ withPam ? true, withHyprland ? true, withI3 ? true, + withPolkit ? true, }: let unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; @@ -76,7 +79,8 @@ ++ lib.optionals (withWayland && libgbm != null) [ libdrm libgbm ] ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam - ++ lib.optional withPipewire pipewire; + ++ lib.optional withPipewire pipewire + ++ lib.optionals withPolkit [ polkit glib ]; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; @@ -91,6 +95,7 @@ (lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) + (lib.cmakeBool "SERVICE_POLKIT" withPolkit) (lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "I3" withI3) ]; diff --git a/quickshell.scm b/quickshell.scm index 26abdc0..3f82160 100644 --- a/quickshell.scm +++ b/quickshell.scm @@ -42,6 +42,7 @@ libxcb libxkbcommon linux-pam + polkit mesa pipewire qtbase diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 5ab5c55..f3912a9 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -14,6 +14,10 @@ if (SERVICE_PAM) add_subdirectory(pam) endif() +if (SERVICE_POLKIT) + add_subdirectory(polkit) +endif() + if (SERVICE_GREETD) add_subdirectory(greetd) endif() diff --git a/src/services/polkit/CMakeLists.txt b/src/services/polkit/CMakeLists.txt new file mode 100644 index 0000000..51791d8 --- /dev/null +++ b/src/services/polkit/CMakeLists.txt @@ -0,0 +1,35 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(glib REQUIRED IMPORTED_TARGET glib-2.0>=2.36) +pkg_check_modules(gobject REQUIRED IMPORTED_TARGET gobject-2.0) +pkg_check_modules(polkit_agent REQUIRED IMPORTED_TARGET polkit-agent-1) +pkg_check_modules(polkit REQUIRED IMPORTED_TARGET polkit-gobject-1) + +qt_add_library(quickshell-service-polkit STATIC + agentimpl.cpp + flow.cpp + identity.cpp + listener.cpp + session.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-service-polkit + URI Quickshell.Services.Polkit + VERSION 0.1 + DEPENDENCIES QtQml +) + +install_qml_module(quickshell-service-polkit) + +target_link_libraries(quickshell-service-polkit PRIVATE + Qt::Qml + Qt::Quick + PkgConfig::glib + PkgConfig::gobject + PkgConfig::polkit_agent + PkgConfig::polkit +) + +qs_module_pch(quickshell-service-polkit) + +target_link_libraries(quickshell PRIVATE quickshell-service-polkitplugin) diff --git a/src/services/polkit/agentimpl.cpp b/src/services/polkit/agentimpl.cpp new file mode 100644 index 0000000..a11882d --- /dev/null +++ b/src/services/polkit/agentimpl.cpp @@ -0,0 +1,179 @@ +#include "agentimpl.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../../core/generation.hpp" +#include "../../core/logcat.hpp" +#include "gobjectref.hpp" +#include "listener.hpp" +#include "qml.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit", QtWarningMsg); +} + +namespace qs::service::polkit { +PolkitAgentImpl* PolkitAgentImpl::instance = nullptr; + +PolkitAgentImpl::PolkitAgentImpl(PolkitAgent* agent) + : QObject(nullptr) + , listener(qs_polkit_agent_new(this), G_OBJECT_NO_REF) + , qmlAgent(agent) + , path(this->qmlAgent->path()) { + auto utf8Path = this->path.toUtf8(); + qs_polkit_agent_register(this->listener.get(), utf8Path.constData()); +} + +PolkitAgentImpl::~PolkitAgentImpl() { this->cancelAllRequests("PolkitAgent is being destroyed"); } + +void PolkitAgentImpl::cancelAllRequests(const QString& reason) { + for (; !this->queuedRequests.empty(); this->queuedRequests.pop_back()) { + AuthRequest* req = this->queuedRequests.back(); + qCDebug(logPolkit) << "destroying queued authentication request for action" << req->actionId; + req->cancel(reason); + delete req; + } + + auto* flow = this->bActiveFlow.value(); + if (flow) { + flow->cancelAuthenticationRequest(); + flow->deleteLater(); + } + + if (this->bIsRegistered.value()) qs_polkit_agent_unregister(this->listener.get()); +} + +PolkitAgentImpl* PolkitAgentImpl::tryGetOrCreate(PolkitAgent* agent) { + if (instance == nullptr) instance = new PolkitAgentImpl(agent); + if (instance->qmlAgent == agent) return instance; + return nullptr; +} + +PolkitAgentImpl* PolkitAgentImpl::tryGet(const PolkitAgent* agent) { + if (instance == nullptr) return nullptr; + if (instance->qmlAgent == agent) return instance; + return nullptr; +} + +PolkitAgentImpl* PolkitAgentImpl::tryTakeoverOrCreate(PolkitAgent* agent) { + if (auto* impl = tryGetOrCreate(agent); impl != nullptr) return impl; + + auto* prevGen = EngineGeneration::findObjectGeneration(instance->qmlAgent); + auto* myGen = EngineGeneration::findObjectGeneration(agent); + if (prevGen == myGen) return nullptr; + + qCDebug(logPolkit) << "taking over listener from previous generation"; + instance->qmlAgent = agent; + instance->setPath(agent->path()); + + return instance; +} + +void PolkitAgentImpl::onEndOfQmlAgent(PolkitAgent* agent) { + if (instance != nullptr && instance->qmlAgent == agent) { + delete instance; + instance = nullptr; + } +} + +void PolkitAgentImpl::setPath(const QString& path) { + if (this->path == path) return; + + this->path = path; + auto utf8Path = path.toUtf8(); + + this->cancelAllRequests("PolkitAgent path changed"); + qs_polkit_agent_unregister(this->listener.get()); + this->bIsRegistered = false; + + qs_polkit_agent_register(this->listener.get(), utf8Path.constData()); +} + +void PolkitAgentImpl::registerComplete(bool success) { + if (success) this->bIsRegistered = true; + else qCWarning(logPolkit) << "failed to register listener on path" << this->qmlAgent->path(); +} + +void PolkitAgentImpl::initiateAuthentication(AuthRequest* request) { + qCDebug(logPolkit) << "incoming authentication request for action" << request->actionId; + + this->queuedRequests.emplace_back(request); + + if (this->queuedRequests.size() == 1) { + this->activateAuthenticationRequest(); + } +} + +void PolkitAgentImpl::cancelAuthentication(AuthRequest* request) { + qCDebug(logPolkit) << "cancelling authentication request from agent"; + + auto* flow = this->bActiveFlow.value(); + if (flow && flow->authRequest() == request) { + flow->cancelFromAgent(); + } else if (auto it = std::ranges::find(this->queuedRequests, request); + it != this->queuedRequests.end()) + { + qCDebug(logPolkit) << "removing queued authentication request for action" << (*it)->actionId; + (*it)->cancel("Authentication request was cancelled"); + delete (*it); + this->queuedRequests.erase(it); + } else { + qCWarning(logPolkit) << "the cancelled request was not found in the queue."; + } +} + +void PolkitAgentImpl::activateAuthenticationRequest() { + if (this->queuedRequests.empty()) return; + + AuthRequest* req = this->queuedRequests.front(); + this->queuedRequests.pop_front(); + qCDebug(logPolkit) << "activating authentication request for action" << req->actionId + << ", cookie: " << req->cookie; + + QList identities; + for (auto& identity: req->identities) { + auto* obj = Identity::fromPolkitIdentity(identity); + if (obj) identities.append(obj); + } + if (identities.isEmpty()) { + qCWarning(logPolkit + ) << "no supported identities available for authentication request, cancelling."; + req->cancel("Error requesting authentication: no supported identities available."); + delete req; + return; + } + + this->bActiveFlow = new AuthFlow(req, std::move(identities)); + + QObject::connect( + this->bActiveFlow.value(), + &AuthFlow::isCompletedChanged, + this, + &PolkitAgentImpl::finishAuthenticationRequest + ); + + emit this->qmlAgent->authenticationRequestStarted(); +} + +void PolkitAgentImpl::finishAuthenticationRequest() { + if (!this->bActiveFlow.value()) return; + + qCDebug(logPolkit) << "finishing authentication request for action" + << this->bActiveFlow.value()->actionId(); + + this->bActiveFlow.value()->deleteLater(); + + if (!this->queuedRequests.empty()) { + this->activateAuthenticationRequest(); + } else { + this->bActiveFlow = nullptr; + } +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/agentimpl.hpp b/src/services/polkit/agentimpl.hpp new file mode 100644 index 0000000..65ae11a --- /dev/null +++ b/src/services/polkit/agentimpl.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include +#include + +#include "flow.hpp" +#include "gobjectref.hpp" +#include "listener.hpp" + +namespace qs::service::polkit { +class PolkitAgent; + +class PolkitAgentImpl + : public QObject + , public ListenerCb { + Q_OBJECT; + Q_DISABLE_COPY_MOVE(PolkitAgentImpl); + +public: + ~PolkitAgentImpl() override; + + static PolkitAgentImpl* tryGetOrCreate(PolkitAgent* agent); + static PolkitAgentImpl* tryGet(const PolkitAgent* agent); + static PolkitAgentImpl* tryTakeoverOrCreate(PolkitAgent* agent); + static void onEndOfQmlAgent(PolkitAgent* agent); + + [[nodiscard]] QBindable activeFlow() { return &this->bActiveFlow; }; + [[nodiscard]] QBindable isRegistered() { return &this->bIsRegistered; }; + + [[nodiscard]] const QString& getPath() const { return this->path; } + void setPath(const QString& path); + + void initiateAuthentication(AuthRequest* request) override; + void cancelAuthentication(AuthRequest* request) override; + void registerComplete(bool success) override; + + void cancelAllRequests(const QString& reason); + +signals: + void activeFlowChanged(); + void isRegisteredChanged(); + +private: + PolkitAgentImpl(PolkitAgent* agent); + + static PolkitAgentImpl* instance; + + /// Start handling of the next authentication request in the queue. + void activateAuthenticationRequest(); + /// Finalize and remove the current authentication request. + void finishAuthenticationRequest(); + + GObjectRef listener; + PolkitAgent* qmlAgent = nullptr; + QString path; + + std::deque queuedRequests; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgentImpl, AuthFlow*, bActiveFlow, &PolkitAgentImpl::activeFlowChanged); + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgentImpl, bool, bIsRegistered, &PolkitAgentImpl::isRegisteredChanged); + // clang-format on +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/flow.cpp b/src/services/polkit/flow.cpp new file mode 100644 index 0000000..2a709eb --- /dev/null +++ b/src/services/polkit/flow.cpp @@ -0,0 +1,163 @@ +#include "flow.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "identity.hpp" +#include "qml.hpp" +#include "session.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkitState, "quickshell.service.polkit.state", QtWarningMsg); +} + +namespace qs::service::polkit { +AuthFlow::AuthFlow(AuthRequest* request, QList&& identities, QObject* parent) + : QObject(parent) + , mRequest(request) + , mIdentities(std::move(identities)) + , bSelectedIdentity(this->mIdentities.isEmpty() ? nullptr : this->mIdentities.first()) { + // We reject auth requests with no identities before a flow is created. + // This should never happen. + if (!this->bSelectedIdentity.value()) + qCFatal(logPolkitState) << "AuthFlow created with no valid identities!"; + + for (auto* identity: this->mIdentities) { + identity->setParent(this); + } + + this->setupSession(); +} + +AuthFlow::~AuthFlow() { delete this->mRequest; }; + +void AuthFlow::setSelectedIdentity(Identity* identity) { + if (this->bSelectedIdentity.value() == identity) return; + if (!identity) { + qmlWarning(this) << "Cannot set selected identity to null."; + return; + } + this->bSelectedIdentity = identity; + this->currentSession->cancel(); + this->setupSession(); +} + +void AuthFlow::cancelFromAgent() { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "cancelling authentication request from agent"; + + // Session cancel can immediately call the cancel handler, which also + // performs property updates. + Qt::beginPropertyUpdateGroup(); + this->bIsCancelled = true; + this->currentSession->cancel(); + Qt::endPropertyUpdateGroup(); + + emit this->authenticationRequestCancelled(); + + this->mRequest->cancel("Authentication request cancelled by agent."); +} + +void AuthFlow::submit(const QString& value) { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "submitting response to authentication request"; + + this->currentSession->respond(value); + + Qt::beginPropertyUpdateGroup(); + this->bIsResponseRequired = false; + this->bInputPrompt = QString(); + this->bResponseVisible = false; + Qt::endPropertyUpdateGroup(); +} + +void AuthFlow::cancelAuthenticationRequest() { + if (!this->currentSession) return; + + qCDebug(logPolkitState) << "cancelling authentication request by user request"; + + // Session cancel can immediately call the cancel handler, which also + // performs property updates. + Qt::beginPropertyUpdateGroup(); + this->bIsCancelled = true; + this->currentSession->cancel(); + Qt::endPropertyUpdateGroup(); + + this->mRequest->cancel("Authentication request cancelled by user."); +} + +void AuthFlow::setupSession() { + delete this->currentSession; + + qCDebug(logPolkitState) << "setting up session for identity" + << this->bSelectedIdentity.value()->name(); + + this->currentSession = new Session( + this->bSelectedIdentity.value()->polkitIdentity.get(), + this->mRequest->cookie, + this + ); + QObject::connect(this->currentSession, &Session::request, this, &AuthFlow::request); + QObject::connect(this->currentSession, &Session::completed, this, &AuthFlow::completed); + QObject::connect(this->currentSession, &Session::showError, this, &AuthFlow::showError); + QObject::connect(this->currentSession, &Session::showInfo, this, &AuthFlow::showInfo); + this->currentSession->initiate(); +} + +void AuthFlow::request(const QString& message, bool echo) { + Qt::beginPropertyUpdateGroup(); + this->bIsResponseRequired = true; + this->bInputPrompt = message; + this->bResponseVisible = echo; + Qt::endPropertyUpdateGroup(); +} + +void AuthFlow::completed(bool gainedAuthorization) { + qCDebug(logPolkitState) << "authentication session completed, gainedAuthorization =" + << gainedAuthorization << ", isCancelled =" << this->bIsCancelled.value(); + + if (gainedAuthorization) { + Qt::beginPropertyUpdateGroup(); + this->bIsCompleted = true; + this->bIsSuccessful = true; + Qt::endPropertyUpdateGroup(); + + this->mRequest->complete(); + + emit this->authenticationSucceeded(); + } else if (this->bIsCancelled.value()) { + Qt::beginPropertyUpdateGroup(); + this->bIsCompleted = true; + this->bIsSuccessful = false; + Qt::endPropertyUpdateGroup(); + } else { + this->bFailed = true; + emit this->authenticationFailed(); + + this->setupSession(); + } +} + +void AuthFlow::showError(const QString& message) { + Qt::beginPropertyUpdateGroup(); + this->bSupplementaryMessage = message; + this->bSupplementaryIsError = true; + Qt::endPropertyUpdateGroup(); +} + +void AuthFlow::showInfo(const QString& message) { + Qt::beginPropertyUpdateGroup(); + this->bSupplementaryMessage = message; + this->bSupplementaryIsError = false; + Qt::endPropertyUpdateGroup(); +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/flow.hpp b/src/services/polkit/flow.hpp new file mode 100644 index 0000000..0b7e845 --- /dev/null +++ b/src/services/polkit/flow.hpp @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include + +#include "../../core/retainable.hpp" +#include "identity.hpp" +#include "listener.hpp" + +namespace qs::service::polkit { +class Session; + +class AuthFlow + : public QObject + , public Retainable { + Q_OBJECT; + QML_ELEMENT; + Q_DISABLE_COPY_MOVE(AuthFlow); + QML_UNCREATABLE("AuthFlow can only be obtained from PolkitAgent."); + + // clang-format off + /// The main message to present to the user. + Q_PROPERTY(QString message READ message CONSTANT); + + /// The icon to present to the user in association with the message. + /// + /// The icon name follows the [FreeDesktop icon naming specification](https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html). + /// Use @@Quickshell.Quickshell.iconPath() to resolve the icon name to an + /// actual file path for display. + Q_PROPERTY(QString iconName READ iconName CONSTANT); + + /// The action ID represents the action that is being authorized. + /// + /// This is a machine-readable identifier. + Q_PROPERTY(QString actionId READ actionId CONSTANT); + + /// A cookie that identifies this authentication request. + /// + /// This is an internal identifier and not recommended to show to users. + Q_PROPERTY(QString cookie READ cookie CONSTANT); + + /// The list of identities that may be used to authenticate. + /// + /// Each identity may be a user or a group. You may select any of them to + /// authenticate by setting @@selectedIdentity. By default, the first identity + /// in the list is selected. + Q_PROPERTY(QList identities READ identities CONSTANT); + + /// The identity that will be used to authenticate. + /// + /// Changing this will abort any ongoing authentication conversations and start a new one. + Q_PROPERTY(Identity* selectedIdentity READ default WRITE setSelectedIdentity NOTIFY selectedIdentityChanged BINDABLE selectedIdentity); + + /// Indicates that a response from the user is required from the user, + /// typically a password. + Q_PROPERTY(bool isResponseRequired READ default NOTIFY isResponseRequiredChanged BINDABLE isResponseRequired); + + /// This message is used to prompt the user for required input. + Q_PROPERTY(QString inputPrompt READ default NOTIFY inputPromptChanged BINDABLE inputPrompt); + + /// Indicates whether the user's response should be visible. (e.g. for passwords this should be false) + Q_PROPERTY(bool responseVisible READ default NOTIFY responseVisibleChanged BINDABLE responseVisible); + + /// An additional message to present to the user. + /// + /// This may be used to show errors or supplementary information. + /// See @@supplementaryIsError to determine if this is an error message. + Q_PROPERTY(QString supplementaryMessage READ default NOTIFY supplementaryMessageChanged BINDABLE supplementaryMessage); + + /// Indicates whether the supplementary message is an error. + Q_PROPERTY(bool supplementaryIsError READ default NOTIFY supplementaryIsErrorChanged BINDABLE supplementaryIsError); + + /// Has the authentication request been completed. + Q_PROPERTY(bool isCompleted READ default NOTIFY isCompletedChanged BINDABLE isCompleted); + + /// Indicates whether the authentication request was successful. + Q_PROPERTY(bool isSuccessful READ default NOTIFY isSuccessfulChanged BINDABLE isSuccessful); + + /// Indicates whether the current authentication request was cancelled. + Q_PROPERTY(bool isCancelled READ default NOTIFY isCancelledChanged BINDABLE isCancelled); + + /// Indicates whether an authentication attempt has failed at least once during this authentication flow. + Q_PROPERTY(bool failed READ default NOTIFY failedChanged BINDABLE failed); + // clang-format on + +public: + explicit AuthFlow(AuthRequest* request, QList&& identities, QObject* parent = nullptr); + ~AuthFlow() override; + + /// Cancel the ongoing authentication request from the agent side. + void cancelFromAgent(); + + /// Submit a response to a request that was previously emitted. Typically the password. + Q_INVOKABLE void submit(const QString& value); + /// Cancel the ongoing authentication request from the user side. + Q_INVOKABLE void cancelAuthenticationRequest(); + + [[nodiscard]] const QString& message() const { return this->mRequest->message; }; + [[nodiscard]] const QString& iconName() const { return this->mRequest->iconName; }; + [[nodiscard]] const QString& actionId() const { return this->mRequest->actionId; }; + [[nodiscard]] const QString& cookie() const { return this->mRequest->cookie; }; + [[nodiscard]] const QList& identities() const { return this->mIdentities; }; + + [[nodiscard]] QBindable selectedIdentity() { return &this->bSelectedIdentity; }; + void setSelectedIdentity(Identity* identity); + + [[nodiscard]] QBindable isResponseRequired() { return &this->bIsResponseRequired; }; + [[nodiscard]] QBindable inputPrompt() { return &this->bInputPrompt; }; + [[nodiscard]] QBindable responseVisible() { return &this->bResponseVisible; }; + + [[nodiscard]] QBindable supplementaryMessage() { return &this->bSupplementaryMessage; }; + [[nodiscard]] QBindable supplementaryIsError() { return &this->bSupplementaryIsError; }; + + [[nodiscard]] QBindable isCompleted() { return &this->bIsCompleted; }; + [[nodiscard]] QBindable isSuccessful() { return &this->bIsSuccessful; }; + [[nodiscard]] QBindable isCancelled() { return &this->bIsCancelled; }; + [[nodiscard]] QBindable failed() { return &this->bFailed; }; + + [[nodiscard]] AuthRequest* authRequest() const { return this->mRequest; }; + +signals: + /// Emitted whenever an authentication request completes successfully. + void authenticationSucceeded(); + + /// Emitted whenever an authentication request completes unsuccessfully. + /// + /// This may be because the user entered the wrong password or otherwise + /// failed to authenticate. + /// This signal is not emmitted when the user canceled the request or it + /// was cancelled by the PolKit daemon. + /// + /// After this signal, a new session is automatically started for the same + /// identity. + void authenticationFailed(); + + /// Emmitted when on ongoing authentication request is cancelled by the PolKit daemon. + void authenticationRequestCancelled(); + + void selectedIdentityChanged(); + void isResponseRequiredChanged(); + void inputPromptChanged(); + void responseVisibleChanged(); + void supplementaryMessageChanged(); + void supplementaryIsErrorChanged(); + void isCompletedChanged(); + void isSuccessfulChanged(); + void isCancelledChanged(); + void failedChanged(); + +private slots: + // Signals received from session objects. + void request(const QString& message, bool echo); + void completed(bool gainedAuthorization); + void showError(const QString& message); + void showInfo(const QString& message); + +private: + /// Start a session for the currently selected identity and the current request. + void setupSession(); + + Session* currentSession = nullptr; + AuthRequest* mRequest = nullptr; + QList mIdentities; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, Identity*, bSelectedIdentity, &AuthFlow::selectedIdentityChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsResponseRequired, &AuthFlow::isResponseRequiredChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, QString, bInputPrompt, &AuthFlow::inputPromptChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bResponseVisible, &AuthFlow::responseVisibleChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, QString, bSupplementaryMessage, &AuthFlow::supplementaryMessageChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bSupplementaryIsError, &AuthFlow::supplementaryIsErrorChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsCompleted, &AuthFlow::isCompletedChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsSuccessful, &AuthFlow::isSuccessfulChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bIsCancelled, &AuthFlow::isCancelledChanged); + Q_OBJECT_BINDABLE_PROPERTY(AuthFlow, bool, bFailed, &AuthFlow::failedChanged); + // clang-format on +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/gobjectref.hpp b/src/services/polkit/gobjectref.hpp new file mode 100644 index 0000000..cd29a9d --- /dev/null +++ b/src/services/polkit/gobjectref.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +namespace qs::service::polkit { + +struct GObjectNoRefTag {}; +constexpr GObjectNoRefTag G_OBJECT_NO_REF; + +template +class GObjectRef { +public: + explicit GObjectRef(T* ptr = nullptr): ptr(ptr) { + if (this->ptr) { + g_object_ref(this->ptr); + } + } + + explicit GObjectRef(T* ptr, GObjectNoRefTag /*tag*/): ptr(ptr) {} + + ~GObjectRef() { + if (this->ptr) { + g_object_unref(this->ptr); + } + } + + // We do handle self-assignment in a more general case by checking the + // included pointers rather than the wrapper objects themselves. + // NOLINTBEGIN(bugprone-unhandled-self-assignment) + + GObjectRef(const GObjectRef& other): GObjectRef(other.ptr) {} + GObjectRef& operator=(const GObjectRef& other) { + if (*this == other) return *this; + if (this->ptr) { + g_object_unref(this->ptr); + } + this->ptr = other.ptr; + if (this->ptr) { + g_object_ref(this->ptr); + } + return *this; + } + + GObjectRef(GObjectRef&& other) noexcept: ptr(other.ptr) { other.ptr = nullptr; } + GObjectRef& operator=(GObjectRef&& other) noexcept { + if (*this == other) return *this; + if (this->ptr) { + g_object_unref(this->ptr); + } + this->ptr = other.ptr; + other.ptr = nullptr; + return *this; + } + + // NOLINTEND(bugprone-unhandled-self-assignment) + + [[nodiscard]] T* get() const { return this->ptr; } + T* operator->() const { return this->ptr; } + + bool operator==(const GObjectRef& other) const { return this->ptr == other.ptr; } + +private: + T* ptr; +}; +} // namespace qs::service::polkit \ No newline at end of file diff --git a/src/services/polkit/identity.cpp b/src/services/polkit/identity.cpp new file mode 100644 index 0000000..7be5f39 --- /dev/null +++ b/src/services/polkit/identity.cpp @@ -0,0 +1,84 @@ +#include "identity.hpp" +#include +#include +#include + +#include +#include +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// Workaround macro collision with glib 'signals' struct member. +#undef signals +#include +#define signals Q_SIGNALS +#include +#include +#include + +#include "gobjectref.hpp" + +namespace qs::service::polkit { +Identity::Identity( + id_t id, + QString name, + QString displayName, + bool isGroup, + GObjectRef polkitIdentity, + QObject* parent +) + : QObject(parent) + , polkitIdentity(std::move(polkitIdentity)) + , mId(id) + , mName(std::move(name)) + , mDisplayName(std::move(displayName)) + , mIsGroup(isGroup) {} + +Identity* Identity::fromPolkitIdentity(GObjectRef identity) { + if (POLKIT_IS_UNIX_USER(identity.get())) { + auto uid = polkit_unix_user_get_uid(POLKIT_UNIX_USER(identity.get())); + + auto bufSize = sysconf(_SC_GETPW_R_SIZE_MAX); + // The call can fail with -1, in this case choose a default that is + // big enough. + if (bufSize == -1) bufSize = 16384; + auto buffer = std::vector(bufSize); + + std::aligned_storage_t pwBuf; + passwd* pw = nullptr; + getpwuid_r(uid, reinterpret_cast(&pwBuf), buffer.data(), bufSize, &pw); + + auto name = + (pw && pw->pw_name && *pw->pw_name) ? QString::fromUtf8(pw->pw_name) : QString::number(uid); + + return new Identity( + uid, + name, + (pw && pw->pw_gecos && *pw->pw_gecos) ? QString::fromUtf8(pw->pw_gecos) : name, + false, + std::move(identity) + ); + } + + if (POLKIT_IS_UNIX_GROUP(identity.get())) { + auto gid = polkit_unix_group_get_gid(POLKIT_UNIX_GROUP(identity.get())); + + auto bufSize = sysconf(_SC_GETGR_R_SIZE_MAX); + // The call can fail with -1, in this case choose a default that is + // big enough. + if (bufSize == -1) bufSize = 16384; + auto buffer = std::vector(bufSize); + + std::aligned_storage_t grBuf; + group* gr = nullptr; + getgrgid_r(gid, reinterpret_cast(&grBuf), buffer.data(), bufSize, &gr); + + auto name = + (gr && gr->gr_name && *gr->gr_name) ? QString::fromUtf8(gr->gr_name) : QString::number(gid); + return new Identity(gid, name, name, true, std::move(identity)); + } + + // A different type of identity is netgroup. + return nullptr; +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/identity.hpp b/src/services/polkit/identity.hpp new file mode 100644 index 0000000..27f3c1c --- /dev/null +++ b/src/services/polkit/identity.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +#include "gobjectref.hpp" + +// _PolkitIdentity is considered a reserved identifier, but I am specifically +// forward declaring this reserved name. +using PolkitIdentity = struct _PolkitIdentity; // NOLINT(bugprone-reserved-identifier) + +namespace qs::service::polkit { +//! Represents a user or group that can be used to authenticate. +class Identity: public QObject { + Q_OBJECT; + Q_DISABLE_COPY_MOVE(Identity); + + // clang-format off + /// The Id of the identity. If the identity is a user, this is the user's uid. See @@isGroup. + Q_PROPERTY(quint32 id READ id CONSTANT); + + /// The name of the user or group. + /// + /// If available, this is the actual username or group name, but may fallback to the ID. + Q_PROPERTY(QString string READ name CONSTANT); + + /// The full name of the user or group, if available. Otherwise the same as @@name. + Q_PROPERTY(QString displayName READ displayName CONSTANT); + + /// Indicates if this identity is a group or a user. + /// + /// If true, @@id is a gid, otherwise it is a uid. + Q_PROPERTY(bool isGroup READ isGroup CONSTANT); + + QML_UNCREATABLE("Identities cannot be created directly."); + // clang-format on + +public: + explicit Identity( + id_t id, + QString name, + QString displayName, + bool isGroup, + GObjectRef polkitIdentity, + QObject* parent = nullptr + ); + ~Identity() override = default; + + static Identity* fromPolkitIdentity(GObjectRef identity); + + [[nodiscard]] quint32 id() const { return static_cast(this->mId); }; + [[nodiscard]] const QString& name() const { return this->mName; }; + [[nodiscard]] const QString& displayName() const { return this->mDisplayName; }; + [[nodiscard]] bool isGroup() const { return this->mIsGroup; }; + + GObjectRef polkitIdentity; + +private: + id_t mId; + QString mName; + QString mDisplayName; + bool mIsGroup; +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/listener.cpp b/src/services/polkit/listener.cpp new file mode 100644 index 0000000..643292c --- /dev/null +++ b/src/services/polkit/listener.cpp @@ -0,0 +1,234 @@ +#include "listener.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "gobjectref.hpp" +#include "qml.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkitListener, "quickshell.service.polkit.listener", QtWarningMsg); +} + +using qs::service::polkit::GObjectRef; + +// This is mostly GObject code, we follow their naming conventions for improved +// clarity and to mark it as such. Additionally, many methods need to be static +// to conform with the expected declarations. +// NOLINTBEGIN(readability-identifier-naming,misc-use-anonymous-namespace) + +using QsPolkitAgent = struct _QsPolkitAgent { + PolkitAgentListener parent_instance; + + qs::service::polkit::ListenerCb* cb; + gpointer registration_handle; +}; + +G_DEFINE_TYPE(QsPolkitAgent, qs_polkit_agent, POLKIT_AGENT_TYPE_LISTENER) + +static void initiate_authentication( + PolkitAgentListener* listener, + const gchar* actionId, + const gchar* message, + const gchar* iconName, + PolkitDetails* details, + const gchar* cookie, + GList* identities, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userData +); + +static gboolean +initiate_authentication_finish(PolkitAgentListener* listener, GAsyncResult* result, GError** error); + +static void qs_polkit_agent_init(QsPolkitAgent* self) { + self->cb = nullptr; + self->registration_handle = nullptr; +} + +static void qs_polkit_agent_finalize(GObject* object) { + if (G_OBJECT_CLASS(qs_polkit_agent_parent_class)) + G_OBJECT_CLASS(qs_polkit_agent_parent_class)->finalize(object); +} + +static void qs_polkit_agent_class_init(QsPolkitAgentClass* klass) { + GObjectClass* gobject_class = G_OBJECT_CLASS(klass); + gobject_class->finalize = qs_polkit_agent_finalize; + + PolkitAgentListenerClass* listener_class = POLKIT_AGENT_LISTENER_CLASS(klass); + listener_class->initiate_authentication = initiate_authentication; + listener_class->initiate_authentication_finish = initiate_authentication_finish; +} + +QsPolkitAgent* qs_polkit_agent_new(qs::service::polkit::ListenerCb* cb) { + QsPolkitAgent* self = QS_POLKIT_AGENT(g_object_new(QS_TYPE_POLKIT_AGENT, nullptr)); + self->cb = cb; + return self; +} + +struct RegisterCbData { + GObjectRef agent; + std::string path; +}; + +static void qs_polkit_agent_register_cb(GObject* /*unused*/, GAsyncResult* res, gpointer userData); +void qs_polkit_agent_register(QsPolkitAgent* agent, const char* path) { + if (path == nullptr || *path == '\0') { + qCWarning(logPolkitListener) << "cannot register listener without a path set."; + agent->cb->registerComplete(false); + return; + } + + auto* data = new RegisterCbData {.agent = GObjectRef(agent), .path = path}; + polkit_unix_session_new_for_process(getpid(), nullptr, &qs_polkit_agent_register_cb, data); +} + +static void qs_polkit_agent_register_cb(GObject* /*unused*/, GAsyncResult* res, gpointer userData) { + std::unique_ptr data(reinterpret_cast(userData)); + + GError* error = nullptr; + auto* subject = polkit_unix_session_new_for_process_finish(res, &error); + + if (subject == nullptr || error != nullptr) { + qCWarning(logPolkitListener) << "failed to create subject for listener:" + << (error ? error->message : ""); + g_clear_error(&error); + data->agent->cb->registerComplete(false); + return; + } + + data->agent->registration_handle = polkit_agent_listener_register( + POLKIT_AGENT_LISTENER(data->agent.get()), + POLKIT_AGENT_REGISTER_FLAGS_NONE, + subject, + data->path.c_str(), + nullptr, + &error + ); + + g_object_unref(subject); + + if (error != nullptr) { + qCWarning(logPolkitListener) << "failed to register listener:" << error->message; + g_clear_error(&error); + data->agent->cb->registerComplete(false); + return; + } + + data->agent->cb->registerComplete(true); +} + +void qs_polkit_agent_unregister(QsPolkitAgent* agent) { + if (agent->registration_handle != nullptr) { + polkit_agent_listener_unregister(agent->registration_handle); + agent->registration_handle = nullptr; + } +} + +static void authentication_cancelled_cb(GCancellable* /*unused*/, gpointer userData) { + auto* request = static_cast(userData); + request->cb->cancelAuthentication(request); +} + +static void initiate_authentication( + PolkitAgentListener* listener, + const gchar* actionId, + const gchar* message, + const gchar* iconName, + PolkitDetails* /*unused*/, + const gchar* cookie, + GList* identities, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userData +) { + auto* self = QS_POLKIT_AGENT(listener); + + auto* asyncResult = g_task_new(reinterpret_cast(self), nullptr, callback, userData); + + // Identities may be duplicated, so we use the hash to filter them out. + std::unordered_set identitySet; + std::vector> identityVector; + for (auto* item = g_list_first(identities); item != nullptr; item = g_list_next(item)) { + auto* identity = static_cast(item->data); + if (identitySet.contains(polkit_identity_hash(identity))) continue; + + identitySet.insert(polkit_identity_hash(identity)); + // The caller unrefs all identities after we return, therefore we need to + // take our own reference for the identities we keep. Our wrapper does + // this automatically. + identityVector.emplace_back(identity); + } + + // The original strings are freed by the caller after we return, so we + // copy them into QStrings. + auto* request = new qs::service::polkit::AuthRequest { + .actionId = QString::fromUtf8(actionId), + .message = QString::fromUtf8(message), + .iconName = QString::fromUtf8(iconName), + .cookie = QString::fromUtf8(cookie), + .identities = std::move(identityVector), + + .task = asyncResult, + .cancellable = cancellable, + .handlerId = 0, + .cb = self->cb + }; + + if (cancellable != nullptr) { + request->handlerId = g_cancellable_connect( + cancellable, + reinterpret_cast(authentication_cancelled_cb), + request, + nullptr + ); + } + + self->cb->initiateAuthentication(request); +} + +static gboolean initiate_authentication_finish( + PolkitAgentListener* /*unused*/, + GAsyncResult* result, + GError** error +) { + return g_task_propagate_boolean(G_TASK(result), error); +} + +namespace qs::service::polkit { +// While these functions can be const since they do not modify member variables, +// they are logically non-const since they modify the state of the +// authentication request. Therefore, we do not mark them as const. +// NOLINTBEGIN(readability-make-member-function-const) +void AuthRequest::complete() { g_task_return_boolean(this->task, true); } + +void AuthRequest::cancel(const QString& reason) { + auto utf8Reason = reason.toUtf8(); + g_task_return_new_error( + this->task, + POLKIT_ERROR, + POLKIT_ERROR_CANCELLED, + "%s", + utf8Reason.constData() + ); +} +// NOLINTEND(readability-make-member-function-const) +} // namespace qs::service::polkit + +// NOLINTEND(readability-identifier-naming,misc-use-anonymous-namespace) \ No newline at end of file diff --git a/src/services/polkit/listener.hpp b/src/services/polkit/listener.hpp new file mode 100644 index 0000000..996fa23 --- /dev/null +++ b/src/services/polkit/listener.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// This causes a problem with variables of the name. +#undef signals + +#include +#include + +#define signals Q_SIGNALS + +#include "gobjectref.hpp" + +namespace qs::service::polkit { +class ListenerCb; +//! All state that comes in from PolKit about an authentication request. +struct AuthRequest { + //! The action ID that this session is for. + QString actionId; + //! Message to present to the user. + QString message; + //! Icon name according to the FreeDesktop specification. May be empty. + QString iconName; + // Details intentionally omitted because nothing seems to use them. + QString cookie; + //! List of users/groups that can be used for authentication. + std::vector> identities; + + //! Implementation detail to mark authentication done. + GTask* task; + //! Implementation detail for requests cancelled by agent. + GCancellable* cancellable; + //! Callback handler ID for the cancellable. + gulong handlerId; + //! Callbacks for the listener + ListenerCb* cb; + + void complete(); + void cancel(const QString& reason); +}; + +//! Callback interface for PolkitAgent listener events. +class ListenerCb { +public: + ListenerCb() = default; + virtual ~ListenerCb() = default; + Q_DISABLE_COPY_MOVE(ListenerCb); + + //! Called when the agent registration is complete. + virtual void registerComplete(bool success) = 0; + //! Called when an authentication request is initiated by PolKit. + virtual void initiateAuthentication(AuthRequest* request) = 0; + //! Called when an authentication request is cancelled by PolKit before completion. + virtual void cancelAuthentication(AuthRequest* request) = 0; +}; +} // namespace qs::service::polkit + +G_BEGIN_DECLS + +// This is GObject code. By using their naming conventions, we clearly mark it +// as such for the rest of the project. +// NOLINTBEGIN(readability-identifier-naming) + +#define QS_TYPE_POLKIT_AGENT (qs_polkit_agent_get_type()) +G_DECLARE_FINAL_TYPE(QsPolkitAgent, qs_polkit_agent, QS, POLKIT_AGENT, PolkitAgentListener) + +QsPolkitAgent* qs_polkit_agent_new(qs::service::polkit::ListenerCb* cb); +void qs_polkit_agent_register(QsPolkitAgent* agent, const char* path); +void qs_polkit_agent_unregister(QsPolkitAgent* agent); + +// NOLINTEND(readability-identifier-naming) + +G_END_DECLS diff --git a/src/services/polkit/module.md b/src/services/polkit/module.md new file mode 100644 index 0000000..b306ecb --- /dev/null +++ b/src/services/polkit/module.md @@ -0,0 +1,52 @@ +name = "Quickshell.Services.Polkit" +description = "Polkit Agent" +headers = [ + "agentimpl.hpp", + "flow.hpp", + "identity.hpp", + "listener.hpp", + "qml.hpp", + "session.hpp", +] +----- +## Purpose of a Polkit Agent + +PolKit is a system for privileged applications to query if a user is permitted to execute an action. +You have probably seen it in the form of a "Please enter your password to continue with X" dialog box before. +This dialog box is presented by your *PolKit agent*, it is a process running as your user that accepts authentication requests from the *daemon* and presents them to you to accept or deny. + +This service enables writing a PolKit agent in Quickshell. + +## Implementing a Polkit Agent + +The backend logic of communicating with the daemon is handled by the @@Quickshell.Services.Polkit.PolkitAgent object. +It exposes incoming requests via @@Quickshell.Services.Polkit.PolkitAgent.flow and provides appropriate signals. + +### Flow of an authentication request + +Incoming authentication requests are queued in the order that they arrive. +If none is queued, a request starts processing right away. +Otherwise, it will wait until prior requests are done. + +A request starts by emitting the @@Quickshell.Services.Polkit.PolkitAgent.authenticationRequestStarted signal. +At this point, information like the action to be performed and permitted users that can authenticate is available. + +An authentication *session* for the request is immediately started, which internally starts a PAM conversation that is likely to prompt for user input. +* Additional prompts may be shared with the user by way of the @@Quickshell.Services.Polkit.AuthFlow.supplementaryMessageChanged / @@Quickshell.Services.Polkit.AuthFlow.supplementaryIsErrorChanged signals and the @@Quickshell.Services.Polkit.AuthFlow.supplementaryMessage and @@Quickshell.Services.Polkit.AuthFlow.supplementaryIsError properties. A common message might be 'Please input your password'. +* An input request is forwarded via the @@Quickshell.Services.Polkit.AuthFlow.isResponseRequiredChanged / @@Quickshell.Services.Polkit.AuthFlow.inputPromptChanged / @@Quickshell.Services.Polkit.AuthFlow.responseVisibleChanged signals and the corresponding properties. Note that the request specifies whether the text box should show the typed input on screen or replace it with placeholders. + +User replies can be submitted via the @@Quickshell.Services.Polkit.AuthFlow.submit method. +A conversation can take multiple turns, for example if second factors are involved. + +If authentication fails, we automatically create a fresh session so the user can try again. +The @@Quickshell.Services.Polkit.AuthFlow.authenticationFailed signal is emitted in this case. + +If authentication is successful, you receive the @@Quickshell.Services.Polkit.AuthFlow.authenticationSucceeeded signal. At this point, the dialog can be closed. +If additional requests are queued, you will receive the @@Quickshell.Services.Polkit.PolkitAgent.authenticationRequestStarted signal again. + +#### Cancelled requests + +Requests may either be canceled by the user or the PolKit daemon. +In this case, we clean up any state and proceed to the next request, if any. + +If the request was cancelled by the daemon and not the user, you also receive the @@Quickshell.Services.Polkit.AuthFlow.authenticationRequestCancelled signal. diff --git a/src/services/polkit/qml.cpp b/src/services/polkit/qml.cpp new file mode 100644 index 0000000..9a08e5d --- /dev/null +++ b/src/services/polkit/qml.cpp @@ -0,0 +1,35 @@ +#include "qml.hpp" + +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "agentimpl.hpp" + +namespace { +QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.polkit", QtWarningMsg); +} + +namespace qs::service::polkit { +PolkitAgent::~PolkitAgent() { PolkitAgentImpl::onEndOfQmlAgent(this); }; + +void PolkitAgent::componentComplete() { + if (this->mPath.isEmpty()) this->mPath = "/org/quickshell/PolkitAgent"; + + auto* impl = PolkitAgentImpl::tryTakeoverOrCreate(this); + if (impl == nullptr) return; + + this->bFlow.setBinding([impl]() { return impl->activeFlow().value(); }); + this->bIsActive.setBinding([impl]() { return impl->activeFlow().value() != nullptr; }); + this->bIsRegistered.setBinding([impl]() { return impl->isRegistered().value(); }); +} + +void PolkitAgent::setPath(const QString& path) { + if (this->mPath.isEmpty()) { + this->mPath = path; + } else if (this->mPath != path) { + qCWarning(logPolkit) << "cannot change path after it has been set."; + } +} +} // namespace qs::service::polkit diff --git a/src/services/polkit/qml.hpp b/src/services/polkit/qml.hpp new file mode 100644 index 0000000..5343bcd --- /dev/null +++ b/src/services/polkit/qml.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "flow.hpp" + +// The reserved identifier is exactly the struct I mean. +using PolkitIdentity = struct _PolkitIdentity; // NOLINT(bugprone-reserved-identifier) +using QsPolkitAgent = struct _QsPolkitAgent; + +namespace qs::service::polkit { + +struct AuthRequest; +class Session; +class Identity; +class AuthFlow; + +//! Contains interface to instantiate a PolKit agent listener. +class PolkitAgent + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); + Q_DISABLE_COPY_MOVE(PolkitAgent); + + /// The D-Bus path that this agent listener will use. + /// + /// If not set, a default of /org/quickshell/Polkit will be used. + Q_PROPERTY(QString path READ path WRITE setPath); + + /// Indicates whether the agent registered successfully and is in use. + Q_PROPERTY(bool isRegistered READ default NOTIFY isRegisteredChanged BINDABLE isRegistered); + + /// Indicates an ongoing authentication request. + /// + /// If this is true, other properties such as @@message and @@iconName will + /// also be populated with relevant information. + Q_PROPERTY(bool isActive READ default NOTIFY isActiveChanged BINDABLE isActive); + + /// The current authentication state if an authentication request is active. + /// + /// Null when no authentication request is active. + Q_PROPERTY(AuthFlow* flow READ default NOTIFY flowChanged BINDABLE flow); + +public: + explicit PolkitAgent(QObject* parent = nullptr): QObject(parent) {}; + ~PolkitAgent() override; + + void classBegin() override {}; + void componentComplete() override; + + [[nodiscard]] QString path() const { return this->mPath; }; + void setPath(const QString& path); + + [[nodiscard]] QBindable flow() { return &this->bFlow; }; + [[nodiscard]] QBindable isActive() { return &this->bIsActive; }; + [[nodiscard]] QBindable isRegistered() { return &this->bIsRegistered; }; + +signals: + /// Emitted when an application makes a request that requires authentication. + /// + /// At this point, @@state will be populated with relevant information. + /// Note that signals for conversation outcome are emitted from the @@AuthFlow instance. + void authenticationRequestStarted(); + + void isRegisteredChanged(); + void isActiveChanged(); + void flowChanged(); + +private: + QString mPath = ""; + + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, AuthFlow*, bFlow, &PolkitAgent::flowChanged); + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, bool, bIsActive, &PolkitAgent::isActiveChanged); + Q_OBJECT_BINDABLE_PROPERTY(PolkitAgent, bool, bIsRegistered, &PolkitAgent::isRegisteredChanged); +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/session.cpp b/src/services/polkit/session.cpp new file mode 100644 index 0000000..71def68 --- /dev/null +++ b/src/services/polkit/session.cpp @@ -0,0 +1,68 @@ +#include "session.hpp" + +#include +#include +#include +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +// This causes a problem with variables of the name. +#undef signals +#include +#define signals Q_SIGNALS + +namespace qs::service::polkit { + +namespace { +void completedCb(PolkitAgentSession* /*session*/, gboolean gainedAuthorization, gpointer userData) { + auto* self = static_cast(userData); + emit self->completed(gainedAuthorization); +} + +void requestCb( + PolkitAgentSession* /*session*/, + const char* message, + gboolean echo, + gpointer userData +) { + auto* self = static_cast(userData); + emit self->request(QString::fromUtf8(message), echo); +} + +void showErrorCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) { + auto* self = static_cast(userData); + emit self->showError(QString::fromUtf8(message)); +} + +void showInfoCb(PolkitAgentSession* /*session*/, const char* message, gpointer userData) { + auto* self = static_cast(userData); + emit self->showInfo(QString::fromUtf8(message)); +} +} // namespace + +Session::Session(PolkitIdentity* identity, const QString& cookie, QObject* parent) + : QObject(parent) { + this->session = polkit_agent_session_new(identity, cookie.toUtf8().constData()); + + g_signal_connect(G_OBJECT(this->session), "completed", G_CALLBACK(completedCb), this); + g_signal_connect(G_OBJECT(this->session), "request", G_CALLBACK(requestCb), this); + g_signal_connect(G_OBJECT(this->session), "show-error", G_CALLBACK(showErrorCb), this); + g_signal_connect(G_OBJECT(this->session), "show-info", G_CALLBACK(showInfoCb), this); +} + +Session::~Session() { + // Signals do not need to be disconnected explicitly. This happens during + // destruction of the gobject. Since we own the session object, we can be + // sure it is being destroyed after the unref. + g_object_unref(this->session); +} + +void Session::initiate() { polkit_agent_session_initiate(this->session); } + +void Session::cancel() { polkit_agent_session_cancel(this->session); } + +void Session::respond(const QString& response) { + polkit_agent_session_response(this->session, response.toUtf8().constData()); +} + +} // namespace qs::service::polkit diff --git a/src/services/polkit/session.hpp b/src/services/polkit/session.hpp new file mode 100644 index 0000000..29331b1 --- /dev/null +++ b/src/services/polkit/session.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +// _PolkitIdentity and _PolkitAgentSession are considered reserved identifiers, +// but I am specifically forward declaring those reserved names. + +// NOLINTBEGIN(bugprone-reserved-identifier) +using PolkitIdentity = struct _PolkitIdentity; +using PolkitAgentSession = struct _PolkitAgentSession; +// NOLINTEND(bugprone-reserved-identifier) + +namespace qs::service::polkit { +//! Represents an authentication session for a specific identity. +class Session: public QObject { + Q_OBJECT; + Q_DISABLE_COPY_MOVE(Session); + +public: + explicit Session(PolkitIdentity* identity, const QString& cookie, QObject* parent = nullptr); + ~Session() override; + + /// Call this after connecting to the relevant signals. + void initiate(); + /// Call this to abort a running authentication session. + void cancel(); + /// Provide a response to an input request. + void respond(const QString& response); + +Q_SIGNALS: + /// Emitted when the session wants to request input from the user. + /// + /// The message is a prompt to present to the user. + /// If echo is false, the user's response should not be displayed (e.g. for passwords). + void request(const QString& message, bool echo); + + /// Emitted when the authentication session completes. + /// + /// If success is true, authentication was successful. + /// Otherwise it failed (e.g. wrong password). + void completed(bool success); + + /// Emitted when an error message should be shown to the user. + void showError(const QString& message); + + /// Emitted when an informational message should be shown to the user. + void showInfo(const QString& message); + +private: + PolkitAgentSession* session = nullptr; +}; +} // namespace qs::service::polkit diff --git a/src/services/polkit/test/manual/agent.qml b/src/services/polkit/test/manual/agent.qml new file mode 100644 index 0000000..4588e4b --- /dev/null +++ b/src/services/polkit/test/manual/agent.qml @@ -0,0 +1,97 @@ +import Quickshell +import Quickshell.Services.Polkit +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Scope { + id: root + + FloatingWindow { + title: "Authentication Required" + + visible: polkitAgent.isActive + color: contentItem.palette.window + + ColumnLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: 18 + spacing: 12 + + Item { Layout.fillHeight: true } + + Label { + Layout.fillWidth: true + text: polkitAgent.flow?.message || "" + wrapMode: Text.Wrap + font.bold: true + } + + Label { + Layout.fillWidth: true + text: polkitAgent.flow?.supplementaryMessage || "" + wrapMode: Text.Wrap + opacity: 0.8 + } + + Label { + Layout.fillWidth: true + text: polkitAgent.flow?.inputPrompt || "" + wrapMode: Text.Wrap + } + + Label { + text: "Authentication failed, try again" + color: "red" + visible: polkitAgent.flow?.failed + } + + TextField { + id: passwordInput + echoMode: polkitAgent.flow?.responseVisible + ? TextInput.Normal : TextInput.Password + selectByMouse: true + Layout.fillWidth: true + onAccepted: okButton.clicked() + } + + RowLayout { + spacing: 8 + Button { + id: okButton + text: "OK" + enabled: passwordInput.text.length > 0 || !!polkitAgent.flow?.isResponseRequired + onClicked: { + polkitAgent.flow.submit(passwordInput.text) + passwordInput.text = "" + passwordInput.forceActiveFocus() + } + } + Button { + text: "Cancel" + visible: polkitAgent.isActive + onClicked: { + polkitAgent.flow.cancelAuthenticationRequest() + passwordInput.text = "" + } + } + } + + Item { Layout.fillHeight: true } + } + + Connections { + target: polkitAgent.flow + function onIsResponseRequiredChanged() { + passwordInput.text = "" + if (polkitAgent.flow.isResponseRequired) + passwordInput.forceActiveFocus() + } + } + } + + PolkitAgent { + id: polkitAgent + } +} From fc704e6b5d445899a1565955268c91942a4f263f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 Oct 2025 00:56:30 -0700 Subject: [PATCH 126/226] core: reference scanned paths by QDir over QString Fixes a bug introduced in 3e2ce40 where a directory imported with a "../name" path import would be passed to scanDir as ending in '/' which created an invalid duplicate scan entry. --- src/core/scan.cpp | 11 ++++++----- src/core/scan.hpp | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 45413fb..d9606bc 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -19,12 +19,13 @@ QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); -void QmlScanner::scanDir(const QString& path) { - if (this->scannedDirs.contains(path)) return; - this->scannedDirs.push_back(path); +void QmlScanner::scanDir(const QDir& dir) { + if (this->scannedDirs.contains(dir)) return; + this->scannedDirs.push_back(dir); + + const auto& path = dir.path(); qCDebug(logQmlScanner) << "Scanning directory" << path; - auto dir = QDir(path); struct Entry { QString name; @@ -166,7 +167,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna auto currentdir = QDir(QFileInfo(path).absolutePath()); // the root can never be a singleton so it dosent matter if we skip it - this->scanDir(currentdir.path()); + this->scanDir(currentdir); for (auto& import: imports) { QString ipath; diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 9d88f07..2dc8c3c 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -16,10 +16,10 @@ public: QmlScanner() = default; QmlScanner(const QDir& rootPath): rootPath(rootPath) {} - void scanDir(const QString& path); + void scanDir(const QDir& dir); void scanQmlRoot(const QString& path); - QVector scannedDirs; + QVector scannedDirs; QVector scannedFiles; QHash fileIntercepts; From a00ff0394431d1fe3f33ae0934c981930e2a1efb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 14 Nov 2025 02:12:42 -0800 Subject: [PATCH 127/226] services/pipewire: cache route device volumes to initialize nodes Nodes referencing a device can be bound later than the device is bound. If this happens, the node will not receive an initial route device volume change event. This change caches the last known route device volume and initializes the device with it if present. --- changelog/next.md | 1 + src/services/pipewire/device.cpp | 10 ++++++++++ src/services/pipewire/device.hpp | 3 +++ src/services/pipewire/node.cpp | 13 ++++++++++++- src/services/pipewire/node.hpp | 2 ++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/changelog/next.md b/changelog/next.md index 5f5aa34..4b255ff 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -24,6 +24,7 @@ set shell id. - Fixed volume control breaking with pipewire pro audio mode. - Fixed escape sequence handling in desktop entries. +- Fixed volumes not initializing if a pipewire device was already loaded before its node. ## Packaging Changes diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 0c111fa..314fd63 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -125,12 +125,22 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { // Insert into the main map as well, staging's purpose is to remove old entries. this->routeDeviceIndexes.insert(device, index); + // Used for initial node volume if the device is bound before the node + // (e.g. multiple nodes pointing to the same device) + this->routeDeviceVolumes.insert(device, volumeProps); + qCDebug(logDevice).nospace() << "Registered device/index pair for " << this << ": [device: " << device << ", index: " << index << ']'; emit this->routeVolumesChanged(device, volumeProps); } +bool PwDevice::tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps) { + if (!this->routeDeviceVolumes.contains(routeDevice)) return false; + volumeProps = this->routeDeviceVolumes.value(routeDevice); + return true; +} + 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. diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 1a1f705..22af699 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -32,6 +32,8 @@ public: void waitForDevice(); [[nodiscard]] bool waitingForDevice() const; + [[nodiscard]] bool tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps); + signals: void deviceReady(); void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps); @@ -46,6 +48,7 @@ private: onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); QHash routeDeviceIndexes; + QHash routeDeviceVolumes; QList stagingIndexes; void addDeviceIndexPairs(const spa_pod* param); diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index f336558..1eceab9 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -218,6 +218,7 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { } self->routeDevice = id; + if (self->boundData) self->boundData->onDeviceChanged(); } else { qCCritical(logNode) << self << "has attached device" << self->device << "but no card.profile.device property."; @@ -277,6 +278,15 @@ PwNodeBoundAudio::PwNodeBoundAudio(PwNode* node): QObject(node), node(node) { } } +void PwNodeBoundAudio::onDeviceChanged() { + PwVolumeProps volumeProps; + if (this->node->device->tryLoadVolumeProps(this->node->routeDevice, volumeProps)) { + qCDebug(logNode) << "Initializing volume props for" << this->node + << "with known values from backing device."; + this->updateVolumeProps(volumeProps); + } +} + void PwNodeBoundAudio::onInfo(const pw_node_info* info) { if ((info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) != 0) { for (quint32 i = 0; i < info->n_params; i++) { @@ -299,7 +309,8 @@ void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* para if (id == SPA_PARAM_Props && index == 0) { if (this->node->shouldUseDevice()) { qCDebug(logNode) << "Skipping node volume props update for" << this->node - << "in favor of device updates."; + << "in favor of device updates from routeDevice" << this->node->routeDevice + << "of" << this->node->device; return; } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 359c0f3..e3e1913 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -169,6 +169,7 @@ public: virtual ~PwNodeBoundData() = default; Q_DISABLE_COPY_MOVE(PwNodeBoundData); + virtual void onDeviceChanged() {}; virtual void onInfo(const pw_node_info* /*info*/) {} virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {} virtual void onUnbind() {} @@ -182,6 +183,7 @@ class PwNodeBoundAudio public: explicit PwNodeBoundAudio(PwNode* node); + void onDeviceChanged() override; void onInfo(const pw_node_info* info) override; void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override; void onUnbind() override; From 0a36e3ed40bf2de41b76ac363bc1f98820473786 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 15 Nov 2025 02:31:58 -0800 Subject: [PATCH 128/226] ci: add qt6.10.0 checkout --- .github/workflows/build.yml | 2 +- ci/nix-checkouts.nix | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a3d097..dc6e8a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Nix strategy: matrix: - 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] + qtver: [qt6.10.0, 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: diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix index 5a95a34..8ef997d 100644 --- a/ci/nix-checkouts.nix +++ b/ci/nix-checkouts.nix @@ -8,7 +8,12 @@ let inherit sha256; }) {}; in rec { - latest = qt6_9_0; + latest = qt6_10_0; + + qt6_10_0 = byCommit { + commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55"; + sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0"; + }; qt6_9_2 = byCommit { commit = "e9f00bd893984bc8ce46c895c3bf7cac95331127"; From 1552aca3df6675cb77e6c04bccbdd93bf7156320 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 15 Nov 2025 04:28:07 -0800 Subject: [PATCH 129/226] build: fix new clang-tidy lints --- .clang-tidy | 3 + src/core/CMakeLists.txt | 1 - src/core/logging.cpp | 4 +- src/core/model.cpp | 72 +----------- src/core/model.hpp | 98 ++++++++++++---- src/core/module.md | 1 - src/core/objectrepeater.cpp | 190 ------------------------------- src/core/objectrepeater.hpp | 85 -------------- src/core/util.hpp | 2 +- src/dbus/properties.hpp | 2 +- src/services/pipewire/device.cpp | 2 +- src/services/polkit/listener.cpp | 2 +- 12 files changed, 84 insertions(+), 378 deletions(-) delete mode 100644 src/core/objectrepeater.cpp delete mode 100644 src/core/objectrepeater.hpp diff --git a/.clang-tidy b/.clang-tidy index 002c444..c83ed8f 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -20,6 +20,7 @@ Checks: > -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-use-enum-class, google-global-names-in-headers, google-readability-casting, google-runtime-int, @@ -63,6 +64,8 @@ CheckOptions: readability-identifier-naming.ParameterCase: camelBack readability-identifier-naming.VariableCase: camelBack + misc-const-correctness.WarnPointersAsPointers: false + # does not appear to work readability-operators-representation.BinaryOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' readability-operators-representation.OverloadedOperators: '&&;&=;&;|;~;!;!=;||;|=;^;^=' diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6029b42..472ae04 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -24,7 +24,6 @@ qt_add_library(quickshell-core STATIC elapsedtimer.cpp desktopentry.cpp desktopentrymonitor.cpp - objectrepeater.cpp platformmenu.cpp qsmenu.cpp retainable.cpp diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 034a14d..3e3abca 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -746,11 +746,11 @@ bool EncodedLogReader::readVarInt(quint32* slot) { if (!this->reader.skip(1)) return false; *slot = qFromLittleEndian(n); } else if ((bytes[1] != 0xff || bytes[2] != 0xff) && readLength >= 3) { - auto n = *reinterpret_cast(bytes.data() + 1); + auto n = *reinterpret_cast(bytes.data() + 1); // NOLINT if (!this->reader.skip(3)) return false; *slot = qFromLittleEndian(n); } else if (readLength == 7) { - auto n = *reinterpret_cast(bytes.data() + 3); + auto n = *reinterpret_cast(bytes.data() + 3); // NOLINT if (!this->reader.skip(7)) return false; *slot = qFromLittleEndian(n); } else return false; diff --git a/src/core/model.cpp b/src/core/model.cpp index 165c606..c2b5d78 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -1,81 +1,13 @@ #include "model.hpp" -#include #include +#include #include -#include -#include -#include -#include -#include - -qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const { - if (parent != QModelIndex()) return 0; - return static_cast(this->valuesList.length()); -} - -QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const { - if (role != Qt::UserRole) return QVariant(); - return QVariant::fromValue(this->valuesList.at(index.row())); -} QHash UntypedObjectModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; } -void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { - auto iindex = index == -1 ? this->valuesList.length() : index; - emit this->objectInsertedPre(object, iindex); - - auto intIndex = static_cast(iindex); - this->beginInsertRows(QModelIndex(), intIndex, intIndex); - this->valuesList.insert(iindex, object); - this->endInsertRows(); - - emit this->valuesChanged(); - emit this->objectInsertedPost(object, iindex); -} - -void UntypedObjectModel::removeAt(qsizetype index) { - auto* object = this->valuesList.at(index); - emit this->objectRemovedPre(object, index); - - auto intIndex = static_cast(index); - this->beginRemoveRows(QModelIndex(), intIndex, intIndex); - this->valuesList.removeAt(index); - this->endRemoveRows(); - - emit this->valuesChanged(); - emit this->objectRemovedPost(object, index); -} - -bool UntypedObjectModel::removeObject(const QObject* object) { - auto index = this->valuesList.indexOf(object); - if (index == -1) return false; - - this->removeAt(index); - return true; -} - -void UntypedObjectModel::diffUpdate(const QVector& newValues) { - for (qsizetype i = 0; i < this->valuesList.length();) { - if (newValues.contains(this->valuesList.at(i))) i++; - else this->removeAt(i); - } - - qsizetype oi = 0; - for (auto* object: newValues) { - if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) { - this->insertObject(object, oi); - } - - oi++; - } -} - -qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } - UntypedObjectModel* UntypedObjectModel::emptyInstance() { - static auto* instance = new UntypedObjectModel(nullptr); // NOLINT - return instance; + return ObjectModel::emptyInstance(); } diff --git a/src/core/model.hpp b/src/core/model.hpp index 3c5822a..0e88025 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include #include @@ -49,14 +49,11 @@ class UntypedObjectModel: public QAbstractListModel { public: explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {} - [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override; - [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override; [[nodiscard]] QHash roleNames() const override; - [[nodiscard]] QList values() const { return this->valuesList; } - void removeAt(qsizetype index); + [[nodiscard]] virtual QList values() = 0; - Q_INVOKABLE qsizetype indexOf(QObject* object); + Q_INVOKABLE virtual qsizetype indexOf(QObject* object) const = 0; static UntypedObjectModel* emptyInstance(); @@ -71,15 +68,6 @@ signals: /// Sent immediately after an object is removed from the list. void objectRemovedPost(QObject* object, qsizetype index); -protected: - void insertObject(QObject* object, qsizetype index = -1); - bool removeObject(const QObject* object); - - // Assumes only one instance of a specific value - void diffUpdate(const QVector& newValues); - - QVector valuesList; - private: static qsizetype valuesCount(QQmlListProperty* property); static QObject* valueAt(QQmlListProperty* property, qsizetype index); @@ -90,14 +78,20 @@ class ObjectModel: public UntypedObjectModel { public: explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} - [[nodiscard]] QVector& valueList() { return *std::bit_cast*>(&this->valuesList); } - - [[nodiscard]] const QVector& valueList() const { - return *std::bit_cast*>(&this->valuesList); - } + [[nodiscard]] const QList& valueList() const { return this->mValuesList; } + [[nodiscard]] QList& valueList() { return this->mValuesList; } void insertObject(T* object, qsizetype index = -1) { - this->UntypedObjectModel::insertObject(object, index); + auto iindex = index == -1 ? this->mValuesList.length() : index; + emit this->objectInsertedPre(object, iindex); + + auto intIndex = static_cast(iindex); + this->beginInsertRows(QModelIndex(), intIndex, intIndex); + this->mValuesList.insert(iindex, object); + this->endInsertRows(); + + emit this->valuesChanged(); + emit this->objectInsertedPost(object, iindex); } void insertObjectSorted(T* object, const std::function& compare) { @@ -110,17 +104,71 @@ public: } auto idx = iter - list.begin(); - this->UntypedObjectModel::insertObject(object, idx); + this->insertObject(object, idx); } - void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } + bool removeObject(const T* object) { + auto index = this->mValuesList.indexOf(object); + if (index == -1) return false; + + this->removeAt(index); + return true; + } + + void removeAt(qsizetype index) { + auto* object = this->mValuesList.at(index); + emit this->objectRemovedPre(object, index); + + auto intIndex = static_cast(index); + this->beginRemoveRows(QModelIndex(), intIndex, intIndex); + this->mValuesList.removeAt(index); + this->endRemoveRows(); + + emit this->valuesChanged(); + emit this->objectRemovedPost(object, index); + } // Assumes only one instance of a specific value - void diffUpdate(const QVector& newValues) { - this->UntypedObjectModel::diffUpdate(*std::bit_cast*>(&newValues)); + void diffUpdate(const QList& newValues) { + for (qsizetype i = 0; i < this->mValuesList.length();) { + if (newValues.contains(this->mValuesList.at(i))) i++; + else this->removeAt(i); + } + + qsizetype oi = 0; + for (auto* object: newValues) { + if (this->mValuesList.length() == oi || this->mValuesList.at(oi) != object) { + this->insertObject(object, oi); + } + + oi++; + } } static ObjectModel* emptyInstance() { return static_cast*>(UntypedObjectModel::emptyInstance()); } + + [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override { + if (parent != QModelIndex()) return 0; + return static_cast(this->mValuesList.length()); + } + + [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override { + if (role != Qt::UserRole) return QVariant(); + // Values must be QObject derived, but we can't assert that here without breaking forward decls, + // so no static_cast. + return QVariant::fromValue(reinterpret_cast(this->mValuesList.at(index.row()))); + } + + qsizetype indexOf(QObject* object) const override { + return this->mValuesList.indexOf(reinterpret_cast(object)); + } + + [[nodiscard]] QList values() override { + return *reinterpret_cast*>(&this->mValuesList); + } + +private: + QList mValuesList; }; diff --git a/src/core/module.md b/src/core/module.md index b9404ea..41f065d 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -21,7 +21,6 @@ headers = [ "model.hpp", "elapsedtimer.hpp", "desktopentry.hpp", - "objectrepeater.hpp", "qsmenu.hpp", "retainable.hpp", "popupanchor.hpp", diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp deleted file mode 100644 index 7971952..0000000 --- a/src/core/objectrepeater.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "objectrepeater.hpp" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -QVariant ObjectRepeater::model() const { return this->mModel; } - -void ObjectRepeater::setModel(QVariant model) { - if (model == this->mModel) return; - - if (this->itemModel != nullptr) { - QObject::disconnect(this->itemModel, nullptr, this, nullptr); - } - - this->mModel = std::move(model); - emit this->modelChanged(); - this->reloadElements(); -} - -void ObjectRepeater::onModelDestroyed() { - this->mModel.clear(); - this->itemModel = nullptr; - emit this->modelChanged(); - this->reloadElements(); -} - -QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; } - -void ObjectRepeater::setDelegate(QQmlComponent* delegate) { - if (delegate == this->mDelegate) return; - - if (this->mDelegate != nullptr) { - QObject::disconnect(this->mDelegate, nullptr, this, nullptr); - } - - this->mDelegate = delegate; - - if (delegate != nullptr) { - QObject::connect( - this->mDelegate, - &QObject::destroyed, - this, - &ObjectRepeater::onDelegateDestroyed - ); - } - - emit this->delegateChanged(); - this->reloadElements(); -} - -void ObjectRepeater::onDelegateDestroyed() { - this->mDelegate = nullptr; - emit this->delegateChanged(); - this->reloadElements(); -} - -void ObjectRepeater::reloadElements() { - for (auto i = this->valuesList.length() - 1; i >= 0; i--) { - this->removeComponent(i); - } - - if (this->mDelegate == nullptr || !this->mModel.isValid()) return; - - if (this->mModel.canConvert()) { - auto* model = this->mModel.value(); - this->itemModel = model; - - this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine - - // clang-format off - QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed); - QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted); - QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &ObjectRepeater::onModelRowsRemoved); - QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved); - QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset); - // clang-format on - } else if (this->mModel.canConvert()) { - auto values = this->mModel.value(); - auto len = values.count(); - - for (auto i = 0; i != len; i++) { - this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}}); - } - } else if (this->mModel.canConvert>()) { - auto values = this->mModel.value>(); - - for (auto& value: values) { - this->insertComponent(this->valuesList.length(), {{"modelData", value}}); - } - } else { - qCritical() << this - << "Cannot create components as the model is not compatible:" << this->mModel; - } -} - -void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) { - auto roles = model->roleNames(); - auto roleDataVec = QVector(); - for (auto id: roles.keys()) { - roleDataVec.push_back(QModelRoleData(id)); - } - - auto values = QModelRoleDataSpan(roleDataVec); - auto props = QVariantMap(); - - for (auto i = first; i != last + 1; i++) { - auto index = model->index(i, 0); - model->multiData(index, values); - - for (auto [id, name]: roles.asKeyValueRange()) { - props.insert(name, *values.dataForRole(id)); - } - - this->insertComponent(i, props); - - props.clear(); - } -} - -void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) { - if (parent != QModelIndex()) return; - - this->insertModelElements(this->itemModel, first, last); -} - -void ObjectRepeater::onModelRowsRemoved(const QModelIndex& parent, int first, int last) { - if (parent != QModelIndex()) return; - - for (auto i = last; i != first - 1; i--) { - this->removeComponent(i); - } -} - -void ObjectRepeater::onModelRowsMoved( - const QModelIndex& sourceParent, - int sourceStart, - int sourceEnd, - const QModelIndex& destParent, - int destStart -) { - auto hasSource = sourceParent != QModelIndex(); - auto hasDest = destParent != QModelIndex(); - - if (!hasSource && !hasDest) return; - - if (hasSource) { - this->onModelRowsRemoved(sourceParent, sourceStart, sourceEnd); - } - - if (hasDest) { - this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart)); - } -} - -void ObjectRepeater::onModelAboutToBeReset() { - auto last = static_cast(this->valuesList.length() - 1); - this->onModelRowsRemoved(QModelIndex(), 0, last); // -1 is fine -} - -void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) { - auto* context = QQmlEngine::contextForObject(this); - auto* instance = this->mDelegate->createWithInitialProperties(properties, context); - - if (instance == nullptr) { - qWarning().noquote() << this->mDelegate->errorString(); - qWarning() << this << "failed to create object for model data" << properties; - } else { - QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership); - instance->setParent(this); - } - - this->insertObject(instance, index); -} - -void ObjectRepeater::removeComponent(qsizetype index) { - auto* instance = this->valuesList.at(index); - this->removeAt(index); - delete instance; -} diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp deleted file mode 100644 index 409b12d..0000000 --- a/src/core/objectrepeater.hpp +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "model.hpp" - -///! A Repeater / for loop / map for non Item derived objects. -/// > [!ERROR] Removed in favor of @@QtQml.Models.Instantiator -/// -/// The ObjectRepeater creates instances of the provided delegate for every entry in the -/// given model, similarly to a @@QtQuick.Repeater but for non visual types. -class ObjectRepeater: public ObjectModel { - Q_OBJECT; - /// The model providing data to the ObjectRepeater. - /// - /// Currently accepted model types are `list` lists, javascript arrays, - /// and [QAbstractListModel] derived models, though only one column will be repeated - /// from the latter. - /// - /// Note: @@ObjectModel is a [QAbstractListModel] with a single column. - /// - /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html - Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged); - /// The delegate component to repeat. - /// - /// The delegate is given the same properties as in a Repeater, except `index` which - /// is not currently implemented. - /// - /// If the model is a `list` or javascript array, a `modelData` property will be - /// exposed containing the entry from the model. If the model is a [QAbstractListModel], - /// the roles from the model will be exposed. - /// - /// Note: @@ObjectModel has a single role named `modelData` for compatibility with normal lists. - /// - /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html - Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged); - Q_CLASSINFO("DefaultProperty", "delegate"); - QML_ELEMENT; - QML_UNCREATABLE("ObjectRepeater has been removed in favor of QtQml.Models.Instantiator."); - -public: - explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {} - - [[nodiscard]] QVariant model() const; - void setModel(QVariant model); - - [[nodiscard]] QQmlComponent* delegate() const; - void setDelegate(QQmlComponent* delegate); - -signals: - void modelChanged(); - void delegateChanged(); - -private slots: - void onDelegateDestroyed(); - void onModelDestroyed(); - void onModelRowsInserted(const QModelIndex& parent, int first, int last); - void onModelRowsRemoved(const QModelIndex& parent, int first, int last); - - void onModelRowsMoved( - const QModelIndex& sourceParent, - int sourceStart, - int sourceEnd, - const QModelIndex& destParent, - int destStart - ); - - void onModelAboutToBeReset(); - -private: - void reloadElements(); - void insertModelElements(QAbstractItemModel* model, int first, int last); - void insertComponent(qsizetype index, const QVariantMap& properties); - void removeComponent(qsizetype index); - - QVariant mModel; - QAbstractItemModel* itemModel = nullptr; - QQmlComponent* mDelegate = nullptr; -}; diff --git a/src/core/util.hpp b/src/core/util.hpp index 88583d0..3b86d28 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -29,7 +29,7 @@ struct StringLiteral16 { } [[nodiscard]] constexpr const QChar* qCharPtr() const noexcept { - return std::bit_cast(&this->value); + return std::bit_cast(&this->value); // NOLINT } [[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept { diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index f6a6330..1596cb7 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -217,7 +217,7 @@ protected: private: [[nodiscard]] constexpr Owner* owner() const { - auto* self = std::bit_cast(this); + auto* self = std::bit_cast(this); // NOLINT return std::bit_cast(self - offset()); // NOLINT } diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 314fd63..e3bc967 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -107,7 +107,7 @@ void PwDevice::addDeviceIndexPairs(const spa_pod* param) { qint32 device = 0; qint32 index = 0; - spa_pod* props = nullptr; + const spa_pod* props = nullptr; // clang-format off quint32 id = SPA_PARAM_Route; diff --git a/src/services/polkit/listener.cpp b/src/services/polkit/listener.cpp index 643292c..875cff6 100644 --- a/src/services/polkit/listener.cpp +++ b/src/services/polkit/listener.cpp @@ -231,4 +231,4 @@ void AuthRequest::cancel(const QString& reason) { // NOLINTEND(readability-make-member-function-const) } // namespace qs::service::polkit -// NOLINTEND(readability-identifier-naming,misc-use-anonymous-namespace) \ No newline at end of file +// NOLINTEND(readability-identifier-naming,misc-use-anonymous-namespace) From 0a7dcf30eaf438aa1ec72a9017cdb952df03f005 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 15 Nov 2025 02:34:43 -0800 Subject: [PATCH 130/226] build: update clang tooling and reformat --- .clang-format | 2 +- flake.lock | 6 +-- src/core/iconprovider.cpp | 4 +- src/core/logging.cpp | 9 ++-- src/core/model.cpp | 2 +- src/core/paths.cpp | 6 ++- src/core/scan.cpp | 3 +- src/core/scriptmodel.cpp | 4 +- src/crash/handler.cpp | 9 ++-- src/crash/interface.cpp | 3 +- src/dbus/properties.cpp | 6 ++- src/ipc/ipc.cpp | 3 +- src/launch/command.cpp | 9 ++-- src/launch/parsecommand.cpp | 52 ++++++++++++------- src/services/greetd/connection.cpp | 3 +- src/services/notifications/server.cpp | 6 ++- src/services/pipewire/defaults.cpp | 3 +- src/services/pipewire/node.cpp | 6 ++- src/services/polkit/agentimpl.cpp | 3 +- src/services/upower/device.cpp | 4 +- src/services/upower/powerprofiles.cpp | 17 +++--- src/wayland/buffer/dmabuf.cpp | 3 +- src/wayland/hyprland/surface/qml.cpp | 3 +- .../wlr_screencopy/wlr_screencopy.cpp | 3 +- src/wayland/session_lock.cpp | 4 +- .../session_lock/shell_integration.hpp | 4 +- src/wayland/toplevel_management/manager.hpp | 4 +- .../wlr_layershell/shell_integration.hpp | 4 +- src/wayland/wlr_layershell/surface.cpp | 12 ++--- src/widgets/marginwrapper.cpp | 4 +- src/window/popupwindow.cpp | 3 +- 31 files changed, 124 insertions(+), 80 deletions(-) diff --git a/.clang-format b/.clang-format index 610ee65..8ec602a 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,6 @@ AlignArrayOfStructures: None AlignAfterOpenBracket: BlockIndent -AllowShortBlocksOnASingleLine: Always +AllowShortBlocksOnASingleLine: Empty AllowShortCaseLabelsOnASingleLine: true AllowShortEnumsOnASingleLine: true AllowShortFunctionsOnASingleLine: All diff --git a/flake.lock b/flake.lock index 6971438..7470161 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1758690382, - "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=", + "lastModified": 1762977756, + "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e643668fd71b949c53f8626614b21ff71a07379d", + "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", "type": "github" }, "original": { diff --git a/src/core/iconprovider.cpp b/src/core/iconprovider.cpp index 99b423e..383f7e1 100644 --- a/src/core/iconprovider.cpp +++ b/src/core/iconprovider.cpp @@ -22,8 +22,8 @@ class PixmapCacheIconEngine: public QIconEngine { QIcon::Mode /*unused*/, QIcon::State /*unused*/ ) override { - qFatal( - ) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug."; + qFatal() + << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug."; } QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override { diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 3e3abca..5c809f6 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -361,7 +361,8 @@ void ThreadLogging::initFs() { auto* runDir = QsPaths::instance()->instanceRunDir(); if (!runDir) { - qCCritical(logLogging + qCCritical( + logLogging ) << "Could not start filesystem logging as the runtime directory could not be created."; return; } @@ -372,7 +373,8 @@ void ThreadLogging::initFs() { auto* detailedFile = new QFile(detailedPath); if (!file->open(QFile::ReadWrite | QFile::Truncate)) { - qCCritical(logLogging + qCCritical( + logLogging ) << "Could not start filesystem logger as the log file could not be created:" << path; delete file; @@ -383,7 +385,8 @@ void ThreadLogging::initFs() { // buffered by WriteBuffer if (!detailedFile->open(QFile::ReadWrite | QFile::Truncate | QFile::Unbuffered)) { - qCCritical(logLogging + qCCritical( + logLogging ) << "Could not start detailed filesystem logger as the log file could not be created:" << detailedPath; delete detailedFile; diff --git a/src/core/model.cpp b/src/core/model.cpp index c2b5d78..ddb616a 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -1,7 +1,7 @@ #include "model.hpp" -#include #include +#include #include QHash UntypedObjectModel::roleNames() const { diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 70e1bd1..55beb87 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -175,7 +175,8 @@ void QsPaths::linkRunDir() { auto* shellDir = this->shellRunDir(); if (!shellDir) { - qCCritical(logPaths + qCCritical( + logPaths ) << "Could not create by-id symlink as the shell runtime path could not be created."; } else { auto shellPath = shellDir->filePath(runDir->dirName()); @@ -378,7 +379,8 @@ void QsPaths::createLock() { qCDebug(logPaths) << "Created instance lock at" << path; } } else { - qCCritical(logPaths + qCCritical( + logPaths ) << "Could not create instance lock, as the instance runtime directory could not be created."; } } diff --git a/src/core/scan.cpp b/src/core/scan.cpp index d9606bc..9a7ee7e 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -38,7 +38,8 @@ void QmlScanner::scanDir(const QDir& dir) { for (auto& name: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { if (name == "qmldir") { - qCDebug(logQmlScanner + qCDebug( + logQmlScanner ) << "Found qmldir file, qmldir synthesization will be disabled for directory" << path; seenQmldir = true; diff --git a/src/core/scriptmodel.cpp b/src/core/scriptmodel.cpp index a8271e7..5407e2b 100644 --- a/src/core/scriptmodel.cpp +++ b/src/core/scriptmodel.cpp @@ -72,8 +72,8 @@ void ScriptModel::updateValuesUnique(const QVariantList& newValues) { do { ++iter; } while (iter != this->mValues.end() - && std::find_if(newIter, newValues.end(), eqPredicate(*iter)) == newValues.end() - ); + && std::find_if(newIter, newValues.end(), eqPredicate(*iter)) + == newValues.end()); auto index = static_cast(std::distance(this->mValues.begin(), iter)); auto startIndex = static_cast(std::distance(this->mValues.begin(), startIter)); diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 43a9792..0baa8e6 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -55,7 +55,8 @@ void CrashHandler::init() { this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC); if (this->d->minidumpFd == -1) { - qCCritical(logCrashHandler + qCCritical( + logCrashHandler ) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory."; createHandler(MinidumpDescriptor(".")); } else { @@ -71,7 +72,8 @@ void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); if (this->d->infoFd == -1) { - qCCritical(logCrashHandler + qCCritical( + logCrashHandler ) << "Failed to allocate instance info memfd, crash recovery will not work."; return; } @@ -79,7 +81,8 @@ void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { QFile file; if (!file.open(this->d->infoFd, QFile::ReadWrite)) { - qCCritical(logCrashHandler + qCCritical( + logCrashHandler ) << "Failed to open instance info memfd, crash recovery will not work."; } diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index c633440..326216a 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -66,7 +66,8 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) mainLayout->addSpacing(textHeight); if (qtVersionMatches) { - mainLayout->addWidget(new QLabel("Please open a bug report for this issue via github or email.") + mainLayout->addWidget( + new QLabel("Please open a bug report for this issue via github or email.") ); } else { mainLayout->addWidget(new QLabel( diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index d0f65d9..2c478ef 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -214,8 +214,10 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool co } } -void DBusPropertyGroup::tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) - const { +void DBusPropertyGroup::tryUpdateProperty( + DBusPropertyCore* property, + const QVariant& variant +) const { property->mExists = true; auto error = property->store(variant); diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index bf66801..0196359 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -36,7 +36,8 @@ void IpcServer::start() { auto path = run->filePath("ipc.sock"); new IpcServer(path); } else { - qCCritical(logIpc + qCCritical( + logIpc ) << "Could not start IPC server as the instance runtime path could not be created."; } } diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 81a9243..3a7a4b1 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -90,9 +90,9 @@ int locateConfigFile(CommandState& cmd, QString& path) { } if (!manifestPath.isEmpty()) { - qWarning( - ) << "Config manifests (manifest.conf) are deprecated and will be removed in a future " - "release."; + qWarning() + << "Config manifests (manifest.conf) are deprecated and will be removed in a future " + "release."; qWarning() << "Consider using symlinks to a subfolder of quickshell's XDG config dirs."; auto file = QFile(manifestPath); @@ -130,7 +130,8 @@ int locateConfigFile(CommandState& cmd, QString& path) { if (path.isEmpty()) { if (name == "default") { - qCCritical(logBare + qCCritical( + logBare ) << "Could not find \"default\" config directory or shell.qml in any valid config path."; } else { qCCritical(logBare) << "Could not find" << name diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index c12d9b9..0776f58 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -43,9 +43,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->excludes(path); group->add_option("-m,--manifest", state.config.manifest) - ->description("[DEPRECATED] Path to a quickshell manifest.\n" - "If a manifest is specified, configs named by -c will point to its entries.\n" - "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf") + ->description( + "[DEPRECATED] Path to a quickshell manifest.\n" + "If a manifest is specified, configs named by -c will point to its entries.\n" + "Defaults to $XDG_CONFIG_HOME/quickshell/manifest.conf" + ) ->envname("QS_MANIFEST") ->excludes(path); @@ -54,8 +56,10 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->description("Operate on the most recently launched instance instead of the oldest"); group->add_flag("--any-display", state.config.anyDisplay) - ->description("If passed, instances will not be filtered by the display connection they " - "were launched on."); + ->description( + "If passed, instances will not be filtered by the display connection they " + "were launched on." + ); } return group; @@ -79,9 +83,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* group = noGroup ? cmd : cmd->add_option_group(noDisplay ? "" : "Logging"); group->add_flag("--no-color", state.log.noColor) - ->description("Disables colored logging.\n" - "Colored logging can also be disabled by specifying a non empty value " - "for the NO_COLOR environment variable."); + ->description( + "Disables colored logging.\n" + "Colored logging can also be disabled by specifying a non empty value " + "for the NO_COLOR environment variable." + ); group->add_flag("--log-times", state.log.timestamp) ->description("Log timestamps with each message."); @@ -90,9 +96,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->description("Log rules to apply, in the format of QT_LOGGING_RULES."); group->add_flag("-v,--verbose", [&](size_t count) { state.log.verbosity = count; }) - ->description("Increases log verbosity.\n" - "-v will show INFO level internal logs.\n" - "-vv will show DEBUG level internal logs."); + ->description( + "Increases log verbosity.\n" + "-v will show INFO level internal logs.\n" + "-vv will show DEBUG level internal logs." + ); auto* hgroup = cmd->add_option_group(""); hgroup->add_flag("--no-detailed-logs", state.log.sparse); @@ -102,9 +110,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* group = cmd->add_option_group("Instance Selection"); group->add_option("-i,--id", state.instance.id) - ->description("The instance id to operate on.\n" - "You may also use a substring the id as long as it is unique, " - "for example \"abc\" will select \"abcdefg\"."); + ->description( + "The instance id to operate on.\n" + "You may also use a substring the id as long as it is unique, " + "for example \"abc\" will select \"abcdefg\"." + ); group->add_option("--pid", state.instance.pid) ->description("The process id of the instance to operate on."); @@ -161,9 +171,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { auto* sub = cli->add_subcommand("list", "List running quickshell instances."); auto* all = sub->add_flag("-a,--all", state.instance.all) - ->description("List all instances.\n" - "If unspecified, only instances of" - "the selected config will be listed."); + ->description( + "List all instances.\n" + "If unspecified, only instances of" + "the selected config will be listed." + ); sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); @@ -239,8 +251,10 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->allow_extra_args(); sub->add_flag("-s,--show", state.ipc.showOld) - ->description("Print information about a function or target if given, or all available " - "targets if not."); + ->description( + "Print information about a function or target if given, or all available " + "targets if not." + ); auto* instance = addInstanceSelection(sub); addConfigSelection(sub, true)->excludes(instance); diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index cb237a0..7130870 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -199,7 +199,8 @@ void GreetdConnection::onSocketReady() { // Special case this error in case a session was already running. // This cancels and restarts the session. if (errorType == "error" && desc == "a session is already being configured") { - qCDebug(logGreetd + qCDebug( + logGreetd ) << "A session was already in progress, cancelling it and starting a new one."; this->setActive(false); this->setActive(true); diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 3f2469d..d2b55d0 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -117,10 +117,12 @@ void NotificationServer::tryRegister() { if (success) { qCInfo(logNotifications) << "Registered notification server with dbus."; } else { - qCWarning(logNotifications + qCWarning( + logNotifications ) << "Could not register notification server at org.freedesktop.Notifications, presumably " "because one is already registered."; - qCWarning(logNotifications + qCWarning( + logNotifications ) << "Registration will be attempted again if the active service is unregistered."; } } diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index b3d8bfc..88a1dc1 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -201,7 +201,8 @@ bool PwDefaultTracker::setConfiguredDefault(const char* key, const QString& valu } if (!meta->hasSetPermission()) { - qCCritical(logDefaults + qCCritical( + logDefaults ) << "Cannot set default node as write+execute permissions are missing for" << meta; return false; diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 1eceab9..d454a46 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -172,7 +172,8 @@ void PwNode::initProps(const spa_dict* props) { this->device = this->registry->devices.value(id); if (this->device == nullptr) { - qCCritical(logNode + qCCritical( + logNode ) << this << "has a device.id property that does not corrospond to a device object. Id:" << id; } @@ -212,7 +213,8 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { auto id = QString::fromUtf8(routeDevice).toInt(&ok); if (!ok) { - qCCritical(logNode + qCCritical( + logNode ) << self << "has a card.profile.device property but the value is not an integer. Value:" << id; } diff --git a/src/services/polkit/agentimpl.cpp b/src/services/polkit/agentimpl.cpp index a11882d..85c62b7 100644 --- a/src/services/polkit/agentimpl.cpp +++ b/src/services/polkit/agentimpl.cpp @@ -143,7 +143,8 @@ void PolkitAgentImpl::activateAuthenticationRequest() { if (obj) identities.append(obj); } if (identities.isEmpty()) { - qCWarning(logPolkit + qCWarning( + logPolkit ) << "no supported identities available for authentication request, cancelling."; req->cancel("Error requesting authentication: no supported identities available."); delete req; diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index adf5923..63382ad 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -126,8 +126,8 @@ DBusDataTransform::fromWire(quint32 wire) { ); } -DBusResult DBusDataTransform::fromWire(quint32 wire -) { +DBusResult +DBusDataTransform::fromWire(quint32 wire) { if (wire >= UPowerDeviceType::Unknown && wire <= UPowerDeviceType::BluetoothGeneric) { return DBusResult(static_cast(wire)); } diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp index 43615ae..8fa91cc 100644 --- a/src/services/upower/powerprofiles.cpp +++ b/src/services/upower/powerprofiles.cpp @@ -66,7 +66,8 @@ PowerProfiles::PowerProfiles() { auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { - qCWarning(logPowerProfiles + qCWarning( + logPowerProfiles ) << "Could not connect to DBus. PowerProfiles services will not work."; } @@ -79,7 +80,8 @@ PowerProfiles::PowerProfiles() { ); if (!this->service->isValid()) { - qCDebug(logPowerProfiles + qCDebug( + logPowerProfiles ) << "PowerProfilesDaemon is not currently running, attempting to start it."; dbus::tryLaunchService(this, bus, "org.freedesktop.UPower.PowerProfiles", [this](bool success) { @@ -103,13 +105,15 @@ void PowerProfiles::init() { void PowerProfiles::setProfile(PowerProfile::Enum profile) { if (!this->properties.isConnected()) { - qCCritical(logPowerProfiles + qCCritical( + logPowerProfiles ) << "Cannot set power profile: power-profiles-daemon not accessible or not running"; return; } if (profile == PowerProfile::Performance && !this->bHasPerformanceProfile) { - qCCritical(logPowerProfiles + qCCritical( + logPowerProfiles ) << "Cannot request performance profile as it is not present for this device."; return; } else if (profile < PowerProfile::PowerSaver || profile > PowerProfile::Performance) { @@ -135,8 +139,9 @@ PowerProfilesQml::PowerProfilesQml(QObject* parent): QObject(parent) { return instance->bHasPerformanceProfile.value(); }); - this->bDegradationReason.setBinding([instance]() { return instance->bDegradationReason.value(); } - ); + this->bDegradationReason.setBinding([instance]() { + return instance->bDegradationReason.value(); + }); this->bHolds.setBinding([instance]() { return instance->bHolds.value(); }); } diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index b33e118..a5f219e 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -414,7 +414,8 @@ WlBuffer* LinuxDmabufManager::createDmabuf( if (modifiers.modifiers.isEmpty()) { if (!modifiers.implicit) { - qCritical(logDmabuf + qCritical( + logDmabuf ) << "Failed to create gbm_bo: format supports no implicit OR explicit modifiers."; return nullptr; } diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index b00ee33..c4f7d67 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -65,7 +65,8 @@ void HyprlandWindow::setOpacity(qreal opacity) { if (opacity == this->mOpacity) return; if (opacity < 0.0 || opacity > 1.0) { - qmlWarning(this + qmlWarning( + this ) << "Cannot set HyprlandWindow.opacity to a value larger than 1.0 or smaller than 0.0"; return; } diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp index 43a2543..927da8d 100644 --- a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -165,7 +165,8 @@ WlrScreencopyContext::OutputTransformQuery::~OutputTransformQuery() { if (this->isInitialized()) this->release(); } -void WlrScreencopyContext::OutputTransformQuery::setScreen(QtWaylandClient::QWaylandScreen* screen +void WlrScreencopyContext::OutputTransformQuery::setScreen( + QtWaylandClient::QWaylandScreen* screen ) { // cursed hack class QWaylandScreenReflector: public QtWaylandClient::QWaylandScreen { diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index 0ecf9ec..d5a3e53 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -79,8 +79,8 @@ void WlSessionLock::updateSurfaces(bool show, WlSessionLock* old) { auto* instance = qobject_cast(instanceObj); if (instance == nullptr) { - qWarning( - ) << "WlSessionLock.surface does not create a WlSessionLockSurface. Aborting lock."; + qWarning() + << "WlSessionLock.surface does not create a WlSessionLockSurface. Aborting lock."; if (instanceObj != nullptr) instanceObj->deleteLater(); this->unlock(); return; diff --git a/src/wayland/session_lock/shell_integration.hpp b/src/wayland/session_lock/shell_integration.hpp index d6f9175..b2e2891 100644 --- a/src/wayland/session_lock/shell_integration.hpp +++ b/src/wayland/session_lock/shell_integration.hpp @@ -8,6 +8,6 @@ class QSWaylandSessionLockIntegration: public QtWaylandClient::QWaylandShellIntegration { public: bool initialize(QtWaylandClient::QWaylandDisplay* /* display */) override { return true; } - QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window - ) override; + QtWaylandClient::QWaylandShellSurface* + createShellSurface(QtWaylandClient::QWaylandWindow* window) override; }; diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp index 4b906a5..83e3e09 100644 --- a/src/wayland/toplevel_management/manager.hpp +++ b/src/wayland/toplevel_management/manager.hpp @@ -33,8 +33,8 @@ signals: protected: explicit ToplevelManager(); - void zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel - ) override; + void + zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel) override; private slots: void onToplevelReady(); diff --git a/src/wayland/wlr_layershell/shell_integration.hpp b/src/wayland/wlr_layershell/shell_integration.hpp index e92b7c6..93cda01 100644 --- a/src/wayland/wlr_layershell/shell_integration.hpp +++ b/src/wayland/wlr_layershell/shell_integration.hpp @@ -15,8 +15,8 @@ public: ~LayerShellIntegration() override; Q_DISABLE_COPY_MOVE(LayerShellIntegration); - QtWaylandClient::QWaylandShellSurface* createShellSurface(QtWaylandClient::QWaylandWindow* window - ) override; + QtWaylandClient::QWaylandShellSurface* + createShellSurface(QtWaylandClient::QWaylandWindow* window) override; }; } // namespace qs::wayland::layershell diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 26d7558..3c71ff9 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -30,8 +30,8 @@ namespace qs::wayland::layershell { namespace { -[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer -) noexcept { +[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer +toWaylandLayer(const WlrLayer::Enum& layer) noexcept { switch (layer) { case WlrLayer::Background: return QtWayland::zwlr_layer_shell_v1::layer_background; case WlrLayer::Bottom: return QtWayland::zwlr_layer_shell_v1::layer_bottom; @@ -42,8 +42,8 @@ namespace { return QtWayland::zwlr_layer_shell_v1::layer_top; } -[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors -) noexcept { +[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor +toWaylandAnchors(const Anchors& anchors) noexcept { quint32 wl = 0; if (anchors.mLeft) wl |= QtWayland::zwlr_layer_surface_v1::anchor_left; if (anchors.mRight) wl |= QtWayland::zwlr_layer_surface_v1::anchor_right; @@ -146,8 +146,8 @@ LayerSurface::LayerSurface(LayerShellIntegration* shell, QtWaylandClient::QWayla if (waylandScreen != nullptr) { output = waylandScreen->output(); } else { - qWarning( - ) << "Layershell screen does not corrospond to a real screen. Letting the compositor pick."; + qWarning() + << "Layershell screen does not corrospond to a real screen. Letting the compositor pick."; } } diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp index 9960bba..b7d410c 100644 --- a/src/widgets/marginwrapper.cpp +++ b/src/widgets/marginwrapper.cpp @@ -12,8 +12,8 @@ namespace qs::widgets { MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(parent) { this->bTopMargin.setBinding([this] { return this->bExtraMargin - + (this->bOverrides.value().testFlag(TopMargin) ? this->bTopMarginOverride : this->bMargin - ); + + (this->bOverrides.value().testFlag(TopMargin) ? this->bTopMarginOverride + : this->bMargin); }); this->bBottomMargin.setBinding([this] { diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index ec2be7e..a1ae448 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -67,7 +67,8 @@ void ProxyPopupWindow::updateTransientParent() { void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { - qmlWarning(this + qmlWarning( + this ) << "Cannot set screen of popup window, as that is controlled by the parent window"; } From fdbb86a06acd6d1af6049a319c87a5c0badc22dc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 15 Nov 2025 17:41:54 -0800 Subject: [PATCH 131/226] core/model: fix recursion in emptyInstance --- src/core/model.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/model.cpp b/src/core/model.cpp index ddb616a..47ef060 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -9,5 +9,6 @@ QHash UntypedObjectModel::roleNames() const { } UntypedObjectModel* UntypedObjectModel::emptyInstance() { - return ObjectModel::emptyInstance(); + static auto* instance = new ObjectModel(nullptr); + return instance; } From ab494dd9820698bc39d85d30c3a9cd4915f9609d Mon Sep 17 00:00:00 2001 From: Ala Alkhafaji <3akevdev@gmail.com> Date: Thu, 13 Nov 2025 19:43:11 +0100 Subject: [PATCH 132/226] i3/ipc: implement IPC listener to receive arbitrary events --- src/x11/i3/ipc/CMakeLists.txt | 2 + src/x11/i3/ipc/connection.cpp | 461 ++++++---------------------------- src/x11/i3/ipc/connection.hpp | 82 +----- src/x11/i3/ipc/controller.cpp | 367 +++++++++++++++++++++++++++ src/x11/i3/ipc/controller.hpp | 94 +++++++ src/x11/i3/ipc/listener.cpp | 49 ++++ src/x11/i3/ipc/listener.hpp | 68 +++++ src/x11/i3/ipc/monitor.cpp | 4 +- src/x11/i3/ipc/monitor.hpp | 7 +- src/x11/i3/ipc/qml.cpp | 31 +-- src/x11/i3/ipc/qml.hpp | 1 + src/x11/i3/ipc/workspace.cpp | 4 +- src/x11/i3/ipc/workspace.hpp | 5 +- src/x11/i3/module.md | 2 + 14 files changed, 693 insertions(+), 484 deletions(-) create mode 100644 src/x11/i3/ipc/controller.cpp create mode 100644 src/x11/i3/ipc/controller.hpp create mode 100644 src/x11/i3/ipc/listener.cpp create mode 100644 src/x11/i3/ipc/listener.hpp diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt index 27a4484..c228ae3 100644 --- a/src/x11/i3/ipc/CMakeLists.txt +++ b/src/x11/i3/ipc/CMakeLists.txt @@ -3,6 +3,8 @@ qt_add_library(quickshell-i3-ipc STATIC qml.cpp workspace.cpp monitor.cpp + controller.cpp + listener.cpp ) qt_add_qml_module(quickshell-i3-ipc diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index c5d2db2..b765ebc 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -1,4 +1,4 @@ -#include +#include "connection.hpp" #include #include #include @@ -23,11 +23,6 @@ #include #include "../../../core/logcat.hpp" -#include "../../../core/model.hpp" -#include "../../../core/qmlscreen.hpp" -#include "connection.hpp" -#include "monitor.hpp" -#include "workspace.hpp" namespace qs::i3::ipc { @@ -36,6 +31,69 @@ QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); } // namespace +QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } +QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } + +EventCode I3IpcEvent::intToEvent(quint32 raw) { + if ((EventCode::Workspace <= raw && raw <= EventCode::Input) + || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) + { + return static_cast(raw); + } else { + return EventCode::Unknown; + } +} + +QString I3IpcEvent::eventToString(EventCode event) { + switch (event) { + case EventCode::RunCommand: return "run_command"; break; + case EventCode::GetWorkspaces: return "get_workspaces"; break; + case EventCode::Subscribe: return "subscribe"; break; + case EventCode::GetOutputs: return "get_outputs"; break; + case EventCode::GetTree: return "get_tree"; break; + + case EventCode::Output: return "output"; break; + case EventCode::Workspace: return "workspace"; break; + case EventCode::Mode: return "mode"; break; + case EventCode::Window: return "window"; break; + case EventCode::BarconfigUpdate: return "barconfig_update"; break; + case EventCode::Binding: return "binding"; break; + case EventCode::Shutdown: return "shutdown"; break; + case EventCode::Tick: return "tick"; break; + case EventCode::BarStateUpdate: return "bar_state_update"; break; + case EventCode::Input: return "input"; break; + + default: return "unknown"; break; + } +} + +I3Ipc::I3Ipc(const QList& events): mEvents(events) { + auto sock = qEnvironmentVariable("I3SOCK"); + + if (sock.isEmpty()) { + qCWarning(logI3Ipc) << "$I3SOCK is unset. Trying $SWAYSOCK."; + + sock = qEnvironmentVariable("SWAYSOCK"); + + if (sock.isEmpty()) { + qCWarning(logI3Ipc) << "$SWAYSOCK and I3SOCK are unset. Cannot connect to socket."; + return; + } + } + + this->mSocketPath = sock; + + // clang-format off + QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); + QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); + QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); + // clang-format on + + this->liveEventSocketDs.setDevice(&this->liveEventSocket); + this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); +} + void I3Ipc::makeRequest(const QByteArray& request) { if (!this->valid) { qCWarning(logI3IpcEvents) << "IPC connection is not open, ignoring request."; @@ -60,50 +118,13 @@ QByteArray I3Ipc::buildRequestMessage(EventCode cmd, const QByteArray& payload) return MAGIC.data() + len + type + payload; } -I3Ipc::I3Ipc() { - auto sock = qEnvironmentVariable("I3SOCK"); - - if (sock.isEmpty()) { - qCWarning(logI3Ipc) << "$I3SOCK is unset. Trying $SWAYSOCK."; - - sock = qEnvironmentVariable("SWAYSOCK"); - - if (sock.isEmpty()) { - qCWarning(logI3Ipc) << "$SWAYSOCK and I3SOCK are unset. Cannot connect to socket."; - return; - } - } - - this->bFocusedWorkspace.setBinding([this]() -> I3Workspace* { - if (!this->bFocusedMonitor) return nullptr; - return this->bFocusedMonitor->bindableActiveWorkspace().value(); - }); - - this->mSocketPath = sock; - - // clang-format off - QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); - QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); - QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); - QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); - // clang-format on - - this->liveEventSocketDs.setDevice(&this->liveEventSocket); - this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); - - this->liveEventSocket.connectToServer(this->mSocketPath); -} - void I3Ipc::subscribe() { - auto payload = QByteArray(R"(["workspace","output"])"); + auto jsonArray = QJsonArray::fromStringList(this->mEvents); + auto jsonDoc = QJsonDocument(jsonArray); + auto payload = jsonDoc.toJson(QJsonDocument::Compact); auto message = I3Ipc::buildRequestMessage(EventCode::Subscribe, payload); this->makeRequest(message); - - // Workspaces must be refreshed before monitors or no focus will be - // detected on launch. - this->refreshWorkspaces(); - this->refreshMonitors(); } void I3Ipc::eventSocketReady() { @@ -111,15 +132,16 @@ void I3Ipc::eventSocketReady() { this->event.mCode = type; this->event.mData = data; - this->onEvent(&this->event); emit this->rawEvent(&this->event); } } +void I3Ipc::connect() { this->liveEventSocket.connectToServer(this->mSocketPath); } + void I3Ipc::reconnectIPC() { qCWarning(logI3Ipc) << "Fatal IPC error occured, recreating connection"; this->liveEventSocket.disconnectFromServer(); - this->liveEventSocket.connectToServer(this->mSocketPath); + this->connect(); } QVector I3Ipc::parseResponse() { @@ -193,347 +215,4 @@ void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { QString I3Ipc::socketPath() const { return this->mSocketPath; } -void I3Ipc::setFocusedMonitor(I3Monitor* monitor) { - auto* oldMonitor = this->bFocusedMonitor.value(); - if (monitor == oldMonitor) return; - - if (oldMonitor != nullptr) { - QObject::disconnect(oldMonitor, nullptr, this, nullptr); - } - - if (monitor != nullptr) { - QObject::connect(monitor, &QObject::destroyed, this, &I3Ipc::onFocusedMonitorDestroyed); - } - - this->bFocusedMonitor = monitor; -} - -void I3Ipc::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; } - -I3Ipc* I3Ipc::instance() { - static I3Ipc* instance = nullptr; // NOLINT - - if (instance == nullptr) { - instance = new I3Ipc(); - } - - return instance; -} - -void I3Ipc::refreshWorkspaces() { - this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); -} - -void I3Ipc::handleGetWorkspacesEvent(I3IpcEvent* event) { - auto data = event->mData; - - auto workspaces = data.array(); - - const auto& mList = this->mWorkspaces.valueList(); - auto names = QVector(); - - qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; - for (auto entry: workspaces) { - auto object = entry.toObject().toVariantMap(); - auto name = object["name"].toString(); - - auto workspaceIter = std::ranges::find_if(mList, [name](I3Workspace* m) { - return m->bindableName().value() == name; - }); - - auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; - auto existed = workspace != nullptr; - - if (workspace == nullptr) { - workspace = new I3Workspace(this); - } - - workspace->updateFromObject(object); - - if (!existed) { - this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces); - } - - if (!this->bFocusedWorkspace && object.value("focused").value()) { - this->bFocusedMonitor = workspace->bindableMonitor().value(); - } - - names.push_back(name); - } - - auto removedWorkspaces = QVector(); - - for (auto* workspace: mList) { - if (!names.contains(workspace->bindableName().value())) { - removedWorkspaces.push_back(workspace); - } - } - - qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; - - for (auto* workspace: removedWorkspaces) { - this->mWorkspaces.removeObject(workspace); - delete workspace; - } -} - -void I3Ipc::refreshMonitors() { - this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); -} - -void I3Ipc::handleGetOutputsEvent(I3IpcEvent* event) { - auto data = event->mData; - - auto monitors = data.array(); - const auto& mList = this->mMonitors.valueList(); - auto names = QVector(); - - qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; - - for (auto elem: monitors) { - auto object = elem.toObject().toVariantMap(); - auto name = object["name"].toString(); - - auto monitorIter = std::ranges::find_if(mList, [name](I3Monitor* m) { - return m->bindableName().value() == name; - }); - - auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; - auto existed = monitor != nullptr; - - if (monitor == nullptr) { - monitor = new I3Monitor(this); - } - - monitor->updateFromObject(object); - - if (monitor->bindableFocused().value()) { - this->setFocusedMonitor(monitor); - } - - if (!existed) { - this->mMonitors.insertObject(monitor); - } - - names.push_back(name); - } - - auto removedMonitors = QVector(); - - for (auto* monitor: mList) { - if (!names.contains(monitor->bindableName().value())) { - removedMonitors.push_back(monitor); - } - } - - qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; - - for (auto* monitor: removedMonitors) { - this->mMonitors.removeObject(monitor); - delete monitor; - } -} - -void I3Ipc::onEvent(I3IpcEvent* event) { - switch (event->mCode) { - case EventCode::Workspace: this->handleWorkspaceEvent(event); return; - case EventCode::Output: - /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves - qCInfo(logI3Ipc) << "Refreshing Monitors..."; - this->refreshMonitors(); - return; - case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; - case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; - case EventCode::GetWorkspaces: this->handleGetWorkspacesEvent(event); return; - case EventCode::RunCommand: I3Ipc::handleRunCommand(event); return; - case EventCode::Unknown: - qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); - return; - default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); - } -} - -void I3Ipc::handleRunCommand(I3IpcEvent* event) { - for (auto r: event->mData.array()) { - auto obj = r.toObject(); - const bool success = obj["success"].toBool(); - - if (!success) { - const QString error = obj["error"].toString(); - qCWarning(logI3Ipc) << "Error occured while running command:" << error; - } - } -} - -void I3Ipc::handleWorkspaceEvent(I3IpcEvent* event) { - // If a workspace doesn't exist, and is being switch to, no focus change event is emited, - // only the init one, which does not contain the previously focused workspace - auto change = event->mData["change"]; - - if (change == "init") { - qCInfo(logI3IpcEvents) << "New workspace has been created"; - - auto workspaceData = event->mData["current"]; - - auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); - auto existed = workspace != nullptr; - - if (!existed) { - workspace = new I3Workspace(this); - } - - if (workspaceData.isObject()) { - workspace->updateFromObject(workspaceData.toObject().toVariantMap()); - } - - if (!existed) { - this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces); - qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "to list"; - } - } else if (change == "focus") { - auto oldData = event->mData["old"]; - auto newData = event->mData["current"]; - auto oldName = oldData["name"].toString(); - auto newName = newData["name"].toString(); - - qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; - - if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { - oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); - } - - auto* newWorkspace = this->findWorkspaceByName(newName); - - if (newWorkspace == nullptr) { - newWorkspace = new I3Workspace(this); - } - - newWorkspace->updateFromObject(newData.toObject().toVariantMap()); - - if (newWorkspace->bindableMonitor().value()) { - auto* monitor = newWorkspace->bindableMonitor().value(); - monitor->setFocusedWorkspace(newWorkspace); - this->bFocusedMonitor = monitor; - } - } else if (change == "empty") { - auto name = event->mData["current"]["name"].toString(); - - auto* oldWorkspace = this->findWorkspaceByName(name); - - if (oldWorkspace != nullptr) { - qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; - - if (this->bFocusedWorkspace == oldWorkspace) { - this->bFocusedMonitor->setFocusedWorkspace(nullptr); - } - - this->workspaces()->removeObject(oldWorkspace); - - delete oldWorkspace; - } else { - qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; - } - } else if (change == "move" || change == "rename" || change == "urgent") { - auto name = event->mData["current"]["name"].toString(); - - auto* workspace = this->findWorkspaceByName(name); - - if (workspace != nullptr) { - auto data = event->mData["current"].toObject().toVariantMap(); - - workspace->updateFromObject(data); - } else { - qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; - } - } else if (change == "reload") { - qCInfo(logI3Ipc) << "Refreshing Workspaces..."; - this->refreshWorkspaces(); - } -} - -I3Monitor* I3Ipc::monitorFor(QuickshellScreenInfo* screen) { - if (screen == nullptr) return nullptr; - - return this->findMonitorByName(screen->name()); -} - -I3Workspace* I3Ipc::findWorkspaceByID(qint32 id) { - auto list = this->mWorkspaces.valueList(); - auto workspaceIter = - std::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; }); - - return workspaceIter == list.end() ? nullptr : *workspaceIter; -} - -I3Workspace* I3Ipc::findWorkspaceByName(const QString& name) { - auto list = this->mWorkspaces.valueList(); - auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) { - return m->bindableName().value() == name; - }); - - return workspaceIter == list.end() ? nullptr : *workspaceIter; -} - -I3Monitor* I3Ipc::findMonitorByName(const QString& name, bool createIfMissing) { - auto list = this->mMonitors.valueList(); - auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) { - return m->bindableName().value() == name; - }); - - if (monitorIter != list.end()) { - return *monitorIter; - } else if (createIfMissing) { - qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init"; - auto* monitor = new I3Monitor(this); - monitor->updateInitial(name); - this->mMonitors.insertObject(monitor); - return monitor; - } else { - return nullptr; - } -} - -ObjectModel* I3Ipc::monitors() { return &this->mMonitors; } -ObjectModel* I3Ipc::workspaces() { return &this->mWorkspaces; } - -bool I3Ipc::compareWorkspaces(I3Workspace* a, I3Workspace* b) { - return a->bindableNumber().value() > b->bindableNumber().value(); -} - -QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } -QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } - -EventCode I3IpcEvent::intToEvent(quint32 raw) { - if ((EventCode::Workspace <= raw && raw <= EventCode::Input) - || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) - { - return static_cast(raw); - } else { - return EventCode::Unknown; - } -} - -QString I3IpcEvent::eventToString(EventCode event) { - switch (event) { - case EventCode::RunCommand: return "run_command"; break; - case EventCode::GetWorkspaces: return "get_workspaces"; break; - case EventCode::Subscribe: return "subscribe"; break; - case EventCode::GetOutputs: return "get_outputs"; break; - case EventCode::GetTree: return "get_tree"; break; - - case EventCode::Output: return "output"; break; - case EventCode::Workspace: return "workspace"; break; - case EventCode::Mode: return "mode"; break; - case EventCode::Window: return "window"; break; - case EventCode::BarconfigUpdate: return "barconfig_update"; break; - case EventCode::Binding: return "binding"; break; - case EventCode::Shutdown: return "shutdown"; break; - case EventCode::Tick: return "tick"; break; - case EventCode::BarStateUpdate: return "bar_state_update"; break; - case EventCode::Input: return "input"; break; - - default: return "unknown"; break; - } -} - } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/connection.hpp b/src/x11/i3/ipc/connection.hpp index af480c5..6100f7e 100644 --- a/src/x11/i3/ipc/connection.hpp +++ b/src/x11/i3/ipc/connection.hpp @@ -1,28 +1,14 @@ #pragma once #include +#include #include -#include #include #include -#include -#include #include #include #include -#include "../../../core/model.hpp" -#include "../../../core/qmlscreen.hpp" - -namespace qs::i3::ipc { - -class I3Workspace; -class I3Monitor; -} // namespace qs::i3::ipc - -Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); -Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); - namespace qs::i3::ipc { constexpr std::string MAGIC = "i3-ipc"; @@ -54,9 +40,7 @@ using Event = std::tuple; class I3IpcEvent: public QObject { Q_OBJECT; - /// The name of the event Q_PROPERTY(QString type READ type CONSTANT); - /// The payload of the event in JSON format. Q_PROPERTY(QString data READ data CONSTANT); QML_NAMED_ELEMENT(I3Event); @@ -75,90 +59,48 @@ public: static QString eventToString(EventCode event); }; +/// Base class that manages the IPC socket, subscriptions and event reception. class I3Ipc: public QObject { Q_OBJECT; public: - static I3Ipc* instance(); + explicit I3Ipc(const QList& events); [[nodiscard]] QString socketPath() const; void makeRequest(const QByteArray& request); void dispatch(const QString& payload); + void connect(); - static QByteArray buildRequestMessage(EventCode cmd, const QByteArray& payload = QByteArray()); - - I3Workspace* findWorkspaceByName(const QString& name); - I3Monitor* findMonitorByName(const QString& name, bool createIfMissing = false); - I3Workspace* findWorkspaceByID(qint32 id); - - void setFocusedMonitor(I3Monitor* monitor); - - void refreshWorkspaces(); - void refreshMonitors(); - - I3Monitor* monitorFor(QuickshellScreenInfo* screen); - - [[nodiscard]] QBindable bindableFocusedMonitor() const { - return &this->bFocusedMonitor; - }; - - [[nodiscard]] QBindable bindableFocusedWorkspace() const { - return &this->bFocusedWorkspace; - }; - - [[nodiscard]] ObjectModel* monitors(); - [[nodiscard]] ObjectModel* workspaces(); + [[nodiscard]] QByteArray static buildRequestMessage( + EventCode cmd, + const QByteArray& payload = QByteArray() + ); signals: void connected(); void rawEvent(I3IpcEvent* event); - void focusedWorkspaceChanged(); - void focusedMonitorChanged(); -private slots: +protected slots: void eventSocketError(QLocalSocket::LocalSocketError error) const; void eventSocketStateChanged(QLocalSocket::LocalSocketState state); void eventSocketReady(); void subscribe(); - void onFocusedMonitorDestroyed(); - -private: - explicit I3Ipc(); - - void onEvent(I3IpcEvent* event); - - void handleWorkspaceEvent(I3IpcEvent* event); - void handleGetWorkspacesEvent(I3IpcEvent* event); - void handleGetOutputsEvent(I3IpcEvent* event); - static void handleRunCommand(I3IpcEvent* event); - static bool compareWorkspaces(I3Workspace* a, I3Workspace* b); - +protected: void reconnectIPC(); - QVector> parseResponse(); QLocalSocket liveEventSocket; QDataStream liveEventSocketDs; QString mSocketPath; - bool valid = false; - ObjectModel mMonitors {this}; - ObjectModel mWorkspaces {this}; - I3IpcEvent event {this}; - Q_OBJECT_BINDABLE_PROPERTY(I3Ipc, I3Monitor*, bFocusedMonitor, &I3Ipc::focusedMonitorChanged); - - Q_OBJECT_BINDABLE_PROPERTY( - I3Ipc, - I3Workspace*, - bFocusedWorkspace, - &I3Ipc::focusedWorkspaceChanged - ); +private: + QList mEvents; }; } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/controller.cpp b/src/x11/i3/ipc/controller.cpp new file mode 100644 index 0000000..1a08c63 --- /dev/null +++ b/src/x11/i3/ipc/controller.cpp @@ -0,0 +1,367 @@ +#include "controller.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/logcat.hpp" +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::i3::ipc { + +namespace { +QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); +QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); +} // namespace + +I3IpcController::I3IpcController(): I3Ipc({"workspace", "output"}) { + // bind focused workspace to focused monitor's active workspace + this->bFocusedWorkspace.setBinding([this]() -> I3Workspace* { + if (!this->bFocusedMonitor) return nullptr; + return this->bFocusedMonitor->bindableActiveWorkspace().value(); + }); + + // clang-format off + QObject::connect(this, &I3Ipc::rawEvent, this, &I3IpcController::onEvent); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3IpcController::onConnected); + // clang-format on +} + +void I3IpcController::onConnected() { + // Workspaces must be refreshed before monitors or no focus will be + // detected on launch. + this->refreshWorkspaces(); + this->refreshMonitors(); +} + +void I3IpcController::setFocusedMonitor(I3Monitor* monitor) { + auto* oldMonitor = this->bFocusedMonitor.value(); + if (monitor == oldMonitor) return; + + if (oldMonitor != nullptr) { + QObject::disconnect(oldMonitor, nullptr, this, nullptr); + } + + if (monitor != nullptr) { + QObject::connect( + monitor, + &QObject::destroyed, + this, + &I3IpcController::onFocusedMonitorDestroyed + ); + } + + this->bFocusedMonitor = monitor; +} + +void I3IpcController::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; } + +I3IpcController* I3IpcController::instance() { + static I3IpcController* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new I3IpcController(); + instance->connect(); + } + + return instance; +} + +void I3IpcController::refreshWorkspaces() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); +} + +void I3IpcController::handleGetWorkspacesEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto workspaces = data.array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; + for (auto entry: workspaces) { + auto object = entry.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto workspaceIter = std::ranges::find_if(mList, [name](I3Workspace* m) { + return m->bindableName().value() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new I3Workspace(this); + } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObjectSorted(workspace, &I3IpcController::compareWorkspaces); + } + + if (!this->bFocusedWorkspace && object.value("focused").value()) { + this->bFocusedMonitor = workspace->bindableMonitor().value(); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->bindableName().value())) { + removedWorkspaces.push_back(workspace); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } +} + +void I3IpcController::refreshMonitors() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); +} + +void I3IpcController::handleGetOutputsEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto monitors = data.array(); + const auto& mList = this->mMonitors.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; + + for (auto elem: monitors) { + auto object = elem.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto monitorIter = std::ranges::find_if(mList, [name](I3Monitor* m) { + return m->bindableName().value() == name; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new I3Monitor(this); + } + + monitor->updateFromObject(object); + + if (monitor->bindableFocused().value()) { + this->setFocusedMonitor(monitor); + } + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + names.push_back(name); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!names.contains(monitor->bindableName().value())) { + removedMonitors.push_back(monitor); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + delete monitor; + } +} + +void I3IpcController::onEvent(I3IpcEvent* event) { + switch (event->mCode) { + case EventCode::Workspace: this->handleWorkspaceEvent(event); return; + case EventCode::Output: + /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves + qCInfo(logI3Ipc) << "Refreshing Monitors..."; + this->refreshMonitors(); + return; + case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; + case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; + case EventCode::GetWorkspaces: this->handleGetWorkspacesEvent(event); return; + case EventCode::RunCommand: I3IpcController::handleRunCommand(event); return; + case EventCode::Unknown: + qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); + return; + default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); + } +} + +void I3IpcController::handleRunCommand(I3IpcEvent* event) { + for (auto r: event->mData.array()) { + auto obj = r.toObject(); + const bool success = obj["success"].toBool(); + + if (!success) { + const QString error = obj["error"].toString(); + qCWarning(logI3Ipc) << "Error occured while running command:" << error; + } + } +} + +void I3IpcController::handleWorkspaceEvent(I3IpcEvent* event) { + // If a workspace doesn't exist, and is being switch to, no focus change event is emited, + // only the init one, which does not contain the previously focused workspace + auto change = event->mData["change"]; + + if (change == "init") { + qCInfo(logI3IpcEvents) << "New workspace has been created"; + + auto workspaceData = event->mData["current"]; + + auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); + auto existed = workspace != nullptr; + + if (!existed) { + workspace = new I3Workspace(this); + } + + if (workspaceData.isObject()) { + workspace->updateFromObject(workspaceData.toObject().toVariantMap()); + } + + if (!existed) { + this->mWorkspaces.insertObjectSorted(workspace, &I3IpcController::compareWorkspaces); + qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "to list"; + } + } else if (change == "focus") { + auto oldData = event->mData["old"]; + auto newData = event->mData["current"]; + auto oldName = oldData["name"].toString(); + auto newName = newData["name"].toString(); + + qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; + + if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { + oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); + } + + auto* newWorkspace = this->findWorkspaceByName(newName); + + if (newWorkspace == nullptr) { + newWorkspace = new I3Workspace(this); + } + + newWorkspace->updateFromObject(newData.toObject().toVariantMap()); + + if (newWorkspace->bindableMonitor().value()) { + auto* monitor = newWorkspace->bindableMonitor().value(); + monitor->setFocusedWorkspace(newWorkspace); + this->bFocusedMonitor = monitor; + } + } else if (change == "empty") { + auto name = event->mData["current"]["name"].toString(); + + auto* oldWorkspace = this->findWorkspaceByName(name); + + if (oldWorkspace != nullptr) { + qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; + + if (this->bFocusedWorkspace == oldWorkspace) { + this->bFocusedMonitor->setFocusedWorkspace(nullptr); + } + + this->workspaces()->removeObject(oldWorkspace); + + delete oldWorkspace; + } else { + qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; + } + } else if (change == "move" || change == "rename" || change == "urgent") { + auto name = event->mData["current"]["name"].toString(); + + auto* workspace = this->findWorkspaceByName(name); + + if (workspace != nullptr) { + auto data = event->mData["current"].toObject().toVariantMap(); + + workspace->updateFromObject(data); + } else { + qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; + } + } else if (change == "reload") { + qCInfo(logI3Ipc) << "Refreshing Workspaces..."; + this->refreshWorkspaces(); + } +} + +I3Monitor* I3IpcController::monitorFor(QuickshellScreenInfo* screen) { + if (screen == nullptr) return nullptr; + + return this->findMonitorByName(screen->name()); +} + +I3Workspace* I3IpcController::findWorkspaceByID(qint32 id) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = + std::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Workspace* I3IpcController::findWorkspaceByName(const QString& name) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) { + return m->bindableName().value() == name; + }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Monitor* I3IpcController::findMonitorByName(const QString& name, bool createIfMissing) { + auto list = this->mMonitors.valueList(); + auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) { + return m->bindableName().value() == name; + }); + + if (monitorIter != list.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init"; + auto* monitor = new I3Monitor(this); + monitor->updateInitial(name); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +ObjectModel* I3IpcController::monitors() { return &this->mMonitors; } +ObjectModel* I3IpcController::workspaces() { return &this->mWorkspaces; } + +bool I3IpcController::compareWorkspaces(I3Workspace* a, I3Workspace* b) { + return a->bindableNumber().value() > b->bindableNumber().value(); +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/controller.hpp b/src/x11/i3/ipc/controller.hpp new file mode 100644 index 0000000..464f6f6 --- /dev/null +++ b/src/x11/i3/ipc/controller.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +class I3Workspace; +class I3Monitor; +} // namespace qs::i3::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); + +namespace qs::i3::ipc { + +/// I3/Sway IPC controller that manages workspaces and monitors +class I3IpcController: public I3Ipc { + Q_OBJECT; + +public: + static I3IpcController* instance(); + + I3Workspace* findWorkspaceByName(const QString& name); + I3Monitor* findMonitorByName(const QString& name, bool createIfMissing = false); + I3Workspace* findWorkspaceByID(qint32 id); + + void setFocusedMonitor(I3Monitor* monitor); + + void refreshWorkspaces(); + void refreshMonitors(); + + I3Monitor* monitorFor(QuickshellScreenInfo* screen); + + [[nodiscard]] QBindable bindableFocusedMonitor() const { + return &this->bFocusedMonitor; + }; + + [[nodiscard]] QBindable bindableFocusedWorkspace() const { + return &this->bFocusedWorkspace; + }; + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + +signals: + void focusedWorkspaceChanged(); + void focusedMonitorChanged(); + +private slots: + void onFocusedMonitorDestroyed(); + + void onEvent(I3IpcEvent* event); + void onConnected(); + +private: + explicit I3IpcController(); + + void handleWorkspaceEvent(I3IpcEvent* event); + void handleGetWorkspacesEvent(I3IpcEvent* event); + void handleGetOutputsEvent(I3IpcEvent* event); + static void handleRunCommand(I3IpcEvent* event); + static bool compareWorkspaces(I3Workspace* a, I3Workspace* b); + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + + Q_OBJECT_BINDABLE_PROPERTY( + I3IpcController, + I3Monitor*, + bFocusedMonitor, + &I3IpcController::focusedMonitorChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + I3IpcController, + I3Workspace*, + bFocusedWorkspace, + &I3IpcController::focusedWorkspaceChanged + ); +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/listener.cpp b/src/x11/i3/ipc/listener.cpp new file mode 100644 index 0000000..aa7719c --- /dev/null +++ b/src/x11/i3/ipc/listener.cpp @@ -0,0 +1,49 @@ +#include "listener.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::i3::ipc { + +I3IpcListener::~I3IpcListener() { this->freeI3Ipc(); } + +void I3IpcListener::onPostReload() { this->startListening(); } + +QList I3IpcListener::subscriptions() const { return this->mSubscriptions; } +void I3IpcListener::setSubscriptions(QList subscriptions) { + if (this->mSubscriptions == subscriptions) return; + this->mSubscriptions = std::move(subscriptions); + + emit this->subscriptionsChanged(); + this->startListening(); +} + +void I3IpcListener::startListening() { + this->freeI3Ipc(); + if (this->mSubscriptions.isEmpty()) return; + + this->i3Ipc = new I3Ipc(this->mSubscriptions); + + // clang-format off + QObject::connect(this->i3Ipc, &I3Ipc::rawEvent, this, &I3IpcListener::receiveEvent); + // clang-format on + + this->i3Ipc->connect(); +} + +void I3IpcListener::receiveEvent(I3IpcEvent* event) { emit this->ipcEvent(event); } + +void I3IpcListener::freeI3Ipc() { + delete this->i3Ipc; + this->i3Ipc = nullptr; +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/listener.hpp b/src/x11/i3/ipc/listener.hpp new file mode 100644 index 0000000..9cb40bb --- /dev/null +++ b/src/x11/i3/ipc/listener.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include // NOLINT +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/doc.hpp" +#include "../../../core/generation.hpp" +#include "../../../core/qmlglobal.hpp" +#include "../../../core/reload.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +///! I3/Sway IPC event listener +/// #### Example +/// ```qml +/// I3IpcListener { +/// subscriptions: ["input"] +/// onIpcEvent: function (event) { +/// handleInputEvent(event.data) +/// } +/// } +/// ``` +class I3IpcListener: public PostReloadHook { + Q_OBJECT; + // clang-format off + /// List of [I3/Sway events](https://man.archlinux.org/man/sway-ipc.7.en#EVENTS) to subscribe to. + Q_PROPERTY(QList subscriptions READ subscriptions WRITE setSubscriptions NOTIFY subscriptionsChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit I3IpcListener(QObject* parent = nullptr): PostReloadHook(parent) {} + ~I3IpcListener() override; + Q_DISABLE_COPY_MOVE(I3IpcListener); + + void onPostReload() override; + + [[nodiscard]] QList subscriptions() const; + void setSubscriptions(QList subscriptions); + +signals: + void ipcEvent(I3IpcEvent* event); + void subscriptionsChanged(); + +private: + void startListening(); + void receiveEvent(I3IpcEvent* event); + + void freeI3Ipc(); + + QList mSubscriptions; + I3Ipc* i3Ipc = nullptr; +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/monitor.cpp b/src/x11/i3/ipc/monitor.cpp index 1bc593c..fb0ec86 100644 --- a/src/x11/i3/ipc/monitor.cpp +++ b/src/x11/i3/ipc/monitor.cpp @@ -7,12 +7,12 @@ #include #include -#include "connection.hpp" +#include "controller.hpp" #include "workspace.hpp" namespace qs::i3::ipc { -I3Monitor::I3Monitor(I3Ipc* ipc): QObject(ipc), ipc(ipc) { +I3Monitor::I3Monitor(I3IpcController* ipc): QObject(ipc), ipc(ipc) { // clang-format off this->bFocused.setBinding([this]() { return this->ipc->bindableFocusedMonitor().value() == this; }); // clang-format on diff --git a/src/x11/i3/ipc/monitor.hpp b/src/x11/i3/ipc/monitor.hpp index 00269a1..cd348b1 100644 --- a/src/x11/i3/ipc/monitor.hpp +++ b/src/x11/i3/ipc/monitor.hpp @@ -4,6 +4,7 @@ #include #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { @@ -39,10 +40,10 @@ class I3Monitor: public QObject { Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); // clang-format on QML_ELEMENT; - QML_UNCREATABLE("I3Monitors must be retrieved from the I3Ipc object."); + QML_UNCREATABLE("I3Monitors must be retrieved from the I3IpcController object."); public: - explicit I3Monitor(I3Ipc* ipc); + explicit I3Monitor(I3IpcController* ipc); [[nodiscard]] QBindable bindableId() { return &this->bId; } [[nodiscard]] QBindable bindableName() { return &this->bName; } @@ -79,7 +80,7 @@ signals: void focusedChanged(); private: - I3Ipc* ipc; + I3IpcController* ipc; QVariantMap mLastIpcObject; diff --git a/src/x11/i3/ipc/qml.cpp b/src/x11/i3/ipc/qml.cpp index 2804161..d835cbd 100644 --- a/src/x11/i3/ipc/qml.cpp +++ b/src/x11/i3/ipc/qml.cpp @@ -7,46 +7,49 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" +#include "controller.hpp" #include "workspace.hpp" namespace qs::i3::ipc { I3IpcQml::I3IpcQml() { - auto* instance = I3Ipc::instance(); + auto* instance = I3IpcController::instance(); // clang-format off QObject::connect(instance, &I3Ipc::rawEvent, this, &I3IpcQml::rawEvent); QObject::connect(instance, &I3Ipc::connected, this, &I3IpcQml::connected); - QObject::connect(instance, &I3Ipc::focusedWorkspaceChanged, this, &I3IpcQml::focusedWorkspaceChanged); - QObject::connect(instance, &I3Ipc::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); + QObject::connect(instance, &I3IpcController::focusedWorkspaceChanged, this, &I3IpcQml::focusedWorkspaceChanged); + QObject::connect(instance, &I3IpcController::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); // clang-format on } -void I3IpcQml::dispatch(const QString& request) { I3Ipc::instance()->dispatch(request); } -void I3IpcQml::refreshMonitors() { I3Ipc::instance()->refreshMonitors(); } -void I3IpcQml::refreshWorkspaces() { I3Ipc::instance()->refreshWorkspaces(); } -QString I3IpcQml::socketPath() { return I3Ipc::instance()->socketPath(); } -ObjectModel* I3IpcQml::monitors() { return I3Ipc::instance()->monitors(); } -ObjectModel* I3IpcQml::workspaces() { return I3Ipc::instance()->workspaces(); } +void I3IpcQml::dispatch(const QString& request) { I3IpcController::instance()->dispatch(request); } +void I3IpcQml::refreshMonitors() { I3IpcController::instance()->refreshMonitors(); } +void I3IpcQml::refreshWorkspaces() { I3IpcController::instance()->refreshWorkspaces(); } +QString I3IpcQml::socketPath() { return I3IpcController::instance()->socketPath(); } +ObjectModel* I3IpcQml::monitors() { return I3IpcController::instance()->monitors(); } +ObjectModel* I3IpcQml::workspaces() { + return I3IpcController::instance()->workspaces(); +} QBindable I3IpcQml::bindableFocusedWorkspace() { - return I3Ipc::instance()->bindableFocusedWorkspace(); + return I3IpcController::instance()->bindableFocusedWorkspace(); } QBindable I3IpcQml::bindableFocusedMonitor() { - return I3Ipc::instance()->bindableFocusedMonitor(); + return I3IpcController::instance()->bindableFocusedMonitor(); } I3Workspace* I3IpcQml::findWorkspaceByName(const QString& name) { - return I3Ipc::instance()->findWorkspaceByName(name); + return I3IpcController::instance()->findWorkspaceByName(name); } I3Monitor* I3IpcQml::findMonitorByName(const QString& name) { - return I3Ipc::instance()->findMonitorByName(name); + return I3IpcController::instance()->findMonitorByName(name); } I3Monitor* I3IpcQml::monitorFor(QuickshellScreenInfo* screen) { - return I3Ipc::instance()->monitorFor(screen); + return I3IpcController::instance()->monitorFor(screen); } } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/qml.hpp b/src/x11/i3/ipc/qml.hpp index 804e42a..2e7c81c 100644 --- a/src/x11/i3/ipc/qml.hpp +++ b/src/x11/i3/ipc/qml.hpp @@ -7,6 +7,7 @@ #include "../../../core/doc.hpp" #include "../../../core/qmlscreen.hpp" #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { diff --git a/src/x11/i3/ipc/workspace.cpp b/src/x11/i3/ipc/workspace.cpp index 7d0b730..03fadc2 100644 --- a/src/x11/i3/ipc/workspace.cpp +++ b/src/x11/i3/ipc/workspace.cpp @@ -7,12 +7,12 @@ #include #include -#include "connection.hpp" +#include "controller.hpp" #include "monitor.hpp" namespace qs::i3::ipc { -I3Workspace::I3Workspace(I3Ipc* ipc): QObject(ipc), ipc(ipc) { +I3Workspace::I3Workspace(I3IpcController* ipc): QObject(ipc), ipc(ipc) { Qt::beginPropertyUpdateGroup(); this->bActive.setBinding([this]() { diff --git a/src/x11/i3/ipc/workspace.hpp b/src/x11/i3/ipc/workspace.hpp index c9cd029..f540545 100644 --- a/src/x11/i3/ipc/workspace.hpp +++ b/src/x11/i3/ipc/workspace.hpp @@ -5,6 +5,7 @@ #include #include "connection.hpp" +#include "controller.hpp" namespace qs::i3::ipc { @@ -40,7 +41,7 @@ class I3Workspace: public QObject { QML_UNCREATABLE("I3Workspaces must be retrieved from the I3 object."); public: - I3Workspace(I3Ipc* ipc); + I3Workspace(I3IpcController* ipc); /// Activate the workspace. /// @@ -72,7 +73,7 @@ signals: void lastIpcObjectChanged(); private: - I3Ipc* ipc; + I3IpcController* ipc; QVariantMap mLastIpcObject; diff --git a/src/x11/i3/module.md b/src/x11/i3/module.md index 10afb98..1dc6180 100644 --- a/src/x11/i3/module.md +++ b/src/x11/i3/module.md @@ -2,8 +2,10 @@ name = "Quickshell.I3" description = "I3 specific Quickshell types" headers = [ "ipc/connection.hpp", + "ipc/controller.hpp", "ipc/qml.hpp", "ipc/workspace.hpp", "ipc/monitor.hpp", + "ipc/listener.hpp", ] ----- From 1ddb355121484bcac70f49edd4bd006b1d3a753e Mon Sep 17 00:00:00 2001 From: cameron Date: Mon, 18 Aug 2025 16:19:51 +1000 Subject: [PATCH 133/226] core/icon: add searching custom file paths --- src/core/iconimageprovider.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index 43e00fd..1dbe3e7 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -19,8 +19,7 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re if (splitIdx != -1) { iconName = id.sliced(0, splitIdx); path = id.sliced(splitIdx + 6); - qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for" - << id; + path = QString("/%1/%2").arg(path, iconName.sliced(iconName.lastIndexOf('/') + 1)); } else { splitIdx = id.indexOf("?fallback="); if (splitIdx != -1) { @@ -32,7 +31,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re } auto icon = QIcon::fromTheme(iconName); - if (icon.isNull()) icon = QIcon::fromTheme(fallbackName); + if (icon.isNull() && !fallbackName.isEmpty()) icon = QIcon::fromTheme(fallbackName); + if (icon.isNull() && !path.isEmpty()) icon = QPixmap(path); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); From ed036d514b0fdbce03158a0b331305be166f4555 Mon Sep 17 00:00:00 2001 From: bbedward Date: Mon, 29 Sep 2025 18:20:04 -0400 Subject: [PATCH 134/226] wayland/shortcuts-inhibit: add shortcuts inhibitor --- changelog/next.md | 1 + src/wayland/CMakeLists.txt | 3 + src/wayland/module.md | 1 + src/wayland/shortcuts_inhibit/CMakeLists.txt | 25 +++ src/wayland/shortcuts_inhibit/inhibitor.cpp | 187 ++++++++++++++++++ src/wayland/shortcuts_inhibit/inhibitor.hpp | 89 +++++++++ src/wayland/shortcuts_inhibit/proto.cpp | 88 +++++++++ src/wayland/shortcuts_inhibit/proto.hpp | 64 ++++++ .../shortcuts_inhibit/test/manual/test.qml | 65 ++++++ 9 files changed, 523 insertions(+) create mode 100644 src/wayland/shortcuts_inhibit/CMakeLists.txt create mode 100644 src/wayland/shortcuts_inhibit/inhibitor.cpp create mode 100644 src/wayland/shortcuts_inhibit/inhibitor.hpp create mode 100644 src/wayland/shortcuts_inhibit/proto.cpp create mode 100644 src/wayland/shortcuts_inhibit/proto.hpp create mode 100644 src/wayland/shortcuts_inhibit/test/manual/test.qml diff --git a/changelog/next.md b/changelog/next.md index 4b255ff..b03a52b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -14,6 +14,7 @@ set shell id. - Added support for creating Polkit agents. - Added support for creating wayland idle inhibitors. - Added support for wayland idle timeouts. +- Added support for inhibiting wayland compositor shortcuts for focused windows. - Added the ability to override Quickshell.cacheDir with a custom path. ## Other Changes diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index a96fe6b..ca49c8f 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -120,6 +120,9 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) add_subdirectory(idle_notify) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) +add_subdirectory(shortcuts_inhibit) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/module.md b/src/wayland/module.md index 0216e6d..9ad15ba 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -7,5 +7,6 @@ headers = [ "screencopy/view.hpp", "idle_inhibit/inhibitor.hpp", "idle_notify/monitor.hpp", + "shortcuts_inhibit/inhibitor.hpp", ] ----- diff --git a/src/wayland/shortcuts_inhibit/CMakeLists.txt b/src/wayland/shortcuts_inhibit/CMakeLists.txt new file mode 100644 index 0000000..8dedd3d --- /dev/null +++ b/src/wayland/shortcuts_inhibit/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_add_library(quickshell-wayland-shortcuts-inhibit STATIC + proto.cpp + inhibitor.cpp +) + +qt_add_qml_module(quickshell-wayland-shortcuts-inhibit + URI Quickshell.Wayland._ShortcutsInhibitor + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-shortcuts-inhibit) + +qs_add_module_deps_light(quickshell-wayland-shortcuts-inhibit Quickshell) + +wl_proto(wlp-shortcuts-inhibit keyboard-shortcuts-inhibit-unstable-v1 "${WAYLAND_PROTOCOLS}/unstable/keyboard-shortcuts-inhibit") + +target_link_libraries(quickshell-wayland-shortcuts-inhibit PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-shortcuts-inhibit +) + +qs_module_pch(quickshell-wayland-shortcuts-inhibit SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-shortcuts-inhibitplugin) \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/inhibitor.cpp b/src/wayland/shortcuts_inhibit/inhibitor.cpp new file mode 100644 index 0000000..2fca9bc --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.cpp @@ -0,0 +1,187 @@ +#include "inhibitor.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { +using QtWaylandClient::QWaylandWindow; + +ShortcutInhibitor::ShortcutInhibitor() { + this->bBoundWindow.setBinding([this] { + return this->bEnabled ? this->bWindowObject.value() : nullptr; + }); + + this->bActive.setBinding([this]() { + auto* inhibitor = this->bInhibitor.value(); + if (!inhibitor) return false; + if (!inhibitor->bindableActive().value()) return false; + return this->bWindow.value() == this->bFocusedWindow; + }); + + QObject::connect( + dynamic_cast(QGuiApplication::instance()), + &QGuiApplication::focusWindowChanged, + this, + &ShortcutInhibitor::onFocusedWindowChanged + ); + + this->onFocusedWindowChanged(QGuiApplication::focusWindow()); +} + +ShortcutInhibitor::~ShortcutInhibitor() { + if (!this->bInhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + manager->unrefShortcutsInhibitor(this->bInhibitor); +} + +void ShortcutInhibitor::onBoundWindowChanged() { + auto* window = this->bBoundWindow.value(); + auto* proxyWindow = qobject_cast(window); + + if (!proxyWindow) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow == this->proxyWindow) return; + + if (this->proxyWindow) { + QObject::disconnect(this->proxyWindow, nullptr, this, nullptr); + this->proxyWindow = nullptr; + } + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + this->mWaylandWindow = nullptr; + this->onWaylandSurfaceDestroyed(); + } + + if (proxyWindow) { + this->proxyWindow = proxyWindow; + + QObject::connect(proxyWindow, &QObject::destroyed, this, &ShortcutInhibitor::onWindowDestroyed); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::backerVisibilityChanged, + this, + &ShortcutInhibitor::onWindowVisibilityChanged + ); + + this->onWindowVisibilityChanged(); + } +} + +void ShortcutInhibitor::onWindowDestroyed() { + this->proxyWindow = nullptr; + this->onWaylandSurfaceDestroyed(); +} + +void ShortcutInhibitor::onWindowVisibilityChanged() { + if (!this->proxyWindow->isVisibleDirect()) return; + + auto* window = this->proxyWindow->backingWindow(); + if (!window->handle()) window->create(); + + auto* waylandWindow = dynamic_cast(window->handle()); + if (!waylandWindow) { + qCCritical(impl::logShortcutsInhibit()) << "Window handle is not a QWaylandWindow"; + return; + } + if (waylandWindow == this->mWaylandWindow) return; + this->mWaylandWindow = waylandWindow; + this->bWindow = window; + + QObject::connect( + waylandWindow, + &QObject::destroyed, + this, + &ShortcutInhibitor::onWaylandWindowDestroyed + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &ShortcutInhibitor::onWaylandSurfaceCreated + ); + + QObject::connect( + waylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &ShortcutInhibitor::onWaylandSurfaceDestroyed + ); + + if (waylandWindow->surface()) this->onWaylandSurfaceCreated(); +} + +void ShortcutInhibitor::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void ShortcutInhibitor::onWaylandSurfaceCreated() { + auto* manager = impl::ShortcutsInhibitManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable shortcuts inhibitor as keyboard-shortcuts-inhibit-unstable-v1 is " + "not supported by " + "the current compositor."; + return; + } + + if (this->bInhibitor) { + qFatal("ShortcutsInhibitor: inhibitor already exists when creating surface"); + } + + this->bInhibitor = manager->createShortcutsInhibitor(this->mWaylandWindow); +} + +void ShortcutInhibitor::onWaylandSurfaceDestroyed() { + if (!this->bInhibitor) return; + + auto* manager = impl::ShortcutsInhibitManager::instance(); + if (!manager) return; + + manager->unrefShortcutsInhibitor(this->bInhibitor); + this->bInhibitor = nullptr; +} + +void ShortcutInhibitor::onInhibitorChanged() { + auto* inhibitor = this->bInhibitor.value(); + if (inhibitor) { + QObject::connect( + inhibitor, + &impl::ShortcutsInhibitor::activeChanged, + this, + &ShortcutInhibitor::onInhibitorActiveChanged + ); + } +} + +void ShortcutInhibitor::onInhibitorActiveChanged() { + auto* inhibitor = this->bInhibitor.value(); + if (inhibitor && !inhibitor->isActive()) { + // Compositor has deactivated the inhibitor, making it invalid. + // Set enabled to false so the user can enable it again to create a new inhibitor. + this->bEnabled = false; + emit this->cancelled(); + } +} + +void ShortcutInhibitor::onFocusedWindowChanged(QWindow* focusedWindow) { + this->bFocusedWindow = focusedWindow; +} + +} // namespace qs::wayland::shortcuts_inhibit diff --git a/src/wayland/shortcuts_inhibit/inhibitor.hpp b/src/wayland/shortcuts_inhibit/inhibitor.hpp new file mode 100644 index 0000000..2eed54f --- /dev/null +++ b/src/wayland/shortcuts_inhibit/inhibitor.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../window/proxywindow.hpp" +#include "proto.hpp" + +namespace qs::wayland::shortcuts_inhibit { + +///! Prevents compositor keyboard shortcuts from being triggered +/// A shortcuts inhibitor prevents the compositor from processing its own keyboard shortcuts +/// for the focused surface. This allows applications to receive key events for shortcuts +/// that would normally be handled by the compositor. +/// +/// The inhibitor only takes effect when the associated window is focused and the inhibitor +/// is enabled. The compositor may choose to ignore inhibitor requests based on its policy. +/// +/// > [!NOTE] Using a shortcuts inhibitor requires the compositor support the [keyboard-shortcuts-inhibit-unstable-v1] protocol. +/// +/// [keyboard-shortcuts-inhibit-unstable-v1]: https://wayland.app/protocols/keyboard-shortcuts-inhibit-unstable-v1 +class ShortcutInhibitor: public QObject { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// If the shortcuts inhibitor should be enabled. Defaults to false. + Q_PROPERTY(bool enabled READ default WRITE default NOTIFY enabledChanged BINDABLE bindableEnabled); + /// The window to associate the shortcuts inhibitor with. + /// The inhibitor will only inhibit shortcuts pressed while this window has keyboard focus. + /// + /// Must be set to a non null value to enable the inhibitor. + Q_PROPERTY(QObject* window READ default WRITE default NOTIFY windowChanged BINDABLE bindableWindow); + /// Whether the inhibitor is currently active. The inhibitor is only active if @@enabled is true, + /// @@window has keyboard focus, and the compositor grants the inhibit request. + /// + /// The compositor may deactivate the inhibitor at any time (for example, if the user requests + /// normal shortcuts to be restored). When deactivated by the compositor, the inhibitor cannot be + /// programmatically reactivated. + Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive); + // clang-format on + +public: + ShortcutInhibitor(); + ~ShortcutInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutInhibitor); + + [[nodiscard]] QBindable bindableEnabled() { return &this->bEnabled; } + [[nodiscard]] QBindable bindableWindow() { return &this->bWindowObject; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + +signals: + void enabledChanged(); + void windowChanged(); + void activeChanged(); + /// Sent if the compositor cancels the inhibitor while it is active. + void cancelled(); + +private slots: + void onWindowDestroyed(); + void onWindowVisibilityChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + void onInhibitorActiveChanged(); + +private: + void onBoundWindowChanged(); + void onInhibitorChanged(); + void onFocusedWindowChanged(QWindow* focusedWindow); + + ProxyWindowBase* proxyWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, bool, bEnabled, &ShortcutInhibitor::enabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QObject*, bWindowObject, &ShortcutInhibitor::windowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QObject*, bBoundWindow, &ShortcutInhibitor::onBoundWindowChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, impl::ShortcutsInhibitor*, bInhibitor, &ShortcutInhibitor::onInhibitorChanged); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QWindow*, bWindow); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, QWindow*, bFocusedWindow); + Q_OBJECT_BINDABLE_PROPERTY(ShortcutInhibitor, bool, bActive, &ShortcutInhibitor::activeChanged); + // clang-format on +}; + +} // namespace qs::wayland::shortcuts_inhibit diff --git a/src/wayland/shortcuts_inhibit/proto.cpp b/src/wayland/shortcuts_inhibit/proto.cpp new file mode 100644 index 0000000..8d35d5e --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.cpp @@ -0,0 +1,88 @@ +#include "proto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::shortcuts_inhibit::impl { + +QS_LOGGING_CATEGORY(logShortcutsInhibit, "quickshell.wayland.shortcuts_inhibit", QtWarningMsg); + +ShortcutsInhibitManager::ShortcutsInhibitManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +ShortcutsInhibitManager* ShortcutsInhibitManager::instance() { + static auto* instance = new ShortcutsInhibitManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +ShortcutsInhibitor* +ShortcutsInhibitManager::createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) inputDevice = display->defaultInputDevice(); + + if (inputDevice == nullptr) { + qCCritical(logShortcutsInhibit) << "Could not create shortcuts inhibitor: No seat."; + return nullptr; + } + + auto* wlSurface = surface->surface(); + + if (this->inhibitors.contains(wlSurface)) { + auto& pair = this->inhibitors[wlSurface]; + pair.second++; + qCDebug(logShortcutsInhibit) << "Reusing existing inhibitor" << pair.first << "for surface" + << wlSurface << "- refcount:" << pair.second; + return pair.first; + } + + auto* inhibitor = + new ShortcutsInhibitor(this->inhibit_shortcuts(wlSurface, inputDevice->object()), wlSurface); + this->inhibitors.insert(wlSurface, qMakePair(inhibitor, 1)); + qCDebug(logShortcutsInhibit) << "Created inhibitor" << inhibitor << "for surface" << wlSurface; + return inhibitor; +} + +void ShortcutsInhibitManager::unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor) { + if (!inhibitor) return; + + auto* surface = inhibitor->surface(); + if (!this->inhibitors.contains(surface)) return; + + auto& pair = this->inhibitors[surface]; + pair.second--; + qCDebug(logShortcutsInhibit) << "Decremented refcount for inhibitor" << inhibitor + << "- refcount:" << pair.second; + + if (pair.second <= 0) { + qCDebug(logShortcutsInhibit) << "Refcount reached 0, destroying inhibitor" << inhibitor; + this->inhibitors.remove(surface); + delete inhibitor; + } +} + +ShortcutsInhibitor::~ShortcutsInhibitor() { + qCDebug(logShortcutsInhibit) << "Destroying inhibitor" << this << "for surface" << this->mSurface; + if (this->isInitialized()) this->destroy(); +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_active() { + qCDebug(logShortcutsInhibit) << "Inhibitor became active" << this; + this->bActive = true; +} + +void ShortcutsInhibitor::zwp_keyboard_shortcuts_inhibitor_v1_inactive() { + qCDebug(logShortcutsInhibit) << "Inhibitor became inactive" << this; + this->bActive = false; +} + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/proto.hpp b/src/wayland/shortcuts_inhibit/proto.hpp new file mode 100644 index 0000000..e79d5ca --- /dev/null +++ b/src/wayland/shortcuts_inhibit/proto.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + +namespace qs::wayland::shortcuts_inhibit::impl { + +QS_DECLARE_LOGGING_CATEGORY(logShortcutsInhibit); + +class ShortcutsInhibitor; + +class ShortcutsInhibitManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_keyboard_shortcuts_inhibit_manager_v1 { +public: + explicit ShortcutsInhibitManager(); + + ShortcutsInhibitor* createShortcutsInhibitor(QtWaylandClient::QWaylandWindow* surface); + void unrefShortcutsInhibitor(ShortcutsInhibitor* inhibitor); + + static ShortcutsInhibitManager* instance(); + +private: + QHash> inhibitors; +}; + +class ShortcutsInhibitor + : public QObject + , public QtWayland::zwp_keyboard_shortcuts_inhibitor_v1 { + Q_OBJECT; + +public: + explicit ShortcutsInhibitor(::zwp_keyboard_shortcuts_inhibitor_v1* inhibitor, wl_surface* surface) + : QtWayland::zwp_keyboard_shortcuts_inhibitor_v1(inhibitor) + , mSurface(surface) + , bActive(false) {} + + ~ShortcutsInhibitor() override; + Q_DISABLE_COPY_MOVE(ShortcutsInhibitor); + + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] bool isActive() const { return this->bActive; } + [[nodiscard]] wl_surface* surface() const { return this->mSurface; } + +signals: + void activeChanged(); + +protected: + void zwp_keyboard_shortcuts_inhibitor_v1_active() override; + void zwp_keyboard_shortcuts_inhibitor_v1_inactive() override; + +private: + wl_surface* mSurface; + Q_OBJECT_BINDABLE_PROPERTY(ShortcutsInhibitor, bool, bActive, &ShortcutsInhibitor::activeChanged); +}; + +} // namespace qs::wayland::shortcuts_inhibit::impl \ No newline at end of file diff --git a/src/wayland/shortcuts_inhibit/test/manual/test.qml b/src/wayland/shortcuts_inhibit/test/manual/test.qml new file mode 100644 index 0000000..1f64cbf --- /dev/null +++ b/src/wayland/shortcuts_inhibit/test/manual/test.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +Scope { + Timer { + id: toggleTimer + interval: 100 + onTriggered: windowLoader.active = true + } + + LazyLoader { + id: windowLoader + active: true + + property bool enabled: false + + FloatingWindow { + id: w + color: contentItem.palette.window + + ColumnLayout { + anchors.centerIn: parent + + CheckBox { + id: loadedCb + text: "Loaded" + checked: true + } + + CheckBox { + id: enabledCb + text: "Enabled" + checked: windowLoader.enabled + onCheckedChanged: windowLoader.enabled = checked + } + + Label { + text: `Active: ${inhibitorLoader.item?.active ?? false}` + } + + Button { + text: "Toggle Window" + onClicked: { + windowLoader.active = false; + toggleTimer.start(); + } + } + } + + LazyLoader { + id: inhibitorLoader + active: loadedCb.checked + + ShortcutInhibitor { + window: w + enabled: enabledCb.checked + onCancelled: enabledCb.checked = false + } + } + } + } +} From e9bad67619ee9937a1bbecfc6ad3b4231d2ecdc3 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 24 Nov 2025 20:39:43 -0800 Subject: [PATCH 135/226] hyprland/ipc: fix activeToplevel not resetting after closewindow --- changelog/next.md | 1 + src/wayland/hyprland/ipc/connection.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index b03a52b..0d15e5e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -26,6 +26,7 @@ set shell id. - Fixed volume control breaking with pipewire pro audio mode. - Fixed escape sequence handling in desktop entries. - Fixed volumes not initializing if a pipewire device was already loaded before its node. +- Fixed hyprland active toplevel not resetting after window closes. ## Packaging Changes diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 067b922..234c299 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -484,6 +484,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { } auto* toplevel = *toplevelIter; + if (toplevel == this->bActiveToplevel.value()) this->bActiveToplevel = nullptr; auto index = toplevelIter - mList.begin(); this->mToplevels.removeAt(index); From d24e8e9736287d01ee73ef9d573d2bc316a62d5c Mon Sep 17 00:00:00 2001 From: nemalex Date: Wed, 26 Nov 2025 13:51:58 +0100 Subject: [PATCH 136/226] hyprland/ipc: swap windowTitle and windowClass in openwindow handler The openwindow event format is ADDRESS,WORKSPACE,CLASS,TITLE but the handler was parsing args.at(2) as title and args.at(3) as class, which is reversed. This caused windows to display their class name instead of their actual title when the openwindow event arrived after windowtitlev2, since updateInitial would overwrite the correct title with the class. --- changelog/next.md | 1 + src/wayland/hyprland/ipc/connection.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 0d15e5e..a33db97 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -27,6 +27,7 @@ set shell id. - Fixed escape sequence handling in desktop entries. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. +- Fixed hyprland ipc window names and titles being reversed. ## Packaging Changes diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 234c299..ad091a6 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -442,8 +442,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { if (!ok) return; auto workspaceName = QString::fromUtf8(args.at(1)); - auto windowTitle = QString::fromUtf8(args.at(2)); - auto windowClass = QString::fromUtf8(args.at(3)); + auto windowClass = QString::fromUtf8(args.at(2)); + auto windowTitle = QString::fromUtf8(args.at(3)); auto* workspace = this->findWorkspaceByName(workspaceName, false); if (!workspace) { From 9cdda21974767012581a2052e7de7647ba8db44d Mon Sep 17 00:00:00 2001 From: EvilLary Date: Mon, 1 Dec 2025 15:10:59 +0300 Subject: [PATCH 137/226] core/command: reset color after compatibility warning msg --- src/launch/command.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 3a7a4b1..94fe239 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -461,7 +461,7 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " << QT_VERSION_STR << " but the system has updated to Qt " << qVersion() << " without rebuilding the package. This is likely to cause crashes, so " - "you must rebuild the quickshell package.\n"; + "you must rebuild the quickshell package.\n\033[0m"; return 1; } From 667bd38489f698bf02945c137e8714f1098adb67 Mon Sep 17 00:00:00 2001 From: Alejandro Pinar Ruiz Date: Mon, 1 Dec 2025 19:08:33 +0100 Subject: [PATCH 138/226] nix: update version to 0.2.1 --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index a00f0f1..4561cc6 100644 --- a/default.nix +++ b/default.nix @@ -49,7 +49,7 @@ }: let unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; - version = "0.2.0"; + version = "0.2.1"; src = nix-gitignore.gitignoreSource "/default.nix\n" ./.; dontWrapQtApps = true; # see wrappers From 26531fc46ef17e9365b03770edd3fb9206fcb460 Mon Sep 17 00:00:00 2001 From: Tobias Pisani Date: Mon, 24 Nov 2025 15:08:42 +0100 Subject: [PATCH 139/226] service/tray: emit change signals for item title and description --- changelog/next.md | 1 + src/services/status_notifier/item.hpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index a33db97..225a3f9 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -28,6 +28,7 @@ set shell id. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. - Fixed hyprland ipc window names and titles being reversed. +- Fixed missing signals for system tray item title and description updates. ## Packaging Changes diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 5ce5a7f..2eff95d 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -207,6 +207,8 @@ private: QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bOverlayIconPixmaps, updatePixmapIndex, onValueChanged); QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bAttentionIconPixmaps, updatePixmapIndex, onValueChanged); QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bMenuPath, onMenuPathChanged, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bTooltip, tooltipTitleChanged, onValueChanged); + QS_BINDING_SUBSCRIBE_METHOD(StatusNotifierItem, bTooltip, tooltipDescriptionChanged, onValueChanged); Q_OBJECT_BINDABLE_PROPERTY(StatusNotifierItem, quint32, pixmapIndex); Q_OBJECT_BINDABLE_PROPERTY(StatusNotifierItem, QString, bIcon, &StatusNotifierItem::iconChanged); From 3918290c1bcd93ed81291844d9f1ed146672dbfc Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 26 Nov 2025 09:54:32 -0500 Subject: [PATCH 140/226] core/window: add min/max/fullscreen properties, and move/resize fns to FloatingWindow --- changelog/next.md | 2 + src/window/floatingwindow.cpp | 103 ++++++++++++++++++++++++++++++++++ src/window/floatingwindow.hpp | 38 ++++++++++++- 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 225a3f9..9d8dd04 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -16,6 +16,8 @@ set shell id. - Added support for wayland idle timeouts. - Added support for inhibiting wayland compositor shortcuts for focused windows. - Added the ability to override Quickshell.cacheDir with a custom path. +- Added minimized, maximized, and fullscreen properties to FloatingWindow. +- Added the ability to handle move and resize events to FloatingWindow. ## Other Changes diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index 0b9e9b1..a0c9fdd 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -1,10 +1,12 @@ #include "floatingwindow.hpp" +#include #include #include #include #include #include +#include #include "proxywindow.hpp" #include "windowinterface.hpp" @@ -55,6 +57,7 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) QObject::connect(this->window, &ProxyFloatingWindow::titleChanged, this, &FloatingWindowInterface::titleChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); + QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::onWindowConnected); // clang-format on } @@ -66,3 +69,103 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) { } ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; } + +void FloatingWindowInterface::onWindowConnected() { + auto* qw = this->window->backingWindow(); + if (qw) { + QObject::connect( + qw, + &QWindow::windowStateChanged, + this, + &FloatingWindowInterface::onWindowStateChanged + ); + this->setMinimized(this->mMinimized); + this->setMaximized(this->mMaximized); + this->setFullscreen(this->mFullscreen); + this->onWindowStateChanged(); + } +} + +void FloatingWindowInterface::onWindowStateChanged() { + auto* qw = this->window->backingWindow(); + auto states = qw ? qw->windowStates() : Qt::WindowStates(); + + auto minimized = states.testFlag(Qt::WindowMinimized); + auto maximized = states.testFlag(Qt::WindowMaximized); + auto fullscreen = states.testFlag(Qt::WindowFullScreen); + + if (minimized != this->mWasMinimized) { + this->mWasMinimized = minimized; + emit this->minimizedChanged(); + } + + if (maximized != this->mWasMaximized) { + this->mWasMaximized = maximized; + emit this->maximizedChanged(); + } + + if (fullscreen != this->mWasFullscreen) { + this->mWasFullscreen = fullscreen; + emit this->fullscreenChanged(); + } +} + +bool FloatingWindowInterface::isMinimized() const { + auto* qw = this->window->backingWindow(); + if (!qw) return this->mWasMinimized; + return qw->windowStates().testFlag(Qt::WindowMinimized); +} + +void FloatingWindowInterface::setMinimized(bool minimized) { + this->mMinimized = minimized; + + if (auto* qw = this->window->backingWindow()) { + auto states = qw->windowStates(); + states.setFlag(Qt::WindowMinimized, minimized); + qw->setWindowStates(states); + } +} + +bool FloatingWindowInterface::isMaximized() const { + auto* qw = this->window->backingWindow(); + if (!qw) return this->mWasMaximized; + return qw->windowStates().testFlag(Qt::WindowMaximized); +} + +void FloatingWindowInterface::setMaximized(bool maximized) { + this->mMaximized = maximized; + + if (auto* qw = this->window->backingWindow()) { + auto states = qw->windowStates(); + states.setFlag(Qt::WindowMaximized, maximized); + qw->setWindowStates(states); + } +} + +bool FloatingWindowInterface::isFullscreen() const { + auto* qw = this->window->backingWindow(); + if (!qw) return this->mWasFullscreen; + return qw->windowStates().testFlag(Qt::WindowFullScreen); +} + +void FloatingWindowInterface::setFullscreen(bool fullscreen) { + this->mFullscreen = fullscreen; + + if (auto* qw = this->window->backingWindow()) { + auto states = qw->windowStates(); + states.setFlag(Qt::WindowFullScreen, fullscreen); + qw->setWindowStates(states); + } +} + +bool FloatingWindowInterface::startSystemMove() const { + auto* qw = this->window->backingWindow(); + if (!qw) return false; + return qw->startSystemMove(); +} + +bool FloatingWindowInterface::startSystemResize(Qt::Edges edges) const { + auto* qw = this->window->backingWindow(); + if (!qw) return false; + return qw->startSystemResize(edges); +} diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index f9cd5ce..06b5b9e 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -68,6 +69,12 @@ class FloatingWindowInterface: public WindowInterface { Q_PROPERTY(QSize minimumSize READ default WRITE default NOTIFY minimumSizeChanged BINDABLE bindableMinimumSize); /// Maximum window size given to the window system. Q_PROPERTY(QSize maximumSize READ default WRITE default NOTIFY maximumSizeChanged BINDABLE bindableMaximumSize); + /// Whether the window is currently minimized. + Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized NOTIFY minimizedChanged); + /// Whether the window is currently maximized. + Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged); + /// Whether the window is currently fullscreen. + Q_PROPERTY(bool fullscreen READ isFullscreen WRITE setFullscreen NOTIFY fullscreenChanged); // clang-format on QML_NAMED_ELEMENT(FloatingWindow); @@ -78,15 +85,40 @@ public: [[nodiscard]] ProxyWindowBase* proxyWindow() const override; - QBindable bindableMinimumSize() { return &this->window->bMinimumSize; } - QBindable bindableMaximumSize() { return &this->window->bMaximumSize; } - QBindable bindableTitle() { return &this->window->bTitle; } + [[nodiscard]] QBindable bindableMinimumSize() { return &this->window->bMinimumSize; } + [[nodiscard]] QBindable bindableMaximumSize() { return &this->window->bMaximumSize; } + [[nodiscard]] QBindable bindableTitle() { return &this->window->bTitle; } + + [[nodiscard]] bool isMinimized() const; + void setMinimized(bool minimized); + [[nodiscard]] bool isMaximized() const; + void setMaximized(bool maximized); + [[nodiscard]] bool isFullscreen() const; + void setFullscreen(bool fullscreen); + + /// Start a system move operation. Must be called during a pointer press/drag. + Q_INVOKABLE [[nodiscard]] bool startSystemMove() const; + /// Start a system resize operation. Must be called during a pointer press/drag. + Q_INVOKABLE [[nodiscard]] bool startSystemResize(Qt::Edges edges) const; signals: void minimumSizeChanged(); void maximumSizeChanged(); void titleChanged(); + void minimizedChanged(); + void maximizedChanged(); + void fullscreenChanged(); + +private slots: + void onWindowConnected(); + void onWindowStateChanged(); private: ProxyFloatingWindow* window; + bool mMinimized = false; + bool mMaximized = false; + bool mFullscreen = false; + bool mWasMinimized = false; + bool mWasMaximized = false; + bool mWasFullscreen = false; }; From 41828c4180fb921df7992a5405f5ff05d2ac2fff Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 25 Dec 2025 20:58:05 -0800 Subject: [PATCH 141/226] services/pipewire: use node volume controls when routeDevice missing For bluez audio streams and potentially other types of synthetic device, volume controls are managed via the node and persistence is handled by the service. --- changelog/next.md | 1 + src/services/pipewire/node.cpp | 7 +++++-- src/services/pipewire/node.hpp | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 9d8dd04..8a48ba5 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -26,6 +26,7 @@ set shell id. ## Bug Fixes - Fixed volume control breaking with pipewire pro audio mode. +- Fixed volume control breaking with bluez streams and potentially others. - Fixed escape sequence handling in desktop entries. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index d454a46..c34fa17 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -222,8 +222,11 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { self->routeDevice = id; if (self->boundData) self->boundData->onDeviceChanged(); } else { - qCCritical(logNode) << self << "has attached device" << self->device - << "but no card.profile.device property."; + qCDebug( + logNode + ) << self + << "has attached device" << self->device + << "but no card.profile.device property. Node volume control will be used."; } } diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index e3e1913..45e1551 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -244,7 +244,9 @@ public: qint32 routeDevice = -1; bool proAudio = false; - [[nodiscard]] bool shouldUseDevice() const { return this->device && !this->proAudio; } + [[nodiscard]] bool shouldUseDevice() const { + return this->device && !this->proAudio && this->routeDevice != -1; + } signals: void propertiesChanged(); From 341a07d05b9a57583944057a02b6755db3001bdd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 6 Jan 2026 01:05:57 -0800 Subject: [PATCH 142/226] io/process: use QVariantHash over QHash in Q_PROPERTY Fixes qmlls for these properties --- src/io/process.hpp | 2 +- src/io/processcore.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/io/process.hpp b/src/io/process.hpp index ab8763e..3c55745 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -102,7 +102,7 @@ class Process: public PostReloadHook { /// If the process is already running changing this property will affect the next /// started process. If the property has been changed after starting a process it will /// return the new value, not the one for the currently running process. - Q_PROPERTY(QHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged); + Q_PROPERTY(QVariantHash environment READ environment WRITE setEnvironment NOTIFY environmentChanged); /// If the process's environment should be cleared prior to applying @@environment. /// Defaults to false. /// diff --git a/src/io/processcore.hpp b/src/io/processcore.hpp index 37ec409..8d566c9 100644 --- a/src/io/processcore.hpp +++ b/src/io/processcore.hpp @@ -13,7 +13,7 @@ namespace qs::io::process { class ProcessContext { Q_PROPERTY(QList command MEMBER command WRITE setCommand); - Q_PROPERTY(QHash environment MEMBER environment WRITE setEnvironment); + Q_PROPERTY(QVariantHash environment MEMBER environment WRITE setEnvironment); Q_PROPERTY(bool clearEnvironment MEMBER clearEnvironment WRITE setClearEnvironment); Q_PROPERTY(QString workingDirectory MEMBER workingDirectory WRITE setWorkingDirectory); Q_PROPERTY(bool unbindStdout MEMBER unbindStdout WRITE setUnbindStdout); From 6742148cf4a8415a9c51fdeb11d8c3ea716c2e14 Mon Sep 17 00:00:00 2001 From: molyuu Date: Mon, 15 Dec 2025 12:27:51 +0800 Subject: [PATCH 143/226] all: initial support for freebsd - Use `copy_file_range(2)` over `sendfile(2)` which has wider compatibility. - Special case pam on freebsd and document `configDirectory` incompatibility. - Disable jemalloc for FreeBSD by default as it is the system allocator. - Disable breakpad by default on FreeBSD as breakpad is not supported. --- CMakeLists.txt | 9 +++++++-- changelog/next.md | 1 + src/core/logging.cpp | 17 ++++++++++++----- src/core/paths.cpp | 4 ++-- src/core/toolsupport.cpp | 2 +- src/services/pam/conversation.cpp | 2 ++ src/services/pam/qml.hpp | 6 ++++++ src/services/pam/subprocess.cpp | 8 ++++++++ 8 files changed, 39 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c867001..257ad94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,8 +44,13 @@ boption(BUILD_TESTING "Build tests (dev)" OFF) boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) -boption(CRASH_REPORTER "Crash Handling" ON) -boption(USE_JEMALLOC "Use jemalloc" ON) +if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + boption(CRASH_REPORTER "Crash Handling" OFF) + boption(USE_JEMALLOC "Use jemalloc" OFF) +else() + boption(CRASH_REPORTER "Crash Handling" ON) + boption(USE_JEMALLOC "Use jemalloc" ON) +endif() boption(SOCKETS "Unix Sockets" ON) boption(WAYLAND "Wayland" ON) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) diff --git a/changelog/next.md b/changelog/next.md index 8a48ba5..7857103 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -21,6 +21,7 @@ set shell id. ## Other Changes +- FreeBSD is now partially supported. - IPC operations filter available instances to the current display connection by default. ## Bug Fixes diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 5c809f6..10ea453 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -27,7 +27,7 @@ #include #include #include -#include +#include #include "instanceinfo.hpp" #include "logcat.hpp" @@ -392,7 +392,7 @@ void ThreadLogging::initFs() { delete detailedFile; detailedFile = nullptr; } else { - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, @@ -414,7 +414,7 @@ void ThreadLogging::initFs() { auto* oldFile = this->file; if (oldFile) { oldFile->seek(0); - sendfile(file->handle(), oldFile->handle(), nullptr, oldFile->size()); + copy_file_range(oldFile->handle(), nullptr, file->handle(), nullptr, oldFile->size(), 0); } this->file = file; @@ -426,7 +426,14 @@ void ThreadLogging::initFs() { auto* oldFile = this->detailedFile; if (oldFile) { oldFile->seek(0); - sendfile(detailedFile->handle(), oldFile->handle(), nullptr, oldFile->size()); + copy_file_range( + oldFile->handle(), + nullptr, + detailedFile->handle(), + nullptr, + oldFile->size(), + 0 + ); } crash::CrashInfo::INSTANCE.logFd = detailedFile->handle(); @@ -889,7 +896,7 @@ bool LogReader::continueReading() { } void LogFollower::FcntlWaitThread::run() { - auto lock = flock { + struct flock lock = { .l_type = F_RDLCK, // won't block other read locks when we take it .l_whence = SEEK_SET, .l_start = 0, diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 55beb87..6555e54 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -361,7 +361,7 @@ void QsPaths::createLock() { return; } - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, @@ -389,7 +389,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info, bool allowD auto file = QFile(QDir(path).filePath("instance.lock")); if (!file.open(QFile::ReadOnly)) return false; - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp index afce008..8aa5ac9 100644 --- a/src/core/toolsupport.cpp +++ b/src/core/toolsupport.cpp @@ -54,7 +54,7 @@ bool QmlToolingSupport::lockTooling() { return false; } - auto lock = flock { + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, // NOLINT (fcntl.h??) .l_start = 0, diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index 6d27978..500abd5 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include "../../core/logcat.hpp" diff --git a/src/services/pam/qml.hpp b/src/services/pam/qml.hpp index a8ffcc3..a36184e 100644 --- a/src/services/pam/qml.hpp +++ b/src/services/pam/qml.hpp @@ -6,7 +6,11 @@ #include #include #include +#ifdef __FreeBSD__ +#include +#else #include +#endif #include #include "conversation.hpp" @@ -35,6 +39,8 @@ class PamContext /// /// The configuration directory is resolved relative to the current file if not an absolute path. /// + /// On FreeBSD this property is ignored as the pam configuration directory cannot be changed. + /// /// This property may not be set while @@active is true. Q_PROPERTY(QString configDirectory READ configDirectory WRITE setConfigDirectory NOTIFY configDirectoryChanged); /// The user to authenticate as. If unset the current user will be used. diff --git a/src/services/pam/subprocess.cpp b/src/services/pam/subprocess.cpp index f99b279..dc36228 100644 --- a/src/services/pam/subprocess.cpp +++ b/src/services/pam/subprocess.cpp @@ -7,7 +7,11 @@ #include #include #include +#ifdef __FreeBSD__ +#include +#else #include +#endif #include #include @@ -83,7 +87,11 @@ PamIpcExitCode PamSubprocess::exec(const char* configDir, const char* config, co logIf(this->log) << "Starting pam session for user \"" << user << "\" with config \"" << config << "\" in dir \"" << configDir << "\"" << std::endl; +#ifdef __FreeBSD__ + auto result = pam_start(config, user, &conv, &handle); +#else auto result = pam_start_confdir(config, user, &conv, configDir, &handle); +#endif if (result != PAM_SUCCESS) { logIf(true) << "Unable to start pam conversation with error \"" << pam_strerror(handle, result) From 8d19beb69ea72585b93d0ec94168d0bf25f1bd68 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 8 Jan 2026 02:35:08 -0800 Subject: [PATCH 144/226] core/log: copy early logs with sendfile/readwrite again copy_file_range does not work across devices and memfds count as a separate device. --- src/core/logging.cpp | 74 ++++++++++++++++++++++++++----- src/services/pam/conversation.cpp | 2 +- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 10ea453..d24225b 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -27,7 +27,10 @@ #include #include #include -#include +#ifdef __linux__ +#include +#include +#endif #include "instanceinfo.hpp" #include "logcat.hpp" @@ -43,6 +46,57 @@ using namespace qt_logging_registry; QS_LOGGING_CATEGORY(logLogging, "quickshell.logging", QtWarningMsg); +namespace { +bool copyFileData(int sourceFd, int destFd, qint64 size) { + auto usize = static_cast(size); + +#ifdef __linux__ + off_t offset = 0; + auto remaining = usize; + + while (remaining > 0) { + auto r = sendfile(destFd, sourceFd, &offset, remaining); + if (r == -1) { + if (errno == EINTR) continue; + return false; + } + if (r == 0) break; + remaining -= static_cast(r); + } + + return true; +#else + std::array buffer = {}; + auto remaining = totalTarget; + + while (remaining > 0) { + auto chunk = std::min(remaining, buffer.size()); + auto r = ::read(sourceFd, buffer.data(), chunk); + if (r == -1) { + if (errno == EINTR) continue; + return false; + } + if (r == 0) break; + + auto readBytes = static_cast(r); + size_t written = 0; + while (written < readBytes) { + auto w = ::write(destFd, buffer.data() + written, readBytes - written); + if (w == -1) { + if (errno == EINTR) continue; + return false; + } + written += static_cast(w); + } + + remaining -= readBytes; + } + + return true; +#endif +} +} // namespace + bool LogMessage::operator==(const LogMessage& other) const { // note: not including time return this->type == other.type && this->category == other.category && this->body == other.body; @@ -414,7 +468,11 @@ void ThreadLogging::initFs() { auto* oldFile = this->file; if (oldFile) { oldFile->seek(0); - copy_file_range(oldFile->handle(), nullptr, file->handle(), nullptr, oldFile->size(), 0); + + if (!copyFileData(oldFile->handle(), file->handle(), oldFile->size())) { + qCritical(logLogging) << "Failed to copy log from memfd with error code " << errno + << qt_error_string(errno); + } } this->file = file; @@ -426,14 +484,10 @@ void ThreadLogging::initFs() { auto* oldFile = this->detailedFile; if (oldFile) { oldFile->seek(0); - copy_file_range( - oldFile->handle(), - nullptr, - detailedFile->handle(), - nullptr, - oldFile->size(), - 0 - ); + if (!copyFileData(oldFile->handle(), detailedFile->handle(), oldFile->size())) { + qCritical(logLogging) << "Failed to copy detailed log from memfd with error code " << errno + << qt_error_string(errno); + } } crash::CrashInfo::INSTANCE.logFd = detailedFile->handle(); diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index 500abd5..a9d498b 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -1,4 +1,5 @@ #include "conversation.hpp" +#include #include #include @@ -6,7 +7,6 @@ #include #include #include -#include #include #include From 5d8354a88be2ce2c16add7457c94e29f6e7c3684 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 8 Jan 2026 03:58:17 -0800 Subject: [PATCH 145/226] services/pipewire: add reconnect support --- changelog/next.md | 1 + src/services/pipewire/connection.cpp | 126 ++++++++++++++++++++++++++- src/services/pipewire/connection.hpp | 21 +++++ src/services/pipewire/core.cpp | 85 +++++++++++++----- src/services/pipewire/core.hpp | 5 ++ src/services/pipewire/defaults.cpp | 16 ++++ src/services/pipewire/defaults.hpp | 1 + src/services/pipewire/qml.cpp | 12 +-- src/services/pipewire/registry.cpp | 40 +++++++++ src/services/pipewire/registry.hpp | 2 + 10 files changed, 277 insertions(+), 32 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 7857103..f79900f 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -18,6 +18,7 @@ set shell id. - Added the ability to override Quickshell.cacheDir with a custom path. - Added minimized, maximized, and fullscreen properties to FloatingWindow. - Added the ability to handle move and resize events to FloatingWindow. +- Pipewire service now reconnects if pipewire dies or a protocol error occurs. ## Other Changes diff --git a/src/services/pipewire/connection.cpp b/src/services/pipewire/connection.cpp index ac4c5e6..c2f505f 100644 --- a/src/services/pipewire/connection.cpp +++ b/src/services/pipewire/connection.cpp @@ -1,15 +1,137 @@ #include "connection.hpp" +#include +#include +#include +#include +#include #include +#include +#include + +#include "../../core/logcat.hpp" +#include "core.hpp" namespace qs::service::pipewire { +namespace { +QS_LOGGING_CATEGORY(logConnection, "quickshell.service.pipewire.connection", QtWarningMsg); +} + PwConnection::PwConnection(QObject* parent): QObject(parent) { - if (this->core.isValid()) { - this->registry.init(this->core); + this->runtimeDir = PwConnection::resolveRuntimeDir(); + + QObject::connect(&this->core, &PwCore::fatalError, this, &PwConnection::queueFatalError); + + if (!this->tryConnect(false) + && qEnvironmentVariableIntValue("QS_PIPEWIRE_IMMEDIATE_RECONNECT") == 1) + { + this->beginReconnect(); } } +QString PwConnection::resolveRuntimeDir() { + auto runtimeDir = qEnvironmentVariable("PIPEWIRE_RUNTIME_DIR"); + if (runtimeDir.isEmpty()) { + runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + } + + if (runtimeDir.isEmpty()) { + runtimeDir = QString("/run/user/%1").arg(getuid()); + } + + return runtimeDir; +} + +void PwConnection::beginReconnect() { + if (this->core.isValid()) { + this->stopSocketWatcher(); + return; + } + + if (!qEnvironmentVariableIsEmpty("PIPEWIRE_REMOTE")) return; + + if (this->runtimeDir.isEmpty()) { + qCWarning( + logConnection + ) << "Cannot watch runtime dir for pipewire reconnects: runtime dir is empty."; + return; + } + + this->startSocketWatcher(); + this->tryConnect(true); +} + +bool PwConnection::tryConnect(bool retry) { + if (this->core.isValid()) return true; + + qCDebug(logConnection) << "Attempting reconnect..."; + if (!this->core.start(retry)) { + return false; + } + + qCInfo(logConnection) << "Connection established"; + this->stopSocketWatcher(); + + this->registry.init(this->core); + return true; +} + +void PwConnection::startSocketWatcher() { + if (this->socketWatcher != nullptr) return; + if (!qEnvironmentVariableIsEmpty("PIPEWIRE_REMOTE")) return; + + auto dir = QDir(this->runtimeDir); + if (!dir.exists()) { + qCWarning(logConnection) << "Cannot wait for a new pipewire socket, runtime dir does not exist:" + << this->runtimeDir; + return; + } + + this->socketWatcher = new QFileSystemWatcher(this); + this->socketWatcher->addPath(this->runtimeDir); + + QObject::connect( + this->socketWatcher, + &QFileSystemWatcher::directoryChanged, + this, + &PwConnection::onRuntimeDirChanged + ); +} + +void PwConnection::stopSocketWatcher() { + if (this->socketWatcher == nullptr) return; + + this->socketWatcher->deleteLater(); + this->socketWatcher = nullptr; +} + +void PwConnection::queueFatalError() { + if (this->fatalErrorQueued) return; + + this->fatalErrorQueued = true; + QMetaObject::invokeMethod(this, &PwConnection::onFatalError, Qt::QueuedConnection); +} + +void PwConnection::onFatalError() { + this->fatalErrorQueued = false; + + this->defaults.reset(); + this->registry.reset(); + this->core.shutdown(); + + this->beginReconnect(); +} + +void PwConnection::onRuntimeDirChanged(const QString& /*path*/) { + if (this->core.isValid()) { + this->stopSocketWatcher(); + return; + } + + this->tryConnect(true); +} + PwConnection* PwConnection::instance() { static PwConnection* instance = nullptr; // NOLINT diff --git a/src/services/pipewire/connection.hpp b/src/services/pipewire/connection.hpp index 2b3e860..d0374f8 100644 --- a/src/services/pipewire/connection.hpp +++ b/src/services/pipewire/connection.hpp @@ -1,9 +1,13 @@ #pragma once +#include + #include "core.hpp" #include "defaults.hpp" #include "registry.hpp" +class QFileSystemWatcher; + namespace qs::service::pipewire { class PwConnection: public QObject { @@ -18,6 +22,23 @@ public: static PwConnection* instance(); private: + static QString resolveRuntimeDir(); + + void beginReconnect(); + bool tryConnect(bool retry); + void startSocketWatcher(); + void stopSocketWatcher(); + +private slots: + void queueFatalError(); + void onFatalError(); + void onRuntimeDirChanged(const QString& path); + +private: + QString runtimeDir; + QFileSystemWatcher* socketWatcher = nullptr; + bool fatalErrorQueued = false; + // init/destroy order is important. do not rearrange. PwCore core; }; diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index 22445aa..e40bc54 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -27,7 +27,7 @@ const pw_core_events PwCore::EVENTS = { .info = nullptr, .done = &PwCore::onSync, .ping = nullptr, - .error = nullptr, + .error = &PwCore::onError, .remove_id = nullptr, .bound_id = nullptr, .add_mem = nullptr, @@ -36,26 +36,46 @@ const pw_core_events PwCore::EVENTS = { }; PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { - qCInfo(logLoop) << "Creating pipewire event loop."; pw_init(nullptr, nullptr); +} + +bool PwCore::start(bool retry) { + if (this->core != nullptr) return true; + + qCInfo(logLoop) << "Creating pipewire event loop."; this->loop = pw_loop_new(nullptr); if (this->loop == nullptr) { - qCCritical(logLoop) << "Failed to create pipewire event loop."; - return; + if (retry) { + qCInfo(logLoop) << "Failed to create pipewire event loop."; + } else { + qCCritical(logLoop) << "Failed to create pipewire event loop."; + } + this->shutdown(); + return false; } this->context = pw_context_new(this->loop, nullptr, 0); if (this->context == nullptr) { - qCCritical(logLoop) << "Failed to create pipewire context."; - return; + if (retry) { + qCInfo(logLoop) << "Failed to create pipewire context."; + } else { + qCCritical(logLoop) << "Failed to create pipewire context."; + } + this->shutdown(); + return false; } qCInfo(logLoop) << "Connecting to pipewire server."; this->core = pw_context_connect(this->context, nullptr, 0); if (this->core == nullptr) { - qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno; - return; + if (retry) { + qCInfo(logLoop) << "Failed to connect pipewire context. Errno:" << errno; + } else { + qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno; + } + this->shutdown(); + return false; } pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this); @@ -66,22 +86,34 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read this->notifier.setSocket(fd); QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PwCore::poll); this->notifier.setEnabled(true); + + return true; +} + +void PwCore::shutdown() { + if (this->core != nullptr) { + this->listener.remove(); + pw_core_disconnect(this->core); + this->core = nullptr; + } + + if (this->context != nullptr) { + pw_context_destroy(this->context); + this->context = nullptr; + } + + if (this->loop != nullptr) { + pw_loop_destroy(this->loop); + this->loop = nullptr; + } + + this->notifier.setEnabled(false); + QObject::disconnect(&this->notifier, nullptr, this, nullptr); } PwCore::~PwCore() { qCInfo(logLoop) << "Destroying PwCore."; - - if (this->loop != nullptr) { - if (this->context != nullptr) { - if (this->core != nullptr) { - pw_core_disconnect(this->core); - } - - pw_context_destroy(this->context); - } - - pw_loop_destroy(this->loop); - } + this->shutdown(); } bool PwCore::isValid() const { @@ -90,6 +122,7 @@ bool PwCore::isValid() const { } void PwCore::poll() { + if (this->loop == nullptr) return; qCDebug(logLoop) << "Pipewire event loop received new events, iterating."; // Spin pw event loop. pw_loop_iterate(this->loop, 0); @@ -107,6 +140,18 @@ void PwCore::onSync(void* data, quint32 id, qint32 seq) { emit self->synced(id, seq); } +void PwCore::onError(void* data, quint32 id, qint32 /*seq*/, qint32 res, const char* message) { + auto* self = static_cast(data); + + if (message != nullptr) { + qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res << message; + } else { + qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res; + } + + emit self->fatalError(); +} + SpaHook::SpaHook() { // NOLINT spa_zero(this->hook); } diff --git a/src/services/pipewire/core.hpp b/src/services/pipewire/core.hpp index 262e2d3..967efaf 100644 --- a/src/services/pipewire/core.hpp +++ b/src/services/pipewire/core.hpp @@ -30,6 +30,9 @@ public: ~PwCore() override; Q_DISABLE_COPY_MOVE(PwCore); + bool start(bool retry); + void shutdown(); + [[nodiscard]] bool isValid() const; [[nodiscard]] qint32 sync(quint32 id) const; @@ -40,6 +43,7 @@ public: signals: void polled(); void synced(quint32 id, qint32 seq); + void fatalError(); private slots: void poll(); @@ -48,6 +52,7 @@ private: static const pw_core_events EVENTS; static void onSync(void* data, quint32 id, qint32 seq); + static void onError(void* data, quint32 id, qint32 seq, qint32 res, const char* message); QSocketNotifier notifier; SpaHook listener; diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 88a1dc1..02463f4 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -31,6 +31,22 @@ PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) { QObject::connect(registry, &PwRegistry::nodeAdded, this, &PwDefaultTracker::onNodeAdded); } +void PwDefaultTracker::reset() { + if (auto* meta = this->defaultsMetadata.object()) { + QObject::disconnect(meta, nullptr, this, nullptr); + } + + this->defaultsMetadata.setObject(nullptr); + this->setDefaultSink(nullptr); + this->setDefaultSinkName(QString()); + this->setDefaultSource(nullptr); + this->setDefaultSourceName(QString()); + this->setDefaultConfiguredSink(nullptr); + this->setDefaultConfiguredSinkName(QString()); + this->setDefaultConfiguredSource(nullptr); + this->setDefaultConfiguredSourceName(QString()); +} + void PwDefaultTracker::onMetadataAdded(PwMetadata* metadata) { if (metadata->name() == "default") { qCDebug(logDefaults) << "Got new defaults metadata object" << metadata; diff --git a/src/services/pipewire/defaults.hpp b/src/services/pipewire/defaults.hpp index f3a8e3f..591c4fd 100644 --- a/src/services/pipewire/defaults.hpp +++ b/src/services/pipewire/defaults.hpp @@ -12,6 +12,7 @@ class PwDefaultTracker: public QObject { public: explicit PwDefaultTracker(PwRegistry* registry); + void reset(); [[nodiscard]] PwNode* defaultSink() const; [[nodiscard]] PwNode* defaultSource() const; diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index 9efb17e..7a0d952 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -99,15 +98,8 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { &Pipewire::defaultConfiguredAudioSourceChanged ); - if (!connection->registry.isInitialized()) { - QObject::connect( - &connection->registry, - &PwRegistry::initialized, - this, - &Pipewire::readyChanged, - Qt::SingleShotConnection - ); - } + QObject::connect(&connection->registry, &PwRegistry::initialized, this, &Pipewire::readyChanged); + QObject::connect(&connection->registry, &PwRegistry::cleared, this, &Pipewire::readyChanged); } ObjectModel* Pipewire::nodes() { return &this->mNodes; } diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index c08fc1d..4b670b1 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -134,6 +134,46 @@ void PwRegistry::init(PwCore& core) { this->coreSyncSeq = this->core->sync(PW_ID_CORE); } +void PwRegistry::reset() { + if (this->core != nullptr) { + QObject::disconnect(this->core, nullptr, this, nullptr); + } + + this->listener.remove(); + + if (this->object != nullptr) { + pw_proxy_destroy(reinterpret_cast(this->object)); + this->object = nullptr; + } + + for (auto* meta: this->metadata.values()) { + meta->safeDestroy(); + } + this->metadata.clear(); + + for (auto* link: this->links.values()) { + link->safeDestroy(); + } + this->links.clear(); + + for (auto* node: this->nodes.values()) { + node->safeDestroy(); + } + this->nodes.clear(); + + for (auto* device: this->devices.values()) { + device->safeDestroy(); + } + this->devices.clear(); + + this->linkGroups.clear(); + this->initState = InitState::SendingObjects; + this->coreSyncSeq = 0; + this->core = nullptr; + + emit this->cleared(); +} + void PwRegistry::onCoreSync(quint32 id, qint32 seq) { if (id != PW_ID_CORE || seq != this->coreSyncSeq) return; diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index 8473f04..bb2db8c 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -116,6 +116,7 @@ class PwRegistry public: void init(PwCore& core); + void reset(); [[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; } @@ -136,6 +137,7 @@ signals: void linkGroupAdded(PwLinkGroup* group); void metadataAdded(PwMetadata* metadata); void initialized(); + void cleared(); private slots: void onLinkGroupDestroyed(QObject* object); From 11d6d67961fa09a764250474fcf73572547a6743 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 8 Jan 2026 22:51:53 -0800 Subject: [PATCH 146/226] services/pipewire: add peak detection --- changelog/next.md | 1 + src/services/pipewire/CMakeLists.txt | 1 + src/services/pipewire/module.md | 1 + src/services/pipewire/node.cpp | 17 +- src/services/pipewire/node.hpp | 3 + src/services/pipewire/peak.cpp | 404 +++++++++++++++++++++++++++ src/services/pipewire/peak.hpp | 87 ++++++ 7 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 src/services/pipewire/peak.cpp create mode 100644 src/services/pipewire/peak.hpp diff --git a/changelog/next.md b/changelog/next.md index f79900f..8ed2fb3 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -19,6 +19,7 @@ set shell id. - Added minimized, maximized, and fullscreen properties to FloatingWindow. - Added the ability to handle move and resize events to FloatingWindow. - Pipewire service now reconnects if pipewire dies or a protocol error occurs. +- Added pipewire audio peak detection. ## Other Changes diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index fddca6f..fe894c9 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -3,6 +3,7 @@ pkg_check_modules(pipewire REQUIRED IMPORTED_TARGET libpipewire-0.3) qt_add_library(quickshell-service-pipewire STATIC qml.cpp + peak.cpp core.cpp connection.cpp registry.cpp diff --git a/src/services/pipewire/module.md b/src/services/pipewire/module.md index d109f05..e34f77d 100644 --- a/src/services/pipewire/module.md +++ b/src/services/pipewire/module.md @@ -2,6 +2,7 @@ name = "Quickshell.Services.Pipewire" description = "Pipewire API" headers = [ "qml.hpp", + "peak.hpp", "link.hpp", "node.hpp", ] diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index c34fa17..b170263 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include #include #include @@ -90,6 +90,8 @@ QString PwAudioChannel::toString(Enum value) { QString PwNodeType::toString(PwNodeType::Flags type) { switch (type) { + // qstringliteral apparently not imported... + // NOLINTBEGIN case PwNodeType::VideoSource: return QStringLiteral("VideoSource"); case PwNodeType::VideoSink: return QStringLiteral("VideoSink"); case PwNodeType::AudioSource: return QStringLiteral("AudioSource"); @@ -99,6 +101,7 @@ QString PwNodeType::toString(PwNodeType::Flags type) { case PwNodeType::AudioInStream: return QStringLiteral("AudioInStream"); case PwNodeType::Untracked: return QStringLiteral("Untracked"); default: return QStringLiteral("Invalid"); + // NOLINTEND } } @@ -161,6 +164,18 @@ void PwNode::initProps(const spa_dict* props) { this->nick = nodeNick; } + if (const auto* serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL)) { + auto ok = false; + auto value = QString::fromUtf8(serial).toULongLong(&ok); + if (!ok) { + qCWarning(logNode) << this + << "has an object.serial property but the value is not valid. Value:" + << serial; + } else { + this->objectSerial = value; + } + } + if (const auto* deviceId = spa_dict_lookup(props, PW_KEY_DEVICE_ID)) { auto ok = false; auto id = QString::fromUtf8(deviceId).toInt(&ok); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 45e1551..f54c63f 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -199,6 +199,8 @@ public: [[nodiscard]] QVector volumes() const; void setVolumes(const QVector& volumes); + [[nodiscard]] QVector server() const; + signals: void volumesChanged(); void channelsChanged(); @@ -233,6 +235,7 @@ public: QString description; QString nick; QMap properties; + quint64 objectSerial = 0; PwNodeType::Flags type = PwNodeType::Untracked; diff --git a/src/services/pipewire/peak.cpp b/src/services/pipewire/peak.cpp new file mode 100644 index 0000000..64b5c42 --- /dev/null +++ b/src/services/pipewire/peak.cpp @@ -0,0 +1,404 @@ +#include "peak.hpp" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "connection.hpp" +#include "core.hpp" +#include "node.hpp" +#include "qml.hpp" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-designated-field-initializers" + +namespace qs::service::pipewire { + +namespace { +QS_LOGGING_CATEGORY(logPeak, "quickshell.service.pipewire.peak", QtWarningMsg); +} + +class PwPeakStream { +public: + PwPeakStream(PwNodePeakMonitor* monitor, PwNode* node): monitor(monitor), node(node) {} + ~PwPeakStream() { this->destroy(); } + Q_DISABLE_COPY_MOVE(PwPeakStream); + + bool start(); + void destroy(); + +private: + static const pw_stream_events EVENTS; + static void onProcess(void* data); + static void onParamChanged(void* data, uint32_t id, const spa_pod* param); + static void + onStateChanged(void* data, pw_stream_state oldState, pw_stream_state state, const char* error); + static void onDestroy(void* data); + + void handleProcess(); + void handleParamChanged(uint32_t id, const spa_pod* param); + void handleStateChanged(pw_stream_state oldState, pw_stream_state state, const char* error); + void resetFormat(); + + PwNodePeakMonitor* monitor = nullptr; + PwNode* node = nullptr; + pw_stream* stream = nullptr; + SpaHook listener; + spa_audio_info_raw format = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN); + bool formatReady = false; + QVector channelPeaks; +}; + +const pw_stream_events PwPeakStream::EVENTS = { + .version = PW_VERSION_STREAM_EVENTS, + .destroy = &PwPeakStream::onDestroy, + .state_changed = &PwPeakStream::onStateChanged, + .param_changed = &PwPeakStream::onParamChanged, + .process = &PwPeakStream::onProcess, +}; + +bool PwPeakStream::start() { + auto* core = PwConnection::instance()->registry.core; + if (core == nullptr || !core->isValid()) { + qCWarning(logPeak) << "Cannot start peak monitor stream: pipewire core is not ready."; + return false; + } + + auto target = + QByteArray::number(this->node->objectSerial ? this->node->objectSerial : this->node->id); + + // clang-format off + auto* props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Monitor", + PW_KEY_MEDIA_NAME, "Peak detect", + PW_KEY_APP_NAME, "Quickshell Peak Detect", + PW_KEY_STREAM_MONITOR, "true", + PW_KEY_STREAM_CAPTURE_SINK, this->node->type.testFlags(PwNodeType::Sink) ? "true" : "false", + PW_KEY_TARGET_OBJECT, target.constData(), + nullptr + ); + // clang-format on + + if (props == nullptr) { + qCWarning(logPeak) << "Failed to create properties for peak monitor stream."; + return false; + } + + this->stream = pw_stream_new(core->core, "quickshell-peak-monitor", props); + if (this->stream == nullptr) { + qCWarning(logPeak) << "Failed to create peak monitor stream."; + return false; + } + + pw_stream_add_listener(this->stream, &this->listener.hook, &PwPeakStream::EVENTS, this); + + auto buffer = std::array {}; + auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size()); // NOLINT + + auto params = std::array {}; + auto raw = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_F32); + params[0] = spa_format_audio_raw_build(&builder, SPA_PARAM_EnumFormat, &raw); + + auto flags = + static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS); + auto res = + pw_stream_connect(this->stream, PW_DIRECTION_INPUT, PW_ID_ANY, flags, params.data(), 1); + + if (res < 0) { + qCWarning(logPeak) << "Failed to connect peak monitor stream:" << res; + this->destroy(); + return false; + } + + return true; +} + +void PwPeakStream::destroy() { + if (this->stream == nullptr) return; + this->listener.remove(); + pw_stream_destroy(this->stream); + this->stream = nullptr; + this->resetFormat(); +} + +void PwPeakStream::onProcess(void* data) { + static_cast(data)->handleProcess(); // NOLINT +} + +void PwPeakStream::onParamChanged(void* data, uint32_t id, const spa_pod* param) { + static_cast(data)->handleParamChanged(id, param); // NOLINT +} + +void PwPeakStream::onStateChanged( + void* data, + pw_stream_state oldState, + pw_stream_state state, + const char* error +) { + static_cast(data)->handleStateChanged(oldState, state, error); // NOLINT +} + +void PwPeakStream::onDestroy(void* data) { + auto* self = static_cast(data); // NOLINT + self->stream = nullptr; + self->listener.remove(); + self->resetFormat(); +} + +void PwPeakStream::handleStateChanged( + pw_stream_state oldState, + pw_stream_state state, + const char* error +) { + if (state == PW_STREAM_STATE_ERROR) { + if (error != nullptr) { + qCWarning(logPeak) << "Peak monitor stream error:" << error; + } else { + qCWarning(logPeak) << "Peak monitor stream error."; + } + } + + if (state == PW_STREAM_STATE_PAUSED && oldState != PW_STREAM_STATE_PAUSED) { + auto peakCount = this->monitor->mChannels.length(); + if (peakCount == 0) { + peakCount = this->monitor->mPeaks.length(); + } + if (peakCount == 0 && this->formatReady) { + peakCount = static_cast(this->format.channels); + } + + if (peakCount > 0) { + auto zeros = QVector(peakCount, 0.0f); + this->monitor->updatePeaks(zeros, 0.0f); + } + } +} + +void PwPeakStream::handleParamChanged(uint32_t id, const spa_pod* param) { + if (param == nullptr || id != SPA_PARAM_Format) return; + + auto info = spa_audio_info {}; + if (spa_format_parse(param, &info.media_type, &info.media_subtype) < 0) return; + + if (info.media_type != SPA_MEDIA_TYPE_audio || info.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + auto raw = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN); // NOLINT + if (spa_format_audio_raw_parse(param, &raw) < 0) return; + + if (raw.format != SPA_AUDIO_FORMAT_F32) { + qCWarning(logPeak) << "Unsupported peak monitor format for" << this->node << ":" << raw.format; + this->resetFormat(); + return; + } + + this->format = raw; + this->formatReady = raw.channels > 0; + + auto channels = QVector(); + channels.reserve(static_cast(raw.channels)); + + for (quint32 i = 0; i < raw.channels; i++) { + if ((raw.flags & SPA_AUDIO_FLAG_UNPOSITIONED) != 0) { + channels.push_back(PwAudioChannel::Unknown); + } else { + channels.push_back(static_cast(raw.position[i])); + } + } + + this->channelPeaks.fill(0.0f, channels.size()); + this->monitor->updateChannels(channels); + this->monitor->updatePeaks(this->channelPeaks, 0.0f); +} + +void PwPeakStream::resetFormat() { + this->format = SPA_AUDIO_INFO_RAW_INIT(.format = SPA_AUDIO_FORMAT_UNKNOWN); + this->formatReady = false; + this->channelPeaks.clear(); + this->monitor->clearPeaks(); +} + +void PwPeakStream::handleProcess() { + if (!this->formatReady || this->stream == nullptr) return; + + auto* buffer = pw_stream_dequeue_buffer(this->stream); + auto requeue = qScopeGuard([&, this] { pw_stream_queue_buffer(this->stream, buffer); }); + + if (buffer == nullptr) { + qCWarning(logPeak) << "Peak monitor ran out of buffers."; + return; + } + + auto* spaBuffer = buffer->buffer; + if (spaBuffer == nullptr || spaBuffer->n_datas < 1) { + return; + } + + auto* data = &spaBuffer->datas[0]; // NOLINT + if (data->data == nullptr || data->chunk == nullptr) { + return; + } + + auto channelCount = static_cast(this->format.channels); + if (channelCount <= 0) { + return; + } + + const auto* base = static_cast(data->data) + data->chunk->offset; // NOLINT + const auto* samples = reinterpret_cast(base); + auto sampleCount = static_cast(data->chunk->size / sizeof(float)); + + if (sampleCount < channelCount) { + return; + } + + QVector volumes; + if (auto* audioData = dynamic_cast(this->node->boundData)) { + if (!this->node->shouldUseDevice()) volumes = audioData->volumes(); + } + + this->channelPeaks.fill(0.0f, channelCount); + + auto maxPeak = 0.0f; + for (auto channel = 0; channel < channelCount; channel++) { + auto peak = 0.0f; + for (auto sample = channel; sample < sampleCount; sample += channelCount) { + peak = std::max(peak, std::abs(samples[sample])); // NOLINT + } + + auto visualPeak = std::cbrt(peak); + if (!volumes.isEmpty() && volumes[channel] != 0.0f) visualPeak *= 1.0f / volumes[channel]; + + this->channelPeaks[channel] = visualPeak; + maxPeak = std::max(maxPeak, visualPeak); + } + + this->monitor->updatePeaks(this->channelPeaks, maxPeak); +} + +PwNodePeakMonitor::PwNodePeakMonitor(QObject* parent): QObject(parent) {} + +PwNodePeakMonitor::~PwNodePeakMonitor() { + delete this->mStream; + this->mStream = nullptr; +} + +PwNodeIface* PwNodePeakMonitor::node() const { return this->mNode; } + +void PwNodePeakMonitor::setNode(PwNodeIface* node) { + if (node == this->mNode) return; + + if (this->mNode != nullptr) { + QObject::disconnect(this->mNode, nullptr, this, nullptr); + } + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwNodePeakMonitor::onNodeDestroyed); + } + + this->mNode = node; + this->mNodeRef.setObject(node != nullptr ? node->node() : nullptr); + this->rebuildStream(); + emit this->nodeChanged(); +} + +bool PwNodePeakMonitor::isEnabled() const { return this->mEnabled; } + +void PwNodePeakMonitor::setEnabled(bool enabled) { + if (enabled == this->mEnabled) return; + this->mEnabled = enabled; + this->rebuildStream(); + emit this->enabledChanged(); +} + +void PwNodePeakMonitor::onNodeDestroyed() { + this->mNode = nullptr; + this->mNodeRef.setObject(nullptr); + this->rebuildStream(); + emit this->nodeChanged(); +} + +void PwNodePeakMonitor::updatePeaks(const QVector& peaks, float peak) { + if (this->mPeaks != peaks) { + this->mPeaks = peaks; + emit this->peaksChanged(); + } + + if (this->mPeak != peak) { + this->mPeak = peak; + emit this->peakChanged(); + } +} + +void PwNodePeakMonitor::updateChannels(const QVector& channels) { + if (this->mChannels == channels) return; + this->mChannels = channels; + emit this->channelsChanged(); +} + +void PwNodePeakMonitor::clearPeaks() { + if (!this->mPeaks.isEmpty()) { + this->mPeaks.clear(); + emit this->peaksChanged(); + } + + if (!this->mChannels.isEmpty()) { + this->mChannels.clear(); + emit this->channelsChanged(); + } + + if (this->mPeak != 0.0f) { + this->mPeak = 0.0f; + emit this->peakChanged(); + } +} + +void PwNodePeakMonitor::rebuildStream() { + delete this->mStream; + this->mStream = nullptr; + + auto* node = this->mNodeRef.object(); + if (!this->mEnabled || node == nullptr) { + this->clearPeaks(); + return; + } + + if (node == nullptr || !node->type.testFlags(PwNodeType::Audio)) { + this->clearPeaks(); + return; + } + + this->mStream = new PwPeakStream(this, node); + if (!this->mStream->start()) { + delete this->mStream; + this->mStream = nullptr; + this->clearPeaks(); + } +} + +} // namespace qs::service::pipewire + +#pragma GCC diagnostic pop diff --git a/src/services/pipewire/peak.hpp b/src/services/pipewire/peak.hpp new file mode 100644 index 0000000..c4af3c2 --- /dev/null +++ b/src/services/pipewire/peak.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "node.hpp" + +namespace qs::service::pipewire { + +class PwNodeIface; +class PwPeakStream; + +} // namespace qs::service::pipewire + +Q_DECLARE_OPAQUE_POINTER(qs::service::pipewire::PwNodeIface*); + +namespace qs::service::pipewire { + +///! Monitors peak levels of an audio node. +/// Tracks volume peaks for a node across all its channels. +/// +/// The peak monitor binds nodes similarly to @@PwObjectTracker when enabled. +class PwNodePeakMonitor: public QObject { + Q_OBJECT; + // clang-format off + /// The node to monitor. Must be an audio node. + Q_PROPERTY(qs::service::pipewire::PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); + /// If true, the monitor is actively capturing and computing peaks. Defaults to true. + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged); + /// Per-channel peak noise levels (0.0-1.0). Length matches @@channels. + /// + /// The channel's volume does not affect this property. + Q_PROPERTY(QVector peaks READ peaks NOTIFY peaksChanged); + /// Maximum value of @@peaks. + Q_PROPERTY(float peak READ peak NOTIFY peakChanged); + /// Channel positions for the captured format. Length matches @@peaks. + Q_PROPERTY(QVector channels READ channels NOTIFY channelsChanged); + // clang-format on + QML_ELEMENT; + +public: + explicit PwNodePeakMonitor(QObject* parent = nullptr); + ~PwNodePeakMonitor() override; + Q_DISABLE_COPY_MOVE(PwNodePeakMonitor); + + [[nodiscard]] PwNodeIface* node() const; + void setNode(PwNodeIface* node); + + [[nodiscard]] bool isEnabled() const; + void setEnabled(bool enabled); + + [[nodiscard]] QVector peaks() const { return this->mPeaks; } + [[nodiscard]] float peak() const { return this->mPeak; } + [[nodiscard]] QVector channels() const { return this->mChannels; } + +signals: + void nodeChanged(); + void enabledChanged(); + void peaksChanged(); + void peakChanged(); + void channelsChanged(); + +private slots: + void onNodeDestroyed(); + +private: + friend class PwPeakStream; + + void updatePeaks(const QVector& peaks, float peak); + void updateChannels(const QVector& channels); + void clearPeaks(); + void rebuildStream(); + + PwNodeIface* mNode = nullptr; + PwBindableRef mNodeRef; + bool mEnabled = true; + QVector mPeaks; + float mPeak = 0.0f; + QVector mChannels; + PwPeakStream* mStream = nullptr; +}; + +} // namespace qs::service::pipewire From eecc2f88b3b12a672df79e74f2bd49ef65f0abdf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 9 Jan 2026 00:58:30 -0800 Subject: [PATCH 147/226] services/pipewire: ignore monitors in PwNodeLinkTracker --- changelog/next.md | 1 + src/services/pipewire/node.cpp | 6 ++++++ src/services/pipewire/node.hpp | 1 + src/services/pipewire/qml.cpp | 5 ++++- src/services/pipewire/qml.hpp | 4 ++-- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 8ed2fb3..e437e6c 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -25,6 +25,7 @@ set shell id. - FreeBSD is now partially supported. - IPC operations filter available instances to the current display connection by default. +- PwNodeLinkTracker ignores sound level monitoring programs. ## Bug Fixes diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index b170263..1b396af 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -164,6 +164,12 @@ void PwNode::initProps(const spa_dict* props) { this->nick = nodeNick; } + if (const auto* nodeCategory = spa_dict_lookup(props, PW_KEY_MEDIA_CATEGORY)) { + if (strcmp(nodeCategory, "Monitor") == 0 || strcmp(nodeCategory, "Manager") == 0) { + this->isMonitor = true; + } + } + if (const auto* serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL)) { auto ok = false; auto value = QString::fromUtf8(serial).toULongLong(&ok); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index f54c63f..fdec72d 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -236,6 +236,7 @@ public: QString nick; QMap properties; quint64 objectSerial = 0; + bool isMonitor = false; PwNodeType::Flags type = PwNodeType::Untracked; diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index 7a0d952..e4424c1 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -213,6 +213,7 @@ void PwNodeLinkTracker::updateLinks() { || (this->mNode->isSink() && link->inputNode() == this->mNode->id())) { auto* iface = PwLinkGroupIface::instance(link); + if (iface->target()->node()->isMonitor) return; // do not connect twice if (!this->mLinkGroups.contains(iface)) { @@ -231,7 +232,7 @@ void PwNodeLinkTracker::updateLinks() { for (auto* iface: this->mLinkGroups) { // only disconnect no longer used nodes - if (!newLinks.contains(iface)) { + if (!newLinks.contains(iface) || iface->target()->node()->isMonitor) { QObject::disconnect(iface, nullptr, this, nullptr); } } @@ -271,6 +272,8 @@ void PwNodeLinkTracker::onLinkGroupCreated(PwLinkGroup* linkGroup) { || (this->mNode->isSink() && linkGroup->inputNode() == this->mNode->id())) { auto* iface = PwLinkGroupIface::instance(linkGroup); + if (iface->target()->node()->isMonitor) return; + QObject::connect(iface, &QObject::destroyed, this, &PwNodeLinkTracker::onLinkGroupDestroyed); this->mLinkGroups.push_back(iface); emit this->linkGroupsChanged(); diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index e3489a1..a43ce19 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -171,13 +171,13 @@ private: ObjectModel mLinkGroups {this}; }; -///! Tracks all link connections to a given node. +///! Tracks non-monitor link connections to a given node. class PwNodeLinkTracker: public QObject { Q_OBJECT; // clang-format off /// The node to track connections to. Q_PROPERTY(qs::service::pipewire::PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged); - /// Link groups connected to the given node. + /// Link groups connected to the given node, excluding monitors. /// /// If the node is a sink, links which target the node will be tracked. /// If the node is a source, links which source the node will be tracked. From bcc3d4265e8b3ed2b17b801923905b60a3927823 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 10 Jan 2026 01:56:34 -0800 Subject: [PATCH 148/226] core: switch to custom incubation controller This change requires more QtPrivate usage but eliminates generation or cleanup related window incubation controller bugs. Additionally it enables async loads prior to rendering windows. --- changelog/next.md | 2 + src/core/CMakeLists.txt | 2 +- src/core/generation.cpp | 28 +++------- src/core/generation.hpp | 4 +- src/core/incubator.cpp | 118 ++++++++++++++++++++++++++++++++++++++++ src/core/incubator.hpp | 37 ++++++++++++- src/core/lazyloader.hpp | 3 - 7 files changed, 166 insertions(+), 28 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index e437e6c..3a932ed 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -36,6 +36,8 @@ set shell id. - Fixed hyprland active toplevel not resetting after window closes. - Fixed hyprland ipc window names and titles being reversed. - Fixed missing signals for system tray item title and description updates. +- Fixed asynchronous loaders not working after reload. +- Fixed asynchronous loaders not working before window creation. ## Packaging Changes diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 472ae04..bbfb8c4 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -51,7 +51,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets) qs_module_pch(quickshell-core SET large) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index e15103a..c68af71 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -49,7 +49,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) this->engine->addImportPath("qs:@/"); this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); - this->engine->setIncubationController(&this->delayedIncubationController); + this->incubationController.initLoop(); + this->engine->setIncubationController(&this->incubationController); this->engine->addImageProvider("icon", new IconImageProvider()); this->engine->addImageProvider("qsimage", new QsImageProvider()); @@ -134,7 +135,7 @@ void EngineGeneration::onReload(EngineGeneration* old) { // new generation acquires it then incubators will hang intermittently qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old; old->incubationControllersLocked = true; - old->assignIncubationController(); + old->updateIncubationMode(); } QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); @@ -288,29 +289,18 @@ void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) { QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed); this->trackedWindows.append(window); - this->assignIncubationController(); + this->updateIncubationMode(); } void EngineGeneration::onTrackedWindowDestroyed(QObject* object) { this->trackedWindows.removeAll(static_cast(object)); // NOLINT - this->assignIncubationController(); + this->updateIncubationMode(); } -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" - << this - << "fallback:" << (controller == &this->delayedIncubationController); - - this->engine->setIncubationController(controller); +void EngineGeneration::updateIncubationMode() { + // If we're in a situation with only hidden but tracked windows this might be wrong, + // but it seems to at least work. + this->incubationController.setIncubationMode(!this->trackedWindows.empty()); } EngineGeneration* EngineGeneration::currentGeneration() { diff --git a/src/core/generation.hpp b/src/core/generation.hpp index fef8363..4543408 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -65,7 +65,7 @@ public: QFileSystemWatcher* watcher = nullptr; QVector deletedWatchedFiles; QVector extraWatchedFiles; - DelayedQmlIncubationController delayedIncubationController; + QsIncubationController incubationController; bool reloadComplete = false; QuickshellGlobal* qsgInstance = nullptr; @@ -89,7 +89,7 @@ private slots: private: void postReload(); - void assignIncubationController(); + void updateIncubationMode(); QVector trackedWindows; bool incubationControllersLocked = false; QHash extensions; diff --git a/src/core/incubator.cpp b/src/core/incubator.cpp index c9d149a..f031b11 100644 --- a/src/core/incubator.cpp +++ b/src/core/incubator.cpp @@ -1,7 +1,16 @@ #include "incubator.hpp" +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include #include "logcat.hpp" @@ -15,3 +24,112 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { default: break; } } + +void QsIncubationController::initLoop() { + auto* app = static_cast(QGuiApplication::instance()); // NOLINT + this->renderLoop = QSGRenderLoop::instance(); + + QObject::connect( + app, + &QGuiApplication::screenAdded, + this, + &QsIncubationController::updateIncubationTime + ); + + QObject::connect( + app, + &QGuiApplication::screenRemoved, + this, + &QsIncubationController::updateIncubationTime + ); + + this->updateIncubationTime(); + + QObject::connect( + this->renderLoop, + &QSGRenderLoop::timeToIncubate, + this, + &QsIncubationController::incubate + ); + + QAnimationDriver* animationDriver = this->renderLoop->animationDriver(); + if (animationDriver) { + QObject::connect( + animationDriver, + &QAnimationDriver::stopped, + this, + &QsIncubationController::animationStopped + ); + } else { + qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot " + "be used to trigger incubation."; + } +} + +void QsIncubationController::setIncubationMode(bool render) { + if (render == this->followRenderloop) return; + this->followRenderloop = render; + + if (render) { + qCDebug(logIncubator) << "Incubation mode changed: render loop driven"; + } else { + qCDebug(logIncubator) << "Incubation mode changed: event loop driven"; + } + + if (!render && this->incubatingObjectCount()) this->incubateLater(); +} + +void QsIncubationController::timerEvent(QTimerEvent* /*event*/) { + this->killTimer(this->timerId); + this->timerId = 0; + this->incubate(); +} + +void QsIncubationController::incubateLater() { + if (this->followRenderloop) { + if (this->timerId != 0) { + this->killTimer(this->timerId); + this->timerId = 0; + } + + // Incubate again at the end of the event processing queue + QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection); + } else if (this->timerId == 0) { + // Wait for a while before processing the next batch. Using a + // timer to avoid starvation of system events. + this->timerId = this->startTimer(this->incubationTime); + } +} + +void QsIncubationController::incubate() { + if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) { + if (!this->followRenderloop) { + this->incubateFor(10); + if (this->incubatingObjectCount()) this->incubateLater(); + } else if (this->renderLoop->interleaveIncubation()) { + this->incubateFor(this->incubationTime); + } else { + this->incubateFor(this->incubationTime * 2); + if (this->incubatingObjectCount()) this->incubateLater(); + } + } +} + +void QsIncubationController::animationStopped() { this->incubate(); } + +void QsIncubationController::incubatingObjectCountChanged(int count) { + if (count + && (!this->followRenderloop + || (this->renderLoop && !this->renderLoop->interleaveIncubation()))) + { + this->incubateLater(); + } +} + +void QsIncubationController::updateIncubationTime() { + auto* screen = QGuiApplication::primaryScreen(); + if (!screen) return; + + // 1/3 frame on primary screen + this->incubationTime = qMax(1, static_cast(1000 / screen->refreshRate() / 3)); +} diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp index 5ebb9a0..15dc49a 100644 --- a/src/core/incubator.hpp +++ b/src/core/incubator.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -25,7 +26,37 @@ signals: void failed(); }; -class DelayedQmlIncubationController: public QQmlIncubationController { - // Do nothing. - // This ensures lazy loaders don't start blocking before onReload creates windows. +class QSGRenderLoop; + +class QsIncubationController + : public QObject + , public QQmlIncubationController { + Q_OBJECT + +public: + void initLoop(); + void setIncubationMode(bool render); + void incubateLater(); + +protected: + void timerEvent(QTimerEvent* event) override; + +public slots: + void incubate(); + void animationStopped(); + void updateIncubationTime(); + +protected: + void incubatingObjectCountChanged(int count) override; + +private: +// QPointer did not work with forward declarations prior to 6.7 +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + QPointer renderLoop = nullptr; +#else + QSGRenderLoop* renderLoop = nullptr; +#endif + int incubationTime = 0; + int timerId = 0; + bool followRenderloop = false; }; diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp index dbaad4b..56cc964 100644 --- a/src/core/lazyloader.hpp +++ b/src/core/lazyloader.hpp @@ -82,9 +82,6 @@ /// > Notably, @@Variants does not corrently support asynchronous /// > loading, meaning using it inside a LazyLoader will block similarly to not /// > having a loader to start with. -/// -/// > [!WARNING] LazyLoaders do not start loading before the first window is created, -/// > meaning if you create all windows inside of lazy loaders, none of them will ever load. class LazyLoader: public Reloadable { Q_OBJECT; /// The fully loaded item if the loader is @@loading or @@active, or `null` From db37dc580afc9db1bc598436649c650138b6166d Mon Sep 17 00:00:00 2001 From: Carson Powers Date: Thu, 3 Jul 2025 13:06:21 -0500 Subject: [PATCH 149/226] networking: add networking library --- CMakeLists.txt | 3 +- changelog/next.md | 1 + default.nix | 2 + src/CMakeLists.txt | 4 + src/network/CMakeLists.txt | 24 + src/network/device.cpp | 82 ++++ src/network/device.hpp | 133 +++++ src/network/module.md | 13 + src/network/network.cpp | 65 +++ src/network/network.hpp | 142 ++++++ src/network/nm/CMakeLists.txt | 79 +++ src/network/nm/accesspoint.cpp | 71 +++ src/network/nm/accesspoint.hpp | 92 ++++ src/network/nm/backend.cpp | 270 +++++++++++ src/network/nm/backend.hpp | 67 +++ src/network/nm/connection.cpp | 151 ++++++ src/network/nm/connection.hpp | 105 ++++ src/network/nm/dbus_types.hpp | 9 + src/network/nm/device.cpp | 143 ++++++ src/network/nm/device.hpp | 100 ++++ src/network/nm/enums.hpp | 156 ++++++ ...freedesktop.NetworkManager.AccessPoint.xml | 4 + ...sktop.NetworkManager.Connection.Active.xml | 8 + ...desktop.NetworkManager.Device.Wireless.xml | 15 + .../org.freedesktop.NetworkManager.Device.xml | 5 + ...top.NetworkManager.Settings.Connection.xml | 11 + .../nm/org.freedesktop.NetworkManager.xml | 27 ++ src/network/nm/utils.cpp | 248 ++++++++++ src/network/nm/utils.hpp | 45 ++ src/network/nm/wireless.cpp | 457 ++++++++++++++++++ src/network/nm/wireless.hpp | 166 +++++++ src/network/test/manual/network.qml | 155 ++++++ src/network/wifi.cpp | 139 ++++++ src/network/wifi.hpp | 186 +++++++ 34 files changed, 3177 insertions(+), 1 deletion(-) create mode 100644 src/network/CMakeLists.txt create mode 100644 src/network/device.cpp create mode 100644 src/network/device.hpp create mode 100644 src/network/module.md create mode 100644 src/network/network.cpp create mode 100644 src/network/network.hpp create mode 100644 src/network/nm/CMakeLists.txt create mode 100644 src/network/nm/accesspoint.cpp create mode 100644 src/network/nm/accesspoint.hpp create mode 100644 src/network/nm/backend.cpp create mode 100644 src/network/nm/backend.hpp create mode 100644 src/network/nm/connection.cpp create mode 100644 src/network/nm/connection.hpp create mode 100644 src/network/nm/dbus_types.hpp create mode 100644 src/network/nm/device.cpp create mode 100644 src/network/nm/device.hpp create mode 100644 src/network/nm/enums.hpp create mode 100644 src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml create mode 100644 src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml create mode 100644 src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml create mode 100644 src/network/nm/org.freedesktop.NetworkManager.Device.xml create mode 100644 src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml create mode 100644 src/network/nm/org.freedesktop.NetworkManager.xml create mode 100644 src/network/nm/utils.cpp create mode 100644 src/network/nm/utils.hpp create mode 100644 src/network/nm/wireless.cpp create mode 100644 src/network/nm/wireless.hpp create mode 100644 src/network/test/manual/network.qml create mode 100644 src/network/wifi.cpp create mode 100644 src/network/wifi.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 257ad94..81e896f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(BLUETOOTH "Bluetooth" ON) +boption(NETWORK "Network" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -125,7 +126,7 @@ if (WAYLAND) list(APPEND QT_PRIVDEPS WaylandClientPrivate) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) set(DBUS ON) endif() diff --git a/changelog/next.md b/changelog/next.md index 3a932ed..05399e5 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -20,6 +20,7 @@ set shell id. - Added the ability to handle move and resize events to FloatingWindow. - Pipewire service now reconnects if pipewire dies or a protocol error occurs. - Added pipewire audio peak detection. +- Added initial support for network management. ## Other Changes diff --git a/default.nix b/default.nix index 4561cc6..0b6f303 100644 --- a/default.nix +++ b/default.nix @@ -46,6 +46,7 @@ withHyprland ? true, withI3 ? true, withPolkit ? true, + withNetworkManager ? true, }: let unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; @@ -95,6 +96,7 @@ (lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) + (lib.cmakeBool "SERVICE_NETWORKMANAGER" withNetworkManager) (lib.cmakeBool "SERVICE_POLKIT" withPolkit) (lib.cmakeBool "HYPRLAND" withHyprland) (lib.cmakeBool "I3" withI3) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a..c95ecf7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,3 +33,7 @@ add_subdirectory(services) if (BLUETOOTH) add_subdirectory(bluetooth) endif() + +if (NETWORK) + add_subdirectory(network) +endif() diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 0000000..6075040 --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,24 @@ +add_subdirectory(nm) + +qt_add_library(quickshell-network STATIC + network.cpp + device.cpp + wifi.cpp +) + +target_include_directories(quickshell-network PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +qt_add_qml_module(quickshell-network + URI Quickshell.Networking + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-network Quickshell) +install_qml_module(quickshell-network) +target_link_libraries(quickshell-network PRIVATE quickshell-network-nm Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-networkplugin) +qs_module_pch(quickshell-network SET dbus) diff --git a/src/network/device.cpp b/src/network/device.cpp new file mode 100644 index 0000000..a47a5ee --- /dev/null +++ b/src/network/device.cpp @@ -0,0 +1,82 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); +} // namespace + +QString DeviceConnectionState::toString(DeviceConnectionState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Connecting: return QStringLiteral("Connecting"); + case Connected: return QStringLiteral("Connected"); + case Disconnecting: return QStringLiteral("Disconnecting"); + case Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +QString DeviceType::toString(DeviceType::Enum type) { + switch (type) { + case None: return QStringLiteral("None"); + case Wifi: return QStringLiteral("Wifi"); + default: return QStringLiteral("Unknown"); + } +} + +QString NMDeviceState::toString(NMDeviceState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Unmanaged: return QStringLiteral("Not managed by NetworkManager"); + case Unavailable: return QStringLiteral("Unavailable"); + case Disconnected: return QStringLiteral("Disconnected"); + case Prepare: return QStringLiteral("Preparing to connect"); + case Config: return QStringLiteral("Connecting to a network"); + case NeedAuth: return QStringLiteral("Waiting for authentication"); + case IPConfig: return QStringLiteral("Requesting IPv4 and/or IPv6 addresses from the network"); + case IPCheck: + return QStringLiteral("Checking if further action is required for the requested connection"); + case Secondaries: + return QStringLiteral("Waiting for a required secondary connection to activate"); + case Activated: return QStringLiteral("Connected"); + case Deactivating: return QStringLiteral("Disconnecting"); + case Failed: return QStringLiteral("Failed to connect"); + default: return QStringLiteral("Unknown"); + }; +} + +NetworkDevice::NetworkDevice(DeviceType::Enum type, QObject* parent): QObject(parent), mType(type) { + this->bindableConnected().setBinding([this]() { + return this->bState == DeviceConnectionState::Connected; + }); +}; + +void NetworkDevice::setAutoconnect(bool autoconnect) { + if (this->bAutoconnect == autoconnect) return; + emit this->requestSetAutoconnect(autoconnect); +} + +void NetworkDevice::disconnect() { + if (this->bState == DeviceConnectionState::Disconnected) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; + return; + } + if (this->bState == DeviceConnectionState::Disconnecting) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; + return; + } + qCDebug(logNetworkDevice) << "Disconnecting from device" << this; + this->requestDisconnect(); +} + +} // namespace qs::network diff --git a/src/network/device.hpp b/src/network/device.hpp new file mode 100644 index 0000000..f3807c2 --- /dev/null +++ b/src/network/device.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::network { + +///! Connection state of a NetworkDevice. +class DeviceConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(DeviceConnectionState::Enum state); +}; + +///! Type of network device. +class DeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + Wifi = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(DeviceType::Enum type); +}; + +///! NetworkManager-specific device state. +/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMDeviceState::Enum state); +}; + +///! A network device. +/// When @@type is `Wifi`, the device is a @@WifiDevice, which can be used to scan for and connect to access points. +class NetworkDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Devices can only be acquired through Network"); + // clang-format off + /// The device type. + Q_PROPERTY(DeviceType::Enum type READ type CONSTANT); + /// The name of the device's control interface. + Q_PROPERTY(QString name READ name NOTIFY nameChanged BINDABLE bindableName); + /// The hardware address of the device in the XX:XX:XX:XX:XX:XX format. + Q_PROPERTY(QString address READ default NOTIFY addressChanged BINDABLE bindableAddress); + /// True if the device is connected. + Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// Connection state of the device. + Q_PROPERTY(qs::network::DeviceConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// A more specific device state when the backend is NetworkManager. + Q_PROPERTY(qs::network::NMDeviceState::Enum nmState READ default NOTIFY nmStateChanged BINDABLE bindableNmState); + /// True if the device is allowed to autoconnect. + Q_PROPERTY(bool autoconnect READ autoconnect WRITE setAutoconnect NOTIFY autoconnectChanged); + // clang-format on + +public: + explicit NetworkDevice(DeviceType::Enum type, QObject* parent = nullptr); + + /// Disconnects the device and prevents it from automatically activating further connections. + Q_INVOKABLE void disconnect(); + + [[nodiscard]] DeviceType::Enum type() const { return this->mType; }; + QBindable bindableName() { return &this->bName; }; + [[nodiscard]] QString name() const { return this->bName; }; + QBindable bindableAddress() { return &this->bAddress; }; + QBindable bindableConnected() { return &this->bConnected; }; + QBindable bindableState() { return &this->bState; }; + QBindable bindableNmState() { return &this->bNmState; }; + [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; + QBindable bindableAutoconnect() { return &this->bAutoconnect; }; + void setAutoconnect(bool autoconnect); + +signals: + void requestDisconnect(); + void requestSetAutoconnect(bool autoconnect); + void nameChanged(); + void addressChanged(); + void connectedChanged(); + void stateChanged(); + void nmStateChanged(); + void autoconnectChanged(); + +private: + DeviceType::Enum mType; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bConnected, &NetworkDevice::connectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, DeviceConnectionState::Enum, bState, &NetworkDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NMDeviceState::Enum, bNmState, &NetworkDevice::nmStateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bAutoconnect, &NetworkDevice::autoconnectChanged); + // clang-format on +}; + +} // namespace qs::network diff --git a/src/network/module.md b/src/network/module.md new file mode 100644 index 0000000..a0c8e64 --- /dev/null +++ b/src/network/module.md @@ -0,0 +1,13 @@ +name = "Quickshell.Networking" +description = "Network API" +headers = [ + "network.hpp", + "device.hpp", + "wifi.hpp", +] +----- +This module exposes Network management APIs provided by a supported network backend. +For now, the only backend available is the NetworkManager DBus interface. +Both DBus and NetworkManager must be running to use it. + +See the @@Quickshell.Networking.Networking singleton. diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 0000000..67ed6a5 --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,65 @@ +#include "network.hpp" +#include + +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" +#include "device.hpp" +#include "nm/backend.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); +} // namespace + +QString NetworkState::toString(NetworkState::Enum state) { + switch (state) { + case NetworkState::Connecting: return QStringLiteral("Connecting"); + case NetworkState::Connected: return QStringLiteral("Connected"); + case NetworkState::Disconnecting: return QStringLiteral("Disconnecting"); + case NetworkState::Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +Networking::Networking(QObject* parent): QObject(parent) { + // Try to create the NetworkManager backend and bind to it. + auto* nm = new NetworkManager(this); + if (nm->isAvailable()) { + QObject::connect(nm, &NetworkManager::deviceAdded, this, &Networking::deviceAdded); + QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Networking::deviceRemoved); + QObject::connect(this, &Networking::requestSetWifiEnabled, nm, &NetworkManager::setWifiEnabled); + this->bindableWifiEnabled().setBinding([nm]() { return nm->wifiEnabled(); }); + this->bindableWifiHardwareEnabled().setBinding([nm]() { return nm->wifiHardwareEnabled(); }); + + this->mBackend = nm; + this->mBackendType = NetworkBackendType::NetworkManager; + return; + } else { + delete nm; + } + + qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; +} + +void Networking::deviceAdded(NetworkDevice* dev) { this->mDevices.insertObject(dev); } +void Networking::deviceRemoved(NetworkDevice* dev) { this->mDevices.removeObject(dev); } + +void Networking::setWifiEnabled(bool enabled) { + if (this->bWifiEnabled == enabled) return; + emit this->requestSetWifiEnabled(enabled); +} + +Network::Network(QString name, QObject* parent): QObject(parent), mName(std::move(name)) { + this->bStateChanging.setBinding([this] { + auto state = this->bState.value(); + return state == NetworkState::Connecting || state == NetworkState::Disconnecting; + }); +}; + +} // namespace qs::network diff --git a/src/network/network.hpp b/src/network/network.hpp new file mode 100644 index 0000000..8af7c9d --- /dev/null +++ b/src/network/network.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "device.hpp" + +namespace qs::network { + +///! The connection state of a Network. +class NetworkState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkState::Enum state); +}; + +///! The backend supplying the Network service. +class NetworkBackendType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + NetworkManager = 1, + }; + Q_ENUM(Enum); +}; + +class NetworkBackend: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] virtual bool isAvailable() const = 0; + +protected: + explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; +}; + +///! The Network service. +/// An interface to a network backend (currently only NetworkManager), +/// which can be used to view, configure, and connect to various networks. +class Networking: public QObject { + Q_OBJECT; + QML_SINGLETON; + QML_ELEMENT; + // clang-format off + /// A list of all network devices. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + /// The backend being used to power the Network service. + Q_PROPERTY(qs::network::NetworkBackendType::Enum backend READ backend CONSTANT); + /// Switch for the rfkill software block of all wireless devices. + Q_PROPERTY(bool wifiEnabled READ wifiEnabled WRITE setWifiEnabled NOTIFY wifiEnabledChanged); + /// State of the rfkill hardware block of all wireless devices. + Q_PROPERTY(bool wifiHardwareEnabled READ default NOTIFY wifiHardwareEnabledChanged BINDABLE bindableWifiHardwareEnabled); + // clang-format on + +public: + explicit Networking(QObject* parent = nullptr); + + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; }; + [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; }; + QBindable bindableWifiEnabled() { return &this->bWifiEnabled; }; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; + void setWifiEnabled(bool enabled); + QBindable bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; }; + +signals: + void requestSetWifiEnabled(bool enabled); + void wifiEnabledChanged(); + void wifiHardwareEnabledChanged(); + +private slots: + void deviceAdded(NetworkDevice* dev); + void deviceRemoved(NetworkDevice* dev); + +private: + ObjectModel mDevices {this}; + NetworkBackend* mBackend = nullptr; + NetworkBackendType::Enum mBackendType = NetworkBackendType::None; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged); + // clang-format on +}; + +///! A network. +class Network: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("BaseNetwork can only be aqcuired through network devices"); + + // clang-format off + /// The name of the network. + Q_PROPERTY(QString name READ name CONSTANT); + /// True if the network is connected. + Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// The connectivity state of the network. + Q_PROPERTY(NetworkState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// If the network is currently connecting or disconnecting. Shorthand for checking @@state. + Q_PROPERTY(bool stateChanging READ default NOTIFY stateChangingChanged BINDABLE bindableStateChanging); + // clang-format on + +public: + explicit Network(QString name, QObject* parent = nullptr); + + [[nodiscard]] QString name() const { return this->mName; }; + QBindable bindableConnected() { return &this->bConnected; } + QBindable bindableState() { return &this->bState; } + QBindable bindableStateChanging() { return &this->bStateChanging; } + +signals: + void connectedChanged(); + void stateChanged(); + void stateChangingChanged(); + +protected: + QString mName; + + Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bConnected, &Network::connectedChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, NetworkState::Enum, bState, &Network::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bStateChanging, &Network::stateChangingChanged); +}; + +} // namespace qs::network diff --git a/src/network/nm/CMakeLists.txt b/src/network/nm/CMakeLists.txt new file mode 100644 index 0000000..bb8635e --- /dev/null +++ b/src/network/nm/CMakeLists.txt @@ -0,0 +1,79 @@ +set_source_files_properties(org.freedesktop.NetworkManager.xml PROPERTIES + CLASSNAME DBusNetworkManagerProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.xml + dbus_nm_backend +) + +set_source_files_properties(org.freedesktop.NetworkManager.Device.xml PROPERTIES + CLASSNAME DBusNMDeviceProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Device.xml + dbus_nm_device +) + +set_source_files_properties(org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES + CLASSNAME DBusNMWirelessProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Device.Wireless.xml + dbus_nm_wireless +) + +set_source_files_properties(org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES + CLASSNAME DBusNMAccessPointProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.AccessPoint.xml + dbus_nm_accesspoint +) + +set_source_files_properties(org.freedesktop.NetworkManager.Settings.Connection.xml PROPERTIES + CLASSNAME DBusNMConnectionSettingsProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Settings.Connection.xml + dbus_nm_connection_settings +) + +set_source_files_properties(org.freedesktop.NetworkManager.Connection.Active.xml PROPERTIES + CLASSNAME DBusNMActiveConnectionProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + org.freedesktop.NetworkManager.Connection.Active.xml + dbus_nm_active_connection +) + +qt_add_library(quickshell-network-nm STATIC + backend.cpp + device.cpp + connection.cpp + accesspoint.cpp + wireless.cpp + utils.cpp + enums.hpp + ${NM_DBUS_INTERFACES} +) + +target_include_directories(quickshell-network-nm PUBLIC + ${CMAKE_CURRENT_BINARY_DIR} +) + +target_link_libraries(quickshell-network-nm PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network-nm quickshell-dbus) diff --git a/src/network/nm/accesspoint.cpp b/src/network/nm/accesspoint.cpp new file mode 100644 index 0000000..b6e3dfb --- /dev/null +++ b/src/network/nm/accesspoint.cpp @@ -0,0 +1,71 @@ +#include "accesspoint.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_nm_accesspoint.h" +#include "enums.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMAccessPoint::NMAccessPoint(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMAccessPointProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for access point at" << path; + return; + } + + QObject::connect( + &this->accessPointProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMAccessPoint::loaded, + Qt::SingleShotConnection + ); + + this->accessPointProperties.setInterface(this->proxy); + this->accessPointProperties.updateAllViaGetAll(); +} + +bool NMAccessPoint::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMAccessPoint::address() const { return this->proxy ? this->proxy->service() : QString(); } +QString NMAccessPoint::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp new file mode 100644 index 0000000..8409089 --- /dev/null +++ b/src/network/nm/accesspoint.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../wifi.hpp" +#include "dbus_nm_accesspoint.h" +#include "enums.hpp" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApSecurityFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211Mode::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +/// Proxy of a /org/freedesktop/NetworkManager/AccessPoint/* object. +class NMAccessPoint: public QObject { + Q_OBJECT; + +public: + explicit NMAccessPoint(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; + [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + +signals: + void loaded(); + void ssidChanged(const QByteArray& ssid); + void signalStrengthChanged(quint8 signal); + void flagsChanged(NM80211ApFlags::Enum flags); + void wpaFlagsChanged(NM80211ApSecurityFlags::Enum wpaFlags); + void rsnFlagsChanged(NM80211ApSecurityFlags::Enum rsnFlags); + void modeChanged(NM80211Mode::Enum mode); + void securityChanged(WifiSecurityType::Enum security); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, QByteArray, bSsid, &NMAccessPoint::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, quint8, bSignalStrength, &NMAccessPoint::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApFlags::Enum, bFlags, &NMAccessPoint::flagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bWpaFlags, &NMAccessPoint::wpaFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bRsnFlags, &NMAccessPoint::rsnFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211Mode::Enum, bMode, &NMAccessPoint::modeChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, WifiSecurityType::Enum, bSecurity, &NMAccessPoint::securityChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSsid, bSsid, accessPointProperties, "Ssid"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSignalStrength, bSignalStrength, accessPointProperties, "Strength"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pFlags, bFlags, accessPointProperties, "Flags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pWpaFlags, bWpaFlags, accessPointProperties, "WpaFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pRsnFlags, bRsnFlags, accessPointProperties, "RsnFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pMode, bMode, accessPointProperties, "Mode"); + // clang-format on + + DBusNMAccessPointProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp new file mode 100644 index 0000000..4b61e33 --- /dev/null +++ b/src/network/nm/backend.cpp @@ -0,0 +1,270 @@ +#include "backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../device.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "dbus_nm_backend.h" +#include "dbus_nm_device.h" +#include "dbus_types.hpp" +#include "device.hpp" +#include "enums.hpp" +#include "wireless.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { + qDBusRegisterMetaType(); + + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning( + logNetworkManager + ) << "Could not connect to DBus. NetworkManager backend will not work."; + return; + } + + this->proxy = new DBusNetworkManagerProxy( + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager", + bus, + this + ); + + if (!this->proxy->isValid()) { + qCDebug( + logNetworkManager + ) << "NetworkManager is not currently running. This network backend will not work"; + } else { + this->init(); + } +} + +void NetworkManager::init() { + // clang-format off + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceAdded, this, &NetworkManager::onDevicePathAdded); + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceRemoved, this, &NetworkManager::onDevicePathRemoved); + // clang-format on + + this->dbusProperties.setInterface(this->proxy); + this->dbusProperties.updateAllViaGetAll(); + + this->registerDevices(); +} + +void NetworkManager::registerDevices() { + auto pending = this->proxy->GetAllDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerDevice(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::registerDevice(const QString& path) { + if (this->mDevices.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path; + return; + } + + auto* temp = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + auto callback = [this, path, temp](uint value, const QDBusError& error) { + if (error.isValid()) { + qCWarning(logNetworkManager) << "Failed to get device type:" << error; + } else { + auto type = static_cast(value); + NMDevice* dev = nullptr; + this->mDevices.insert(path, nullptr); + + switch (type) { + case NMDeviceType::Wifi: dev = new NMWirelessDevice(path); break; + default: break; + } + + if (dev) { + if (!dev->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete dev; + } else { + this->mDevices[path] = dev; + // Only register a frontend device while it's managed by NM. + auto onManagedChanged = [this, dev, type](bool managed) { + managed ? this->registerFrontendDevice(type, dev) : this->removeFrontendDevice(dev); + }; + // clang-format off + QObject::connect(dev, &NMDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection); + QObject::connect(dev, &NMDevice::activateConnection, this, &NetworkManager::activateConnection); + QObject::connect(dev, &NMDevice::managedChanged, this, onManagedChanged); + // clang-format on + + if (dev->managed()) this->registerFrontendDevice(type, dev); + } + } + temp->deleteLater(); + } + }; + + qs::dbus::asyncReadProperty(*temp, "DeviceType", callback); +} + +void NetworkManager::registerFrontendDevice(NMDeviceType::Enum type, NMDevice* dev) { + NetworkDevice* frontendDev = nullptr; + switch (type) { + case NMDeviceType::Wifi: { + auto* frontendWifiDev = new WifiDevice(dev); + auto* wifiDev = qobject_cast(dev); + // Bind WifiDevice-specific properties + auto translateMode = [wifiDev]() { + switch (wifiDev->mode()) { + case NM80211Mode::Unknown: return WifiDeviceMode::Unknown; + case NM80211Mode::Adhoc: return WifiDeviceMode::AdHoc; + case NM80211Mode::Infra: return WifiDeviceMode::Station; + case NM80211Mode::Ap: return WifiDeviceMode::AccessPoint; + case NM80211Mode::Mesh: return WifiDeviceMode::Mesh; + } + }; + // clang-format off + frontendWifiDev->bindableMode().setBinding(translateMode); + wifiDev->bindableScanning().setBinding([frontendWifiDev]() { return frontendWifiDev->scannerEnabled(); }); + QObject::connect(wifiDev, &NMWirelessDevice::networkAdded, frontendWifiDev, &WifiDevice::networkAdded); + QObject::connect(wifiDev, &NMWirelessDevice::networkRemoved, frontendWifiDev, &WifiDevice::networkRemoved); + // clang-format on + frontendDev = frontendWifiDev; + break; + } + default: return; + } + + // Bind generic NetworkDevice properties + auto translateState = [dev]() { + switch (dev->state()) { + case 0 ... 20: return DeviceConnectionState::Unknown; + case 30: return DeviceConnectionState::Disconnected; + case 40 ... 90: return DeviceConnectionState::Connecting; + case 100: return DeviceConnectionState::Connected; + case 110 ... 120: return DeviceConnectionState::Disconnecting; + } + }; + // clang-format off + frontendDev->bindableName().setBinding([dev]() { return dev->interface(); }); + frontendDev->bindableAddress().setBinding([dev]() { return dev->hwAddress(); }); + frontendDev->bindableNmState().setBinding([dev]() { return dev->state(); }); + frontendDev->bindableState().setBinding(translateState); + frontendDev->bindableAutoconnect().setBinding([dev]() { return dev->autoconnect(); }); + QObject::connect(frontendDev, &WifiDevice::requestDisconnect, dev, &NMDevice::disconnect); + QObject::connect(frontendDev, &NetworkDevice::requestSetAutoconnect, dev, &NMDevice::setAutoconnect); + // clang-format on + + this->mFrontendDevices.insert(dev->path(), frontendDev); + emit this->deviceAdded(frontendDev); +} + +void NetworkManager::removeFrontendDevice(NMDevice* dev) { + auto* frontendDev = this->mFrontendDevices.take(dev->path()); + if (frontendDev) { + emit this->deviceRemoved(frontendDev); + frontendDev->deleteLater(); + } +} + +void NetworkManager::onDevicePathAdded(const QDBusObjectPath& path) { + this->registerDevice(path.path()); +} + +void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) { + auto iter = this->mDevices.find(path.path()); + if (iter == this->mDevices.end()) { + qCWarning(logNetworkManager) << "Sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* dev = iter.value(); + this->mDevices.erase(iter); + if (dev) { + this->removeFrontendDevice(dev); + delete dev; + } + } +} + +void NetworkManager::activateConnection( + const QDBusObjectPath& connPath, + const QDBusObjectPath& devPath +) { + auto pending = this->proxy->ActivateConnection(connPath, devPath, QDBusObjectPath("/")); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath +) { + auto pending = this->proxy->AddAndActivateConnection(settings, devPath, specificObjectPath); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to add and activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::setWifiEnabled(bool enabled) { + if (enabled == this->bWifiEnabled) return; + this->bWifiEnabled = enabled; + this->pWifiEnabled.write(); +} + +bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; + +} // namespace qs::network diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp new file mode 100644 index 0000000..471f57a --- /dev/null +++ b/src/network/nm/backend.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "dbus_nm_backend.h" +#include "device.hpp" + +namespace qs::network { + +class NetworkManager: public NetworkBackend { + Q_OBJECT; + +public: + explicit NetworkManager(QObject* parent = nullptr); + + [[nodiscard]] bool isAvailable() const override; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; + [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; }; + +signals: + void deviceAdded(NetworkDevice* device); + void deviceRemoved(NetworkDevice* device); + void wifiEnabledChanged(bool enabled); + void wifiHardwareEnabledChanged(bool enabled); + +public slots: + void setWifiEnabled(bool enabled); + +private slots: + void onDevicePathAdded(const QDBusObjectPath& path); + void onDevicePathRemoved(const QDBusObjectPath& path); + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath + ); + +private: + void init(); + void registerDevices(); + void registerDevice(const QString& path); + void registerFrontendDevice(NMDeviceType::Enum type, NMDevice* dev); + void removeFrontendDevice(NMDevice* dev); + + QHash mDevices; + QHash mFrontendDevices; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled"); + // clang-format on + DBusNetworkManagerProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp new file mode 100644 index 0000000..39b6f66 --- /dev/null +++ b/src/network/nm/connection.cpp @@ -0,0 +1,151 @@ +#include "connection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../wifi.hpp" +#include "dbus_nm_active_connection.h" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" +#include "enums.hpp" +#include "utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + QObject::connect( + this->proxy, + &DBusNMConnectionSettingsProxy::Updated, + this, + &NMConnectionSettings::updateSettings + ); + this->bSecurity.setBinding([this]() { return securityFromConnectionSettings(this->bSettings); }); + + this->connectionSettingsProperties.setInterface(this->proxy); + this->connectionSettingsProperties.updateAllViaGetAll(); + + this->updateSettings(); +} + +void NMConnectionSettings::updateSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get" << this->path() << "settings:" << reply.error().message(); + } else { + this->bSettings = reply.value(); + } + + if (!this->mLoaded) { + emit this->loaded(); + this->mLoaded = true; + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMConnectionSettings::forget() { + auto pending = this->proxy->Delete(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to forget" << this->path() << ":" << reply.error().message(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMConnectionSettings::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } + +NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::loaded, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { + auto enumReason = static_cast(reason); + if (this->mStateReason == enumReason) return; + this->mStateReason = enumReason; + emit this->stateReasonChanged(enumReason); +} + +bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnection::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/connection.hpp b/src/network/nm/connection.hpp new file mode 100644 index 0000000..4f126c8 --- /dev/null +++ b/src/network/nm/connection.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../wifi.hpp" +#include "dbus_nm_active_connection.h" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMConnectionState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. +class NMConnectionSettings: public QObject { + Q_OBJECT; + +public: + explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr); + + void forget(); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; + +signals: + void loaded(); + void settingsChanged(ConnectionSettingsMap settings); + void securityChanged(WifiSecurityType::Enum security); + void ssidChanged(QString ssid); + +private: + bool mLoaded = false; + void updateSettings(); + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, WifiSecurityType::Enum, bSecurity, &NMConnectionSettings::securityChanged); + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties); + // clang-format on + + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +// Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object. +class NMActiveConnection: public QObject { + Q_OBJECT; + +public: + explicit NMActiveConnection(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + +signals: + void loaded(); + void connectionChanged(QDBusObjectPath path); + void stateChanged(NMConnectionState::Enum state); + void stateReasonChanged(NMConnectionStateReason::Enum reason); + void uuidChanged(const QString& uuid); + +private slots: + void onStateChanged(quint32 state, quint32 reason); + +private: + NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State"); + // clang-format on + DBusNMActiveConnectionProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp new file mode 100644 index 0000000..dadbcf3 --- /dev/null +++ b/src/network/nm/dbus_types.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include +#include + +using ConnectionSettingsMap = QMap; +Q_DECLARE_METATYPE(ConnectionSettingsMap); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp new file mode 100644 index 0000000..aad565d --- /dev/null +++ b/src/network/nm/device.cpp @@ -0,0 +1,143 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../device.hpp" +#include "connection.hpp" +#include "dbus_nm_device.h" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) { + this->deviceProxy = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->deviceProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for device at" << path; + return; + } + + // clang-format off + QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged); + QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged); + // clang-format on + + this->deviceProperties.setInterface(this->deviceProxy); + this->deviceProperties.updateAllViaGetAll(); +} + +void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { + const QString stringPath = path.path(); + + // Remove old active connection + if (this->mActiveConnection) { + QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); + delete this->mActiveConnection; + this->mActiveConnection = nullptr; + } + + // Create new active connection + if (stringPath != "/") { + auto* active = new NMActiveConnection(stringPath, this); + if (!active->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << stringPath; + delete active; + } else { + this->mActiveConnection = active; + QObject::connect( + active, + &NMActiveConnection::loaded, + this, + [this, active]() { emit this->activeConnectionLoaded(active); }, + Qt::SingleShotConnection + ); + } + } +} + +void NMDevice::onAvailableConnectionPathsChanged(const QList& paths) { + QSet newPathSet; + for (const QDBusObjectPath& path: paths) { + newPathSet.insert(path.path()); + } + const auto existingPaths = this->mConnections.keys(); + const QSet existingPathSet(existingPaths.begin(), existingPaths.end()); + + const auto addedConnections = newPathSet - existingPathSet; + const auto removedConnections = existingPathSet - newPathSet; + + for (const QString& path: addedConnections) { + this->registerConnection(path); + } + for (const QString& path: removedConnections) { + auto* connection = this->mConnections.take(path); + if (!connection) { + qCDebug(logNetworkManager) << "Sent removal signal for" << path << "which is not registered."; + } else { + delete connection; + } + }; +} + +void NMDevice::registerConnection(const QString& path) { + auto* connection = new NMConnectionSettings(path, this); + if (!connection->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete connection; + } else { + this->mConnections.insert(path, connection); + QObject::connect( + connection, + &NMConnectionSettings::loaded, + this, + [this, connection]() { emit this->connectionLoaded(connection); }, + Qt::SingleShotConnection + ); + } +} + +void NMDevice::disconnect() { this->deviceProxy->Disconnect(); } + +void NMDevice::setAutoconnect(bool autoconnect) { + if (autoconnect == this->bAutoconnect) return; + this->bAutoconnect = autoconnect; + this->pAutoconnect.write(); +} + +bool NMDevice::isValid() const { return this->deviceProxy && this->deviceProxy->isValid(); } +QString NMDevice::address() const { + return this->deviceProxy ? this->deviceProxy->service() : QString(); +} +QString NMDevice::path() const { return this->deviceProxy ? this->deviceProxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp new file mode 100644 index 0000000..e3ff4b9 --- /dev/null +++ b/src/network/nm/device.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "connection.hpp" +#include "dbus_nm_device.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Only the members from the org.freedesktop.NetworkManager.Device interface. +// Owns the lifetime of NMActiveConnection(s) and NMConnectionSetting(s). +class NMDevice: public QObject { + Q_OBJECT; + +public: + explicit NMDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] virtual bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString interface() const { return this->bInterface; }; + [[nodiscard]] QString hwAddress() const { return this->bHwAddress; }; + [[nodiscard]] bool managed() const { return this->bManaged; }; + [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; }; + [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; + [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; }; + +signals: + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& apPath + ); + void connectionLoaded(NMConnectionSettings* connection); + void connectionRemoved(NMConnectionSettings* connection); + void availableConnectionPathsChanged(QList paths); + void activeConnectionPathChanged(const QDBusObjectPath& connection); + void activeConnectionLoaded(NMActiveConnection* active); + void interfaceChanged(const QString& interface); + void hwAddressChanged(const QString& hwAddress); + void managedChanged(bool managed); + void stateChanged(NMDeviceState::Enum state); + void autoconnectChanged(bool autoconnect); + +public slots: + void disconnect(); + void setAutoconnect(bool autoconnect); + +private slots: + void onAvailableConnectionPathsChanged(const QList& paths); + void onActiveConnectionPathChanged(const QDBusObjectPath& path); + +private: + void registerConnection(const QString& path); + + QHash mConnections; + NMActiveConnection* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bInterface, &NMDevice::interfaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bManaged, &NMDevice::managedChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bAutoconnect, &NMDevice::autoconnectChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableConnectionPathsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); + QS_DBUS_PROPERTY_BINDING(NMDevice, pName, bInterface, deviceProperties, "Interface"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAddress, bHwAddress, deviceProperties, "HwAddress"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pManaged, bManaged, deviceProperties, "Managed"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pState, bState, deviceProperties, "State"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAutoconnect, bAutoconnect, deviceProperties, "Autoconnect"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAvailableConnections, bAvailableConnections, deviceProperties, "AvailableConnections"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pActiveConnection, bActiveConnection, deviceProperties, "ActiveConnection"); + // clang-format on + + DBusNMDeviceProxy* deviceProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp new file mode 100644 index 0000000..34e5b65 --- /dev/null +++ b/src/network/nm/enums.hpp @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include + +namespace qs::network { + +// Indicates the type of hardware represented by a device object. +class NMDeviceType: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Ethernet = 1, + Wifi = 2, + Unused1 = 3, + Unused2 = 4, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + InfiniBand = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Generic = 14, + Team = 15, + Tun = 16, + IpTunnel = 17, + MacVlan = 18, + VxLan = 19, + Veth = 20, + MacSec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + Wireguard = 29, + WifiP2P = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, + IpVlan = 34, + }; + Q_ENUM(Enum); +}; + +// 802.11 specific device encryption and authentication capabilities. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. +class NMWirelessCapabilities: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + CipherWep40 = 1, + CipherWep104 = 2, + CipherTkip = 4, + CipherCcmp = 8, + Wpa = 16, + Rsn = 32, + Ap = 64, + Adhoc = 128, + FreqValid = 256, + Freq2Ghz = 512, + Freq5Ghz = 1024, + Freq6Ghz = 2048, + Mesh = 4096, + IbssRsn = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the 802.11 mode an access point is currently in. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211Mode. +class NM80211Mode: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Adhoc = 1, + Infra = 2, + Ap = 3, + Mesh = 4, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point flags. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + None = 0, + Privacy = 1, + Wps = 2, + WpsPbc = 4, + WpsPin = 8, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point security and authentication flags. +// These flags describe the current system requirements of an access point as determined from the access point's beacon. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApSecurityFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + PairWep40 = 1, + PairWep104 = 2, + PairTkip = 4, + PairCcmp = 8, + GroupWep40 = 16, + GroupWep104 = 32, + GroupTkip = 64, + GroupCcmp = 128, + KeyMgmtPsk = 256, + KeyMgmt8021x = 512, + KeyMgmtSae = 1024, + KeyMgmtOwe = 2048, + KeyMgmtOweTm = 4096, + KeyMgmtEapSuiteB192 = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the state of a connection to a specific network while it is starting, connected, or disconnected from that network. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState. +class NMConnectionState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Activating = 1, + Activated = 2, + Deactivating = 3, + Deactivated = 4 + }; + Q_ENUM(Enum); +}; + +} // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml new file mode 100644 index 0000000..c5e7737 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml new file mode 100644 index 0000000..fa0e778 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml new file mode 100644 index 0000000..ccfe333 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml new file mode 100644 index 0000000..322635f --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml new file mode 100644 index 0000000..0283847 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml new file mode 100644 index 0000000..d4470ea --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp new file mode 100644 index 0000000..0be29e5 --- /dev/null +++ b/src/network/nm/utils.cpp @@ -0,0 +1,248 @@ +#include "utils.hpp" + +// We depend on non-std Linux extensions that ctime doesn't put in the global namespace +// NOLINTNEXTLINE(modernize-deprecated-headers) +#include + +#include +#include +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { + const QVariantMap& security = settings.value("802-11-wireless-security"); + if (security.isEmpty()) { + return WifiSecurityType::Open; + }; + + const QString keyMgmt = security["key-mgmt"].toString(); + const QString authAlg = security["auth-alg"].toString(); + const QList proto = security["proto"].toList(); + + if (keyMgmt == "none") { + return WifiSecurityType::StaticWep; + } else if (keyMgmt == "ieee8021x") { + if (authAlg == "leap") { + return WifiSecurityType::Leap; + } else { + return WifiSecurityType::DynamicWep; + } + } else if (keyMgmt == "wpa-psk") { + if (proto.contains("wpa") && proto.contains("rsn")) return WifiSecurityType::WpaPsk; + return WifiSecurityType::Wpa2Psk; + } else if (keyMgmt == "wpa-eap") { + if (proto.contains("wpa") && proto.contains("rsn")) return WifiSecurityType::WpaEap; + return WifiSecurityType::Wpa2Eap; + } else if (keyMgmt == "sae") { + return WifiSecurityType::Sae; + } else if (keyMgmt == "wpa-eap-suite-b-192") { + return WifiSecurityType::Wpa3SuiteB192; + } + return WifiSecurityType::Open; +} + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + WifiSecurityType::Enum type +) { + bool havePair = false; + bool haveGroup = false; + // Device needs to support at least one pairwise and one group cipher + + if (type == WifiSecurityType::StaticWep) { + // Static WEP only uses group ciphers + havePair = true; + } else { + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::PairWep40) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::PairWep104) + { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::PairTkip) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::PairCcmp) { + havePair = true; + } + } + + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::GroupWep40) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::GroupWep104) + { + haveGroup = true; + } + if (type != WifiSecurityType::StaticWep) { + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::GroupTkip) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::GroupCcmp) { + haveGroup = true; + } + } + + return (havePair && haveGroup); +} + +bool securityIsValid( + WifiSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + switch (type) { + case WifiSecurityType::Open: + if (apFlags & NM80211ApFlags::Privacy) return false; + if (apWpa || apRsn) return false; + break; + case WifiSecurityType::Leap: + if (adhoc) return false; + case WifiSecurityType::StaticWep: + if (!(apFlags & NM80211ApFlags::Privacy)) return false; + if (apWpa || apRsn) { + if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::StaticWep)) { + if (!deviceSupportsApCiphers(caps, apRsn, WifiSecurityType::StaticWep)) return false; + } + } + break; + case WifiSecurityType::DynamicWep: + if (adhoc) return false; + if (apRsn || !(apFlags & NM80211ApFlags::Privacy)) return false; + if (apWpa) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::DynamicWep)) return false; + } + break; + case WifiSecurityType::WpaPsk: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Wpa)) return false; + if (apWpa & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apWpa & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apWpa & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + return false; + case WifiSecurityType::Wpa2Psk: + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) return false; + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + } + return false; + case WifiSecurityType::WpaEap: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Wpa)) return false; + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apWpa, WifiSecurityType::WpaEap)) return false; + break; + case WifiSecurityType::Wpa2Eap: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmt8021x)) return false; + if (!deviceSupportsApCiphers(caps, apRsn, WifiSecurityType::Wpa2Eap)) return false; + break; + case WifiSecurityType::Sae: + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) return false; + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtSae) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + } + return false; + case WifiSecurityType::Owe: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtOwe) + && !(apRsn & NM80211ApSecurityFlags::KeyMgmtOweTm)) + { + return false; + } + break; + case WifiSecurityType::Wpa3SuiteB192: + if (adhoc) return false; + if (!(caps & NMWirelessCapabilities::Rsn)) return false; + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtEapSuiteB192)) return false; + break; + default: return false; + } + return true; +} + +WifiSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + // Loop through security types from most to least secure since the enum + // values are sequential and in priority order (0-10, excluding Unknown=11) + for (int i = WifiSecurityType::Wpa3SuiteB192; i <= WifiSecurityType::Open; ++i) { + auto type = static_cast(i); + if (securityIsValid(type, caps, adHoc, apFlags, apWpa, apRsn)) { + return type; + } + } + return WifiSecurityType::Unknown; +} + +// NOLINTBEGIN +QDateTime clockBootTimeToDateTime(qint64 clockBootTime) { + clockid_t clkId = CLOCK_BOOTTIME; + struct timespec tp {}; + + const QDateTime now = QDateTime::currentDateTime(); + int r = clock_gettime(clkId, &tp); + if (r == -1 && errno == EINVAL) { + clkId = CLOCK_MONOTONIC; + r = clock_gettime(clkId, &tp); + } + + // Convert to milliseconds + const qint64 nowInMs = tp.tv_sec * 1000 + tp.tv_nsec / 1000000; + + // Return a QDateTime of the millisecond diff + const qint64 offset = clockBootTime - nowInMs; + return QDateTime::fromMSecsSinceEpoch(now.toMSecsSinceEpoch() + offset); +} +// NOLINTEND + +} // namespace qs::network diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp new file mode 100644 index 0000000..ce8b784 --- /dev/null +++ b/src/network/nm/utils.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + WifiSecurityType::Enum type +); + +// In sync with NetworkManager/libnm-core/nm-utils.c:nm_utils_security_valid() +// Given a set of device capabilities, and a desired security type to check +// against, determines whether the combination of device, desired security type, +// and AP capabilities intersect. +bool securityIsValid( + WifiSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +WifiSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +QDateTime clockBootTimeToDateTime(qint64 clockBootTime); + +} // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp new file mode 100644 index 0000000..9dff14b --- /dev/null +++ b/src/network/nm/wireless.cpp @@ -0,0 +1,457 @@ +#include "wireless.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "dbus_nm_wireless.h" +#include "dbus_types.hpp" +#include "device.hpp" +#include "enums.hpp" +#include "utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent) + : QObject(parent) + , mSsid(std::move(ssid)) + , bKnown(false) + , bSecurity(WifiSecurityType::Unknown) + , bReason(NMConnectionStateReason::None) + , bState(NMConnectionState::Deactivated) {} + +void NMWirelessNetwork::updateReferenceConnection() { + // If the network has no connections, the reference is nullptr. + if (this->mConnections.isEmpty()) { + this->mReferenceConn = nullptr; + this->bSecurity = WifiSecurityType::Unknown; + // Set security back to reference AP. + if (this->mReferenceAp) { + this->bSecurity.setBinding([this]() { return this->mReferenceAp->security(); }); + } + return; + }; + + // If the network has an active connection, use it as the reference. + if (this->mActiveConnection) { + auto* conn = this->mConnections.value(this->mActiveConnection->connection().path()); + if (conn && conn != this->mReferenceConn) { + this->mReferenceConn = conn; + this->bSecurity.setBinding([conn]() { return conn->security(); }); + } + return; + } + + // Otherwise, choose the connection with the strongest security settings. + NMConnectionSettings* selectedConn = nullptr; + for (auto* conn: this->mConnections.values()) { + if (!selectedConn || conn->security() > selectedConn->security()) { + selectedConn = conn; + } + } + if (this->mReferenceConn != selectedConn) { + this->mReferenceConn = selectedConn; + this->bSecurity.setBinding([selectedConn]() { return selectedConn->security(); }); + } +} + +void NMWirelessNetwork::updateReferenceAp() { + // If the network has no APs, the reference is a nullptr. + if (this->mAccessPoints.isEmpty()) { + this->mReferenceAp = nullptr; + this->bSignalStrength = 0; + return; + } + + // Otherwise, choose the AP with the strongest signal. + NMAccessPoint* selectedAp = nullptr; + for (auto* ap: this->mAccessPoints.values()) { + // Always prefer the active AP. + if (ap->path() == this->bActiveApPath) { + selectedAp = ap; + break; + } + if (!selectedAp || ap->signalStrength() > selectedAp->signalStrength()) { + selectedAp = ap; + } + } + if (this->mReferenceAp != selectedAp) { + this->mReferenceAp = selectedAp; + this->bSignalStrength.setBinding([selectedAp]() { return selectedAp->signalStrength(); }); + // Reference AP is used for security when there's no connection settings. + if (!this->mReferenceConn) { + this->bSecurity.setBinding([selectedAp]() { return selectedAp->security(); }); + } + } +} + +void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { + if (this->mAccessPoints.contains(ap->path())) return; + this->mAccessPoints.insert(ap->path(), ap); + auto onDestroyed = [this, ap]() { + if (this->mAccessPoints.take(ap->path())) { + this->updateReferenceAp(); + if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); + } + }; + // clang-format off + QObject::connect(ap, &NMAccessPoint::signalStrengthChanged, this, &NMWirelessNetwork::updateReferenceAp); + QObject::connect(ap, &NMAccessPoint::destroyed, this, onDestroyed); + // clang-format on + this->updateReferenceAp(); +}; + +void NMWirelessNetwork::addConnection(NMConnectionSettings* conn) { + if (this->mConnections.contains(conn->path())) return; + this->mConnections.insert(conn->path(), conn); + auto onDestroyed = [this, conn]() { + if (this->mConnections.take(conn->path())) { + this->updateReferenceConnection(); + if (this->mConnections.isEmpty()) this->bKnown = false; + if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); + } + }; + // clang-format off + QObject::connect(conn, &NMConnectionSettings::securityChanged, this, &NMWirelessNetwork::updateReferenceConnection); + QObject::connect(conn, &NMConnectionSettings::destroyed, this, onDestroyed); + // clang-format on + this->bKnown = true; + this->updateReferenceConnection(); +}; + +void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) { + if (this->mActiveConnection) return; + this->mActiveConnection = active; + this->bState.setBinding([active]() { return active->state(); }); + this->bReason.setBinding([active]() { return active->stateReason(); }); + auto onDestroyed = [this, active]() { + if (this->mActiveConnection && this->mActiveConnection == active) { + this->mActiveConnection = nullptr; + this->updateReferenceConnection(); + this->bState = NMConnectionState::Deactivated; + this->bReason = NMConnectionStateReason::None; + } + }; + QObject::connect(active, &NMActiveConnection::destroyed, this, onDestroyed); + this->updateReferenceConnection(); +}; + +void NMWirelessNetwork::forget() { + if (this->mConnections.isEmpty()) return; + for (auto* conn: this->mConnections.values()) { + conn->forget(); + } +} + +NMWirelessDevice::NMWirelessDevice(const QString& path, QObject* parent) + : NMDevice(path, parent) + , mScanTimer(this) { + this->wirelessProxy = new DBusNMWirelessProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->wirelessProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for wireless device at" << path; + return; + } + + QObject::connect( + &this->wirelessProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMWirelessDevice::initWireless, + Qt::SingleShotConnection + ); + + QObject::connect(&this->mScanTimer, &QTimer::timeout, this, &NMWirelessDevice::onScanTimeout); + this->mScanTimer.setSingleShot(true); + + this->wirelessProperties.setInterface(this->wirelessProxy); + this->wirelessProperties.updateAllViaGetAll(); +} + +void NMWirelessDevice::initWireless() { + // clang-format off + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointAdded); + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointRemoved); + QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded); + QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::scanningChanged, this, &NMWirelessDevice::onScanningChanged); + // clang-format on + this->registerAccessPoints(); +} + +void NMWirelessDevice::onAccessPointAdded(const QDBusObjectPath& path) { + this->registerAccessPoint(path.path()); +} + +void NMWirelessDevice::onAccessPointRemoved(const QDBusObjectPath& path) { + auto* ap = this->mAccessPoints.take(path.path()); + if (!ap) { + qCDebug(logNetworkManager) << "Sent removal signal for" << path.path() + << "which is not registered."; + return; + } + delete ap; +} + +void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) { + const QString ssid = ap->ssid(); + if (!ssid.isEmpty()) { + auto mode = ap->mode(); + if (mode == NM80211Mode::Infra) { + auto* net = this->mNetworks.value(ssid); + if (!net) net = this->registerNetwork(ssid); + net->addAccessPoint(ap); + } + } +} + +void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) { + const ConnectionSettingsMap& settings = conn->settings(); + // Filter connections that aren't wireless or have missing settings + if (settings["connection"]["id"].toString().isEmpty() + || settings["connection"]["uuid"].toString().isEmpty() + || !settings.contains("802-11-wireless") + || settings["802-11-wireless"]["ssid"].toString().isEmpty()) + { + return; + } + + const auto ssid = settings["802-11-wireless"]["ssid"].toString(); + const auto mode = settings["802-11-wireless"]["mode"].toString(); + + if (mode == "infrastructure") { + auto* net = this->mNetworks.value(ssid); + if (!net) net = this->registerNetwork(ssid); + net->addConnection(conn); + + // Check for active connections that loaded before their respective connection settings + auto* active = this->activeConnection(); + if (active && conn->path() == active->connection().path()) { + net->addActiveConnection(active); + } + } + // TODO: Create hotspots when mode == "ap" +} + +void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) { + // Find an exisiting network with connection settings that matches the active + const QString activeConnPath = active->connection().path(); + for (const auto& net: this->mNetworks.values()) { + for (auto* conn: net->connections()) { + if (activeConnPath == conn->path()) { + net->addActiveConnection(active); + return; + } + } + } +} + +void NMWirelessDevice::onScanTimeout() { + const QDateTime now = QDateTime::currentDateTime(); + const QDateTime lastScan = this->bLastScan; + const QDateTime lastScanRequest = this->mLastScanRequest; + + if (lastScan.isValid() && lastScan.msecsTo(now) < this->mScanIntervalMs) { + // Rate limit if backend last scan property updated within the interval + auto diff = static_cast(this->mScanIntervalMs - lastScan.msecsTo(now)); + this->mScanTimer.start(diff); + } else if (lastScanRequest.isValid() && lastScanRequest.msecsTo(now) < this->mScanIntervalMs) { + // Rate limit if frontend changes scanner state within the interval + auto diff = static_cast(this->mScanIntervalMs - lastScanRequest.msecsTo(now)); + this->mScanTimer.start(diff); + } else { + this->wirelessProxy->RequestScan({}); + this->mLastScanRequest = now; + this->mScanTimer.start(this->mScanIntervalMs); + } +} + +void NMWirelessDevice::onScanningChanged(bool scanning) { + scanning ? this->onScanTimeout() : this->mScanTimer.stop(); +} + +void NMWirelessDevice::registerAccessPoints() { + auto pending = this->wirelessProxy->GetAllAccessPoints(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get all access points: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerAccessPoint(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMWirelessDevice::registerAccessPoint(const QString& path) { + if (this->mAccessPoints.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path; + return; + } + + auto* ap = new NMAccessPoint(path, this); + + if (!ap->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete ap; + return; + } + + this->mAccessPoints.insert(path, ap); + QObject::connect( + ap, + &NMAccessPoint::loaded, + this, + [this, ap]() { emit this->accessPointLoaded(ap); }, + Qt::SingleShotConnection + ); + ap->bindableSecurity().setBinding([this, ap]() { + return findBestWirelessSecurity( + this->bCapabilities, + ap->mode() == NM80211Mode::Adhoc, + ap->flags(), + ap->wpaFlags(), + ap->rsnFlags() + ); + }); +} + +NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) { + auto* net = new NMWirelessNetwork(ssid, this); + + // To avoid exposing outdated state to the frontend, filter the backend networks to only show + // the known or currently connected networks when the scanner is off. + auto visible = [this, net]() { + return this->bScanning || net->state() == NMConnectionState::Activated || net->known(); + }; + auto onVisibilityChanged = [this, net](bool visible) { + visible ? this->registerFrontendNetwork(net) : this->removeFrontendNetwork(net); + }; + + net->bindableVisible().setBinding(visible); + net->bindableActiveApPath().setBinding([this]() { return this->activeApPath().path(); }); + QObject::connect(net, &NMWirelessNetwork::disappeared, this, &NMWirelessDevice::removeNetwork); + QObject::connect(net, &NMWirelessNetwork::visibilityChanged, this, onVisibilityChanged); + + this->mNetworks.insert(ssid, net); + if (net->visible()) this->registerFrontendNetwork(net); + return net; +} + +void NMWirelessDevice::registerFrontendNetwork(NMWirelessNetwork* net) { + auto ssid = net->ssid(); + auto* frontendNet = new WifiNetwork(ssid, net); + + // Bind WifiNetwork to NMWirelessNetwork + auto translateSignal = [net]() { return net->signalStrength() / 100.0; }; + auto translateState = [net]() { return net->state() == NMConnectionState::Activated; }; + frontendNet->bindableSignalStrength().setBinding(translateSignal); + frontendNet->bindableConnected().setBinding(translateState); + frontendNet->bindableKnown().setBinding([net]() { return net->known(); }); + frontendNet->bindableNmReason().setBinding([net]() { return net->reason(); }); + frontendNet->bindableSecurity().setBinding([net]() { return net->security(); }); + frontendNet->bindableState().setBinding([net]() { + return static_cast(net->state()); + }); + + QObject::connect(frontendNet, &WifiNetwork::requestConnect, this, [this, net]() { + if (net->referenceConnection()) { + emit this->activateConnection( + QDBusObjectPath(net->referenceConnection()->path()), + QDBusObjectPath(this->path()) + ); + return; + } + if (net->referenceAp()) { + emit this->addAndActivateConnection( + ConnectionSettingsMap(), + QDBusObjectPath(this->path()), + QDBusObjectPath(net->referenceAp()->path()) + ); + } + }); + + QObject::connect( + frontendNet, + &WifiNetwork::requestDisconnect, + this, + &NMWirelessDevice::disconnect + ); + + QObject::connect(frontendNet, &WifiNetwork::requestForget, net, &NMWirelessNetwork::forget); + + this->mFrontendNetworks.insert(ssid, frontendNet); + emit this->networkAdded(frontendNet); +} + +void NMWirelessDevice::removeFrontendNetwork(NMWirelessNetwork* net) { + auto* frontendNet = this->mFrontendNetworks.take(net->ssid()); + if (frontendNet) { + emit this->networkRemoved(frontendNet); + frontendNet->deleteLater(); + } +} + +void NMWirelessDevice::removeNetwork() { + auto* net = qobject_cast(this->sender()); + if (this->mNetworks.take(net->ssid())) { + this->removeFrontendNetwork(net); + delete net; + }; +} + +bool NMWirelessDevice::isValid() const { + return this->NMDevice::isValid() && (this->wirelessProxy && this->wirelessProxy->isValid()); +} + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult DBusDataTransform::fromWire(qint64 wire) { + return DBusResult(qs::network::clockBootTimeToDateTime(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp new file mode 100644 index 0000000..fe4010e --- /dev/null +++ b/src/network/nm/wireless.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "dbus_nm_wireless.h" +#include "device.hpp" +#include "enums.hpp" + +namespace qs::dbus { +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMWirelessCapabilities::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = qint64; + using Data = QDateTime; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus +namespace qs::network { + +// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMConnectionSetting objects. +class NMWirelessNetwork: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr); + + void addAccessPoint(NMAccessPoint* ap); + void addConnection(NMConnectionSettings* conn); + void addActiveConnection(NMActiveConnection* active); + void forget(); + + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] bool known() const { return this->bKnown; }; + [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; }; + [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; }; + [[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; }; + [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); }; + [[nodiscard]] QList connections() const { + return this->mConnections.values(); + } + [[nodiscard]] QBindable bindableActiveApPath() { return &this->bActiveApPath; }; + [[nodiscard]] QBindable bindableVisible() { return &this->bVisible; }; + [[nodiscard]] bool visible() const { return this->bVisible; }; + +signals: + void disappeared(); + void visibilityChanged(bool visible); + void signalStrengthChanged(quint8 signal); + void stateChanged(NMConnectionState::Enum state); + void knownChanged(bool known); + void securityChanged(WifiSecurityType::Enum security); + void reasonChanged(NMConnectionStateReason::Enum reason); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeApPathChanged(QString path); + +private: + void updateReferenceAp(); + void updateReferenceConnection(); + + QString mSsid; + QHash mAccessPoints; + QHash mConnections; + NMAccessPoint* mReferenceAp = nullptr; + NMConnectionSettings* mReferenceConn = nullptr; + NMActiveConnection* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bVisible, &NMWirelessNetwork::visibilityChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, bool, bKnown, &NMWirelessNetwork::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, WifiSecurityType::Enum, bSecurity, &NMWirelessNetwork::securityChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionStateReason::Enum, bReason, &NMWirelessNetwork::reasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionState::Enum, bState, &NMWirelessNetwork::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bSignalStrength, &NMWirelessNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, QString, bActiveApPath, &NMWirelessNetwork::activeApPathChanged); + // clang-format on +}; + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Extends NMDevice to also include members from the org.freedesktop.NetworkManager.Device.Wireless interface +// Owns the lifetime of NMAccessPoints(s), NMWirelessNetwork(s), frontend WifiNetwork(s). +class NMWirelessDevice: public NMDevice { + Q_OBJECT; + +public: + explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const override; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; + [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; }; + [[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; }; + [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; }; + +signals: + void accessPointLoaded(NMAccessPoint* ap); + void accessPointRemoved(NMAccessPoint* ap); + void networkAdded(WifiNetwork* net); + void networkRemoved(WifiNetwork* net); + void lastScanChanged(QDateTime lastScan); + void scanningChanged(bool scanning); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeAccessPointChanged(const QDBusObjectPath& path); + void modeChanged(NM80211Mode::Enum mode); + +private slots: + void onAccessPointAdded(const QDBusObjectPath& path); + void onAccessPointRemoved(const QDBusObjectPath& path); + void onAccessPointLoaded(NMAccessPoint* ap); + void onConnectionLoaded(NMConnectionSettings* conn); + void onActiveConnectionLoaded(NMActiveConnection* active); + void onScanTimeout(); + void onScanningChanged(bool scanning); + +private: + void registerAccessPoint(const QString& path); + void registerFrontendNetwork(NMWirelessNetwork* net); + void removeFrontendNetwork(NMWirelessNetwork* net); + void removeNetwork(); + bool checkVisibility(WifiNetwork* net); + void registerAccessPoints(); + void initWireless(); + NMWirelessNetwork* registerNetwork(const QString& ssid); + + QHash mAccessPoints; + QHash mNetworks; + QHash mFrontendNetworks; + + QDateTime mLastScanRequest; + QTimer mScanTimer; + qint32 mScanIntervalMs = 10001; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, bool, bScanning, &NMWirelessDevice::scanningChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDateTime, bLastScan, &NMWirelessDevice::lastScanChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NMWirelessCapabilities::Enum, bCapabilities, &NMWirelessDevice::capabilitiesChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDBusObjectPath, bActiveAccessPoint, &NMWirelessDevice::activeAccessPointChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NM80211Mode::Enum, bMode, &NMWirelessDevice::modeChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWireless, wirelessProperties); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pLastScan, bLastScan, wirelessProperties, "LastScan"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pCapabilities, bCapabilities, wirelessProperties, "WirelessCapabilities"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pActiveAccessPoint, bActiveAccessPoint, wirelessProperties, "ActiveAccessPoint"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pMode, bMode, wirelessProperties, "Mode"); + // clang-format on + + DBusNMWirelessProxy* wirelessProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/test/manual/network.qml b/src/network/test/manual/network.qml new file mode 100644 index 0000000..0fd0f72 --- /dev/null +++ b/src/network/test/manual/network.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Networking + +FloatingWindow { + color: contentItem.palette.window + + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 + + Column { + Layout.fillWidth: true + RowLayout { + Label { + text: "WiFi" + font.bold: true + font.pointSize: 12 + } + CheckBox { + text: "Software" + checked: Networking.wifiEnabled + onClicked: Networking.wifiEnabled = !Networking.wifiEnabled + } + CheckBox { + enabled: false + text: "Hardware" + checked: Networking.wifiHardwareEnabled + } + } + } + + ListView { + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + model: Networking.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 5 + + ColumnLayout { + RowLayout { + Label { text: modelData.name; font.bold: true } + Label { text: modelData.address } + Label { text: `(Type: ${DeviceType.toString(modelData.type)})` } + } + RowLayout { + Label { + text: DeviceConnectionState.toString(modelData.state) + color: modelData.connected ? palette.link : palette.placeholderText + } + Label { + visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.state == DeviceConnectionState.Connecting || modelData.state == DeviceConnectionState.Disconnecting) + text: `(${NMDeviceState.toString(modelData.nmState)})` + } + Button { + visible: modelData.state == DeviceConnectionState.Connected + text: "Disconnect" + onClicked: modelData.disconnect() + } + CheckBox { + text: "Autoconnect" + checked: modelData.autoconnect + onClicked: modelData.autoconnect = !modelData.autoconnect + } + Label { + text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}` + visible: modelData.type == DeviceType.Wifi + } + CheckBox { + text: "Scanner" + checked: modelData.scannerEnabled + onClicked: modelData.scannerEnabled = !modelData.scannerEnabled + visible: modelData.type === DeviceType.Wifi + } + } + + Repeater { + Layout.fillWidth: true + model: { + if (modelData.type !== DeviceType.Wifi) return [] + return [...modelData.networks.values].sort((a, b) => { + if (a.connected !== b.connected) { + return b.connected - a.connected + } + return b.signalStrength - a.signalStrength + }) + } + + WrapperRectangle { + Layout.fillWidth: true + color: modelData.connected ? palette.highlight : palette.button + border.color: palette.mid + border.width: 1 + margin: 5 + + RowLayout { + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Label { text: modelData.name; font.bold: true } + Label { + text: modelData.known ? "Known" : "" + color: palette.placeholderText + } + } + RowLayout { + Label { + text: `Security: ${WifiSecurityType.toString(modelData.security)}` + color: palette.placeholderText + } + Label { + text: `| Signal strength: ${Math.round(modelData.signalStrength*100)}%` + color: palette.placeholderText + } + } + Label { + visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.nmReason != NMConnectionStateReason.Unknown && modelData.nmReason != NMConnectionStateReason.None) + text: `Connection change reason: ${NMConnectionStateReason.toString(modelData.nmReason)}` + } + } + RowLayout { + Layout.alignment: Qt.AlignRight + Button { + text: "Connect" + onClicked: modelData.connect() + visible: !modelData.connected + } + Button { + text: "Disconnect" + onClicked: modelData.disconnect() + visible: modelData.connected + } + Button { + text: "Forget" + onClicked: modelData.forget() + visible: modelData.known + } + } + } + } + } + } + } + } + } +} diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp new file mode 100644 index 0000000..dcd20f6 --- /dev/null +++ b/src/network/wifi.cpp @@ -0,0 +1,139 @@ +#include "wifi.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" +#include "device.hpp" +#include "network.hpp" + +namespace qs::network { + +namespace { +QS_LOGGING_CATEGORY(logWifi, "quickshell.network.wifi", QtWarningMsg); +} // namespace + +QString WifiSecurityType::toString(WifiSecurityType::Enum type) { + switch (type) { + case Unknown: return QStringLiteral("Unknown"); + case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); + case Sae: return QStringLiteral("WPA3"); + case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); + case Wpa2Psk: return QStringLiteral("WPA2"); + case WpaEap: return QStringLiteral("WPA Enterprise"); + case WpaPsk: return QStringLiteral("WPA"); + case StaticWep: return QStringLiteral("WEP"); + case DynamicWep: return QStringLiteral("Dynamic WEP"); + case Leap: return QStringLiteral("LEAP"); + case Owe: return QStringLiteral("OWE"); + case Open: return QStringLiteral("Open"); + default: return QStringLiteral("Unknown"); + } +} + +QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) { + switch (mode) { + case Unknown: return QStringLiteral("Unknown"); + case AdHoc: return QStringLiteral("Ad-Hoc"); + case Station: return QStringLiteral("Station"); + case AccessPoint: return QStringLiteral("Access Point"); + case Mesh: return QStringLiteral("Mesh"); + default: return QStringLiteral("Unknown"); + }; +} + +QString NMConnectionStateReason::toString(NMConnectionStateReason::Enum reason) { + switch (reason) { + case Unknown: return QStringLiteral("Unknown"); + case None: return QStringLiteral("No reason"); + case UserDisconnected: return QStringLiteral("User disconnection"); + case DeviceDisconnected: + return QStringLiteral("The device the connection was using was disconnected."); + case ServiceStopped: + return QStringLiteral("The service providing the VPN connection was stopped."); + case IpConfigInvalid: + return QStringLiteral("The IP config of the active connection was invalid."); + case ConnectTimeout: + return QStringLiteral("The connection attempt to the VPN service timed out."); + case ServiceStartTimeout: + return QStringLiteral( + "A timeout occurred while starting the service providing the VPN connection." + ); + case ServiceStartFailed: + return QStringLiteral("Starting the service providing the VPN connection failed."); + case NoSecrets: return QStringLiteral("Necessary secrets for the connection were not provided."); + case LoginFailed: return QStringLiteral("Authentication to the server failed."); + case ConnectionRemoved: + return QStringLiteral("Necessary secrets for the connection were not provided."); + case DependencyFailed: + return QStringLiteral("Master connection of this connection failed to activate."); + case DeviceRealizeFailed: return QStringLiteral("Could not create the software device link."); + case DeviceRemoved: return QStringLiteral("The device this connection depended on disappeared."); + default: return QStringLiteral("Unknown"); + }; +}; + +WifiNetwork::WifiNetwork(QString ssid, QObject* parent): Network(std::move(ssid), parent) {}; + +void WifiNetwork::connect() { + if (this->bConnected) { + qCCritical(logWifi) << this << "is already connected."; + return; + } + + this->requestConnect(); +} + +void WifiNetwork::disconnect() { + if (!this->bConnected) { + qCCritical(logWifi) << this << "is not currently connected"; + return; + } + + this->requestDisconnect(); +} + +void WifiNetwork::forget() { this->requestForget(); } + +WifiDevice::WifiDevice(QObject* parent): NetworkDevice(DeviceType::Wifi, parent) {}; + +void WifiDevice::setScannerEnabled(bool enabled) { + if (this->bScannerEnabled == enabled) return; + this->bScannerEnabled = enabled; +} + +void WifiDevice::networkAdded(WifiNetwork* net) { this->mNetworks.insertObject(net); } +void WifiDevice::networkRemoved(WifiNetwork* net) { this->mNetworks.removeObject(net); } + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network) { + auto saver = QDebugStateSaver(debug); + + if (network) { + debug.nospace() << "WifiNetwork(" << static_cast(network) + << ", name=" << network->name() << ")"; + } else { + debug << "WifiNetwork(nullptr)"; + } + + return debug; +} + +QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device) { + auto saver = QDebugStateSaver(debug); + + if (device) { + debug.nospace() << "WifiDevice(" << static_cast(device) + << ", name=" << device->name() << ")"; + } else { + debug << "WifiDevice(nullptr)"; + } + + return debug; +} diff --git a/src/network/wifi.hpp b/src/network/wifi.hpp new file mode 100644 index 0000000..15b093d --- /dev/null +++ b/src/network/wifi.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "device.hpp" +#include "network.hpp" + +namespace qs::network { + +///! The security type of a wifi network. +class WifiSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + Open = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiSecurityType::Enum type); +}; + +///! The 802.11 mode of a wifi device. +class WifiDeviceMode: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device is part of an Ad-Hoc network without a central access point. + AdHoc = 0, + /// The device is a station that can connect to networks. + Station = 1, + /// The device is a local hotspot/access point. + AccessPoint = 2, + /// The device is an 802.11s mesh point. + Mesh = 3, + /// The device mode is unknown. + Unknown = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode); +}; + +///! NetworkManager-specific reason for a WifiNetworks connection state. +/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMConnectionStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMConnectionStateReason::Enum reason); +}; + +///! An available wifi network. +class WifiNetwork: public Network { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WifiNetwork can only be acquired through WifiDevice"); + // clang-format off + /// The current signal strength of the network, from 0.0 to 1.0. + Q_PROPERTY(qreal signalStrength READ default NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength); + /// True if the wifi network has known connection settings saved. + Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); + /// The security type of the wifi network. + Q_PROPERTY(WifiSecurityType::Enum security READ default NOTIFY securityChanged BINDABLE bindableSecurity); + /// A specific reason for the connection state when the backend is NetworkManager. + Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ default NOTIFY nmReasonChanged BINDABLE bindableNmReason); + // clang-format on + +public: + explicit WifiNetwork(QString ssid, QObject* parent = nullptr); + + /// Attempt to connect to the wifi network. + /// + /// > [!WARNING] Quickshell does not yet provide a NetworkManager authentication agent, + /// > meaning another agent will need to be active to enter passwords for unsaved networks. + Q_INVOKABLE void connect(); + /// Disconnect from the wifi network. + Q_INVOKABLE void disconnect(); + /// Forget all connection settings for this wifi network. + Q_INVOKABLE void forget(); + + QBindable bindableSignalStrength() { return &this->bSignalStrength; } + QBindable bindableKnown() { return &this->bKnown; } + QBindable bindableNmReason() { return &this->bNmReason; } + QBindable bindableSecurity() { return &this->bSecurity; } + +signals: + void requestConnect(); + void requestDisconnect(); + void requestForget(); + void signalStrengthChanged(); + void knownChanged(); + void securityChanged(); + void nmReasonChanged(); + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, qreal, bSignalStrength, &WifiNetwork::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMConnectionStateReason::Enum, bNmReason, &WifiNetwork::nmReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, WifiSecurityType::Enum, bSecurity, &WifiNetwork::securityChanged); + // clang-format on +}; + +///! Wireless variant of a NetworkDevice. +class WifiDevice: public NetworkDevice { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + + // clang-format off + /// A list of this available and connected wifi networks. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); + /// True when currently scanning for networks. + /// When enabled, the scanner populates the device with an active list of available wifi networks. + Q_PROPERTY(bool scannerEnabled READ scannerEnabled WRITE setScannerEnabled NOTIFY scannerEnabledChanged BINDABLE bindableScannerEnabled); + /// The 802.11 mode the device is in. + Q_PROPERTY(WifiDeviceMode::Enum mode READ default NOTIFY modeChanged BINDABLE bindableMode); + // clang-format on + +public: + explicit WifiDevice(QObject* parent = nullptr); + + void networkAdded(WifiNetwork* net); + void networkRemoved(WifiNetwork* net); + + [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; }; + QBindable bindableScannerEnabled() { return &this->bScannerEnabled; }; + [[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; }; + void setScannerEnabled(bool enabled); + QBindable bindableMode() { return &this->bMode; } + +signals: + void modeChanged(); + void scannerEnabledChanged(bool enabled); + +private: + ObjectModel mNetworks {this}; + Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, bool, bScannerEnabled, &WifiDevice::scannerEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(WifiDevice, WifiDeviceMode::Enum, bMode, &WifiDevice::modeChanged); +}; + +}; // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::WifiNetwork* network); +QDebug operator<<(QDebug debug, const qs::network::WifiDevice* device); From de1bfe028d6982ac9dce08e5063ea5611498b204 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 13 Jan 2026 00:42:35 -0800 Subject: [PATCH 150/226] core/popupwindow: clean up popup lifecycle and window init - Makes popup lifecycle less complex - Creates all QWindows lazily - May break live reloading of open popups to some degree --- src/core/popupanchor.cpp | 18 +++--- src/core/popupanchor.hpp | 8 ++- src/wayland/popupanchor.cpp | 1 - src/window/popupwindow.cpp | 104 ++++++++++++++++---------------- src/window/popupwindow.hpp | 20 ++++-- src/window/proxywindow.cpp | 38 ++++++------ src/window/proxywindow.hpp | 11 ++++ src/window/test/popupwindow.cpp | 27 +++++---- 8 files changed, 127 insertions(+), 100 deletions(-) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index bbcc3a5..151dd5d 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -28,7 +28,7 @@ void PopupAnchor::markClean() { this->lastState = this->state; } void PopupAnchor::markDirty() { this->lastState.reset(); } QWindow* PopupAnchor::backingWindow() const { - return this->mProxyWindow ? this->mProxyWindow->backingWindow() : nullptr; + return this->bProxyWindow ? this->bProxyWindow->backingWindow() : nullptr; } void PopupAnchor::setWindowInternal(QObject* window) { @@ -36,14 +36,14 @@ void PopupAnchor::setWindowInternal(QObject* window) { if (this->mWindow) { QObject::disconnect(this->mWindow, nullptr, this, nullptr); - QObject::disconnect(this->mProxyWindow, nullptr, this, nullptr); + QObject::disconnect(this->bProxyWindow, nullptr, this, nullptr); } if (window) { if (auto* proxy = qobject_cast(window)) { - this->mProxyWindow = proxy; + this->bProxyWindow = proxy; } else if (auto* interface = qobject_cast(window)) { - this->mProxyWindow = interface->proxyWindow(); + this->bProxyWindow = interface->proxyWindow(); } else { qWarning() << "Tried to set popup anchor window to" << window << "which is not a quickshell window."; @@ -55,7 +55,7 @@ void PopupAnchor::setWindowInternal(QObject* window) { QObject::connect(this->mWindow, &QObject::destroyed, this, &PopupAnchor::onWindowDestroyed); QObject::connect( - this->mProxyWindow, + this->bProxyWindow, &ProxyWindowBase::backerVisibilityChanged, this, &PopupAnchor::backingWindowVisibilityChanged @@ -70,7 +70,7 @@ void PopupAnchor::setWindowInternal(QObject* window) { setnull: if (this->mWindow) { this->mWindow = nullptr; - this->mProxyWindow = nullptr; + this->bProxyWindow = nullptr; emit this->windowChanged(); emit this->backingWindowVisibilityChanged(); @@ -100,7 +100,7 @@ void PopupAnchor::setItem(QQuickItem* item) { void PopupAnchor::onWindowDestroyed() { this->mWindow = nullptr; - this->mProxyWindow = nullptr; + this->bProxyWindow = nullptr; emit this->windowChanged(); emit this->backingWindowVisibilityChanged(); } @@ -186,11 +186,11 @@ void PopupAnchor::updatePlacement(const QPoint& anchorpoint, const QSize& size) } void PopupAnchor::updateAnchor() { - if (this->mItem && this->mProxyWindow) { + if (this->mItem && this->bProxyWindow) { auto baseRect = this->mUserRect.isEmpty() ? this->mItem->boundingRect() : this->mUserRect.qrect(); - auto rect = this->mProxyWindow->contentItem()->mapFromItem( + auto rect = this->bProxyWindow->contentItem()->mapFromItem( this->mItem, baseRect.marginsRemoved(this->mMargins.qmargins()) ); diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index a9b121e..9f08512 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -139,7 +140,9 @@ public: void markDirty(); [[nodiscard]] QObject* window() const { return this->mWindow; } - [[nodiscard]] ProxyWindowBase* proxyWindow() const { return this->mProxyWindow; } + [[nodiscard]] QBindable bindableProxyWindow() const { + return &this->bProxyWindow; + } [[nodiscard]] QWindow* backingWindow() const; void setWindowInternal(QObject* window); void setWindow(QObject* window); @@ -193,11 +196,12 @@ private slots: private: QObject* mWindow = nullptr; QQuickItem* mItem = nullptr; - ProxyWindowBase* mProxyWindow = nullptr; PopupAnchorState state; Box mUserRect; Margins mMargins; std::optional lastState; + + Q_OBJECT_BINDABLE_PROPERTY(PopupAnchor, ProxyWindowBase*, bProxyWindow); }; class PopupPositioner { diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index cbbccae..14e1923 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -16,7 +16,6 @@ using XdgPositioner = QtWayland::xdg_positioner; using qs::wayland::xdg_shell::XdgWmBase; void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool onlyIfDirty) { - auto* waylandWindow = dynamic_cast(window->handle()); auto* popupRole = waylandWindow ? waylandWindow->surfaceRole<::xdg_popup>() : nullptr; diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index a1ae448..0b63416 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -12,29 +12,74 @@ ProxyPopupWindow::ProxyPopupWindow(QObject* parent): ProxyWindowBase(parent) { this->mVisible = false; + // clang-format off - QObject::connect(&this->mAnchor, &PopupAnchor::windowChanged, this, &ProxyPopupWindow::parentWindowChanged); + QObject::connect(&this->mAnchor, &PopupAnchor::windowChanged, this, &ProxyPopupWindow::onParentWindowChanged); QObject::connect(&this->mAnchor, &PopupAnchor::windowRectChanged, this, &ProxyPopupWindow::reposition); QObject::connect(&this->mAnchor, &PopupAnchor::edgesChanged, this, &ProxyPopupWindow::reposition); QObject::connect(&this->mAnchor, &PopupAnchor::gravityChanged, this, &ProxyPopupWindow::reposition); QObject::connect(&this->mAnchor, &PopupAnchor::adjustmentChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(&this->mAnchor, &PopupAnchor::backingWindowVisibilityChanged, this, &ProxyPopupWindow::onParentUpdated); // clang-format on + + this->bTargetVisible.setBinding([this] { + auto* window = this->mAnchor.bindableProxyWindow().value(); + + if (window == this) { + qmlWarning(this) << "Anchor assigned to current window"; + return false; + } + + if (!window) return false; + + if (!this->bWantsVisible) return false; + return window->bindableBackerVisibility().value(); + }); +} + +void ProxyPopupWindow::targetVisibleChanged() { + this->ProxyWindowBase::setVisible(this->bTargetVisible); } void ProxyPopupWindow::completeWindow() { this->ProxyWindowBase::completeWindow(); // clang-format off - QObject::connect(this->window, &QWindow::visibleChanged, this, &ProxyPopupWindow::onVisibleChanged); + QObject::connect(this, &ProxyWindowBase::closed, this, &ProxyPopupWindow::onClosed); QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); // clang-format on + auto* bw = this->mAnchor.backingWindow(); + + if (bw && PopupPositioner::instance()->shouldRepositionOnMove()) { + QObject::connect(bw, &QWindow::xChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::yChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); + QObject::connect(bw, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); + } + + this->window->setTransientParent(bw); this->window->setFlag(Qt::ToolTip); + + this->mAnchor.markDirty(); + PopupPositioner::instance()->reposition(&this->mAnchor, this->window); } -void ProxyPopupWindow::postCompleteWindow() { this->updateTransientParent(); } +void ProxyPopupWindow::postCompleteWindow() { + this->ProxyWindowBase::setVisible(this->bTargetVisible); +} + +void ProxyPopupWindow::onClosed() { this->bWantsVisible = false; } + +void ProxyPopupWindow::onParentWindowChanged() { + // recreate for new parent + if (this->bTargetVisible && this->isVisibleDirect()) { + this->ProxyWindowBase::setVisibleDirect(false); + this->ProxyWindowBase::setVisibleDirect(true); + } + + emit this->parentWindowChanged(); +} void ProxyPopupWindow::setParentWindow(QObject* parent) { qmlWarning(this) << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; @@ -43,60 +88,13 @@ void ProxyPopupWindow::setParentWindow(QObject* parent) { QObject* ProxyPopupWindow::parentWindow() const { return this->mAnchor.window(); } -void ProxyPopupWindow::updateTransientParent() { - auto* bw = this->mAnchor.backingWindow(); - - if (this->window != nullptr && bw != this->window->transientParent()) { - if (this->window->transientParent()) { - QObject::disconnect(this->window->transientParent(), nullptr, this, nullptr); - } - - if (bw && PopupPositioner::instance()->shouldRepositionOnMove()) { - QObject::connect(bw, &QWindow::xChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(bw, &QWindow::yChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(bw, &QWindow::widthChanged, this, &ProxyPopupWindow::reposition); - QObject::connect(bw, &QWindow::heightChanged, this, &ProxyPopupWindow::reposition); - } - - this->window->setTransientParent(bw); - } - - this->updateVisible(); -} - -void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } - void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { qmlWarning( this ) << "Cannot set screen of popup window, as that is controlled by the parent window"; } -void ProxyPopupWindow::setVisible(bool visible) { - if (visible == this->wantsVisible) return; - this->wantsVisible = visible; - this->updateVisible(); -} - -void ProxyPopupWindow::updateVisible() { - auto target = this->wantsVisible && this->mAnchor.window() != nullptr - && this->mAnchor.proxyWindow()->isVisibleDirect(); - - if (target && this->window != nullptr && !this->window->isVisible()) { - PopupPositioner::instance()->reposition(&this->mAnchor, this->window); - } - - this->ProxyWindowBase::setVisible(target); -} - -void ProxyPopupWindow::onVisibleChanged() { - // If the window was made invisible without its parent becoming invisible - // the compositor probably destroyed it. Without this the window won't ever - // be able to become visible again. - if (this->window->transientParent() && this->window->transientParent()->isVisible()) { - this->wantsVisible = this->window->isVisible(); - } -} +void ProxyPopupWindow::setVisible(bool visible) { this->bWantsVisible = visible; } void ProxyPopupWindow::setRelativeX(qint32 x) { qmlWarning(this) << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; @@ -144,3 +142,5 @@ void ProxyPopupWindow::onPolished() { } } } + +bool ProxyPopupWindow::deleteOnInvisible() const { return true; } diff --git a/src/window/popupwindow.hpp b/src/window/popupwindow.hpp index e00495c..6e5cfd8 100644 --- a/src/window/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -88,6 +89,7 @@ public: void completeWindow() override; void postCompleteWindow() override; void onPolished() override; + bool deleteOnInvisible() const override; void setScreen(QuickshellScreenInfo* screen) override; void setVisible(bool visible) override; @@ -109,16 +111,24 @@ signals: void relativeYChanged(); private slots: - void onVisibleChanged(); - void onParentUpdated(); + void onParentWindowChanged(); + void onClosed(); void reposition(); private: + void targetVisibleChanged(); + QQuickWindow* parentBackingWindow(); - void updateTransientParent(); - void updateVisible(); PopupAnchor mAnchor {this}; - bool wantsVisible = false; bool pendingReposition = false; + + Q_OBJECT_BINDABLE_PROPERTY(ProxyPopupWindow, bool, bWantsVisible); + + Q_OBJECT_BINDABLE_PROPERTY( + ProxyPopupWindow, + bool, + bTargetVisible, + &ProxyPopupWindow::targetVisibleChanged + ); }; diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index ea2904b..3cc4378 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -57,9 +57,10 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } void ProxyWindowBase::onReload(QObject* oldInstance) { - this->window = this->retrieveWindow(oldInstance); + if (this->mVisible) this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); - this->ensureQWindow(); + + if (this->mVisible) this->ensureQWindow(); // The qml engine will leave the WindowInterface as owner of everything // nested in an item, so we have to make sure the interface's children @@ -76,17 +77,21 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { Reloadable::reloadChildrenRecursive(this, oldInstance); - this->connectWindow(); - this->completeWindow(); + if (this->mVisible) { + this->connectWindow(); + this->completeWindow(); + } this->reloadComplete = true; - emit this->windowConnected(); - this->postCompleteWindow(); + if (this->mVisible) { + emit this->windowConnected(); + this->postCompleteWindow(); - if (wasVisible && this->isVisibleDirect()) { - emit this->backerVisibilityChanged(); - this->onExposed(); + if (wasVisible && this->isVisibleDirect()) { + this->bBackerVisibility = true; + this->onExposed(); + } } } @@ -272,24 +277,21 @@ void ProxyWindowBase::setVisible(bool visible) { void ProxyWindowBase::setVisibleDirect(bool visible) { if (this->deleteOnInvisible()) { - if (visible == this->isVisibleDirect()) return; - if (visible) { + if (visible == this->isVisibleDirect()) return; this->createWindow(); this->polishItems(); this->window->setVisible(true); - emit this->backerVisibilityChanged(); + this->bBackerVisibility = true; } else { - if (this->window != nullptr) { - this->window->setVisible(false); - emit this->backerVisibilityChanged(); - this->deleteWindow(); - } + if (this->window != nullptr) this->window->setVisible(false); + this->bBackerVisibility = false; + this->deleteWindow(); } } else if (this->window != nullptr) { if (visible) this->polishItems(); this->window->setVisible(visible); - emit this->backerVisibilityChanged(); + this->bBackerVisibility = visible; } } diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 025b970..86d66f8 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -101,6 +101,10 @@ public: virtual void setVisible(bool visible); virtual void setVisibleDirect(bool visible); + [[nodiscard]] QBindable bindableBackerVisibility() const { + return &this->bBackerVisibility; + } + void schedulePolish(); [[nodiscard]] virtual qint32 x() const; @@ -206,6 +210,13 @@ protected: &ProxyWindowBase::implicitHeightChanged ); + Q_OBJECT_BINDABLE_PROPERTY( + ProxyWindowBase, + bool, + bBackerVisibility, + &ProxyWindowBase::backerVisibilityChanged + ); + private: void polishItems(); void updateMask(); diff --git a/src/window/test/popupwindow.cpp b/src/window/test/popupwindow.cpp index 1262044..f9498d2 100644 --- a/src/window/test/popupwindow.cpp +++ b/src/window/test/popupwindow.cpp @@ -13,7 +13,7 @@ void TestPopupWindow::initiallyVisible() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -33,7 +33,7 @@ void TestPopupWindow::reloadReparent() { // NOLINT win2->setVisible(true); parent.setVisible(true); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -43,7 +43,7 @@ void TestPopupWindow::reloadReparent() { // NOLINT auto newParent = ProxyWindowBase(); auto newPopup = ProxyPopupWindow(); - newPopup.setParentWindow(&newParent); + newPopup.anchor()->setWindow(&newParent); newPopup.setVisible(true); auto* oldWindow = popup.backingWindow(); @@ -66,7 +66,7 @@ void TestPopupWindow::reloadUnparent() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -80,8 +80,7 @@ void TestPopupWindow::reloadUnparent() { // NOLINT newPopup.reload(&popup); QVERIFY(!newPopup.isVisible()); - QVERIFY(!newPopup.backingWindow()->isVisible()); - QCOMPARE(newPopup.backingWindow()->transientParent(), nullptr); + QVERIFY(!newPopup.backingWindow() || !newPopup.backingWindow()->isVisible()); } void TestPopupWindow::invisibleWithoutParent() { // NOLINT @@ -97,9 +96,11 @@ void TestPopupWindow::moveWithParent() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); - popup.setRelativeX(10); - popup.setRelativeY(10); + popup.anchor()->setWindow(&parent); + auto rect = popup.anchor()->rect(); + rect.x = 10; + rect.y = 10; + popup.anchor()->setRect(rect); popup.setVisible(true); parent.reload(); @@ -126,7 +127,7 @@ void TestPopupWindow::attachParentLate() { // NOLINT QVERIFY(!popup.isVisible()); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); QVERIFY(popup.isVisible()); QVERIFY(popup.backingWindow()->isVisible()); QCOMPARE(popup.backingWindow()->transientParent(), parent.backingWindow()); @@ -136,7 +137,7 @@ void TestPopupWindow::reparentLate() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); @@ -151,7 +152,7 @@ void TestPopupWindow::reparentLate() { // NOLINT parent2.backingWindow()->setX(10); parent2.backingWindow()->setY(10); - popup.setParentWindow(&parent2); + popup.anchor()->setWindow(&parent2); QVERIFY(popup.isVisible()); QVERIFY(popup.backingWindow()->isVisible()); QCOMPARE(popup.backingWindow()->transientParent(), parent2.backingWindow()); @@ -163,7 +164,7 @@ void TestPopupWindow::xMigrationFix() { // NOLINT auto parent = ProxyWindowBase(); auto popup = ProxyPopupWindow(); - popup.setParentWindow(&parent); + popup.anchor()->setWindow(&parent); popup.setVisible(true); parent.reload(); From dca652366ad0d005ac8ec7991417d88afcbcbd60 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 13 Jan 2026 01:24:20 -0800 Subject: [PATCH 151/226] core/popupwindow: add grabFocus Allows standard wayland focus grabs on non hyprland compositors. --- changelog/next.md | 1 + src/window/popupwindow.cpp | 2 +- src/window/popupwindow.hpp | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/changelog/next.md b/changelog/next.md index 05399e5..0cdff57 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -21,6 +21,7 @@ set shell id. - Pipewire service now reconnects if pipewire dies or a protocol error occurs. - Added pipewire audio peak detection. - Added initial support for network management. +- Added support for grabbing focus from popup windows. ## Other Changes diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index 0b63416..0b35948 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -59,7 +59,7 @@ void ProxyPopupWindow::completeWindow() { } this->window->setTransientParent(bw); - this->window->setFlag(Qt::ToolTip); + this->window->setFlag(this->bWantsGrab ? Qt::Popup : Qt::ToolTip); this->mAnchor.markDirty(); PopupPositioner::instance()->reposition(&this->mAnchor, this->window); diff --git a/src/window/popupwindow.hpp b/src/window/popupwindow.hpp index 6e5cfd8..d95eac0 100644 --- a/src/window/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -76,6 +76,15 @@ class ProxyPopupWindow: public ProxyWindowBase { /// /// The popup will not be shown until @@anchor is valid, regardless of this property. QSDOC_PROPERTY_OVERRIDE(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); + /// If true, the popup window will be dismissed and @@visible will change to false + /// if the user clicks outside of the popup or it is otherwise closed. + /// + /// > [!WARNING] Changes to this property while the window is open will only take + /// > effect after the window is hidden and shown again. + /// + /// > [!NOTE] Under Hyprland, @@Quickshell.Hyprland.HyprlandFocusGrab provides more advanced + /// > functionality such as detecting clicks outside without closing the popup. + Q_PROPERTY(bool grabFocus READ default WRITE default NOTIFY grabFocusChanged BINDABLE bindableGrabFocus); /// The screen that the window currently occupies. /// /// This may be modified to move the window to the given screen. @@ -103,12 +112,15 @@ public: [[nodiscard]] qint32 relativeY() const; void setRelativeY(qint32 y); + [[nodiscard]] QBindable bindableGrabFocus() { return &this->bWantsGrab; } + [[nodiscard]] PopupAnchor* anchor(); signals: void parentWindowChanged(); void relativeXChanged(); void relativeYChanged(); + void grabFocusChanged(); private slots: void onParentWindowChanged(); @@ -131,4 +143,6 @@ private: bTargetVisible, &ProxyPopupWindow::targetVisibleChanged ); + + Q_OBJECT_BINDABLE_PROPERTY(ProxyPopupWindow, bool, bWantsGrab); }; From 783b97152a25739340845c479d539bdd2a7c4d9c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 13 Jan 2026 23:20:55 -0800 Subject: [PATCH 152/226] build: update CI, nix checkouts, lints --- .github/workflows/build.yml | 2 +- ci/nix-checkouts.nix | 5 +++++ flake.lock | 6 +++--- src/bluetooth/adapter.cpp | 1 - src/bluetooth/device.cpp | 1 - src/core/scan.cpp | 1 - src/debug/lint.cpp | 2 +- src/io/ipccomm.cpp | 1 - src/launch/command.cpp | 1 - src/network/device.cpp | 2 +- src/network/network.cpp | 2 +- src/network/wifi.cpp | 1 - src/services/pam/conversation.cpp | 1 - src/services/pipewire/node.cpp | 3 --- src/services/polkit/listener.cpp | 2 -- src/services/status_notifier/item.cpp | 1 - src/services/upower/powerprofiles.cpp | 2 +- src/wayland/buffer/dmabuf.cpp | 1 - src/wayland/hyprland/surface/surface.cpp | 1 - src/window/popupwindow.cpp | 1 + 20 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc6e8a7..66c3691 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Nix strategy: matrix: - qtver: [qt6.10.0, 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] + qtver: [qt6.10.1, qt6.10.0, 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: diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix index 8ef997d..945973c 100644 --- a/ci/nix-checkouts.nix +++ b/ci/nix-checkouts.nix @@ -10,6 +10,11 @@ let in rec { latest = qt6_10_0; + qt6_10_1 = byCommit { + commit = "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38"; + sha256 = "0fvbizl7j5rv2rf8j76yw0xb3d9l06hahkjys2a7k1yraznvnafm"; + }; + qt6_10_0 = byCommit { commit = "c5ae371f1a6a7fd27823bc500d9390b38c05fa55"; sha256 = "18g0f8cb9m8mxnz9cf48sks0hib79b282iajl2nysyszph993yp0"; diff --git a/flake.lock b/flake.lock index 7470161..2f95a44 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1762977756, - "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", + "lastModified": 1768127708, + "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", + "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "type": "github" }, "original": { diff --git a/src/bluetooth/adapter.cpp b/src/bluetooth/adapter.cpp index 0d8a319..7f70a27 100644 --- a/src/bluetooth/adapter.cpp +++ b/src/bluetooth/adapter.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "../core/logcat.hpp" diff --git a/src/bluetooth/device.cpp b/src/bluetooth/device.cpp index 7265b24..b140aa0 100644 --- a/src/bluetooth/device.cpp +++ b/src/bluetooth/device.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 9a7ee7e..453b7dc 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include "logcat.hpp" diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp index dd65a28..5e12f76 100644 --- a/src/debug/lint.cpp +++ b/src/debug/lint.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include "../core/logcat.hpp" diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 7203a30..6c7e4f6 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -1,5 +1,4 @@ #include "ipccomm.hpp" -#include #include #include diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 94fe239..1a58cb8 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include diff --git a/src/network/device.cpp b/src/network/device.cpp index a47a5ee..22e3949 100644 --- a/src/network/device.cpp +++ b/src/network/device.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include "../core/logcat.hpp" diff --git a/src/network/network.cpp b/src/network/network.cpp index 67ed6a5..e325b05 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include "../core/logcat.hpp" diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp index dcd20f6..57fb8ea 100644 --- a/src/network/wifi.cpp +++ b/src/network/wifi.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "../core/logcat.hpp" #include "device.hpp" diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index a9d498b..f8f5a09 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -1,5 +1,4 @@ #include "conversation.hpp" -#include #include #include diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index 1b396af..b6f0529 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -90,8 +90,6 @@ QString PwAudioChannel::toString(Enum value) { QString PwNodeType::toString(PwNodeType::Flags type) { switch (type) { - // qstringliteral apparently not imported... - // NOLINTBEGIN case PwNodeType::VideoSource: return QStringLiteral("VideoSource"); case PwNodeType::VideoSink: return QStringLiteral("VideoSink"); case PwNodeType::AudioSource: return QStringLiteral("AudioSource"); @@ -101,7 +99,6 @@ QString PwNodeType::toString(PwNodeType::Flags type) { case PwNodeType::AudioInStream: return QStringLiteral("AudioInStream"); case PwNodeType::Untracked: return QStringLiteral("Untracked"); default: return QStringLiteral("Invalid"); - // NOLINTEND } } diff --git a/src/services/polkit/listener.cpp b/src/services/polkit/listener.cpp index 875cff6..e4bca4c 100644 --- a/src/services/polkit/listener.cpp +++ b/src/services/polkit/listener.cpp @@ -1,6 +1,4 @@ #include "listener.hpp" -#include -#include #include #include #include diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 650c812..17404e1 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp index 8fa91cc..f59b871 100644 --- a/src/services/upower/powerprofiles.cpp +++ b/src/services/upower/powerprofiles.cpp @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include "../../core/logcat.hpp" #include "../../dbus/bus.hpp" diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index a5f219e..e51a1d0 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -1,7 +1,6 @@ #include "dmabuf.hpp" #include #include -#include #include #include #include diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp index f49ab8f..774acd0 100644 --- a/src/wayland/hyprland/surface/surface.cpp +++ b/src/wayland/hyprland/surface/surface.cpp @@ -1,5 +1,4 @@ #include "surface.hpp" -#include #include #include diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index 0b35948..bfe261e 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include From d03c59768c680f052dff6e7a7918bbf990b0f743 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 15 Jan 2026 23:04:10 -0800 Subject: [PATCH 153/226] io/ipchandler: add signal listener support --- changelog/next.md | 2 + src/io/CMakeLists.txt | 2 +- src/io/ipc.cpp | 12 ++++ src/io/ipc.hpp | 25 ++++++-- src/io/ipccomm.cpp | 108 ++++++++++++++++++++++++++++++-- src/io/ipccomm.hpp | 50 +++++++++++++++ src/io/ipchandler.cpp | 120 +++++++++++++++++++++++++++++++++--- src/io/ipchandler.hpp | 82 +++++++++++++++++++++++- src/ipc/ipc.cpp | 6 ++ src/ipc/ipccommand.hpp | 1 + src/launch/command.cpp | 4 ++ src/launch/launch_p.hpp | 2 + src/launch/parsecommand.cpp | 11 ++++ 13 files changed, 405 insertions(+), 20 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 0cdff57..cab03e6 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -22,6 +22,7 @@ set shell id. - Added pipewire audio peak detection. - Added initial support for network management. - Added support for grabbing focus from popup windows. +- Added support for IPC signal listeners. ## Other Changes @@ -40,6 +41,7 @@ set shell id. - Fixed missing signals for system tray item title and description updates. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. +- Fixed memory leak in IPC handlers. ## Packaging Changes diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 17628d3..991beaa 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -24,7 +24,7 @@ qt_add_qml_module(quickshell-io qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-io) -target_link_libraries(quickshell-io PRIVATE Qt::Quick) +target_link_libraries(quickshell-io PRIVATE Qt::Quick quickshell-ipc) target_link_libraries(quickshell PRIVATE quickshell-ioplugin) qs_module_pch(quickshell-io) diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp index 768299e..c381567 100644 --- a/src/io/ipc.cpp +++ b/src/io/ipc.cpp @@ -190,6 +190,14 @@ QString WirePropertyDefinition::toString() const { return "property " % this->name % ": " % this->type; } +QString WireSignalDefinition::toString() const { + if (this->rettype.isEmpty()) { + return "signal " % this->name % "()"; + } else { + return "signal " % this->name % "(" % this->retname % ": " % this->rettype % ')'; + } +} + QString WireTargetDefinition::toString() const { QString accum = "target " % this->name; @@ -201,6 +209,10 @@ QString WireTargetDefinition::toString() const { accum += "\n " % prop.toString(); } + for (const auto& sig: this->signalFunctions) { + accum += "\n " % sig.toString(); + } + return accum; } diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index d2b865a..32486d6 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -146,14 +146,31 @@ struct WirePropertyDefinition { DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); -struct WireTargetDefinition { +struct WireSignalDefinition { QString name; - QVector functions; - QVector properties; + QString retname; + QString rettype; [[nodiscard]] QString toString() const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); +DEFINE_SIMPLE_DATASTREAM_OPS(WireSignalDefinition, data.name, data.retname, data.rettype); + +struct WireTargetDefinition { + QString name; + QVector functions; + QVector properties; + QVector signalFunctions; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS( + WireTargetDefinition, + data.name, + data.functions, + data.properties, + data.signalFunctions +); } // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 6c7e4f6..03b688a 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -1,9 +1,11 @@ #include "ipccomm.hpp" +#include #include #include #include #include +#include #include #include @@ -18,10 +20,6 @@ using namespace qs::ipc; namespace qs::io::ipc::comm { -struct NoCurrentGeneration: std::monostate {}; -struct TargetNotFound: std::monostate {}; -struct EntryNotFound: std::monostate {}; - using QueryResponse = std::variant< std::monostate, NoCurrentGeneration, @@ -313,4 +311,106 @@ int getProperty(IpcClient* client, const QString& target, const QString& propert return -1; } +int listenToSignal(IpcClient* client, const QString& target, const QString& signal, bool once) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to listen for signals."; + return -1; + } else if (signal.isEmpty()) { + qCCritical(logBare) << "Signal required to listen."; + return -1; + } + + client->sendMessage(IpcCommand(SignalListenCommand {.target = target, .signal = signal})); + + while (true) { + SignalListenResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + QTextStream(stdout) << result.response << Qt::endl; + if (once) return 0; + else continue; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Signal not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + break; + } + + return -1; +} + +void SignalListenCommand::exec(qs::ipc::IpcServerConnection* conn) { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + auto* handler = registry->findHandler(this->target); + if (!handler) { + resp << TargetNotFound(); + return; + } + + auto* signal = handler->findSignal(this->signal); + if (!signal) { + resp << EntryNotFound(); + return; + } + + new RemoteSignalListener(conn, *this); + } else { + conn->respond(SignalListenResponse(NoCurrentGeneration())); + } +} + +RemoteSignalListener::RemoteSignalListener( + qs::ipc::IpcServerConnection* conn, + SignalListenCommand command +) + : conn(conn) + , command(std::move(command)) { + conn->setParent(this); + + QObject::connect( + IpcSignalRemoteListener::instance(), + &IpcSignalRemoteListener::triggered, + this, + &RemoteSignalListener::onSignal + ); + + QObject::connect( + conn, + &qs::ipc::IpcServerConnection::destroyed, + this, + &RemoteSignalListener::onConnDestroyed + ); + + qCDebug(logIpc) << "Remote listener created for" << this->command.target << this->command.signal + << ":" << this; +} + +RemoteSignalListener::~RemoteSignalListener() { + qCDebug(logIpc) << "Destroying remote listener" << this; +} + +void RemoteSignalListener::onSignal( + const QString& target, + const QString& signal, + const QString& value +) { + if (target != this->command.target || signal != this->command.signal) return; + qCDebug(logIpc) << "Remote signal" << signal << "triggered on" << target << "with value" << value; + + this->conn->respond(SignalListenResponse(SignalResponse {.response = value})); +} + +void RemoteSignalListener::onConnDestroyed() { this->deleteLater(); } + } // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index bc7dbf9..ac12979 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include "../ipc/ipc.hpp" @@ -48,4 +50,52 @@ DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property); int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); +struct SignalListenCommand { + QString target; + QString signal; + + void exec(qs::ipc::IpcServerConnection* conn); +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalListenCommand, data.target, data.signal); + +int listenToSignal( + qs::ipc::IpcClient* client, + const QString& target, + const QString& signal, + bool once +); + +struct NoCurrentGeneration: std::monostate {}; +struct TargetNotFound: std::monostate {}; +struct EntryNotFound: std::monostate {}; + +struct SignalResponse { + QString response; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalResponse, data.response); + +using SignalListenResponse = std:: + variant; + +class RemoteSignalListener: public QObject { + Q_OBJECT; + +public: + explicit RemoteSignalListener(qs::ipc::IpcServerConnection* conn, SignalListenCommand command); + + ~RemoteSignalListener() override; + + Q_DISABLE_COPY_MOVE(RemoteSignalListener); + +private slots: + void onSignal(const QString& target, const QString& signal, const QString& value); + void onConnDestroyed(); + +private: + qs::ipc::IpcServerConnection* conn; + SignalListenCommand command; +}; + } // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 5ffa0ad..e80cf4b 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -1,5 +1,7 @@ #include "ipchandler.hpp" #include +#include +#include #include #include @@ -139,6 +141,75 @@ WirePropertyDefinition IpcProperty::wireDef() const { return wire; } +WireSignalDefinition IpcSignal::wireDef() const { + WireSignalDefinition wire; + wire.name = this->signal.name(); + if (this->targetSlot != IpcSignalListener::SLOT_VOID) { + wire.retname = this->signal.parameterNames().value(0); + if (this->targetSlot == IpcSignalListener::SLOT_STRING) wire.rettype = "string"; + else if (this->targetSlot == IpcSignalListener::SLOT_INT) wire.rettype = "int"; + else if (this->targetSlot == IpcSignalListener::SLOT_BOOL) wire.rettype = "bool"; + else if (this->targetSlot == IpcSignalListener::SLOT_REAL) wire.rettype = "real"; + else if (this->targetSlot == IpcSignalListener::SLOT_COLOR) wire.rettype = "color"; + } + return wire; +} + +// NOLINTBEGIN (cppcoreguidelines-interfaces-global-init) +// clang-format off +const int IpcSignalListener::SLOT_VOID = IpcSignalListener::staticMetaObject.indexOfSlot("invokeVoid()"); +const int IpcSignalListener::SLOT_STRING = IpcSignalListener::staticMetaObject.indexOfSlot("invokeString(QString)"); +const int IpcSignalListener::SLOT_INT = IpcSignalListener::staticMetaObject.indexOfSlot("invokeInt(int)"); +const int IpcSignalListener::SLOT_BOOL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeBool(bool)"); +const int IpcSignalListener::SLOT_REAL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeReal(double)"); +const int IpcSignalListener::SLOT_COLOR = IpcSignalListener::staticMetaObject.indexOfSlot("invokeColor(QColor)"); +// clang-format on +// NOLINTEND + +bool IpcSignal::resolve(QString& error) { + if (this->signal.parameterCount() > 1) { + error = "Due to technical limitations, IPC signals can have at most one argument."; + return false; + } + + auto slot = IpcSignalListener::SLOT_VOID; + + if (this->signal.parameterCount() == 1) { + auto paramType = this->signal.parameterType(0); + if (paramType == QMetaType::QString) slot = IpcSignalListener::SLOT_STRING; + else if (paramType == QMetaType::Int) slot = IpcSignalListener::SLOT_INT; + else if (paramType == QMetaType::Bool) slot = IpcSignalListener::SLOT_BOOL; + else if (paramType == QMetaType::Double) slot = IpcSignalListener::SLOT_REAL; + else if (paramType == QMetaType::QColor) slot = IpcSignalListener::SLOT_COLOR; + else { + error = QString("Type of argument (%2: %3) cannot be used across IPC.") + .arg(this->signal.parameterNames().value(0)) + .arg(QMetaType(paramType).name()); + + return false; + } + } + + this->targetSlot = slot; + return true; +} + +void IpcSignal::connectListener(IpcHandler* handler) { + if (this->targetSlot == -1) { + qFatal() << "Tried to connect unresolved IPC signal"; + } + + this->listener = std::make_shared(this->signal.name()); + QMetaObject::connect(handler, this->signal.methodIndex(), this->listener.get(), this->targetSlot); + + QObject::connect( + this->listener.get(), + &IpcSignalListener::triggered, + handler, + &IpcHandler::onSignalTriggered + ); +} + IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { for (const auto& arg: function.argumentTypes) { this->argumentSlots.emplace_back(arg); @@ -172,16 +243,28 @@ void IpcHandler::onPostReload() { // which should handle inheritance on the qml side. for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) { const auto& method = meta->method(i); - if (method.methodType() != QMetaMethod::Slot) continue; + if (method.methodType() == QMetaMethod::Slot) { + auto ipcFunc = IpcFunction(method); + QString error; - auto ipcFunc = IpcFunction(method); - QString error; + if (!ipcFunc.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing function \"" << method.name() << "\": " << error; + } else { + this->functionMap.insert(method.name(), ipcFunc); + } + } else if (method.methodType() == QMetaMethod::Signal) { + qmlDebug(this) << "Signal detected: " << method.name(); + auto ipcSig = IpcSignal(method); + QString error; - if (!ipcFunc.resolve(error)) { - qmlWarning(this).nospace().noquote() - << "Error parsing function \"" << method.name() << "\": " << error; - } else { - this->functionMap.insert(method.name(), ipcFunc); + if (!ipcSig.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing signal \"" << method.name() << "\": " << error; + } else { + ipcSig.connectListener(this); + this->signalMap.emplace(method.name(), std::move(ipcSig)); + } } } @@ -222,6 +305,11 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati return dynamic_cast(ext); } +void IpcHandler::onSignalTriggered(const QString& signal, const QString& value) const { + emit IpcSignalRemoteListener::instance() + -> triggered(this->registeredState.target, signal, value); +} + void IpcHandler::updateRegistration(bool destroying) { if (!this->complete) return; @@ -324,6 +412,10 @@ WireTargetDefinition IpcHandler::wireDef() const { wire.properties += prop.wireDef(); } + for (const auto& sig: this->signalMap.values()) { + wire.signalFunctions += sig.wireDef(); + } + return wire; } @@ -368,6 +460,13 @@ IpcProperty* IpcHandler::findProperty(const QString& name) { else return &*itr; } +IpcSignal* IpcHandler::findSignal(const QString& name) { + auto itr = this->signalMap.find(name); + + if (itr == this->signalMap.end()) return nullptr; + else return &*itr; +} + IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { return this->handlers.value(target); } @@ -382,4 +481,9 @@ QVector IpcHandlerRegistry::wireTargets() const { return wire; } +IpcSignalRemoteListener* IpcSignalRemoteListener::instance() { + static auto* instance = new IpcSignalRemoteListener(); + return instance; +} + } // namespace qs::io::ipc diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 4c5d9bc..eb274e3 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -67,6 +69,54 @@ public: const IpcType* type = nullptr; }; +class IpcSignalListener: public QObject { + Q_OBJECT; + +public: + IpcSignalListener(QString signal): signal(std::move(signal)) {} + + static const int SLOT_VOID; + static const int SLOT_STRING; + static const int SLOT_INT; + static const int SLOT_BOOL; + static const int SLOT_REAL; + static const int SLOT_COLOR; + +signals: + void triggered(const QString& signal, const QString& value); + +private slots: + void invokeVoid() { this->triggered(this->signal, "void"); } + void invokeString(const QString& value) { this->triggered(this->signal, value); } + void invokeInt(int value) { this->triggered(this->signal, QString::number(value)); } + void invokeBool(bool value) { this->triggered(this->signal, value ? "true" : "false"); } + void invokeReal(double value) { this->triggered(this->signal, QString::number(value)); } + void invokeColor(QColor value) { this->triggered(this->signal, value.name(QColor::HexArgb)); } + +private: + QString signal; +}; + +class IpcHandler; + +class IpcSignal { +public: + explicit IpcSignal(QMetaMethod signal): signal(signal) {} + + bool resolve(QString& error); + + [[nodiscard]] WireSignalDefinition wireDef() const; + + QMetaMethod signal; + int targetSlot = -1; + + void connectListener(IpcHandler* handler); + +private: + void connectListener(QObject* handler, IpcSignalListener* listener) const; + std::shared_ptr listener; +}; + class IpcHandlerRegistry; ///! Handler for IPC message calls. @@ -100,6 +150,11 @@ class IpcHandlerRegistry; /// - `real` will be converted to a string and returned. /// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. /// +/// #### Signals +/// IPC handler signals can be observed remotely using `qs ipc wait` (one call) +/// and `qs ipc listen` (many calls). IPC signals may have zero or one argument, where +/// the argument is one of the types listed above, or no arguments for void. +/// /// #### Example /// The following example creates ipc functions to control and retrieve the appearance /// of a Rectangle. @@ -119,10 +174,18 @@ class IpcHandlerRegistry; /// /// function setColor(color: color): void { rect.color = color; } /// function getColor(): color { return rect.color; } +/// /// function setAngle(angle: real): void { rect.rotation = angle; } /// function getAngle(): real { return rect.rotation; } -/// function setRadius(radius: int): void { rect.radius = radius; } +/// +/// function setRadius(radius: int): void { +/// rect.radius = radius; +/// this.radiusChanged(radius); +/// } +/// /// function getRadius(): int { return rect.radius; } +/// +/// signal radiusChanged(newRadius: int); /// } /// } /// ``` @@ -136,6 +199,7 @@ class IpcHandlerRegistry; /// function getAngle(): real /// function setRadius(radius: int): void /// function getRadius(): int +/// signal radiusChanged(newRadius: int) /// ``` /// /// and then invoked using `qs ipc call`. @@ -179,14 +243,15 @@ public: QString listMembers(qsizetype indent); [[nodiscard]] IpcFunction* findFunction(const QString& name); [[nodiscard]] IpcProperty* findProperty(const QString& name); + [[nodiscard]] IpcSignal* findSignal(const QString& name); [[nodiscard]] WireTargetDefinition wireDef() const; signals: void enabledChanged(); void targetChanged(); -private slots: - //void handleIpcPropertyChange(); +public slots: + void onSignalTriggered(const QString& signal, const QString& value) const; private: void updateRegistration(bool destroying = false); @@ -204,6 +269,7 @@ private: QHash functionMap; QHash propertyMap; + QHash signalMap; friend class IpcHandlerRegistry; }; @@ -227,4 +293,14 @@ private: QHash> knownHandlers; }; +class IpcSignalRemoteListener: public QObject { + Q_OBJECT; + +public: + static IpcSignalRemoteListener* instance(); + +signals: + void triggered(const QString& target, const QString& signal, const QString& value); +}; + } // namespace qs::io::ipc diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 0196359..32d8482 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -61,6 +61,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server void IpcServerConnection::onDisconnected() { qCInfo(logIpc) << "IPC connection disconnected" << this; + delete this; } void IpcServerConnection::onReadyRead() { @@ -84,6 +85,11 @@ void IpcServerConnection::onReadyRead() { ); if (!this->stream.commitTransaction()) return; + + // async connections reparent + if (dynamic_cast(this->parent()) != nullptr) { + delete this; + } } IpcClient::IpcClient(const QString& path) { diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp index b221b46..105ce1e 100644 --- a/src/ipc/ipccommand.hpp +++ b/src/ipc/ipccommand.hpp @@ -16,6 +16,7 @@ using IpcCommand = std::variant< IpcKillCommand, qs::io::ipc::comm::QueryMetadataCommand, qs::io::ipc::comm::StringCallCommand, + qs::io::ipc::comm::SignalListenCommand, qs::io::ipc::comm::StringPropReadCommand>; } // namespace qs::ipc diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 1a58cb8..d867584 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -411,6 +411,10 @@ int ipcCommand(CommandState& cmd) { return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); } else if (*cmd.ipc.getprop) { return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); + } else if (*cmd.ipc.wait) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, true); + } else if (*cmd.ipc.listen) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, false); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index a186ddb..f666e7a 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -74,6 +74,8 @@ struct CommandState { CLI::App* show = nullptr; CLI::App* call = nullptr; CLI::App* getprop = nullptr; + CLI::App* wait = nullptr; + CLI::App* listen = nullptr; bool showOld = false; QStringOption target; QStringOption name; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 0776f58..fc43b6b 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -226,6 +227,16 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->allow_extra_args(); } + auto signalCmd = [&](std::string cmd, std::string desc) { + auto* scmd = sub->add_subcommand(std::move(cmd), std::move(desc)); + scmd->add_option("target", state.ipc.target, "The target to listen on."); + scmd->add_option("signal", state.ipc.name, "The signal to listen for."); + return scmd; + }; + + state.ipc.wait = signalCmd("wait", "Wait for one IpcHandler signal."); + state.ipc.listen = signalCmd("listen", "Listen for IpcHandler signals."); + { auto* prop = sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand(); From 5eb6f51f4a2a84d3f0f3f7352253780730beee1b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 17 Jan 2026 03:05:50 -0800 Subject: [PATCH 154/226] core: add preprocessor for versioning --- CMakeLists.txt | 2 + changelog/next.md | 1 + src/build/build.hpp.in | 5 ++ src/core/CMakeLists.txt | 3 +- src/core/qmlglobal.cpp | 9 +++ src/core/qmlglobal.hpp | 15 +++++ src/core/rootwrapper.cpp | 30 ++++++++- src/core/scan.cpp | 133 +++++++++++++++++++++++++++++---------- src/core/scan.hpp | 8 +++ src/core/scanenv.cpp | 22 +++++++ src/core/scanenv.hpp | 17 +++++ src/launch/command.cpp | 4 +- 12 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 src/core/scanenv.cpp create mode 100644 src/core/scanenv.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 81e896f..7633f4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.20) project(quickshell VERSION "0.2.1" LANGUAGES CXX C) +set(UNRELEASED_FEATURES) + set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/changelog/next.md b/changelog/next.md index cab03e6..30e998b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -23,6 +23,7 @@ set shell id. - Added initial support for network management. - Added support for grabbing focus from popup windows. - Added support for IPC signal listeners. +- Added Quickshell version checking and version gated preprocessing. ## Other Changes diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 075abd1..66fb664 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -1,6 +1,11 @@ #pragma once // NOLINTBEGIN +#define QS_VERSION "@quickshell_VERSION@" +#define QS_VERSION_MAJOR @quickshell_VERSION_MAJOR@ +#define QS_VERSION_MINOR @quickshell_VERSION_MINOR@ +#define QS_VERSION_PATCH @quickshell_VERSION_PATCH@ +#define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@" #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bbfb8c4..fb63f40 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -12,6 +12,7 @@ qt_add_library(quickshell-core STATIC singleton.cpp generation.cpp scan.cpp + scanenv.cpp qsintercept.cpp incubator.cpp lazyloader.cpp @@ -51,7 +52,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build) qs_module_pch(quickshell-core SET large) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 07238f6..03fb818 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -29,6 +29,7 @@ #include "paths.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" +#include "scanenv.hpp" QuickshellSettings::QuickshellSettings() { QObject::connect( @@ -313,6 +314,14 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) return IconImageProvider::requestString(icon, "", fallback); } +bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) { + return qs::scan::env::PreprocEnv::hasVersion(major, minor, features); +} + +bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor) { + return QuickshellGlobal::hasVersion(major, minor, QStringList()); +} + QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { auto* qsg = new QuickshellGlobal(); auto* generation = EngineGeneration::findEngineGeneration(engine); diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 1fc363b..3ca70be 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -217,6 +217,21 @@ public: /// /// The popup can also be blocked by setting `QS_NO_RELOAD_POPUP=1`. Q_INVOKABLE void inhibitReloadPopup() { this->mInhibitReloadPopup = true; } + /// Check if Quickshell's version is at least `major.minor` and the listed + /// unreleased features are available. If Quickshell is newer than the given version + /// it is assumed that all unreleased features are present. The unreleased feature list + /// may be omitted. + /// + /// > [!NOTE] You can feature gate code blocks using Quickshell's preprocessor which + /// > has the same function available. + /// > + /// > ```qml + /// > //@ if hasVersion(0, 3, ["feature"]) + /// > ... + /// > //@ endif + /// > ``` + Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor, const QStringList& features); + Q_INVOKABLE static bool hasVersion(qint32 major, qint32 minor); void clearReloadPopupInhibit() { this->mInhibitReloadPopup = false; } [[nodiscard]] bool isReloadPopupInhibited() const { return this->mInhibitReloadPopup; } diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 25c46cc..1e75819 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -63,9 +63,6 @@ void RootWrapper::reloadGraph(bool hard) { qs::core::QmlToolingSupport::updateTooling(rootPath, scanner); this->configDirWatcher.addPath(rootPath.path()); - auto* generation = new EngineGeneration(rootPath, std::move(scanner)); - generation->wrapper = this; - // todo: move into EngineGeneration if (this->generation != nullptr) { qInfo() << "Reloading configuration..."; @@ -74,6 +71,33 @@ void RootWrapper::reloadGraph(bool hard) { QDir::setCurrent(this->originalWorkingDirectory); + if (!scanner.scanErrors.isEmpty()) { + qCritical() << "Failed to load configuration"; + QString errorString = "Failed to load configuration"; + for (auto& error: scanner.scanErrors) { + const auto& file = error.file; + QString rel; + if (file.startsWith(rootPath.path() % '/')) { + rel = '@' % file.sliced(rootPath.path().length() + 1); + } else { + rel = file; + } + + auto msg = " error in " % rel % '[' % QString::number(error.line) % ":0]: " % error.message; + errorString += '\n' % msg; + qCritical().noquote() << msg; + } + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(errorString); + } + + return; + } + + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); + generation->wrapper = this; + QUrl url; url.setScheme("qs"); url.setPath("@/qs/" % rootFile.fileName()); diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 453b7dc..8ca1f51 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -1,9 +1,11 @@ #include "scan.hpp" #include +#include #include #include #include +#include #include #include #include @@ -15,6 +17,7 @@ #include #include "logcat.hpp" +#include "scanenv.hpp" QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); @@ -115,51 +118,113 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna auto stream = QTextStream(&file); auto imports = QVector(); - while (!stream.atEnd()) { - auto line = stream.readLine().trimmed(); - if (!singleton && line == "pragma Singleton") { - singleton = true; - } else if (!internal && line == "//@ pragma Internal") { - internal = true; - } else if (line.startsWith("import")) { - // we dont care about "import qs" as we always load the root folder - if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { - importCursor += 4; - QString path; + bool inHeader = false; + auto ifScopes = QVector(); + bool sourceMasked = false; + int lineNum = 0; + QString overrideText; + bool isOverridden = false; - while (importCursor != line.length()) { - auto c = line.at(importCursor); - if (c == '.') c = '/'; - else if (c == ' ') break; - else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') - || c == '_') - { - } else { - qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; - goto next; + auto pragmaEngine = QJSEngine(); + pragmaEngine.globalObject().setPrototype( + pragmaEngine.newQObject(new qs::scan::env::PreprocEnv()) + ); + + auto postError = [&, this](QString error) { + this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum}); + }; + + while (!stream.atEnd()) { + ++lineNum; + bool hideMask = false; + auto rawLine = stream.readLine(); + auto line = rawLine.trimmed(); + if (!sourceMasked && inHeader) { + if (!singleton && line == "pragma Singleton") { + singleton = true; + } else if (line.startsWith("import")) { + // we dont care about "import qs" as we always load the root folder + if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { + importCursor += 4; + QString path; + + while (importCursor != line.length()) { + auto c = line.at(importCursor); + if (c == '.') c = '/'; + else if (c == ' ') break; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_') + { + } else { + qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; + goto next; + } + + path.append(c); + importCursor += 1; } - path.append(c); - importCursor += 1; + imports.append(this->rootPath.filePath(path)); + } else if (auto startQuot = line.indexOf('"'); + startQuot != -1 && line.length() >= startQuot + 3) + { + auto endQuot = line.indexOf('"', startQuot + 1); + if (endQuot == -1) continue; + + auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); + imports.push_back(name); } - - imports.append(this->rootPath.filePath(path)); - } else if (auto startQuot = line.indexOf('"'); - startQuot != -1 && line.length() >= startQuot + 3) - { - auto endQuot = line.indexOf('"', startQuot + 1); - if (endQuot == -1) continue; - - auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); - imports.push_back(name); + } else if (!internal && line == "//@ pragma Internal") { + internal = true; + } else if (line.contains('{')) { + inHeader = true; } - } else if (line.contains('{')) break; + } + + if (line.startsWith("//@ if ")) { + auto code = line.sliced(7); + auto value = pragmaEngine.evaluate(code, path, 1234); + bool mask = true; + + if (value.isError()) { + postError(QString("Evaluating if: %0").arg(value.toString())); + } else if (!value.isBool()) { + postError(QString("If expression \"%0\" is not a boolean").arg(value.toString())); + } else if (value.toBool()) { + mask = false; + } + if (!sourceMasked && mask) hideMask = true; + mask = sourceMasked || mask; // cant unmask if a nested if passes + ifScopes.append(mask); + if (mask) isOverridden = true; + sourceMasked = mask; + } else if (line.startsWith("//@ endif")) { + if (ifScopes.isEmpty()) { + postError("endif without matching if"); + } else { + ifScopes.pop_back(); + + if (ifScopes.isEmpty()) sourceMasked = false; + else sourceMasked = ifScopes.last(); + } + } + + if (!hideMask && sourceMasked) overrideText.append("// MASKED: " % rawLine % '\n'); + else overrideText.append(rawLine % '\n'); next:; } + if (!ifScopes.isEmpty()) { + postError("unclosed preprocessor if block"); + } + file.close(); + if (isOverridden) { + this->fileIntercepts.insert(path, overrideText); + } + if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { qCDebug(logQmlScanner) << "Found imports" << imports; } diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 2dc8c3c..29f8f6a 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -23,6 +23,14 @@ public: QVector scannedFiles; QHash fileIntercepts; + struct ScanError { + QString file; + QString message; + int line; + }; + + QVector scanErrors; + private: QDir rootPath; diff --git a/src/core/scanenv.cpp b/src/core/scanenv.cpp new file mode 100644 index 0000000..b8c514c --- /dev/null +++ b/src/core/scanenv.cpp @@ -0,0 +1,22 @@ +#include "scanenv.hpp" + +#include + +#include "build.hpp" + +namespace qs::scan::env { + +bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) { + if (QS_VERSION_MAJOR > major) return true; + if (QS_VERSION_MAJOR == major && QS_VERSION_MINOR > minor) return true; + + auto availFeatures = QString(QS_UNRELEASED_FEATURES).split(';'); + + for (const auto& feature: features) { + if (!availFeatures.contains(feature)) return false; + } + + return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor; +} + +} // namespace qs::scan::env diff --git a/src/core/scanenv.hpp b/src/core/scanenv.hpp new file mode 100644 index 0000000..0abde2e --- /dev/null +++ b/src/core/scanenv.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace qs::scan::env { + +class PreprocEnv: public QObject { + Q_OBJECT; + +public: + Q_INVOKABLE static bool + hasVersion(int major, int minor, const QStringList& features = QStringList()); +}; + +} // namespace qs::scan::env diff --git a/src/launch/command.cpp b/src/launch/command.cpp index d867584..151fc24 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -519,8 +519,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() - << "quickshell 0.2.1, revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; + qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision " + << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; if (state.log.verbosity > 1) { qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; From 7a427ce1979ce7447e885c4f30129b40f3d466f5 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 17 Jan 2026 17:30:40 -0500 Subject: [PATCH 155/226] core: fix inverted inHeader condition in preprocesso --- src/core/scan.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 8ca1f51..37b0fac 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -118,7 +118,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna auto stream = QTextStream(&file); auto imports = QVector(); - bool inHeader = false; + bool inHeader = true; auto ifScopes = QVector(); bool sourceMasked = false; int lineNum = 0; @@ -177,7 +177,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna } else if (!internal && line == "//@ pragma Internal") { internal = true; } else if (line.contains('{')) { - inHeader = true; + inHeader = false; } } From 8fd0de458034174cf271fac56953d98026b291a4 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 20 Jan 2026 16:10:45 -0500 Subject: [PATCH 156/226] core/proxywindow: create window on visibility for lazily initialized windows --- src/window/proxywindow.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 3cc4378..4423547 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -288,10 +288,16 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { this->bBackerVisibility = false; this->deleteWindow(); } - } else if (this->window != nullptr) { - if (visible) this->polishItems(); - this->window->setVisible(visible); - this->bBackerVisibility = visible; + } else { + if (visible && this->window == nullptr) { + this->createWindow(); + } + + if (this->window != nullptr) { + if (visible) this->polishItems(); + this->window->setVisible(visible); + this->bBackerVisibility = visible; + } } } From 191085a8821b35680bba16ce5411fc9dbe912237 Mon Sep 17 00:00:00 2001 From: Manuel Romei Date: Sun, 18 Jan 2026 17:20:12 +0100 Subject: [PATCH 157/226] ipc: use deleteLater() in IpcServerConnection to prevent use-after-free --- src/ipc/ipc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 32d8482..40e8f0c 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -61,7 +61,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server void IpcServerConnection::onDisconnected() { qCInfo(logIpc) << "IPC connection disconnected" << this; - delete this; + this->deleteLater(); } void IpcServerConnection::onReadyRead() { @@ -88,7 +88,7 @@ void IpcServerConnection::onReadyRead() { // async connections reparent if (dynamic_cast(this->parent()) != nullptr) { - delete this; + this->deleteLater(); } } From 1e4d804e7f3fa7465811030e8da2bf10d544426a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 28 Jan 2026 00:23:38 -0800 Subject: [PATCH 158/226] widgets/cliprect: use layer.effect on content item over property ShaderEffectSource as a property not parented to an item does not update its sourceItem's QQuickWindow when its own is changed. This lead to use after frees and broken effects when using ClippingRectangle. --- changelog/next.md | 1 + src/widgets/ClippingRectangle.qml | 23 ++++++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 30e998b..bccd780 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -43,6 +43,7 @@ set shell id. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. - Fixed memory leak in IPC handlers. +- Fixed ClippingRectangle related crashes. ## Packaging Changes diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 86fe601..3fc64d8 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick ///! Rectangle capable of clipping content inside its border. @@ -24,7 +26,7 @@ Item { /// Defaults to true if any corner has a non-zero radius, otherwise false. property /*bool*/alias antialiasing: rectangle.antialiasing /// The background color of the rectangle, which goes under its content. - property /*color*/alias color: shader.backgroundColor + property color color: "white" /// See @@QtQuick.Rectangle.border. property clippingRectangleBorder border /// Radius of all corners. Defaults to 0. @@ -70,19 +72,14 @@ Item { anchors.fill: parent anchors.margins: root.contentInsideBorder ? root.border.width : 0 } - } - ShaderEffect { - id: shader - anchors.fill: root - fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` - property Rectangle rect: rectangle - property color backgroundColor: "white" - property color borderColor: root.border.color - - property ShaderEffectSource content: ShaderEffectSource { - hideSource: true - sourceItem: contentItemContainer + layer.enabled: true + layer.samplerName: "content" + layer.effect: ShaderEffect { + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Rectangle rect: rectangle + property color backgroundColor: root.color + property color borderColor: root.border.color } } } From 395a1301a83e98dafc325289630ccacda5d69607 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Fri, 6 Feb 2026 03:02:58 -0500 Subject: [PATCH 159/226] core: add hasThemeIcon mapping --- changelog/next.md | 1 + src/core/qmlglobal.cpp | 2 ++ src/core/qmlglobal.hpp | 2 ++ 3 files changed, 5 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index bccd780..583a2f4 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -24,6 +24,7 @@ set shell id. - Added support for grabbing focus from popup windows. - Added support for IPC signal listeners. - Added Quickshell version checking and version gated preprocessing. +- Added a way to detect if an icon is from the system icon theme or not. ## Other Changes diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 03fb818..6c26609 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -314,6 +314,8 @@ QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) return IconImageProvider::requestString(icon, "", fallback); } +bool QuickshellGlobal::hasThemeIcon(const QString& icon) { return QIcon::hasThemeIcon(icon); } + bool QuickshellGlobal::hasVersion(qint32 major, qint32 minor, const QStringList& features) { return qs::scan::env::PreprocEnv::hasVersion(major, minor, features); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 3ca70be..94b42f6 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -202,6 +202,8 @@ public: /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback /// icon if the requested one could not be loaded. Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); + /// Check if specified icon has an available icon in your icon theme + Q_INVOKABLE static bool hasThemeIcon(const QString& icon); /// Equivalent to `${Quickshell.configDir}/${path}` Q_INVOKABLE [[nodiscard]] QString shellPath(const QString& path) const; /// > [!WARNING] Deprecated: Renamed to @@shellPath() for clarity. From 4429c038377a2c59dfcab6fe2424fb2c3a99d2cd Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 8 Feb 2026 20:10:11 -0800 Subject: [PATCH 160/226] widgets/cliprect: fix ShaderEffect warnings on reload layer.effect causes warnings on reload for an unknown reason which seems to be ownership or destruction time related. This commit uses an alternate strategy to create the shader which does not show this warning. --- src/widgets/ClippingRectangle.qml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 3fc64d8..749b331 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -66,20 +66,22 @@ Item { Item { id: contentItemContainer anchors.fill: root + layer.enabled: true + visible: false Item { id: contentItem anchors.fill: parent anchors.margins: root.contentInsideBorder ? root.border.width : 0 } + } - layer.enabled: true - layer.samplerName: "content" - layer.effect: ShaderEffect { - fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` - property Rectangle rect: rectangle - property color backgroundColor: root.color - property color borderColor: root.border.color - } + ShaderEffect { + anchors.fill: contentItemContainer + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Item content: contentItemContainer + property Rectangle rect: rectangle + property color backgroundColor: root.color + property color borderColor: root.border.color } } From dacfa9de829ac7cb173825f593236bf2c21f637e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 9 Feb 2026 19:14:36 -0800 Subject: [PATCH 161/226] widgets/cliprect: use ShaderEffectSource to propagate mouse events --- src/widgets/ClippingRectangle.qml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml index 749b331..604f346 100644 --- a/src/widgets/ClippingRectangle.qml +++ b/src/widgets/ClippingRectangle.qml @@ -26,7 +26,7 @@ Item { /// Defaults to true if any corner has a non-zero radius, otherwise false. property /*bool*/alias antialiasing: rectangle.antialiasing /// The background color of the rectangle, which goes under its content. - property color color: "white" + property /*color*/alias color: shader.backgroundColor /// See @@QtQuick.Rectangle.border. property clippingRectangleBorder border /// Radius of all corners. Defaults to 0. @@ -66,8 +66,6 @@ Item { Item { id: contentItemContainer anchors.fill: root - layer.enabled: true - visible: false Item { id: contentItem @@ -76,12 +74,19 @@ Item { } } + ShaderEffectSource { + id: shaderSource + hideSource: true + sourceItem: contentItemContainer + } + ShaderEffect { - anchors.fill: contentItemContainer + id: shader + anchors.fill: root fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` - property Item content: contentItemContainer property Rectangle rect: rectangle - property color backgroundColor: root.color + property color backgroundColor: "white" property color borderColor: root.border.color + property ShaderEffectSource content: shaderSource } } From afbc5ffd4e846515ae5efeb41580eb25171faa52 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 15 Feb 2026 23:23:34 +0700 Subject: [PATCH 162/226] services/pipewire: use node volume control when device missing Some outputs which present a pipewire device object do not present routes, instead expecting volume to be set on the associated pipewire node. --- changelog/next.md | 1 + src/services/pipewire/device.cpp | 4 ++++ src/services/pipewire/device.hpp | 5 ++++- src/services/pipewire/node.cpp | 34 +++++++++++++++++++++----------- src/services/pipewire/node.hpp | 5 ++++- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 583a2f4..66f87c1 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -36,6 +36,7 @@ set shell id. - Fixed volume control breaking with pipewire pro audio mode. - Fixed volume control breaking with bluez streams and potentially others. +- Fixed volume control breaking for devices without route definitions. - Fixed escape sequence handling in desktop entries. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index e3bc967..61079a1 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -141,6 +141,10 @@ bool PwDevice::tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps return true; } +bool PwDevice::hasRouteDevice(qint32 routeDevice) const { + return this->routeDeviceIndexes.contains(routeDevice); +} + 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. diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index 22af699..cd61709 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -12,13 +12,15 @@ #include #include "core.hpp" -#include "node.hpp" #include "registry.hpp" namespace qs::service::pipewire { class PwDevice; +// Forward declare to avoid circular dependency with node.hpp +struct PwVolumeProps; + class PwDevice: public PwBindable { Q_OBJECT; @@ -33,6 +35,7 @@ public: [[nodiscard]] bool waitingForDevice() const; [[nodiscard]] bool tryLoadVolumeProps(qint32 routeDevice, PwVolumeProps& volumeProps); + [[nodiscard]] bool hasRouteDevice(qint32 routeDevice) const; signals: void deviceReady(); diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index b6f0529..075a7ec 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -429,6 +429,10 @@ void PwNodeBoundAudio::setMuted(bool muted) { } float PwNodeBoundAudio::averageVolume() const { + if (this->mVolumes.isEmpty()) { + return 0.0f; + } + float total = 0; for (auto volume: this->mVolumes) { @@ -572,22 +576,28 @@ PwVolumeProps PwVolumeProps::parseSpaPod(const spa_pod* param) { 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); - - spa_pod* iter = nullptr; - SPA_POD_ARRAY_FOREACH(volumes, iter) { - // Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly. - auto linear = *reinterpret_cast(iter); - auto visual = std::cbrt(linear); - props.volumes.push_back(visual); + if (volumesProp) { + const auto* volumes = reinterpret_cast(&volumesProp->value); + spa_pod* iter = nullptr; + SPA_POD_ARRAY_FOREACH(volumes, iter) { + // Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly. + auto linear = *reinterpret_cast(iter); + auto visual = std::cbrt(linear); + props.volumes.push_back(visual); + } } - SPA_POD_ARRAY_FOREACH(channels, iter) { - props.channels.push_back(*reinterpret_cast(iter)); + if (channelsProp) { + const auto* channels = reinterpret_cast(&channelsProp->value); + spa_pod* iter = nullptr; + SPA_POD_ARRAY_FOREACH(channels, iter) { + props.channels.push_back(*reinterpret_cast(iter)); + } } - spa_pod_get_bool(&muteProp->value, &props.mute); + if (muteProp) { + spa_pod_get_bool(&muteProp->value, &props.mute); + } if (volumeStepProp) { spa_pod_get_float(&volumeStepProp->value, &props.volumeStep); diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index fdec72d..efc819c 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -15,6 +15,7 @@ #include #include "core.hpp" +#include "device.hpp" #include "registry.hpp" namespace qs::service::pipewire { @@ -249,7 +250,9 @@ public: bool proAudio = false; [[nodiscard]] bool shouldUseDevice() const { - return this->device && !this->proAudio && this->routeDevice != -1; + if (!this->device || this->proAudio || this->routeDevice == -1) return false; + // Only use device control if the device actually has route indexes for this routeDevice + return this->device->hasRouteDevice(this->routeDevice); } signals: From e7cd1e9982426fdcc617910597ab3d8f71346e4f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Feb 2026 21:11:45 -0800 Subject: [PATCH 163/226] core: add env and isEnvSet functions to pragma context --- src/core/scanenv.cpp | 9 +++++++++ src/core/scanenv.hpp | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/core/scanenv.cpp b/src/core/scanenv.cpp index b8c514c..047f472 100644 --- a/src/core/scanenv.cpp +++ b/src/core/scanenv.cpp @@ -1,6 +1,7 @@ #include "scanenv.hpp" #include +#include #include "build.hpp" @@ -19,4 +20,12 @@ bool PreprocEnv::hasVersion(int major, int minor, const QStringList& features) { return QS_VERSION_MAJOR == major && QS_VERSION_MINOR == minor; } +QString PreprocEnv::env(const QString& variable) { + return qEnvironmentVariable(variable.toStdString().c_str()); +} + +bool PreprocEnv::isEnvSet(const QString& variable) { + return qEnvironmentVariableIsSet(variable.toStdString().c_str()); +} + } // namespace qs::scan::env diff --git a/src/core/scanenv.hpp b/src/core/scanenv.hpp index 0abde2e..c1c6814 100644 --- a/src/core/scanenv.hpp +++ b/src/core/scanenv.hpp @@ -12,6 +12,9 @@ class PreprocEnv: public QObject { public: Q_INVOKABLE static bool hasVersion(int major, int minor, const QStringList& features = QStringList()); + + Q_INVOKABLE static QString env(const QString& variable); + Q_INVOKABLE static bool isEnvSet(const QString& variable); }; } // namespace qs::scan::env From 158db16b931d04b43ec84748ee49390ea9f7c3f8 Mon Sep 17 00:00:00 2001 From: Bryan Paradis Date: Wed, 18 Feb 2026 07:36:21 -0800 Subject: [PATCH 164/226] wayland: check screen isPlaceholder and if wl_output is null Fixes crashes on disconnected monitors --- changelog/next.md | 1 + src/wayland/session_lock/surface.cpp | 2 +- src/wayland/wlr_layershell/surface.cpp | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 66f87c1..042a6ea 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -46,6 +46,7 @@ set shell id. - Fixed asynchronous loaders not working before window creation. - Fixed memory leak in IPC handlers. - Fixed ClippingRectangle related crashes. +- Fixed crashes when monitors are unplugged. ## Packaging Changes diff --git a/src/wayland/session_lock/surface.cpp b/src/wayland/session_lock/surface.cpp index 6ec4eb6..c73f459 100644 --- a/src/wayland/session_lock/surface.cpp +++ b/src/wayland/session_lock/surface.cpp @@ -28,7 +28,7 @@ QSWaylandSessionLockSurface::QSWaylandSessionLockSurface(QtWaylandClient::QWayla wl_output* output = nullptr; // NOLINT (include) auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); - if (waylandScreen != nullptr) { + if (waylandScreen != nullptr && !waylandScreen->isPlaceholder() && waylandScreen->output()) { output = waylandScreen->output(); } else { qFatal() << "Session lock screen does not corrospond to a real screen. Force closing window"; diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 3c71ff9..4a5015e 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -143,11 +143,11 @@ LayerSurface::LayerSurface(LayerShellIntegration* shell, QtWaylandClient::QWayla auto* waylandScreen = dynamic_cast(qwindow->screen()->handle()); - if (waylandScreen != nullptr) { + if (waylandScreen != nullptr && !waylandScreen->isPlaceholder() && waylandScreen->output()) { output = waylandScreen->output(); } else { qWarning() - << "Layershell screen does not corrospond to a real screen. Letting the compositor pick."; + << "Layershell screen does not correspond to a real screen. Letting the compositor pick."; } } From a99519c3adbc9eb9a80b32cde7264e9f147e3416 Mon Sep 17 00:00:00 2001 From: reakjra Date: Mon, 9 Feb 2026 18:20:14 +0100 Subject: [PATCH 165/226] wayland/screencopy: support dmabufs in vulkan mode --- .github/workflows/build.yml | 1 + BUILD.md | 1 + changelog/next.md | 2 + default.nix | 3 +- src/wayland/buffer/CMakeLists.txt | 5 +- src/wayland/buffer/dmabuf.cpp | 319 ++++++++++++++++++++++++++++++ src/wayland/buffer/dmabuf.hpp | 36 ++++ src/window/proxywindow.cpp | 10 + 8 files changed, 375 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66c3691..8d19f58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: wayland-protocols \ wayland \ libdrm \ + vulkan-headers \ libxcb \ libpipewire \ cli11 \ diff --git a/BUILD.md b/BUILD.md index fdea27e..c9459b5 100644 --- a/BUILD.md +++ b/BUILD.md @@ -146,6 +146,7 @@ To disable: `-DSCREENCOPY=OFF` Dependencies: - `libdrm` - `libgbm` +- `vulkan-headers` (build-time) Specific protocols can also be disabled: - `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] diff --git a/changelog/next.md b/changelog/next.md index 042a6ea..7180d53 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -25,6 +25,7 @@ set shell id. - Added support for IPC signal listeners. - Added Quickshell version checking and version gated preprocessing. - Added a way to detect if an icon is from the system icon theme or not. +- Added vulkan support to screencopy. ## Other Changes @@ -51,3 +52,4 @@ set shell id. ## Packaging Changes `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. +`vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). diff --git a/default.nix b/default.nix index 0b6f303..7783774 100644 --- a/default.nix +++ b/default.nix @@ -19,6 +19,7 @@ xorg, libdrm, libgbm ? null, + vulkan-headers, pipewire, pam, polkit, @@ -77,7 +78,7 @@ ++ lib.optional withJemalloc jemalloc ++ 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.optionals (withWayland && libgbm != null) [ libdrm libgbm vulkan-headers ] ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt index f80c53a..a8f2d2d 100644 --- a/src/wayland/buffer/CMakeLists.txt +++ b/src/wayland/buffer/CMakeLists.txt @@ -1,6 +1,8 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(dmabuf-deps REQUIRED IMPORTED_TARGET libdrm gbm egl) +find_package(VulkanHeaders REQUIRED) + qt_add_library(quickshell-wayland-buffer STATIC manager.cpp dmabuf.cpp @@ -10,9 +12,10 @@ qt_add_library(quickshell-wayland-buffer STATIC wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") target_link_libraries(quickshell-wayland-buffer PRIVATE - Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick Qt::QuickPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client PkgConfig::dmabuf-deps wlp-linux-dmabuf + Vulkan::Headers ) qs_pch(quickshell-wayland-buffer SET large) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index e51a1d0..89c9108 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -24,12 +25,17 @@ #include #include #include +#include #include +#include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -48,6 +54,25 @@ QS_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg) LinuxDmabufManager* MANAGER = nullptr; // NOLINT +VkFormat drmFormatToVkFormat(uint32_t drmFormat) { + // NOLINTBEGIN(bugprone-branch-clone): XRGB/ARGB intentionally map to the same VK format + switch (drmFormat) { + case DRM_FORMAT_ARGB8888: return VK_FORMAT_B8G8R8A8_UNORM; + case DRM_FORMAT_XRGB8888: return VK_FORMAT_B8G8R8A8_UNORM; + case DRM_FORMAT_ABGR8888: return VK_FORMAT_R8G8B8A8_UNORM; + case DRM_FORMAT_XBGR8888: return VK_FORMAT_R8G8B8A8_UNORM; + case DRM_FORMAT_ARGB2101010: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case DRM_FORMAT_XRGB2101010: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + case DRM_FORMAT_ABGR2101010: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case DRM_FORMAT_XBGR2101010: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + case DRM_FORMAT_ABGR16161616F: return VK_FORMAT_R16G16B16A16_SFLOAT; + case DRM_FORMAT_RGB565: return VK_FORMAT_R5G6B5_UNORM_PACK16; + case DRM_FORMAT_BGR565: return VK_FORMAT_B5G6R5_UNORM_PACK16; + default: return VK_FORMAT_UNDEFINED; + } + // NOLINTEND(bugprone-branch-clone) +} + } // namespace QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { @@ -532,6 +557,15 @@ bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { } WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { + auto* ri = window->rendererInterface(); + if (ri && ri->graphicsApi() == QSGRendererInterface::Vulkan) { + return this->createQsgTextureVulkan(window); + } + + return this->createQsgTextureGl(window); +} + +WlBufferQSGTexture* WlDmaBuffer::createQsgTextureGl(QQuickWindow* window) const { static auto* glEGLImageTargetTexture2DOES = []() { auto* fn = reinterpret_cast( eglGetProcAddress("glEGLImageTargetTexture2DOES") @@ -662,6 +696,291 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { return tex; } +WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) const { + auto* ri = window->rendererInterface(); + auto* vkInst = window->vulkanInstance(); + + if (!vkInst) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: no QVulkanInstance."; + return nullptr; + } + + auto* vkDevicePtr = + static_cast(ri->getResource(window, QSGRendererInterface::DeviceResource)); + auto* vkPhysDevicePtr = static_cast( + ri->getResource(window, QSGRendererInterface::PhysicalDeviceResource) + ); + + if (!vkDevicePtr || !vkPhysDevicePtr) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: could not get Vulkan device."; + return nullptr; + } + + VkDevice device = *vkDevicePtr; + VkPhysicalDevice physDevice = *vkPhysDevicePtr; + + auto* devFuncs = vkInst->deviceFunctions(device); + auto* instFuncs = vkInst->functions(); + + if (!devFuncs || !instFuncs) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: " + "could not get Vulkan functions."; + return nullptr; + } + + auto getMemoryFdPropertiesKHR = reinterpret_cast( + instFuncs->vkGetDeviceProcAddr(device, "vkGetMemoryFdPropertiesKHR") + ); + + if (!getMemoryFdPropertiesKHR) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: " + "vkGetMemoryFdPropertiesKHR not available. " + "Missing VK_KHR_external_memory_fd extension."; + return nullptr; + } + + const VkFormat vkFormat = drmFormatToVkFormat(this->format); + if (vkFormat == VK_FORMAT_UNDEFINED) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: unsupported DRM format" + << FourCCStr(this->format); + return nullptr; + } + + if (this->planeCount > 4) { + qCWarning(logDmabuf) << "Failed to create Vulkan QSG texture: too many planes" + << this->planeCount; + return nullptr; + } + + std::array planeLayouts = {}; + for (int i = 0; i < this->planeCount; ++i) { + planeLayouts[i].offset = this->planes[i].offset; // NOLINT + planeLayouts[i].rowPitch = this->planes[i].stride; // NOLINT + planeLayouts[i].size = 0; + planeLayouts[i].arrayPitch = 0; + planeLayouts[i].depthPitch = 0; + } + + const bool useModifier = this->modifier != DRM_FORMAT_MOD_INVALID; + + VkExternalMemoryImageCreateInfo externalInfo = {}; + externalInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO; + externalInfo.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + + VkImageDrmFormatModifierExplicitCreateInfoEXT modifierInfo = {}; + modifierInfo.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT; + modifierInfo.drmFormatModifier = this->modifier; + modifierInfo.drmFormatModifierPlaneCount = static_cast(this->planeCount); + modifierInfo.pPlaneLayouts = planeLayouts.data(); + + if (useModifier) { + externalInfo.pNext = &modifierInfo; + } + + VkImageCreateInfo imageInfo = {}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.pNext = &externalInfo; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = vkFormat; + imageInfo.extent = {.width = this->width, .height = this->height, .depth = 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = useModifier ? VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT : VK_IMAGE_TILING_LINEAR; + imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImage image = VK_NULL_HANDLE; + VkResult result = devFuncs->vkCreateImage(device, &imageInfo, nullptr, &image); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "Failed to create VkImage for DMA-BUF import, result:" << result; + return nullptr; + } + + VkDeviceMemory memory = VK_NULL_HANDLE; + + // dup() is required because vkAllocateMemory with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT + // takes ownership of the fd on succcess. Without dup, WlDmaBuffer would double-close. + const int dupFd = dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (dupFd < 0) { + qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; + goto cleanup_fail; // NOLINT + } + + { + VkMemoryRequirements memReqs = {}; + devFuncs->vkGetImageMemoryRequirements(device, image, &memReqs); + + VkMemoryFdPropertiesKHR fdProps = {}; + fdProps.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR; + + result = getMemoryFdPropertiesKHR( + device, + VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + dupFd, + &fdProps + ); + + if (result != VK_SUCCESS) { + close(dupFd); + qCWarning(logDmabuf) << "vkGetMemoryFdPropertiesKHR failed, result:" << result; + goto cleanup_fail; // NOLINT + } + + const uint32_t memTypeBits = memReqs.memoryTypeBits & fdProps.memoryTypeBits; + + VkPhysicalDeviceMemoryProperties memProps = {}; + instFuncs->vkGetPhysicalDeviceMemoryProperties(physDevice, &memProps); + + uint32_t memTypeIndex = UINT32_MAX; + for (uint32_t j = 0; j < memProps.memoryTypeCount; ++j) { + if (memTypeBits & (1u << j)) { + memTypeIndex = j; + break; + } + } + + if (memTypeIndex == UINT32_MAX) { + close(dupFd); + qCWarning(logDmabuf) << "No compatible memory type for DMA-BUF import"; + goto cleanup_fail; // NOLINT + } + + VkImportMemoryFdInfoKHR importInfo = {}; + importInfo.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; + importInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; + importInfo.fd = dupFd; + + VkMemoryDedicatedAllocateInfo dedicatedInfo = {}; + dedicatedInfo.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; + dedicatedInfo.image = image; + dedicatedInfo.pNext = &importInfo; + + VkMemoryAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.pNext = &dedicatedInfo; + allocInfo.allocationSize = memReqs.size; + allocInfo.memoryTypeIndex = memTypeIndex; + + result = devFuncs->vkAllocateMemory(device, &allocInfo, nullptr, &memory); + if (result != VK_SUCCESS) { + close(dupFd); + qCWarning(logDmabuf) << "vkAllocateMemory failed, result:" << result; + goto cleanup_fail; // NOLINT + } + + result = devFuncs->vkBindImageMemory(device, image, memory, 0); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "vkBindImageMemory failed, result:" << result; + goto cleanup_fail; // NOLINT + } + } + + { + // acquire the DMA-BUF from the foreign (compositor) queue and transition + // to shader-read layout. oldLayout must be GENERAL (not UNDEFINED) to + // preserve the DMA-BUF contents written by the external producer. Hopefully. + window->beginExternalCommands(); + + auto* cmdBufPtr = static_cast( + ri->getResource(window, QSGRendererInterface::CommandListResource) + ); + + if (cmdBufPtr && *cmdBufPtr) { + VkCommandBuffer cmdBuf = *cmdBufPtr; + + // find the graphics queue family index for the ownrship transfer. + uint32_t graphicsQueueFamily = 0; + uint32_t queueFamilyCount = 0; + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( + physDevice, &queueFamilyCount, nullptr + ); + std::vector queueFamilies(queueFamilyCount); + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( + physDevice, &queueFamilyCount, queueFamilies.data() + ); + for (uint32_t i = 0; i < queueFamilyCount; ++i) { + if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphicsQueueFamily = i; + break; + } + } + + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT; + barrier.dstQueueFamilyIndex = graphicsQueueFamily; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + devFuncs->vkCmdPipelineBarrier( + cmdBuf, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, + 0, + nullptr, + 0, + nullptr, + 1, + &barrier + ); + } + + window->endExternalCommands(); + + auto* qsgTexture = QQuickWindowPrivate::get(window)->createTextureFromNativeTexture( + reinterpret_cast(image), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + static_cast(vkFormat), + QSize(static_cast(this->width), static_cast(this->height)), + {} + ); + + auto* tex = new WlDmaBufferVulkanQSGTexture( + devFuncs, + device, + image, + memory, + qsgTexture + ); + qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; + return tex; + } + +cleanup_fail: + if (image != VK_NULL_HANDLE) { + devFuncs->vkDestroyImage(device, image, nullptr); + } + if (memory != VK_NULL_HANDLE) { + devFuncs->vkFreeMemory(device, memory, nullptr); + } + return nullptr; +} + +WlDmaBufferVulkanQSGTexture::~WlDmaBufferVulkanQSGTexture() { + delete this->qsgTexture; + + if (this->image != VK_NULL_HANDLE) { + this->devFuncs->vkDestroyImage(this->device, this->image, nullptr); + } + + if (this->memory != VK_NULL_HANDLE) { + this->devFuncs->vkFreeMemory(this->device, this->memory, nullptr); + } + + qCDebug(logDmabuf) << "WlDmaBufferVulkanQSGTexture" << this << "destroyed."; +} + WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() { auto* context = QOpenGLContext::currentContext(); auto* display = context->nativeInterface()->display(); diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp index 1e4ef1a..ffe5d02 100644 --- a/src/wayland/buffer/dmabuf.hpp +++ b/src/wayland/buffer/dmabuf.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,9 +13,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -114,6 +117,36 @@ private: friend class WlDmaBuffer; }; +class WlDmaBufferVulkanQSGTexture: public WlBufferQSGTexture { +public: + ~WlDmaBufferVulkanQSGTexture() override; + Q_DISABLE_COPY_MOVE(WlDmaBufferVulkanQSGTexture); + + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture; } + +private: + WlDmaBufferVulkanQSGTexture( + QVulkanDeviceFunctions* devFuncs, + VkDevice device, + VkImage image, + VkDeviceMemory memory, + QSGTexture* qsgTexture + ) + : devFuncs(devFuncs) + , device(device) + , image(image) + , memory(memory) + , qsgTexture(qsgTexture) {} + + QVulkanDeviceFunctions* devFuncs = nullptr; + VkDevice device = VK_NULL_HANDLE; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + QSGTexture* qsgTexture = nullptr; + + friend class WlDmaBuffer; +}; + class WlDmaBuffer: public WlBuffer { public: ~WlDmaBuffer() override; @@ -151,6 +184,9 @@ private: friend class LinuxDmabufManager; friend QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); + + [[nodiscard]] WlBufferQSGTexture* createQsgTextureGl(QQuickWindow* window) const; + [[nodiscard]] WlBufferQSGTexture* createQsgTextureVulkan(QQuickWindow* window) const; }; QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 4423547..b4f79da 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -147,6 +148,15 @@ void ProxyWindowBase::ensureQWindow() { this->window = nullptr; // createQQuickWindow may indirectly reference this->window this->window = this->createQQuickWindow(); this->window->setFormat(format); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } void ProxyWindowBase::createWindow() { From 2cf57f43d5f2a5b139d1f1702c83e126e17f27f8 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Feb 2026 12:14:36 -0500 Subject: [PATCH 166/226] core/proxywindow: expose updatesEnabled property --- src/window/proxywindow.cpp | 14 ++++++++++++++ src/window/proxywindow.hpp | 6 ++++++ src/window/windowinterface.cpp | 4 ++++ src/window/windowinterface.hpp | 10 ++++++++++ 4 files changed, 34 insertions(+) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index b4f79da..62126bd 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -223,6 +223,7 @@ void ProxyWindowBase::completeWindow() { this->trySetHeight(this->implicitHeight()); this->setColor(this->mColor); this->updateMask(); + QQuickWindowPrivate::get(this->window)->updatesEnabled = this->mUpdatesEnabled; // notify initial / post-connection geometry emit this->xChanged(); @@ -479,6 +480,19 @@ void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) { emit this->surfaceFormatChanged(); } +bool ProxyWindowBase::updatesEnabled() const { return this->mUpdatesEnabled; } + +void ProxyWindowBase::setUpdatesEnabled(bool updatesEnabled) { + if (updatesEnabled == this->mUpdatesEnabled) return; + this->mUpdatesEnabled = updatesEnabled; + + if (this->window != nullptr) { + QQuickWindowPrivate::get(this->window)->updatesEnabled = updatesEnabled; + } + + emit this->updatesEnabledChanged(); +} + qreal ProxyWindowBase::devicePixelRatio() const { if (this->window != nullptr) return this->window->devicePixelRatio(); if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio(); diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 86d66f8..aec821e 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -57,6 +57,7 @@ class ProxyWindowBase: public Reloadable { Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); Q_PROPERTY(bool backingWindowVisible READ isVisibleDirect NOTIFY backerVisibilityChanged); Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); + Q_PROPERTY(bool updatesEnabled READ updatesEnabled WRITE setUpdatesEnabled NOTIFY updatesEnabledChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -140,6 +141,9 @@ public: [[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; } void setSurfaceFormat(QsSurfaceFormat format); + [[nodiscard]] bool updatesEnabled() const; + void setUpdatesEnabled(bool updatesEnabled); + [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT [[nodiscard]] QQmlListProperty data(); @@ -163,6 +167,7 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void updatesEnabledChanged(); void polished(); protected slots: @@ -187,6 +192,7 @@ protected: ProxyWindowContentItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; + bool mUpdatesEnabled = true; QsSurfaceFormat qsSurfaceFormat; QSurfaceFormat mSurfaceFormat; diff --git a/src/window/windowinterface.cpp b/src/window/windowinterface.cpp index 8917f12..e41afc2 100644 --- a/src/window/windowinterface.cpp +++ b/src/window/windowinterface.cpp @@ -127,6 +127,9 @@ void WindowInterface::setMask(PendingRegion* mask) const { this->proxyWindow()-> QsSurfaceFormat WindowInterface::surfaceFormat() const { return this->proxyWindow()->surfaceFormat(); }; void WindowInterface::setSurfaceFormat(QsSurfaceFormat format) const { this->proxyWindow()->setSurfaceFormat(format); }; +bool WindowInterface::updatesEnabled() const { return this->proxyWindow()->updatesEnabled(); }; +void WindowInterface::setUpdatesEnabled(bool updatesEnabled) const { this->proxyWindow()->setUpdatesEnabled(updatesEnabled); }; + QQmlListProperty WindowInterface::data() const { return this->proxyWindow()->data(); }; // clang-format on @@ -148,6 +151,7 @@ void WindowInterface::connectSignals() const { QObject::connect(window, &ProxyWindowBase::colorChanged, this, &WindowInterface::colorChanged); QObject::connect(window, &ProxyWindowBase::maskChanged, this, &WindowInterface::maskChanged); QObject::connect(window, &ProxyWindowBase::surfaceFormatChanged, this, &WindowInterface::surfaceFormatChanged); + QObject::connect(window, &ProxyWindowBase::updatesEnabledChanged, this, &WindowInterface::updatesEnabledChanged); // clang-format on } diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index 9e917b9..6f3db20 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -143,6 +143,12 @@ class WindowInterface: public Reloadable { /// /// > [!NOTE] The surface format cannot be changed after the window is created. Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); + /// If the window should receive render updates. Defaults to true. + /// + /// When set to false, the window will not re-render in response to animations + /// or other visual updates from other windows. This is useful for static windows + /// such as wallpapers that do not need to update frequently, saving GPU cycles. + Q_PROPERTY(bool updatesEnabled READ updatesEnabled WRITE setUpdatesEnabled NOTIFY updatesEnabledChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -231,6 +237,9 @@ public: [[nodiscard]] QsSurfaceFormat surfaceFormat() const; void setSurfaceFormat(QsSurfaceFormat format) const; + [[nodiscard]] bool updatesEnabled() const; + void setUpdatesEnabled(bool updatesEnabled) const; + [[nodiscard]] QQmlListProperty data() const; static QsWindowAttached* qmlAttachedProperties(QObject* object); @@ -258,6 +267,7 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void updatesEnabledChanged(); protected: void connectSignals() const; From c3c3e2ca251a430dbe1b2d46ab0af4e5ca82c7e8 Mon Sep 17 00:00:00 2001 From: reakjra Date: Mon, 23 Feb 2026 18:28:01 +0100 Subject: [PATCH 167/226] wayland/screencopy: pin XRGB alpha to 1 in vulkan mode While EGL handles this internally, vulkan's alpha channel behavior is undefined when rendering depending on the driver. Notably intel does not treat it as 1.0. --- src/wayland/buffer/CMakeLists.txt | 2 +- src/wayland/buffer/dmabuf.cpp | 65 +++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt index a8f2d2d..15818fc 100644 --- a/src/wayland/buffer/CMakeLists.txt +++ b/src/wayland/buffer/CMakeLists.txt @@ -12,7 +12,7 @@ qt_add_library(quickshell-wayland-buffer STATIC wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") target_link_libraries(quickshell-wayland-buffer PRIVATE - Qt::Quick Qt::QuickPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick Qt::QuickPrivate Qt::GuiPrivate Qt::WaylandClient Qt::WaylandClientPrivate wayland-client PkgConfig::dmabuf-deps wlp-linux-dmabuf Vulkan::Headers diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 89c9108..7d17884 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +74,19 @@ VkFormat drmFormatToVkFormat(uint32_t drmFormat) { // NOLINTEND(bugprone-branch-clone) } +bool drmFormatHasAlpha(uint32_t drmFormat) { + switch (drmFormat) { + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_ABGR8888: + case DRM_FORMAT_ARGB2101010: + case DRM_FORMAT_ABGR2101010: + case DRM_FORMAT_ABGR16161616F: + return true; + default: + return false; + } +} + } // namespace QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { @@ -106,25 +120,27 @@ GbmDeviceHandle::~GbmDeviceHandle() { } } -// This will definitely backfire later +// Prefer ARGB over XRGB: XRGB has undefined alpha bytes which cause +// transparency artifacts on Vulkan (notably Intel GPUs) since Vulkan +// doesn't auto-fill alpha=1.0 for X formats like EGL does. void LinuxDmabufFormatSelection::ensureSorted() { if (this->sorted) return; auto beginIter = this->formats.begin(); - auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { - return format.first == DRM_FORMAT_XRGB8888; - }); - - if (xrgbIter != this->formats.end()) { - std::swap(*beginIter, *xrgbIter); - ++beginIter; - } - auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) { return format.first == DRM_FORMAT_ARGB8888; }); - if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter); + if (argbIter != this->formats.end()) { + std::swap(*beginIter, *argbIter); + ++beginIter; + } + + auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_XRGB8888; + }); + + if (xrgbIter != this->formats.end()) std::swap(*beginIter, *xrgbIter); this->sorted = true; } @@ -946,6 +962,33 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co {} ); + // For opaque DRM formats (XRGB, XBGR, etc.), the alpha bytes are underfined. + // EGL silently forces alpha=1.0 for these, but Vulkan doesn't. Replace Qt's + // default identity-swizzle VkImageView with one that maps alpha to ONE. + if (!drmFormatHasAlpha(this->format)) { + auto* vkTexture = static_cast(qsgTexture->rhiTexture()); // NOLINT + + devFuncs->vkDestroyImageView(device, vkTexture->imageView, nullptr); + + VkImageViewCreateInfo viewInfo = {}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = vkFormat; + viewInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + viewInfo.components.a = VK_COMPONENT_SWIZZLE_ONE; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.layerCount = 1; + + result = devFuncs->vkCreateImageView(device, &viewInfo, nullptr, &vkTexture->imageView); + if (result != VK_SUCCESS) { + qCWarning(logDmabuf) << "Failed to create alpha-swizzled VkImageView, result:" << result; + } + } + auto* tex = new WlDmaBufferVulkanQSGTexture( devFuncs, device, From 36517a2c10d206bbde30f6a43e0002b3c3ce139f Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 13 Feb 2026 17:54:43 -0500 Subject: [PATCH 168/226] services/pipewire: manage default objs using normal qt properties Fixes use after free bugs due to pointer mismatches in destructors. Drops SimpleObjectHandle. --- changelog/next.md | 1 + src/core/util.hpp | 31 -------- src/services/pipewire/defaults.cpp | 121 +++++++++++++++++++---------- src/services/pipewire/defaults.hpp | 5 +- 4 files changed, 83 insertions(+), 75 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 7180d53..b9000c2 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -48,6 +48,7 @@ set shell id. - Fixed memory leak in IPC handlers. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. +- Fixed crashes when default pipewire devices are lost. ## Packaging Changes diff --git a/src/core/util.hpp b/src/core/util.hpp index 3b86d28..bb8dd85 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -251,37 +251,6 @@ public: GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } }; -template -class SimpleObjectHandleOps { - using Traits = MemberPointerTraits; - -public: - static bool setObject(Traits::Class* parent, Traits::Type value) { - if (value == parent->*member) return false; - - if (parent->*member != nullptr) { - QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot); - } - - parent->*member = value; - - if (value != nullptr) { - QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot); - } - - if constexpr (changedSignal != nullptr) { - emit(parent->*changedSignal)(); - } - - return true; - } -}; - -template -bool setSimpleObjectHandle(auto* parent, auto* value) { - return SimpleObjectHandleOps::setObject(parent, value); -} - template class MethodFunctor { using PtrMeta = MemberPointerTraits; diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 02463f4..7a24a65 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -12,7 +12,6 @@ #include #include "../../core/logcat.hpp" -#include "../../core/util.hpp" #include "metadata.hpp" #include "node.hpp" #include "registry.hpp" @@ -138,32 +137,6 @@ void PwDefaultTracker::onNodeAdded(PwNode* node) { } } -void PwDefaultTracker::onNodeDestroyed(QObject* node) { - if (node == this->mDefaultSink) { - qCInfo(logDefaults) << "Default sink destroyed."; - this->mDefaultSink = nullptr; - emit this->defaultSinkChanged(); - } - - if (node == this->mDefaultSource) { - qCInfo(logDefaults) << "Default source destroyed."; - this->mDefaultSource = nullptr; - emit this->defaultSourceChanged(); - } - - if (node == this->mDefaultConfiguredSink) { - qCInfo(logDefaults) << "Default configured sink destroyed."; - this->mDefaultConfiguredSink = nullptr; - emit this->defaultConfiguredSinkChanged(); - } - - if (node == this->mDefaultConfiguredSource) { - qCInfo(logDefaults) << "Default configured source destroyed."; - this->mDefaultConfiguredSource = nullptr; - emit this->defaultConfiguredSourceChanged(); - } -} - void PwDefaultTracker::changeConfiguredSink(PwNode* node) { if (node != nullptr) { if (!node->type.testFlags(PwNodeType::AudioSink)) { @@ -240,10 +213,23 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) { if (node == this->mDefaultSink) return; qCInfo(logDefaults) << "Default sink changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultSink, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultSinkChanged>(this, node); + if (this->mDefaultSink != nullptr) { + QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr); + } + + this->mDefaultSink = node; + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSinkDestroyed); + } + + emit this->defaultSinkChanged(); +} + +void PwDefaultTracker::onDefaultSinkDestroyed() { + qCInfo(logDefaults) << "Default sink destroyed."; + this->mDefaultSink = nullptr; + emit this->defaultSinkChanged(); } void PwDefaultTracker::setDefaultSinkName(const QString& name) { @@ -257,10 +243,23 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) { if (node == this->mDefaultSource) return; qCInfo(logDefaults) << "Default source changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultSource, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultSourceChanged>(this, node); + if (this->mDefaultSource != nullptr) { + QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr); + } + + this->mDefaultSource = node; + + if (node != nullptr) { + QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSourceDestroyed); + } + + emit this->defaultSourceChanged(); +} + +void PwDefaultTracker::onDefaultSourceDestroyed() { + qCInfo(logDefaults) << "Default source destroyed."; + this->mDefaultSource = nullptr; + emit this->defaultSourceChanged(); } void PwDefaultTracker::setDefaultSourceName(const QString& name) { @@ -274,10 +273,28 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { if (node == this->mDefaultConfiguredSink) return; qCInfo(logDefaults) << "Default configured sink changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultConfiguredSink, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultConfiguredSinkChanged>(this, node); + if (this->mDefaultConfiguredSink != nullptr) { + QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr); + } + + this->mDefaultConfiguredSink = node; + + if (node != nullptr) { + QObject::connect( + node, + &QObject::destroyed, + this, + &PwDefaultTracker::onDefaultConfiguredSinkDestroyed + ); + } + + emit this->defaultConfiguredSinkChanged(); +} + +void PwDefaultTracker::onDefaultConfiguredSinkDestroyed() { + qCInfo(logDefaults) << "Default configured sink destroyed."; + this->mDefaultConfiguredSink = nullptr; + emit this->defaultConfiguredSinkChanged(); } void PwDefaultTracker::setDefaultConfiguredSinkName(const QString& name) { @@ -291,10 +308,28 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { if (node == this->mDefaultConfiguredSource) return; qCInfo(logDefaults) << "Default configured source changed to" << node; - setSimpleObjectHandle< - &PwDefaultTracker::mDefaultConfiguredSource, - &PwDefaultTracker::onNodeDestroyed, - &PwDefaultTracker::defaultConfiguredSourceChanged>(this, node); + if (this->mDefaultConfiguredSource != nullptr) { + QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr); + } + + this->mDefaultConfiguredSource = node; + + if (node != nullptr) { + QObject::connect( + node, + &QObject::destroyed, + this, + &PwDefaultTracker::onDefaultConfiguredSourceDestroyed + ); + } + + emit this->defaultConfiguredSourceChanged(); +} + +void PwDefaultTracker::onDefaultConfiguredSourceDestroyed() { + qCInfo(logDefaults) << "Default configured source destroyed."; + this->mDefaultConfiguredSource = nullptr; + emit this->defaultConfiguredSourceChanged(); } void PwDefaultTracker::setDefaultConfiguredSourceName(const QString& name) { diff --git a/src/services/pipewire/defaults.hpp b/src/services/pipewire/defaults.hpp index 591c4fd..f31669e 100644 --- a/src/services/pipewire/defaults.hpp +++ b/src/services/pipewire/defaults.hpp @@ -44,7 +44,10 @@ private slots: void onMetadataAdded(PwMetadata* metadata); void onMetadataProperty(const char* key, const char* type, const char* value); void onNodeAdded(PwNode* node); - void onNodeDestroyed(QObject* node); + void onDefaultSinkDestroyed(); + void onDefaultSourceDestroyed(); + void onDefaultConfiguredSinkDestroyed(); + void onDefaultConfiguredSourceDestroyed(); private: void setDefaultSink(PwNode* node); From 6e17efab83d3a5ad5d6e59bc08d26095c6660502 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 23 Feb 2026 23:03:48 -0800 Subject: [PATCH 169/226] wayland/screencopy: enable vulkan dmabuf support on session locks Also reformat dmabuf --- src/wayland/buffer/dmabuf.cpp | 27 ++++++++++----------------- src/wayland/session_lock.cpp | 10 ++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 7d17884..ed9dbeb 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,6 @@ #include #include #include -#include #include #include #include @@ -80,10 +80,8 @@ bool drmFormatHasAlpha(uint32_t drmFormat) { case DRM_FORMAT_ABGR8888: case DRM_FORMAT_ARGB2101010: case DRM_FORMAT_ABGR2101010: - case DRM_FORMAT_ABGR16161616F: - return true; - default: - return false; + case DRM_FORMAT_ABGR16161616F: return true; + default: return false; } } @@ -818,7 +816,8 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co // dup() is required because vkAllocateMemory with VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT // takes ownership of the fd on succcess. Without dup, WlDmaBuffer would double-close. - const int dupFd = dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + const int dupFd = + dup(this->planes[0].fd); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) if (dupFd < 0) { qCWarning(logDmabuf) << "Failed to dup() fd for DMA-BUF import"; goto cleanup_fail; // NOLINT @@ -909,12 +908,12 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co // find the graphics queue family index for the ownrship transfer. uint32_t graphicsQueueFamily = 0; uint32_t queueFamilyCount = 0; - instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( - physDevice, &queueFamilyCount, nullptr - ); + instFuncs->vkGetPhysicalDeviceQueueFamilyProperties(physDevice, &queueFamilyCount, nullptr); std::vector queueFamilies(queueFamilyCount); instFuncs->vkGetPhysicalDeviceQueueFamilyProperties( - physDevice, &queueFamilyCount, queueFamilies.data() + physDevice, + &queueFamilyCount, + queueFamilies.data() ); for (uint32_t i = 0; i < queueFamilyCount; ++i) { if (queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { @@ -989,13 +988,7 @@ WlBufferQSGTexture* WlDmaBuffer::createQsgTextureVulkan(QQuickWindow* window) co } } - auto* tex = new WlDmaBufferVulkanQSGTexture( - devFuncs, - device, - image, - memory, - qsgTexture - ); + auto* tex = new WlDmaBufferVulkanQSGTexture(devFuncs, device, image, memory, qsgTexture); qCDebug(logDmabuf) << "Created WlDmaBufferVulkanQSGTexture" << tex << "from" << this; return tex; } diff --git a/src/wayland/session_lock.cpp b/src/wayland/session_lock.cpp index d5a3e53..2ebe3fd 100644 --- a/src/wayland/session_lock.cpp +++ b/src/wayland/session_lock.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -216,6 +217,15 @@ void WlSessionLockSurface::onReload(QObject* oldInstance) { if (this->window == nullptr) { this->window = new QQuickWindow(); + + // needed for vulkan dmabuf import, qt ignores these if not applicable + auto graphicsConfig = this->window->graphicsConfiguration(); + graphicsConfig.setDeviceExtensions({ + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_EXT_image_drm_format_modifier", + }); + this->window->setGraphicsConfiguration(graphicsConfig); } this->mContentItem->setParentItem(this->window->contentItem()); From cddb4f061bab495f4473ca5f2c571b6c710efef7 Mon Sep 17 00:00:00 2001 From: Carson Powers Date: Fri, 6 Feb 2026 17:25:50 -0600 Subject: [PATCH 170/226] build: fix lint-staged to ignore deleted files --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 2d6377e..801eb2a 100644 --- a/Justfile +++ b/Justfile @@ -13,7 +13,7 @@ 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") }} + git diff --staged --name-only --diff-filter=d 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}} \ From cdde4c63f4dd09e92a960e27f1202ca2e0d830d1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 2 Mar 2026 08:09:57 -0800 Subject: [PATCH 171/226] crash: switch to cpptrace from breakpad --- .github/ISSUE_TEMPLATE/crash.yml | 2 +- .github/ISSUE_TEMPLATE/crash2.yml | 49 +++++++ .github/workflows/build.yml | 7 +- BUILD.md | 12 +- CMakeLists.txt | 3 +- changelog/next.md | 6 +- default.nix | 15 +- quickshell.scm | 3 +- src/CMakeLists.txt | 2 +- src/build/CMakeLists.txt | 6 +- src/build/build.hpp.in | 2 +- src/core/instanceinfo.hpp | 2 + src/crash/CMakeLists.txt | 49 ++++++- src/crash/handler.cpp | 233 +++++++++++++++++------------- src/crash/handler.hpp | 13 +- src/crash/interface.cpp | 4 +- src/crash/main.cpp | 122 +++++++++++++--- src/launch/launch.cpp | 9 +- src/launch/main.cpp | 6 +- 19 files changed, 372 insertions(+), 173 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/crash2.yml diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index c8b4804..80fa827 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,4 +1,4 @@ -name: Crash Report +name: Crash Report (v1) description: Quickshell has crashed labels: ["bug", "crash"] body: diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml new file mode 100644 index 0000000..84beef8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash2.yml @@ -0,0 +1,49 @@ +name: Crash Report (v2) +description: Quickshell has crashed +labels: ["bug", "crash"] +body: + - type: textarea + id: userinfo + attributes: + label: What caused the crash + description: | + Any information likely to help debug the crash. What were you doing when the crash occurred, + what changes did you make, can you get it to happen again? + - type: textarea + id: report + attributes: + label: Report file + description: Attach `report.txt` here. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Log file + description: | + Attach `log.qslog.log` here. If it is too big to upload, compress it. + + You can preview the log if you'd like using `quickshell read-log `. + validations: + required: true + - type: textarea + id: config + attributes: + label: Configuration + description: | + Attach your configuration here, preferrably in full (not just one file). + Compress it into a zip, tar, etc. + + This will help us reproduce the crash ourselves. + - type: textarea + id: bt + attributes: + label: Backtrace + description: | + GDB usually produces better stacktraces than quickshell can. Consider attaching a gdb backtrace + following the instructions below. + + 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" + in the crash reporter. + 2. Once it loads, type `bt -full` (then enter) + 3. Copy the output and attach it as a file or in a spoiler. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d19f58..7b8cbce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,10 +55,11 @@ jobs: libpipewire \ cli11 \ polkit \ - jemalloc + jemalloc \ + libunwind \ + git # for cpptrace clone - name: Build - # breakpad is annoying to build in ci due to makepkg not running as root run: | - cmake -GNinja -B build -DCRASH_REPORTER=OFF + cmake -GNinja -B build -DVENDOR_CPPTRACE=ON cmake --build build diff --git a/BUILD.md b/BUILD.md index c9459b5..6a3f422 100644 --- a/BUILD.md +++ b/BUILD.md @@ -64,14 +64,18 @@ At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. -### Crash Reporter -The crash reporter catches crashes, restarts quickshell when it crashes, +### Crash Handler +The crash reporter catches crashes, restarts Quickshell when it crashes, and collects useful crash information in one place. Leaving this enabled will enable us to fix bugs far more easily. -To disable: `-DCRASH_REPORTER=OFF` +To disable: `-DCRASH_HANDLER=OFF` -Dependencies: `google-breakpad` (static library) +Dependencies: `cpptrace` + +Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent. + +When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent. ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused diff --git a/CMakeLists.txt b/CMakeLists.txt index 7633f4f..fabda0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,12 +47,11 @@ boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang boption(FRAME_POINTERS "Keep Frame Pointers (dev)" ${ASAN}) if (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") - boption(CRASH_REPORTER "Crash Handling" OFF) boption(USE_JEMALLOC "Use jemalloc" OFF) else() - boption(CRASH_REPORTER "Crash Handling" ON) boption(USE_JEMALLOC "Use jemalloc" ON) endif() +boption(CRASH_HANDLER "Crash Handling" ON) boption(SOCKETS "Unix Sockets" ON) boption(WAYLAND "Wayland" ON) boption(WAYLAND_WLR_LAYERSHELL " Wlroots Layer-Shell" ON REQUIRES WAYLAND) diff --git a/changelog/next.md b/changelog/next.md index b9000c2..2083462 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -32,6 +32,7 @@ set shell id. - FreeBSD is now partially supported. - IPC operations filter available instances to the current display connection by default. - PwNodeLinkTracker ignores sound level monitoring programs. +- Replaced breakpad with cpptrace. ## Bug Fixes @@ -52,5 +53,6 @@ set shell id. ## Packaging Changes -`glib` and `polkit` have been added as dependencies when compiling with polkit agent support. -`vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). +- `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. +- `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). +- `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. diff --git a/default.nix b/default.nix index 7783774..59e68b0 100644 --- a/default.nix +++ b/default.nix @@ -10,7 +10,9 @@ ninja, spirv-tools, qt6, - breakpad, + cpptrace ? null, + libunwind, + libdwarf, jemalloc, cli11, wayland, @@ -49,6 +51,8 @@ withPolkit ? true, withNetworkManager ? true, }: let + withCrashHandler = withCrashReporter && cpptrace != null && lib.strings.compareVersions cpptrace.version "0.7.2" >= 0; + unwrapped = stdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.2.1"; @@ -74,7 +78,12 @@ cli11 ] ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optional withCrashReporter breakpad + ++ lib.optional withCrashHandler (cpptrace.overrideAttrs (prev: { + cmakeFlags = prev.cmakeFlags ++ [ + "-DCPPTRACE_UNWIND_WITH_LIBUNWIND=TRUE" + ]; + buildInputs = prev.buildInputs ++ [ libunwind ]; + })) ++ lib.optional withJemalloc jemalloc ++ lib.optional (withWayland && lib.strings.compareVersions qt6.qtbase.version "6.10.0" == -1) qt6.qtwayland ++ lib.optionals withWayland [ wayland wayland-protocols ] @@ -91,7 +100,7 @@ (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix) (lib.cmakeBool "DISTRIBUTOR_DEBUGINFO_AVAILABLE" true) (lib.cmakeFeature "GIT_REVISION" gitRev) - (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) + (lib.cmakeBool "CRASH_HANDLER" withCrashHandler) (lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "WAYLAND" withWayland) (lib.cmakeBool "SCREENCOPY" (libgbm != null)) diff --git a/quickshell.scm b/quickshell.scm index 3f82160..780bb96 100644 --- a/quickshell.scm +++ b/quickshell.scm @@ -56,8 +56,7 @@ #~(list "-GNinja" "-DDISTRIBUTOR=\"In-tree Guix channel\"" "-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=NO" - ;; Breakpad is not currently packaged for Guix. - "-DCRASH_REPORTER=OFF") + "-DCRASH_HANDLER=OFF") #:phases #~(modify-phases %standard-phases (replace 'build (lambda _ (invoke "cmake" "--build" "."))) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c95ecf7..4b13d45 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,7 +12,7 @@ add_subdirectory(io) add_subdirectory(widgets) add_subdirectory(ui) -if (CRASH_REPORTER) +if (CRASH_HANDLER) add_subdirectory(crash) endif() diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt index bb35da9..62574d9 100644 --- a/src/build/CMakeLists.txt +++ b/src/build/CMakeLists.txt @@ -9,10 +9,10 @@ if (NOT DEFINED GIT_REVISION) ) endif() -if (CRASH_REPORTER) - set(CRASH_REPORTER_DEF 1) +if (CRASH_HANDLER) + set(CRASH_HANDLER_DEF 1) else() - set(CRASH_REPORTER_DEF 0) + set(CRASH_HANDLER_DEF 0) endif() if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 66fb664..93e78a9 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -9,7 +9,7 @@ #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" #define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ -#define CRASH_REPORTER @CRASH_REPORTER_DEF@ +#define CRASH_HANDLER @CRASH_HANDLER_DEF@ #define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index d462f6e..977e4c2 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -35,6 +35,8 @@ namespace qs::crash { struct CrashInfo { int logFd = -1; + int traceFd = -1; + int infoFd = -1; static CrashInfo INSTANCE; // NOLINT }; diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 7fdd830..a891ee9 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -6,12 +6,51 @@ qt_add_library(quickshell-crash STATIC qs_pch(quickshell-crash SET large) -find_package(PkgConfig REQUIRED) -pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad) -# only need client?? take only includes from pkg config todo -target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client) +if (VENDOR_CPPTRACE) + message(STATUS "Vendoring cpptrace...") + include(FetchContent) + + # For use without internet access see: https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_SOURCE_DIR_%3CuppercaseName%3E + FetchContent_Declare( + cpptrace + GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git + GIT_TAG v1.0.4 + ) + + set(CPPTRACE_UNWIND_WITH_LIBUNWIND TRUE) + FetchContent_MakeAvailable(cpptrace) +else () + find_package(cpptrace REQUIRED) + + # useful for cross after you have already checked cpptrace is built correctly + if (NOT DO_NOT_CHECK_CPPTRACE_USABILITY) + try_run(CPPTRACE_SIGNAL_SAFE_UNWIND CPPTRACE_SIGNAL_SAFE_UNWIND_COMP + SOURCE_FROM_CONTENT check.cxx " + #include + int main() { + return cpptrace::can_signal_safe_unwind() ? 0 : 1; + } + " + LOG_DESCRIPTION "Checking ${CPPTRACE_SIGNAL_SAFE_UNWIND}" + LINK_LIBRARIES cpptrace::cpptrace + COMPILE_OUTPUT_VARIABLE CPPTRACE_SIGNAL_SAFE_UNWIND_LOG + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + + if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND_COMP) + message(STATUS "${CPPTRACE_SIGNAL_SAFE_UNWIND_LOG}") + message(FATAL_ERROR "Failed to compile cpptrace signal safe unwind tester.") + endif() + + if (NOT CPPTRACE_SIGNAL_SAFE_UNWIND EQUAL 0) + message(STATUS "Cpptrace signal safe unwind test exited with: ${CPPTRACE_SIGNAL_SAFE_UNWIND}") + message(FATAL_ERROR "Cpptrace was built without CPPTRACE_UNWIND_WITH_LIBUNWIND set to true. Enable libunwind support in the package or set VENDOR_CPPTRACE to true when building Quickshell.") + endif() + endif () +endif () # quick linked for pch compat -target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) +target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets cpptrace::cpptrace) target_link_libraries(quickshell PRIVATE quickshell-crash) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 0baa8e6..fd40f94 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -1,12 +1,12 @@ #include "handler.hpp" +#include #include +#include #include #include -#include -#include -#include -#include +#include +#include #include #include #include @@ -19,98 +19,60 @@ extern char** environ; // NOLINT -using namespace google_breakpad; - namespace qs::crash { namespace { + QS_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); -} -struct CrashHandlerPrivate { - ExceptionHandler* exceptionHandler = nullptr; - int minidumpFd = -1; - int infoFd = -1; +void writeEnvInt(char* buf, const char* name, int value) { + // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic) + while (*name != '\0') *buf++ = *name++; + *buf++ = '='; - static bool minidumpCallback(const MinidumpDescriptor& descriptor, void* context, bool succeeded); -}; - -CrashHandler::CrashHandler(): d(new CrashHandlerPrivate()) {} - -void CrashHandler::init() { - // MinidumpDescriptor has no move constructor and the copy constructor breaks fds. - auto createHandler = [this](const MinidumpDescriptor& desc) { - this->d->exceptionHandler = new ExceptionHandler( - desc, - nullptr, - &CrashHandlerPrivate::minidumpCallback, - this->d, - true, - -1 - ); - }; - - qCDebug(logCrashHandler) << "Starting crash handler..."; - - this->d->minidumpFd = memfd_create("quickshell:minidump", MFD_CLOEXEC); - - if (this->d->minidumpFd == -1) { - qCCritical( - logCrashHandler - ) << "Failed to allocate minidump memfd, minidumps will be saved in the working directory."; - createHandler(MinidumpDescriptor(".")); - } else { - qCDebug(logCrashHandler) << "Created memfd" << this->d->minidumpFd - << "for holding possible minidumps."; - createHandler(MinidumpDescriptor(this->d->minidumpFd)); + if (value < 0) { + *buf++ = '-'; + value = -value; } - qCInfo(logCrashHandler) << "Crash handler initialized."; -} - -void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { - this->d->infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); - - if (this->d->infoFd == -1) { - qCCritical( - logCrashHandler - ) << "Failed to allocate instance info memfd, crash recovery will not work."; + if (value == 0) { + *buf++ = '0'; + *buf = '\0'; return; } - QFile file; - - if (!file.open(this->d->infoFd, QFile::ReadWrite)) { - qCCritical( - logCrashHandler - ) << "Failed to open instance info memfd, crash recovery will not work."; + auto* start = buf; + while (value > 0) { + *buf++ = static_cast('0' + (value % 10)); + value /= 10; } - QDataStream ds(&file); - ds << info; - file.flush(); - - qCDebug(logCrashHandler) << "Stored instance info in memfd" << this->d->infoFd; + *buf = '\0'; + std::reverse(start, buf); + // NOLINTEND } -CrashHandler::~CrashHandler() { - delete this->d->exceptionHandler; - delete this->d; -} - -bool CrashHandlerPrivate::minidumpCallback( - const MinidumpDescriptor& /*descriptor*/, - void* context, - bool /*success*/ +void signalHandler( + int sig, + siginfo_t* /*info*/, // NOLINT (misc-include-cleaner) + void* /*context*/ ) { - // A fork that just dies to ensure the coredump is caught by the system. - auto coredumpPid = fork(); + if (CrashInfo::INSTANCE.traceFd != -1) { + auto traceBuffer = std::array(); + auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1); - if (coredumpPid == 0) { - return false; + for (size_t i = 0; i < static_cast(frameCount); i++) { + auto frame = cpptrace::safe_object_frame(); + cpptrace::get_safe_object_frame(traceBuffer[i], &frame); + write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + } } - auto* self = static_cast(context); + auto coredumpPid = fork(); + if (coredumpPid == 0) { + raise(sig); + _exit(-1); + } auto exe = std::array(); if (readlink("/proc/self/exe", exe.data(), exe.size() - 1) == -1) { @@ -123,17 +85,19 @@ bool CrashHandlerPrivate::minidumpCallback( auto env = std::array(); auto envi = 0; - auto infoFd = dup(self->infoFd); - auto infoFdStr = std::array(); - memcpy(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD=-1" /*\0*/, 30); - if (infoFd != -1) my_uitos(&infoFdStr[27], infoFd, 10); + // dup to remove CLOEXEC + auto infoFdStr = std::array(); + writeEnvInt(infoFdStr.data(), "__QUICKSHELL_CRASH_INFO_FD", dup(CrashInfo::INSTANCE.infoFd)); env[envi++] = infoFdStr.data(); - auto corePidStr = std::array(); - memcpy(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID=-1" /*\0*/, 31); - if (coredumpPid != -1) my_uitos(&corePidStr[28], coredumpPid, 10); + auto corePidStr = std::array(); + writeEnvInt(corePidStr.data(), "__QUICKSHELL_CRASH_DUMP_PID", coredumpPid); env[envi++] = corePidStr.data(); + auto sigStr = std::array(); + writeEnvInt(sigStr.data(), "__QUICKSHELL_CRASH_SIGNAL", sig); + env[envi++] = sigStr.data(); + auto populateEnv = [&]() { auto senvi = 0; while (envi != 4095) { @@ -145,30 +109,18 @@ bool CrashHandlerPrivate::minidumpCallback( env[envi] = nullptr; }; - sigset_t sigset; - sigemptyset(&sigset); // NOLINT (include) - sigprocmask(SIG_SETMASK, &sigset, nullptr); // NOLINT - auto pid = fork(); if (pid == -1) { perror("Failed to fork and launch crash reporter.\n"); - return false; + _exit(-1); } else if (pid == 0) { + // dup to remove CLOEXEC - // if already -1 will return -1 - auto dumpFd = dup(self->minidumpFd); - auto logFd = dup(CrashInfo::INSTANCE.logFd); - - // allow up to 10 digits, which should never happen - auto dumpFdStr = std::array(); - auto logFdStr = std::array(); - - memcpy(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD=-1" /*\0*/, 30); - memcpy(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD=-1" /*\0*/, 29); - - if (dumpFd != -1) my_uitos(&dumpFdStr[27], dumpFd, 10); - if (logFd != -1) my_uitos(&logFdStr[26], logFd, 10); + auto dumpFdStr = std::array(); + auto logFdStr = std::array(); + writeEnvInt(dumpFdStr.data(), "__QUICKSHELL_CRASH_DUMP_FD", dup(CrashInfo::INSTANCE.traceFd)); + writeEnvInt(logFdStr.data(), "__QUICKSHELL_CRASH_LOG_FD", dup(CrashInfo::INSTANCE.logFd)); env[envi++] = dumpFdStr.data(); env[envi++] = logFdStr.data(); @@ -185,8 +137,83 @@ bool CrashHandlerPrivate::minidumpCallback( perror("Failed to relaunch quickshell.\n"); _exit(-1); } +} - return false; // should make sure it hits the system coredump handler +} // namespace + +void CrashHandler::init() { + qCDebug(logCrashHandler) << "Starting crash handler..."; + + CrashInfo::INSTANCE.traceFd = memfd_create("quickshell:trace", MFD_CLOEXEC); + + if (CrashInfo::INSTANCE.traceFd == -1) { + qCCritical(logCrashHandler) << "Failed to allocate trace memfd, stack traces will not be " + "available in crash reports."; + } else { + qCDebug(logCrashHandler) << "Created memfd" << CrashInfo::INSTANCE.traceFd + << "for holding possible stack traces."; + } + + { + // Preload anything dynamically linked to avoid malloc etc in the dynamic loader. + // See cpptrace documentation for more information. + auto buffer = std::array(); + cpptrace::safe_generate_raw_trace(buffer.data(), buffer.size()); + auto frame = cpptrace::safe_object_frame(); + cpptrace::get_safe_object_frame(buffer[0], &frame); + } + + // NOLINTBEGIN (misc-include-cleaner) + + // Set up alternate signal stack for stack overflow handling + auto ss = stack_t(); + ss.ss_sp = new char[SIGSTKSZ]; + ; + ss.ss_size = SIGSTKSZ; + ss.ss_flags = 0; + sigaltstack(&ss, nullptr); + + // Install signal handlers + struct sigaction sa {}; + sa.sa_sigaction = &signalHandler; + sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESETHAND; + sigemptyset(&sa.sa_mask); + + sigaction(SIGSEGV, &sa, nullptr); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGFPE, &sa, nullptr); + sigaction(SIGILL, &sa, nullptr); + sigaction(SIGBUS, &sa, nullptr); + sigaction(SIGTRAP, &sa, nullptr); + + // NOLINTEND (misc-include-cleaner) + + qCInfo(logCrashHandler) << "Crash handler initialized."; +} + +void CrashHandler::setRelaunchInfo(const RelaunchInfo& info) { + CrashInfo::INSTANCE.infoFd = memfd_create("quickshell:instance_info", MFD_CLOEXEC); + + if (CrashInfo::INSTANCE.infoFd == -1) { + qCCritical( + logCrashHandler + ) << "Failed to allocate instance info memfd, crash recovery will not work."; + return; + } + + QFile file; + + if (!file.open(CrashInfo::INSTANCE.infoFd, QFile::ReadWrite)) { + qCCritical( + logCrashHandler + ) << "Failed to open instance info memfd, crash recovery will not work."; + } + + QDataStream ds(&file); + ds << info; + file.flush(); + + qCDebug(logCrashHandler) << "Stored instance info in memfd" << CrashInfo::INSTANCE.infoFd; } } // namespace qs::crash diff --git a/src/crash/handler.hpp b/src/crash/handler.hpp index 2a1d86f..9488d71 100644 --- a/src/crash/handler.hpp +++ b/src/crash/handler.hpp @@ -5,19 +5,10 @@ #include "../core/instanceinfo.hpp" namespace qs::crash { -struct CrashHandlerPrivate; - class CrashHandler { public: - explicit CrashHandler(); - ~CrashHandler(); - Q_DISABLE_COPY_MOVE(CrashHandler); - - void init(); - void setRelaunchInfo(const RelaunchInfo& info); - -private: - CrashHandlerPrivate* d; + static void init(); + static void setRelaunchInfo(const RelaunchInfo& info); }; } // namespace qs::crash diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index 326216a..a3422d3 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -78,7 +78,7 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) mainLayout->addWidget(new ReportLabel( "Github:", - "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash.yml", + "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml", this )); @@ -114,7 +114,7 @@ void CrashReporterGui::openFolder() { void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl( - QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash.yml") + QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml") ); } diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 6571660..c406ba6 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -1,7 +1,10 @@ #include "main.hpp" #include #include +#include +#include +#include #include #include #include @@ -13,13 +16,17 @@ #include #include #include +#include #include #include +#include #include "../core/instanceinfo.hpp" #include "../core/logcat.hpp" #include "../core/logging.hpp" +#include "../core/logging_p.hpp" #include "../core/paths.hpp" +#include "../core/ringbuf.hpp" #include "build.hpp" #include "interface.hpp" @@ -61,6 +68,76 @@ int tryDup(int fd, const QString& path) { return 0; } +QString readRecentLogs(int logFd, int maxLines, qint64 maxAgeSecs) { + QFile file; + if (!file.open(logFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + return QStringLiteral("(failed to open log fd)\n"); + } + + file.seek(0); + + qs::log::EncodedLogReader reader; + reader.setDevice(&file); + + bool readable = false; + quint8 logVersion = 0; + quint8 readerVersion = 0; + if (!reader.readHeader(&readable, &logVersion, &readerVersion) || !readable) { + return QStringLiteral("(failed to read log header)\n"); + } + + // Read all messages, keeping last maxLines in a ring buffer + auto tail = RingBuffer(maxLines); + qs::log::LogMessage message; + while (reader.read(&message)) { + tail.emplace(message); + } + + if (tail.size() == 0) { + return QStringLiteral("(no logs)\n"); + } + + // Filter to only messages within maxAgeSecs of the newest message + auto cutoff = tail.at(0).time.addSecs(-maxAgeSecs); + + QString result; + auto stream = QTextStream(&result); + for (auto i = tail.size() - 1; i != -1; i--) { + if (tail.at(i).time < cutoff) continue; + qs::log::LogMessage::formatMessage(stream, tail.at(i), false, true); + stream << '\n'; + } + + if (result.isEmpty()) { + return QStringLiteral("(no recent logs)\n"); + } + + return result; +} + +cpptrace::stacktrace resolveStacktrace(int dumpFd) { + QFile sourceFile; + if (!sourceFile.open(dumpFd, QFile::ReadOnly, QFile::AutoCloseHandle)) { + qCCritical(logCrashReporter) << "Failed to open trace memfd."; + return {}; + } + + sourceFile.seek(0); + auto data = sourceFile.readAll(); + + auto frameCount = static_cast(data.size()) / sizeof(cpptrace::safe_object_frame); + if (frameCount == 0) return {}; + + const auto* frames = reinterpret_cast(data.constData()); + + cpptrace::object_trace objectTrace; + for (size_t i = 0; i < frameCount; i++) { + objectTrace.frames.push_back(frames[i].resolve()); // NOLINT + } + + return objectTrace.resolve(); +} + void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCDebug(logCrashReporter) << "Recording crash information at" << crashDir.path(); @@ -71,32 +148,25 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { } auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); + auto crashSignal = qEnvironmentVariable("__QUICKSHELL_CRASH_SIGNAL").toInt(); auto dumpFd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD").toInt(); auto logFd = qEnvironmentVariable("__QUICKSHELL_CRASH_LOG_FD").toInt(); - qCDebug(logCrashReporter) << "Saving minidump from fd" << dumpFd; - auto dumpDupStatus = tryDup(dumpFd, crashDir.filePath("minidump.dmp.log")); - if (dumpDupStatus != 0) { - qCCritical(logCrashReporter) << "Failed to write minidump:" << dumpDupStatus; - } + qCDebug(logCrashReporter) << "Resolving stacktrace from fd" << dumpFd; + auto stacktrace = resolveStacktrace(dumpFd); - qCDebug(logCrashReporter) << "Saving log from fd" << logFd; - auto logDupStatus = tryDup(logFd, crashDir.filePath("log.qslog.log")); + qCDebug(logCrashReporter) << "Reading recent log lines from fd" << logFd; + auto logDupFd = dup(logFd); + auto recentLogs = readRecentLogs(logFd, 100, 10); + + qCDebug(logCrashReporter) << "Saving log from fd" << logDupFd; + auto logDupStatus = tryDup(logDupFd, crashDir.filePath("log.qslog.log")); if (logDupStatus != 0) { qCCritical(logCrashReporter) << "Failed to save log:" << logDupStatus; } - auto copyBinStatus = 0; - if (!DISTRIBUTOR_DEBUGINFO_AVAILABLE) { - qCDebug(logCrashReporter) << "Copying binary to crash folder"; - if (!QFile(QCoreApplication::applicationFilePath()).copy(crashDir.filePath("executable.txt"))) { - copyBinStatus = 1; - qCCritical(logCrashReporter) << "Failed to copy binary."; - } - } - { - auto extraInfoFile = QFile(crashDir.filePath("info.txt")); + auto extraInfoFile = QFile(crashDir.filePath("report.txt")); if (!extraInfoFile.open(QFile::WriteOnly)) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { @@ -111,16 +181,12 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "\n===== Runtime Information =====\n"; stream << "Runtime Qt Version: " << qVersion() << '\n'; + stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; stream << "Shell ID: " << instance.shellId << '\n'; stream << "Config Path: " << instance.configPath << '\n'; - stream << "\n===== Report Integrity =====\n"; - stream << "Minidump save status: " << dumpDupStatus << '\n'; - stream << "Log save status: " << logDupStatus << '\n'; - stream << "Binary copy status: " << copyBinStatus << '\n'; - stream << "\n===== System Information =====\n\n"; stream << "/etc/os-release:"; auto osReleaseFile = QFile("/etc/os-release"); @@ -140,6 +206,18 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { stream << "FAILED TO OPEN\n"; } + stream << "\n===== Stacktrace =====\n"; + if (stacktrace.empty()) { + stream << "(no trace available)\n"; + } else { + auto formatter = cpptrace::formatter().header(std::string()); + auto traceStr = formatter.format(stacktrace); + stream << QString::fromStdString(traceStr) << '\n'; + } + + stream << "\n===== Log Tail =====\n"; + stream << recentLogs; + extraInfoFile.close(); } } diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index f269f61..ee7ca64 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -27,7 +27,7 @@ #include "build.hpp" #include "launch_p.hpp" -#if CRASH_REPORTER +#if CRASH_HANDLER #include "../crash/handler.hpp" #endif @@ -137,13 +137,12 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio .display = getDisplayConnection(), }; -#if CRASH_REPORTER - auto crashHandler = crash::CrashHandler(); - crashHandler.init(); +#if CRASH_HANDLER + crash::CrashHandler::init(); { auto* log = LogManager::instance(); - crashHandler.setRelaunchInfo({ + crash::CrashHandler::setRelaunchInfo({ .instance = InstanceInfo::CURRENT, .noColor = !log->colorLogs, .timestamp = log->timestampLogs, diff --git a/src/launch/main.cpp b/src/launch/main.cpp index 7a801fc..a324e09 100644 --- a/src/launch/main.cpp +++ b/src/launch/main.cpp @@ -16,7 +16,7 @@ #include "build.hpp" #include "launch_p.hpp" -#if CRASH_REPORTER +#if CRASH_HANDLER #include "../crash/main.hpp" #endif @@ -25,7 +25,7 @@ namespace qs::launch { namespace { void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { -#if CRASH_REPORTER +#if CRASH_HANDLER auto lastInfoFdStr = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD"); if (!lastInfoFdStr.isEmpty()) { @@ -104,7 +104,7 @@ void exitDaemon(int code) { int main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); -#if CRASH_REPORTER +#if CRASH_HANDLER qsCheckCrash(argc, argv); #endif From a849a88893c71d409aecef0b999e6cc3d9b50034 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 3 Mar 2026 00:40:36 -0800 Subject: [PATCH 172/226] build: remove DISTRIBUTOR_DEBUGINFO_AVAILABLE --- BUILD.md | 10 +--------- CMakeLists.txt | 1 - changelog/next.md | 1 + src/build/CMakeLists.txt | 6 ------ src/build/build.hpp.in | 1 - src/crash/handler.cpp | 14 ++++++++++++-- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/BUILD.md b/BUILD.md index 6a3f422..29aecac 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,15 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp - `Nixpkgs` - `Fedora COPR (errornointernet/quickshell)` -`-DDISTRIBUTOR_DEBUGINFO_AVAILABLE=YES/NO` - -If we can retrieve binaries and debug information for the package without actually running your -distribution (e.g. from an website), and you would like to strip the binary, please set this to `YES`. - -If we cannot retrieve debug information, please set this to `NO` and -**ensure you aren't distributing stripped (non debuggable) binaries**. - -In both cases you should build with `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (then split or keep the debuginfo). +Please leave at least symbol names attached to the binary for debugging purposes. ### QML Module dir Currently all QML modules are statically linked to quickshell, but this is where diff --git a/CMakeLists.txt b/CMakeLists.txt index fabda0e..d57e322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,7 +40,6 @@ string(APPEND QS_BUILD_OPTIONS " Distributor: ${DISTRIBUTOR}") message(STATUS "Quickshell configuration") message(STATUS " Distributor: ${DISTRIBUTOR}") -boption(DISTRIBUTOR_DEBUGINFO_AVAILABLE "Distributor provided debuginfo" NO) boption(NO_PCH "Disable precompild headers (dev)" OFF) boption(BUILD_TESTING "Build tests (dev)" OFF) boption(ASAN "ASAN (dev)" OFF) # note: better output with gcc than clang diff --git a/changelog/next.md b/changelog/next.md index 2083462..0feffe1 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -56,3 +56,4 @@ set shell id. - `glib` and `polkit` have been added as dependencies when compiling with polkit agent support. - `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). - `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. +- `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad. diff --git a/src/build/CMakeLists.txt b/src/build/CMakeLists.txt index 62574d9..c1ffa59 100644 --- a/src/build/CMakeLists.txt +++ b/src/build/CMakeLists.txt @@ -15,12 +15,6 @@ else() set(CRASH_HANDLER_DEF 0) endif() -if (DISTRIBUTOR_DEBUGINFO_AVAILABLE) - set(DEBUGINFO_AVAILABLE 1) -else() - set(DEBUGINFO_AVAILABLE 0) -endif() - configure_file(build.hpp.in build.hpp @ONLY ESCAPE_QUOTES) target_include_directories(quickshell-build INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 93e78a9..2ab2db2 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -8,7 +8,6 @@ #define QS_UNRELEASED_FEATURES "@UNRELEASED_FEATURES@" #define GIT_REVISION "@GIT_REVISION@" #define DISTRIBUTOR "@DISTRIBUTOR@" -#define DISTRIBUTOR_DEBUGINFO_AVAILABLE @DEBUGINFO_AVAILABLE@ #define CRASH_HANDLER @CRASH_HANDLER_DEF@ #define BUILD_TYPE "@CMAKE_BUILD_TYPE@" #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index fd40f94..c875c2e 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -1,6 +1,7 @@ #include "handler.hpp" #include #include +#include #include #include #include @@ -64,8 +65,18 @@ void signalHandler( for (size_t i = 0; i < static_cast(frameCount); i++) { auto frame = cpptrace::safe_object_frame(); cpptrace::get_safe_object_frame(traceBuffer[i], &frame); - write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + + auto* wptr = reinterpret_cast(&frame); + auto* end = wptr + sizeof(cpptrace::safe_object_frame); // NOLINT + while (wptr != end) { + auto r = write(CrashInfo::INSTANCE.traceFd, &frame, sizeof(cpptrace::safe_object_frame)); + if (r < 0 && errno == EINTR) continue; + if (r <= 0) goto fail; + wptr += r; // NOLINT + } } + + fail:; } auto coredumpPid = fork(); @@ -168,7 +179,6 @@ void CrashHandler::init() { // Set up alternate signal stack for stack overflow handling auto ss = stack_t(); ss.ss_sp = new char[SIGSTKSZ]; - ; ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; sigaltstack(&ss, nullptr); From 5721955686a474b814c27bc0ec743f86e473ac4f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 4 Mar 2026 23:26:33 -0800 Subject: [PATCH 173/226] services/pipewire: ignore ENOENT errors Pipewire describes all errors as fatal, however these just aren't, don't seem to be squashable, and resetting for them breaks users. --- src/services/pipewire/core.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index e40bc54..5077abe 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -143,12 +143,17 @@ void PwCore::onSync(void* data, quint32 id, qint32 seq) { void PwCore::onError(void* data, quint32 id, qint32 /*seq*/, qint32 res, const char* message) { auto* self = static_cast(data); - if (message != nullptr) { - qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res << message; - } else { - qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res; + // Pipewire's documentation describes the error event as being fatal, however it isn't. + // We're not sure what causes these ENOENTs on device removal, presumably something in + // the teardown sequence, but they're harmless. Attempting to handle them as a fatal + // error causes unnecessary triggers for shells. + if (res == -ENOENT) { + qCDebug(logLoop) << "Pipewire ENOENT on object" << id << "with code" << res << message; + return; } + qCWarning(logLoop) << "Pipewire error on object" << id << "with code" << res << message; + emit self->fatalError(); } From c03030019100718d473ae86c89656e98124f5b3a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 6 Mar 2026 01:39:24 -0800 Subject: [PATCH 174/226] core/desktopentry: preserve desktop action order --- changelog/next.md | 1 + src/core/desktopentry.cpp | 31 ++++++++++++++++++++++--------- src/core/desktopentry.hpp | 6 +++--- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 0feffe1..ef63323 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -50,6 +50,7 @@ set shell id. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. - Fixed crashes when default pipewire devices are lost. +- Desktop action order is now preserved. ## Packaging Changes diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 2dbafea..637f758 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -107,7 +107,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& auto groupName = QString(); auto entries = QHash>(); - auto finishCategory = [&data, &groupName, &entries]() { + auto actionOrder = QStringList(); + auto pendingActions = QHash(); + + auto finishCategory = [&data, &groupName, &entries, &actionOrder, &pendingActions]() { if (groupName == "Desktop Entry") { if (entries.value("Type").second != "Application") return; @@ -129,9 +132,10 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& 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 (key == "Actions") actionOrder = value.split(u';', Qt::SkipEmptyParts); } } else if (groupName.startsWith("Desktop Action ")) { - auto actionName = groupName.sliced(16); + auto actionName = groupName.sliced(15); DesktopActionData action; action.id = actionName; @@ -147,7 +151,7 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& } } - data.actions.insert(actionName, action); + pendingActions.insert(actionName, action); } entries.clear(); @@ -193,6 +197,13 @@ ParsedDesktopEntryData DesktopEntry::parseText(const QString& id, const QString& } finishCategory(); + + for (const auto& actionId: actionOrder) { + if (pendingActions.contains(actionId)) { + data.actions.append(pendingActions.value(actionId)); + } + } + return data; } @@ -216,17 +227,18 @@ void DesktopEntry::updateState(const ParsedDesktopEntryData& newState) { this->updateActions(newState.actions); } -void DesktopEntry::updateActions(const QHash& newActions) { +void DesktopEntry::updateActions(const QVector& newActions) { auto old = this->mActions; + this->mActions.clear(); - for (const auto& [key, d]: newActions.asKeyValueRange()) { + for (const auto& d: newActions) { DesktopAction* act = nullptr; - if (auto found = old.find(key); found != old.end()) { - act = found.value(); + auto found = std::ranges::find(old, d.id, &DesktopAction::mId); + if (found != old.end()) { + act = *found; old.erase(found); } else { act = new DesktopAction(d.id, this); - this->mActions.insert(key, act); } Qt::beginPropertyUpdateGroup(); @@ -237,6 +249,7 @@ void DesktopEntry::updateActions(const QHash& newAct Qt::endPropertyUpdateGroup(); act->mEntries = d.entries; + this->mActions.append(act); } for (auto* leftover: old) { @@ -250,7 +263,7 @@ void DesktopEntry::execute() const { bool DesktopEntry::isValid() const { return !this->bName.value().isEmpty(); } -QVector DesktopEntry::actions() const { return this->mActions.values(); } +QVector DesktopEntry::actions() const { return this->mActions; } QVector DesktopEntry::parseExecString(const QString& execString) { QVector arguments; diff --git a/src/core/desktopentry.hpp b/src/core/desktopentry.hpp index 623019d..0d1eff2 100644 --- a/src/core/desktopentry.hpp +++ b/src/core/desktopentry.hpp @@ -43,7 +43,7 @@ struct ParsedDesktopEntryData { QVector categories; QVector keywords; QHash entries; - QHash actions; + QVector actions; }; /// A desktop entry. See @@DesktopEntries for details. @@ -164,10 +164,10 @@ public: // clang-format on private: - void updateActions(const QHash& newActions); + void updateActions(const QVector& newActions); ParsedDesktopEntryData state; - QHash mActions; + QVector mActions; friend class DesktopAction; }; From 6bcd3d9bbf81efdd8620409b268b90310bc1374c Mon Sep 17 00:00:00 2001 From: Moraxyc Date: Mon, 9 Feb 2026 22:03:45 +0800 Subject: [PATCH 175/226] nix: use libxcb directly --- default.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 59e68b0..02b8659 100644 --- a/default.nix +++ b/default.nix @@ -19,6 +19,7 @@ wayland-protocols, wayland-scanner, xorg, + libxcb ? xorg.libxcb, libdrm, libgbm ? null, vulkan-headers, @@ -88,7 +89,7 @@ ++ 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 vulkan-headers ] - ++ lib.optional withX11 xorg.libxcb + ++ lib.optional withX11 libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire ++ lib.optionals withPolkit [ polkit glib ]; From 15a84097653593dd15fad59a56befc2b7bdc270d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 7 Mar 2026 14:36:59 -0800 Subject: [PATCH 176/226] ipc: handle null currentGeneration in IpcKillCommand::exec --- src/ipc/ipc.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 40e8f0c..4bfea4c 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -127,7 +128,9 @@ int IpcClient::connect(const QString& id, const std::functionquit(); + auto* generation = EngineGeneration::currentGeneration(); + if (generation) generation->quit(); + else QCoreApplication::exit(0); } } // namespace qs::ipc From cf1a2aeb2d01e446346fcd37c4b8f4e7d40d6f2c Mon Sep 17 00:00:00 2001 From: -k Date: Mon, 9 Mar 2026 09:11:52 -0400 Subject: [PATCH 177/226] wayland/toplevel: clear activeToplevel on deactivation --- changelog/next.md | 1 + src/wayland/toplevel_management/qml.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog/next.md b/changelog/next.md index ef63323..fee8599 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -50,6 +50,7 @@ set shell id. - Fixed ClippingRectangle related crashes. - Fixed crashes when monitors are unplugged. - Fixed crashes when default pipewire devices are lost. +- Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. ## Packaging Changes diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 0eae3de..6a1d96b 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -161,7 +161,11 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { void ToplevelManager::onToplevelActiveChanged() { auto* toplevel = qobject_cast(this->sender()); - if (toplevel->activated()) this->setActiveToplevel(toplevel); + if (toplevel->activated()) { + this->setActiveToplevel(toplevel); + } else if (toplevel == this->mActiveToplevel) { + this->setActiveToplevel(nullptr); + } } void ToplevelManager::onToplevelClosed() { From bd6217927739a79c1c4ff279051f9625cd4b2b5e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 10 Mar 2026 00:54:45 -0700 Subject: [PATCH 178/226] all: retry incomplete socket reads Fixes greetd and hyprland ipc sockets reads being incomplete and breaking said integrations on slow machines. --- changelog/next.md | 1 + src/core/CMakeLists.txt | 1 + src/core/streamreader.cpp | 98 +++++++++++++++++ src/core/streamreader.hpp | 26 +++++ src/services/greetd/CMakeLists.txt | 2 +- src/services/greetd/connection.cpp | 133 ++++++++++++------------ src/services/greetd/connection.hpp | 3 + src/wayland/hyprland/ipc/CMakeLists.txt | 2 +- src/wayland/hyprland/ipc/connection.cpp | 9 +- src/wayland/hyprland/ipc/connection.hpp | 2 + src/x11/i3/ipc/CMakeLists.txt | 2 +- src/x11/i3/ipc/connection.cpp | 40 ++----- src/x11/i3/ipc/connection.hpp | 5 +- 13 files changed, 221 insertions(+), 103 deletions(-) create mode 100644 src/core/streamreader.cpp create mode 100644 src/core/streamreader.hpp diff --git a/changelog/next.md b/changelog/next.md index fee8599..4f550e8 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -52,6 +52,7 @@ set shell id. - Fixed crashes when default pipewire devices are lost. - Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. +- Fixed partial socket reads in greetd and hyprland on slow machines. ## Packaging Changes diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fb63f40..f0ca8ef 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -40,6 +40,7 @@ qt_add_library(quickshell-core STATIC scriptmodel.cpp colorquantizer.cpp toolsupport.cpp + streamreader.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/streamreader.cpp b/src/core/streamreader.cpp new file mode 100644 index 0000000..1f66e29 --- /dev/null +++ b/src/core/streamreader.cpp @@ -0,0 +1,98 @@ +#include "streamreader.hpp" +#include + +#include +#include +#include + +void StreamReader::setDevice(QIODevice* device) { + this->reset(); + this->device = device; +} + +void StreamReader::startTransaction() { + this->cursor = 0; + this->failed = false; +} + +bool StreamReader::fill() { + auto available = this->device->bytesAvailable(); + if (available <= 0) return false; + auto oldSize = this->buffer.size(); + this->buffer.resize(oldSize + available); + auto bytesRead = this->device->read(this->buffer.data() + oldSize, available); // NOLINT + + if (bytesRead <= 0) { + this->buffer.resize(oldSize); + return false; + } + + this->buffer.resize(oldSize + bytesRead); + return true; +} + +QByteArray StreamReader::readBytes(qsizetype count) { + if (this->failed) return {}; + + auto needed = this->cursor + count; + + while (this->buffer.size() < needed) { + if (!this->fill()) { + this->failed = true; + return {}; + } + } + + auto result = this->buffer.mid(this->cursor, count); + this->cursor += count; + return result; +} + +QByteArray StreamReader::readUntil(char terminator) { + if (this->failed) return {}; + + auto searchFrom = this->cursor; + auto idx = this->buffer.indexOf(terminator, searchFrom); + + while (idx == -1) { + searchFrom = this->buffer.size(); + if (!this->fill()) { + this->failed = true; + return {}; + } + + idx = this->buffer.indexOf(terminator, searchFrom); + } + + auto length = idx - this->cursor + 1; + auto result = this->buffer.mid(this->cursor, length); + this->cursor += length; + return result; +} + +void StreamReader::readInto(char* ptr, qsizetype count) { + auto data = this->readBytes(count); + if (!data.isEmpty()) memcpy(ptr, data.data(), count); +} + +qint32 StreamReader::readI32() { + qint32 value = 0; + this->readInto(reinterpret_cast(&value), sizeof(qint32)); + return value; +} + +bool StreamReader::commitTransaction() { + if (this->failed) { + this->cursor = 0; + return false; + } + + this->buffer.remove(0, this->cursor); + this->cursor = 0; + return true; +} + +void StreamReader::reset() { + this->buffer.clear(); + this->cursor = 0; +} diff --git a/src/core/streamreader.hpp b/src/core/streamreader.hpp new file mode 100644 index 0000000..abf14ef --- /dev/null +++ b/src/core/streamreader.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class StreamReader { +public: + void setDevice(QIODevice* device); + + void startTransaction(); + QByteArray readBytes(qsizetype count); + QByteArray readUntil(char terminator); + void readInto(char* ptr, qsizetype count); + qint32 readI32(); + bool commitTransaction(); + void reset(); + +private: + bool fill(); + + QIODevice* device = nullptr; + QByteArray buffer; + qsizetype cursor = 0; + bool failed = false; +}; diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt index 2252f8c..a103531 100644 --- a/src/services/greetd/CMakeLists.txt +++ b/src/services/greetd/CMakeLists.txt @@ -12,7 +12,7 @@ qt_add_qml_module(quickshell-service-greetd install_qml_module(quickshell-service-greetd) # can't be Qt::Qml because generation.hpp pulls in gui types -target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick) +target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick quickshell-core) qs_module_pch(quickshell-service-greetd) diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index 7130870..3b8fa24 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -145,6 +145,7 @@ void GreetdConnection::setInactive() { QString GreetdConnection::user() const { return this->mUser; } void GreetdConnection::onSocketConnected() { + this->reader.setDevice(&this->socket); qCDebug(logGreetd) << "Connected to greetd socket."; if (this->mTargetActive) { @@ -160,82 +161,84 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) { } void GreetdConnection::onSocketReady() { - qint32 length = 0; + while (true) { + this->reader.startTransaction(); + auto length = this->reader.readI32(); + auto text = this->reader.readBytes(length); + if (!this->reader.commitTransaction()) return; - this->socket.read(reinterpret_cast(&length), sizeof(qint32)); + auto json = QJsonDocument::fromJson(text).object(); + auto type = json.value("type").toString(); - auto text = this->socket.read(length); - auto json = QJsonDocument::fromJson(text).object(); - auto type = json.value("type").toString(); + qCDebug(logGreetd).noquote() << "Received greetd response:" << text; - qCDebug(logGreetd).noquote() << "Received greetd response:" << text; + if (type == "success") { + switch (this->mState) { + case GreetdState::Authenticating: + qCDebug(logGreetd) << "Authentication complete."; + this->mState = GreetdState::ReadyToLaunch; + emit this->stateChanged(); + emit this->readyToLaunch(); + break; + case GreetdState::Launching: + qCDebug(logGreetd) << "Target session set successfully."; + this->mState = GreetdState::Launched; + emit this->stateChanged(); + emit this->launched(); - if (type == "success") { - switch (this->mState) { - case GreetdState::Authenticating: - qCDebug(logGreetd) << "Authentication complete."; - this->mState = GreetdState::ReadyToLaunch; - emit this->stateChanged(); - emit this->readyToLaunch(); - break; - case GreetdState::Launching: - qCDebug(logGreetd) << "Target session set successfully."; - this->mState = GreetdState::Launched; - emit this->stateChanged(); - emit this->launched(); + if (this->mExitAfterLaunch) { + qCDebug(logGreetd) << "Quitting."; + EngineGeneration::currentGeneration()->quit(); + } - if (this->mExitAfterLaunch) { - qCDebug(logGreetd) << "Quitting."; - EngineGeneration::currentGeneration()->quit(); + break; + default: goto unexpected; + } + } else if (type == "error") { + auto errorType = json.value("error_type").toString(); + auto desc = json.value("description").toString(); + + // Special case this error in case a session was already running. + // This cancels and restarts the session. + if (errorType == "error" && desc == "a session is already being configured") { + qCDebug( + logGreetd + ) << "A session was already in progress, cancelling it and starting a new one."; + this->setActive(false); + this->setActive(true); + return; } - break; - default: goto unexpected; - } - } else if (type == "error") { - auto errorType = json.value("error_type").toString(); - auto desc = json.value("description").toString(); + if (errorType == "auth_error") { + emit this->authFailure(desc); + this->setActive(false); + } else if (errorType == "error") { + qCWarning(logGreetd) << "Greetd error occurred" << desc; + emit this->error(desc); + } else goto unexpected; - // Special case this error in case a session was already running. - // This cancels and restarts the session. - if (errorType == "error" && desc == "a session is already being configured") { - qCDebug( - logGreetd - ) << "A session was already in progress, cancelling it and starting a new one."; - this->setActive(false); - this->setActive(true); - return; - } + // errors terminate the session + this->setInactive(); + } else if (type == "auth_message") { + auto message = json.value("auth_message").toString(); + auto type = json.value("auth_message_type").toString(); + auto error = type == "error"; + auto responseRequired = type == "visible" || type == "secret"; + auto echoResponse = type != "secret"; - if (errorType == "auth_error") { - emit this->authFailure(desc); - this->setActive(false); - } else if (errorType == "error") { - qCWarning(logGreetd) << "Greetd error occurred" << desc; - emit this->error(desc); + this->mResponseRequired = responseRequired; + emit this->authMessage(message, error, responseRequired, echoResponse); + + if (!responseRequired) { + this->sendRequest({{"type", "post_auth_message_response"}}); + } } else goto unexpected; - // errors terminate the session - this->setInactive(); - } else if (type == "auth_message") { - auto message = json.value("auth_message").toString(); - auto type = json.value("auth_message_type").toString(); - auto error = type == "error"; - auto responseRequired = type == "visible" || type == "secret"; - auto echoResponse = type != "secret"; - - this->mResponseRequired = responseRequired; - emit this->authMessage(message, error, responseRequired, echoResponse); - - if (!responseRequired) { - this->sendRequest({{"type", "post_auth_message_response"}}); - } - } else goto unexpected; - - return; -unexpected: - qCCritical(logGreetd) << "Received unexpected greetd response" << text; - this->setActive(false); + continue; + unexpected: + qCCritical(logGreetd) << "Received unexpected greetd response" << text; + this->setActive(false); + } } void GreetdConnection::sendRequest(const QJsonObject& json) { diff --git a/src/services/greetd/connection.hpp b/src/services/greetd/connection.hpp index 0c1d1eb..89348dc 100644 --- a/src/services/greetd/connection.hpp +++ b/src/services/greetd/connection.hpp @@ -8,6 +8,8 @@ #include #include +#include "../../core/streamreader.hpp" + ///! State of the Greetd connection. /// See @@Greetd.state. class GreetdState: public QObject { @@ -74,4 +76,5 @@ private: bool mResponseRequired = false; QString mUser; QLocalSocket socket; + StreamReader reader; }; diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt index fd01463..9e42520 100644 --- a/src/wayland/hyprland/ipc/CMakeLists.txt +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -15,7 +15,7 @@ qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell) install_qml_module(quickshell-hyprland-ipc) -target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick) +target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick quickshell-core) if (WAYLAND_TOPLEVEL_MANAGEMENT) target_sources(quickshell-hyprland-ipc PRIVATE diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index ad091a6..d2d5105 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -93,6 +93,7 @@ void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const { void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::ConnectedState) { + this->eventReader.setDevice(&this->eventSocket); qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; emit this->connected(); } else if (state == QLocalSocket::UnconnectedState && this->valid) { @@ -104,11 +105,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) void HyprlandIpc::eventSocketReady() { while (true) { - auto rawEvent = this->eventSocket.readLine(); - if (rawEvent.isEmpty()) break; + this->eventReader.startTransaction(); + auto rawEvent = this->eventReader.readUntil('\n'); + if (!this->eventReader.commitTransaction()) return; - // remove trailing \n - rawEvent.truncate(rawEvent.length() - 1); + rawEvent.chop(1); // remove trailing \n auto splitIdx = rawEvent.indexOf(">>"); auto event = QByteArrayView(rawEvent.data(), splitIdx); auto data = QByteArrayView( diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index e15d5cd..ba1e7c9 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -14,6 +14,7 @@ #include "../../../core/model.hpp" #include "../../../core/qmlscreen.hpp" +#include "../../../core/streamreader.hpp" #include "../../../wayland/toplevel_management/handle.hpp" namespace qs::hyprland::ipc { @@ -139,6 +140,7 @@ private: static bool compareWorkspaces(HyprlandWorkspace* a, HyprlandWorkspace* b); QLocalSocket eventSocket; + StreamReader eventReader; QString mRequestSocketPath; QString mEventSocketPath; bool valid = false; diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt index c228ae3..a073459 100644 --- a/src/x11/i3/ipc/CMakeLists.txt +++ b/src/x11/i3/ipc/CMakeLists.txt @@ -17,7 +17,7 @@ qs_add_module_deps_light(quickshell-i3-ipc Quickshell) install_qml_module(quickshell-i3-ipc) -target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick) +target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick quickshell-core) qs_module_pch(quickshell-i3-ipc SET large) diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index b765ebc..976167b 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -15,9 +14,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -89,9 +86,6 @@ I3Ipc::I3Ipc(const QList& events): mEvents(events) { QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); // clang-format on - - this->liveEventSocketDs.setDevice(&this->liveEventSocket); - this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); } void I3Ipc::makeRequest(const QByteArray& request) { @@ -145,34 +139,21 @@ void I3Ipc::reconnectIPC() { } QVector I3Ipc::parseResponse() { - QVector> events; - const int magicLen = 6; + QVector events; - while (!this->liveEventSocketDs.atEnd()) { - this->liveEventSocketDs.startTransaction(); - this->liveEventSocketDs.startTransaction(); + while (true) { + this->eventReader.startTransaction(); + auto magic = this->eventReader.readBytes(6); + auto size = this->eventReader.readI32(); + auto type = this->eventReader.readI32(); + auto payload = this->eventReader.readBytes(size); + if (!this->eventReader.commitTransaction()) return events; - std::array buffer = {}; - qint32 size = 0; - qint32 type = EventCode::Unknown; - - this->liveEventSocketDs.readRawData(buffer.data(), magicLen); - this->liveEventSocketDs >> size; - this->liveEventSocketDs >> type; - - if (!this->liveEventSocketDs.commitTransaction()) break; - - QByteArray payload(size, Qt::Uninitialized); - - this->liveEventSocketDs.readRawData(payload.data(), size); - - if (!this->liveEventSocketDs.commitTransaction()) break; - - if (strncmp(buffer.data(), MAGIC.data(), 6) != 0) { + if (magic.size() < 6 || strncmp(magic.data(), MAGIC.data(), 6) != 0) { qCWarning(logI3Ipc) << "No magic sequence found in string."; this->reconnectIPC(); break; - }; + } if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) { qCWarning(logI3Ipc) << "Received unknown event"; @@ -204,6 +185,7 @@ void I3Ipc::eventSocketError(QLocalSocket::LocalSocketError error) const { void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::ConnectedState) { + this->eventReader.setDevice(&this->liveEventSocket); qCInfo(logI3Ipc) << "I3 event socket connected."; emit this->connected(); } else if (state == QLocalSocket::UnconnectedState && this->valid) { diff --git a/src/x11/i3/ipc/connection.hpp b/src/x11/i3/ipc/connection.hpp index 6100f7e..7d03ecd 100644 --- a/src/x11/i3/ipc/connection.hpp +++ b/src/x11/i3/ipc/connection.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -9,6 +8,8 @@ #include #include +#include "../../../core/streamreader.hpp" + namespace qs::i3::ipc { constexpr std::string MAGIC = "i3-ipc"; @@ -92,7 +93,7 @@ protected: QVector> parseResponse(); QLocalSocket liveEventSocket; - QDataStream liveEventSocketDs; + StreamReader eventReader; QString mSocketPath; bool valid = false; From 9a9c60525014bcdf83aace03db4b53c19168edcc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 11 Mar 2026 19:45:14 -0700 Subject: [PATCH 179/226] core: hash scanned files and don't trigger a reload if matching Nix builds often trip QFileSystemWatcher, causing random reloads. --- changelog/next.md | 1 + src/core/generation.cpp | 12 ++++++++++++ src/core/scan.cpp | 41 ++++++++++++++++++++++++++++++----------- src/core/scan.hpp | 5 +++++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 4f550e8..4883c93 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -33,6 +33,7 @@ set shell id. - IPC operations filter available instances to the current display connection by default. - PwNodeLinkTracker ignores sound level monitoring programs. - Replaced breakpad with cpptrace. +- Reloads are prevented if no file content has changed. ## Bug Fixes diff --git a/src/core/generation.cpp b/src/core/generation.cpp index c68af71..21febc3 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -209,6 +209,8 @@ bool EngineGeneration::setExtraWatchedFiles(const QVector& files) { for (const auto& file: files) { if (!this->scanner.scannedFiles.contains(file)) { this->extraWatchedFiles.append(file); + QByteArray data; + this->scanner.readAndHashFile(file, data); } } @@ -229,6 +231,11 @@ void EngineGeneration::onFileChanged(const QString& name) { auto fileInfo = QFileInfo(name); if (fileInfo.isFile() && fileInfo.size() == 0) return; + if (!this->scanner.hasFileContentChanged(name)) { + qCDebug(logQmlScanner) << "Ignoring file change with unchanged content:" << name; + return; + } + emit this->filesChanged(); } } @@ -237,6 +244,11 @@ void EngineGeneration::onDirectoryChanged() { // try to find any files that were just deleted from a replace operation for (auto& file: this->deletedWatchedFiles) { if (QFileInfo(file).exists()) { + if (!this->scanner.hasFileContentChanged(file)) { + qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file; + continue; + } + emit this->filesChanged(); break; } diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 37b0fac..58da38c 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,25 @@ QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); +bool QmlScanner::readAndHashFile(const QString& path, QByteArray& data) { + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) return false; + data = file.readAll(); + this->fileHashes.insert(path, QCryptographicHash::hash(data, QCryptographicHash::Md5)); + return true; +} + +bool QmlScanner::hasFileContentChanged(const QString& path) const { + auto it = this->fileHashes.constFind(path); + if (it == this->fileHashes.constEnd()) return true; + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) return true; + + auto newHash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5); + return newHash != it.value(); +} + void QmlScanner::scanDir(const QDir& dir) { if (this->scannedDirs.contains(dir)) return; this->scannedDirs.push_back(dir); @@ -109,13 +129,13 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna qCDebug(logQmlScanner) << "Scanning qml file" << path; - auto file = QFile(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { + QByteArray fileData; + if (!this->readAndHashFile(path, fileData)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return false; } - auto stream = QTextStream(&file); + auto stream = QTextStream(&fileData); auto imports = QVector(); bool inHeader = true; @@ -219,8 +239,6 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna postError("unclosed preprocessor if block"); } - file.close(); - if (isOverridden) { this->fileIntercepts.insert(path, overrideText); } @@ -257,8 +275,11 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna continue; } - if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); - else this->scanDir(cpath); + if (import.endsWith(".js")) { + this->scannedFiles.push_back(cpath); + QByteArray jsData; + this->readAndHashFile(cpath, jsData); + } else this->scanDir(cpath); } return true; @@ -273,14 +294,12 @@ void QmlScanner::scanQmlRoot(const QString& path) { bool QmlScanner::scanQmlJson(const QString& path) { qCDebug(logQmlScanner) << "Scanning qml.json file" << path; - auto file = QFile(path); - if (!file.open(QFile::ReadOnly | QFile::Text)) { + QByteArray data; + if (!this->readAndHashFile(path, data)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return false; } - auto data = file.readAll(); - // Importing this makes CI builds fail for some reason. QJsonParseError error; // NOLINT (misc-include-cleaner) auto json = QJsonDocument::fromJson(data, &error); diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 29f8f6a..26034e1 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -21,6 +22,7 @@ public: QVector scannedDirs; QVector scannedFiles; + QHash fileHashes; QHash fileIntercepts; struct ScanError { @@ -31,6 +33,9 @@ public: QVector scanErrors; + bool readAndHashFile(const QString& path, QByteArray& data); + [[nodiscard]] bool hasFileContentChanged(const QString& path) const; + private: QDir rootPath; From 706d6de7b0236cec2c25556e284b91104a4e834b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 12 Mar 2026 03:57:14 -0700 Subject: [PATCH 180/226] crash: unmask signals in coredump fork Fixes the fork just sticking around and not dumping a core. --- src/crash/handler.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index c875c2e..8f37085 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -81,6 +81,11 @@ void signalHandler( auto coredumpPid = fork(); if (coredumpPid == 0) { + // NOLINTBEGIN (misc-include-cleaner) + sigset_t set; + sigfillset(&set); + sigprocmask(SIG_UNBLOCK, &set, nullptr); + // NOLINTEND raise(sig); _exit(-1); } From 178c04b59cfc387efb90fbf2460f5171512ebfc4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Mar 2026 00:32:43 -0700 Subject: [PATCH 181/226] docs: revise contribution policy and related files --- BUILD.md | 10 +- CONTRIBUTING.md | 247 +++++------------------------------------------- HACKING.md | 226 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +- 4 files changed, 263 insertions(+), 224 deletions(-) create mode 100644 HACKING.md diff --git a/BUILD.md b/BUILD.md index 29aecac..aa04bbd 100644 --- a/BUILD.md +++ b/BUILD.md @@ -67,7 +67,13 @@ Dependencies: `cpptrace` Note: `-DVENDOR_CPPTRACE=ON` can be set to vendor cpptrace using FetchContent. -When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the package manager or fetched with FetchContent. +When using FetchContent, `libunwind` is required, and `libdwarf` can be provided by the +package manager or fetched with FetchContent. + +*Please ensure binaries have usable symbols.* We do not necessarily need full debuginfo, but +leaving symbols in the binary is extremely helpful. You can check if symbols are useful +by sending a SIGSEGV to the process and ensuring symbols for the quickshell binary are present +in the trace. ### Jemalloc We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused @@ -236,7 +242,7 @@ Only `ninja` builds are tested, but makefiles may work. #### Configuring the build ```sh -$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] +$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=Release [additional disable flags from above here] ``` Note that features you do not supply dependencies for MUST be disabled with their associated flags diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39fab13..73e7931 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,235 +1,40 @@ -# Contributing / Development -Instructions for development setup and upstreaming patches. +# Contributing -If you just want to build or package quickshell see [BUILD.md](BUILD.md). +Thank you for taking the time to contribute. +To ensure nobody's time is wasted, please follow the rules below. -## Development +## Acceptable Code Contributions -Install the dependencies listed in [BUILD.md](BUILD.md). -You probably want all of them even if you don't use all of them -to ensure tests work correctly and avoid passing a bunch of configure -flags when you need to wipe the build directory. +- All changes submitted MUST be **fully understood by the submitter**. If you do not know why or how + your change works, do not submit it to be merged. You must be able to explain your reasoning + for every change. -Quickshell also uses `just` for common development command aliases. +- Changes MUST be submitted by a human who will be responsible for them. Changes submitted without + a human in the loop such as automated tooling and AI Agents are **strictly disallowed**. Accounts + responsible for such contribution attempts **will be banned**. -The dependencies are also available as a nix shell or nix flake which we recommend -using with nix-direnv. +- Changes MUST respect Quickshell's license and the license of any source works. Changes including + code from any other works must disclose the source of the code, explain why it was used, and + ensure the license is compatible. -Common aliases: -- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) -- `just build` - runs the build, configuring if not configured already. -- `just run [args]` - runs quickshell with the given arguments -- `just clean` - clean up build artifacts. `just clean build` is somewhat common. +- Changes must follow the guidelines outlined in [HACKING.md](HACKING.md) for style and substance. -### Formatting -All contributions should be formatted similarly to what already exists. -Group related functionality together. +- Changes must stand on their own as a unit. Do not make multiple unrelated changes in one PR. + Changes depending on prior merges should be marked as a draft. -Run the formatter using `just fmt`. -If the results look stupid, fix the clang-format file if possible, -or disable clang-format in the affected area -using `// clang-format off` and `// clang-format on`. +## Acceptable Non-code Contributions -#### Style preferences not caught by clang-format -These are flexible. You can ignore them if it looks or works better to -for one reason or another. +- Bug and crash reports. You must follow the instructions in the issue templates and provide the + information requested. -Use `auto` if the type of a variable can be deduced automatically, instead of -redeclaring the returned value's type. Additionally, auto should be used when a -constructor takes arguments. +- Feature requests can be made via Issues. Please check to ensure nobody else has requested the same feature. -```cpp -auto x = ; // ok -auto x = QString::number(3); // ok -QString x; // ok -QString x = "foo"; // ok -auto x = QString("foo"); // ok +- Do not make insubstantial or pointless changes. -auto x = QString(); // avoid -QString x(); // avoid -QString x("foo"); // avoid -``` +- Changes to project rules / policy / governance will not be entertained, except from significant + long-term contributors. These changes should not be addressed through contribution channels. -Put newlines around logical units of code, and after closing braces. If the -most reasonable logical unit of code takes only a single line, it should be -merged into the next single line logical unit if applicable. -```cpp -// multiple units -auto x = ; // unit 1 -auto y = ; // unit 2 +## Merge timelines -auto x = ; // unit 1 -emit this->y(); // unit 2 - -auto x1 = ; // unit 1 -auto x2 = ; // unit 1 -auto x3 = ; // unit 1 - -auto y1 = ; // unit 2 -auto y2 = ; // unit 2 -auto y3 = ; // unit 2 - -// one unit -auto x = ; -if (x...) { - // ... -} - -// if more than one variable needs to be used then add a newline -auto x = ; -auto y = ; - -if (x && y) { - // ... -} -``` - -Class formatting: -```cpp -//! Doc comment summary -/// Doc comment body -class Foo: public QObject { - // The Q_OBJECT macro comes first. Macros are ; terminated. - Q_OBJECT; - QML_ELEMENT; - QML_CLASSINFO(...); - // Properties must stay on a single line or the doc generator won't be able to pick them up - Q_PROPERTY(...); - /// Doc comment - Q_PROPERTY(...); - /// Doc comment - Q_PROPERTY(...); - -public: - // Classes should have explicit constructors if they aren't intended to - // implicitly cast. The constructor can be inline in the header if it has no body. - explicit Foo(QObject* parent = nullptr): QObject(parent) {} - - // Instance functions if applicable. - static Foo* instance(); - - // Member functions unrelated to properties come next - void function(); - void function(); - void function(); - - // Then Q_INVOKABLEs - Q_INVOKABLE function(); - /// Doc comment - Q_INVOKABLE function(); - /// Doc comment - Q_INVOKABLE function(); - - // Then property related functions, in the order (bindable, getter, setter). - // Related functions may be included here as well. Function bodies may be inline - // if they are a single expression. There should be a newline between each - // property's methods. - [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; } - [[nodiscard]] T foo() const { return this->foo; } - void setFoo(); - - [[nodiscard]] T bar() const { return this->foo; } - void setBar(); - -signals: - // Signals that are not property change related go first. - // Property change signals go in property definition order. - void asd(); - void asd2(); - void fooChanged(); - void barChanged(); - -public slots: - // generally Q_INVOKABLEs are preferred to public slots. - void slot(); - -private slots: - // ... - -private: - // statics, then functions, then fields - static const foo BAR; - static void foo(); - - void foo(); - void bar(); - - // property related members are prefixed with `m`. - QString mFoo; - QString bar; - - // Bindables go last and should be prefixed with `b`. - Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged); -}; -``` - -### Linter -All contributions should pass the linter. - -Note that running the linter requires disabling precompiled -headers and including the test codepaths: -```sh -$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON -$ just lint-changed -``` - -If the linter is complaining about something that you think it should not, -please disable the lint in your MR and explain your reasoning if it isn't obvious. - -### Tests -If you feel like the feature you are working on is very complex or likely to break, -please write some tests. We will ask you to directly if you send in an MR for an -overly complex or breakable feature. - -At least all tests that passed before your changes should still be passing -by the time your contribution is ready. - -You can run the tests using `just test` but you must enable them first -using `-DBUILD_TESTING=ON`. - -### Documentation -Most of quickshell's documentation is automatically generated from the source code. -You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser -cannot handle random line breaks and will usually require you to disable clang-format if the -lines are too long. - -Before submitting an MR, if adding new features please make sure the documentation is generated -reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. - -Doc comments take the form `///` or `///!` (summary) and work with markdown. -You can reference other types using the `@@[Module.][Type.][member]` shorthand -where all parts are optional. If module or type are not specified they will -be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`. -Look at existing code for how it works. - -Quickshell modules additionally have a `module.md` file which contains a summary, description, -and list of headers to scan for documentation. - -## Contributing - -### Commits -Please structure your commit messages as `scope[!]: commit` where -the scope is something like `core` or `service/mpris`. (pick what has been -used historically or what makes sense if new). Add `!` for changes that break -existing APIs or functionality. - -Commit descriptions should contain a summary of the changes if they are not -sufficiently addressed in the commit message. - -Please squash/rebase additions or edits to previous changes and follow the -commit style to keep the history easily searchable at a glance. -Depending on the change, it is often reasonable to squash it into just -a single commit. (If you do not follow this we will squash your changes -for you.) - -### Sending patches -You may contribute by submitting a pull request on github, asking for -an account on our git server, or emailing patches / git bundles -directly to `outfoxxed@outfoxxed.me`. - -### Getting help -If you're getting stuck, you can come talk to us in the -[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) -for help on implementation, conventions, etc. -Feel free to ask for advice early in your implementation if you are -unsure. +We handle work for the most part on a push basis. If your PR has been ignored for a while +and is still relevant please bump it. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..69357f1 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,226 @@ +## Development + +Install the dependencies listed in [BUILD.md](BUILD.md). +You probably want all of them even if you don't use all of them +to ensure tests work correctly and avoid passing a bunch of configure +flags when you need to wipe the build directory. + +The dependencies are also available as a nix shell or nix flake which we recommend +using with nix-direnv. + +Quickshell uses `just` for common development command aliases. + +Common aliases: +- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) +- `just build` - runs the build, configuring if not configured already. +- `just run [args]` - runs quickshell with the given arguments +- `just clean` - clean up build artifacts. `just clean build` is somewhat common. + +### Formatting +All contributions should be formatted similarly to what already exists. +Group related functionality together. + +Run the formatter using `just fmt`. +If the results look stupid, fix the clang-format file if possible, +or disable clang-format in the affected area +using `// clang-format off` and `// clang-format on`. + +#### Style preferences not caught by clang-format +These are flexible. You can ignore them if it looks or works better to +for one reason or another. + +Use `auto` if the type of a variable can be deduced automatically, instead of +redeclaring the returned value's type. Additionally, auto should be used when a +constructor takes arguments. + +```cpp +auto x = ; // ok +auto x = QString::number(3); // ok +QString x; // ok +QString x = "foo"; // ok +auto x = QString("foo"); // ok + +auto x = QString(); // avoid +QString x(); // avoid +QString x("foo"); // avoid +``` + +Put newlines around logical units of code, and after closing braces. If the +most reasonable logical unit of code takes only a single line, it should be +merged into the next single line logical unit if applicable. +```cpp +// multiple units +auto x = ; // unit 1 +auto y = ; // unit 2 + +auto x = ; // unit 1 +emit this->y(); // unit 2 + +auto x1 = ; // unit 1 +auto x2 = ; // unit 1 +auto x3 = ; // unit 1 + +auto y1 = ; // unit 2 +auto y2 = ; // unit 2 +auto y3 = ; // unit 2 + +// one unit +auto x = ; +if (x...) { + // ... +} + +// if more than one variable needs to be used then add a newline +auto x = ; +auto y = ; + +if (x && y) { + // ... +} +``` + +Class formatting: +```cpp +//! Doc comment summary +/// Doc comment body +class Foo: public QObject { + // The Q_OBJECT macro comes first. Macros are ; terminated. + Q_OBJECT; + QML_ELEMENT; + QML_CLASSINFO(...); + // Properties must stay on a single line or the doc generator won't be able to pick them up + Q_PROPERTY(...); + /// Doc comment + Q_PROPERTY(...); + /// Doc comment + Q_PROPERTY(...); + +public: + // Classes should have explicit constructors if they aren't intended to + // implicitly cast. The constructor can be inline in the header if it has no body. + explicit Foo(QObject* parent = nullptr): QObject(parent) {} + + // Instance functions if applicable. + static Foo* instance(); + + // Member functions unrelated to properties come next + void function(); + void function(); + void function(); + + // Then Q_INVOKABLEs + Q_INVOKABLE function(); + /// Doc comment + Q_INVOKABLE function(); + /// Doc comment + Q_INVOKABLE function(); + + // Then property related functions, in the order (bindable, getter, setter). + // Related functions may be included here as well. Function bodies may be inline + // if they are a single expression. There should be a newline between each + // property's methods. + [[nodiscard]] QBindable bindableFoo() { return &this->bFoo; } + [[nodiscard]] T foo() const { return this->foo; } + void setFoo(); + + [[nodiscard]] T bar() const { return this->foo; } + void setBar(); + +signals: + // Signals that are not property change related go first. + // Property change signals go in property definition order. + void asd(); + void asd2(); + void fooChanged(); + void barChanged(); + +public slots: + // generally Q_INVOKABLEs are preferred to public slots. + void slot(); + +private slots: + // ... + +private: + // statics, then functions, then fields + static const foo BAR; + static void foo(); + + void foo(); + void bar(); + + // property related members are prefixed with `m`. + QString mFoo; + QString bar; + + // Bindables go last and should be prefixed with `b`. + Q_OBJECT_BINDABLE_PROPERTY(Foo, QString, bFoo, &Foo::fooChanged); +}; +``` + +Use lowercase .h suffixed Qt headers, e.g. `` over ``. + +### Linter +All contributions should pass the linter. + +Note that running the linter requires disabling precompiled +headers and including the test codepaths: +```sh +$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON +$ just lint-changed +``` + +If the linter is complaining about something that you think it should not, +please disable the lint in your MR and explain your reasoning if it isn't obvious. + +### Tests +If you feel like the feature you are working on is very complex or likely to break, +please write some tests. We will ask you to directly if you send in an MR for an +overly complex or breakable feature. + +At least all tests that passed before your changes should still be passing +by the time your contribution is ready. + +You can run the tests using `just test` but you must enable them first +using `-DBUILD_TESTING=ON`. + +### Documentation +Most of quickshell's documentation is automatically generated from the source code. +You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser +cannot handle random line breaks and will usually require you to disable clang-format if the +lines are too long. + +Make sure new files containing doc comments are added to a `module.md` file. +See existing module files for reference. + +Doc comments take the form `///` or `///!` (summary) and work with markdown. +You can reference other types using the `@@[Module.][Type.][member]` shorthand +where all parts are optional. If module or type are not specified they will +be inferred as the current module. Member can be a `property`, `function()` or `signal(s)`. +Look at existing code for how it works. + +If you have made a user visible change since the last tagged release, describe it in +[changelog/next.md](changelog/next.md). + +## Contributing + +### Commits +Please structure your commit messages as `scope: commit` where +the scope is something like `core` or `service/mpris`. (pick what has been +used historically or what makes sense if new). + +Commit descriptions should contain a summary of the changes if they are not +sufficiently addressed in the commit message. + +Please squash/rebase additions or edits to previous changes and follow the +commit style to keep the history easily searchable at a glance. +Depending on the change, it is often reasonable to squash it into just +a single commit. (If you do not follow this we will squash your changes +for you.) + +### Getting help +If you're getting stuck, you can come talk to us in the +[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) +for help on implementation, conventions, etc. There is also a bridged [discord server](https://discord.gg/UtZeT3xNyT). +Feel free to ask for advice early in your implementation if you are +unsure. diff --git a/README.md b/README.md index 4491d24..365bdb5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ This repo is hosted at: - https://github.com/quickshell-mirror/quickshell # Contributing / Development -See [CONTRIBUTING.md](CONTRIBUTING.md) for details. +- [HACKING.md](HACKING.md) - Development instructions and policy. +- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution policy. +- [BUILD.md](BUILD.md) - Packaging and build instructions. #### License From e32b9093545e7719bd91d8e219bb30aabd688230 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Mar 2026 01:10:09 -0700 Subject: [PATCH 182/226] core: add disable env vars for file watcher and crash handler --- changelog/next.md | 2 ++ src/core/qmlglobal.cpp | 4 +++- src/launch/launch.cpp | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 4883c93..fa6d845 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -34,6 +34,8 @@ set shell id. - PwNodeLinkTracker ignores sound level monitoring programs. - Replaced breakpad with cpptrace. - Reloads are prevented if no file content has changed. +- Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching. +- Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. ## Bug Fixes diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 6c26609..35504f6 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -60,7 +60,9 @@ void QuickshellSettings::setWorkingDirectory(QString workingDirectory) { // NOLI emit this->workingDirectoryChanged(); } -bool QuickshellSettings::watchFiles() const { return this->mWatchFiles; } +bool QuickshellSettings::watchFiles() const { + return this->mWatchFiles && qEnvironmentVariableIsEmpty("QS_DISABLE_FILE_WATCHER"); +} void QuickshellSettings::setWatchFiles(bool watchFiles) { if (watchFiles == this->mWatchFiles) return; diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index ee7ca64..3a9a2a5 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -138,9 +138,11 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio }; #if CRASH_HANDLER - crash::CrashHandler::init(); + if (qEnvironmentVariableIsSet("QS_DISABLE_CRASH_HANDLER")) { + qInfo() << "Crash handling disabled."; + } else { + crash::CrashHandler::init(); - { auto* log = LogManager::instance(); crash::CrashHandler::setRelaunchInfo({ .instance = InstanceInfo::CURRENT, From 4b77936c8019e0f51e0e62414c6de3556d5f8870 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Mar 2026 02:04:01 -0700 Subject: [PATCH 183/226] crash: allow overriding crash reporter url --- BUILD.md | 2 +- CMakeLists.txt | 3 +++ changelog/next.md | 1 + src/build/build.hpp.in | 1 + src/crash/interface.cpp | 31 ++++++++++++++++--------------- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/BUILD.md b/BUILD.md index aa04bbd..04421c0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,7 +15,7 @@ Please make this descriptive enough to identify your specific package, for examp - `Nixpkgs` - `Fedora COPR (errornointernet/quickshell)` -Please leave at least symbol names attached to the binary for debugging purposes. +If you are forking quickshell, please change `CRASHREPORT_URL` to your own issue tracker. ### QML Module dir Currently all QML modules are statically linked to quickshell, but this is where diff --git a/CMakeLists.txt b/CMakeLists.txt index d57e322..1226342 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(QS_BUILD_OPTIONS "") +# should be changed for forks +set(CRASHREPORT_URL "https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml" CACHE STRING "Bugreport URL") + function(boption VAR NAME DEFAULT) cmake_parse_arguments(PARSE_ARGV 3 arg "" "REQUIRES" "") diff --git a/changelog/next.md b/changelog/next.md index fa6d845..587e667 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -36,6 +36,7 @@ set shell id. - Reloads are prevented if no file content has changed. - Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching. - Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. +- Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link. ## Bug Fixes diff --git a/src/build/build.hpp.in b/src/build/build.hpp.in index 2ab2db2..acc3c58 100644 --- a/src/build/build.hpp.in +++ b/src/build/build.hpp.in @@ -13,4 +13,5 @@ #define COMPILER "@CMAKE_CXX_COMPILER_ID@ (@CMAKE_CXX_COMPILER_VERSION@)" #define COMPILE_FLAGS "@CMAKE_CXX_FLAGS@" #define BUILD_CONFIGURATION "@QS_BUILD_OPTIONS@" +#define CRASHREPORT_URL "@CRASHREPORT_URL@" // NOLINTEND diff --git a/src/crash/interface.cpp b/src/crash/interface.cpp index a3422d3..6a370ce 100644 --- a/src/crash/interface.cpp +++ b/src/crash/interface.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -12,11 +13,22 @@ #include #include #include +#include #include #include #include "build.hpp" +namespace { +QString crashreportUrl() { + if (auto url = qEnvironmentVariable("QS_CRASHREPORT_URL"); !url.isEmpty()) { + return url; + } + + return CRASHREPORT_URL; +} +} // namespace + class ReportLabel: public QWidget { public: ReportLabel(const QString& label, const QString& content, QWidget* parent): QWidget(parent) { @@ -67,22 +79,16 @@ CrashReporterGui::CrashReporterGui(QString reportFolder, int pid) if (qtVersionMatches) { mainLayout->addWidget( - new QLabel("Please open a bug report for this issue via github or email.") + new QLabel("Please open a bug report for this issue on the issue tracker.") ); } else { mainLayout->addWidget(new QLabel( "Please rebuild Quickshell against the current Qt version.\n" - "If this does not solve the problem, please open a bug report via github or email." + "If this does not solve the problem, please open a bug report on the issue tracker." )); } - mainLayout->addWidget(new ReportLabel( - "Github:", - "https://github.com/quickshell-mirror/quickshell/issues/new?template=crash2.yml", - this - )); - - mainLayout->addWidget(new ReportLabel("Email:", "quickshell-bugs@outfoxxed.me", this)); + mainLayout->addWidget(new ReportLabel("Tracker:", crashreportUrl(), this)); auto* buttons = new QWidget(this); buttons->setMinimumWidth(900); @@ -112,10 +118,5 @@ void CrashReporterGui::openFolder() { QDesktopServices::openUrl(QUrl::fromLocalFile(this->reportFolder)); } -void CrashReporterGui::openReportUrl() { - QDesktopServices::openUrl( - QUrl("https://github.com/outfoxxed/quickshell/issues/new?template=crash2.yml") - ); -} - +void CrashReporterGui::openReportUrl() { QDesktopServices::openUrl(QUrl(crashreportUrl())); } void CrashReporterGui::cancel() { QApplication::quit(); } From 1123d5ab4fa9bdde1d0888ed56f6987449eaf267 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Mar 2026 00:30:53 -0700 Subject: [PATCH 184/226] core: move crash/version debug info to one place --- src/core/CMakeLists.txt | 1 + src/core/debuginfo.cpp | 68 +++++++++++++++++++++++++++++++++++++++++ src/core/debuginfo.hpp | 12 ++++++++ src/crash/main.cpp | 34 ++------------------- src/launch/command.cpp | 20 +++--------- 5 files changed, 89 insertions(+), 46 deletions(-) create mode 100644 src/core/debuginfo.cpp create mode 100644 src/core/debuginfo.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f0ca8ef..076ab90 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -41,6 +41,7 @@ qt_add_library(quickshell-core STATIC colorquantizer.cpp toolsupport.cpp streamreader.cpp + debuginfo.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp new file mode 100644 index 0000000..f948d42 --- /dev/null +++ b/src/core/debuginfo.cpp @@ -0,0 +1,68 @@ +#include "debuginfo.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build.hpp" + +namespace qs::debuginfo { + +QString qsVersion() { + return QS_VERSION " (revision " GIT_REVISION ", distributed by " DISTRIBUTOR ")"; +} + +QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); } + +QString systemInfo() { + QString info; + auto stream = QTextStream(&info); + + stream << "/etc/os-release:"; + auto osReleaseFile = QFile("/etc/os-release"); + if (osReleaseFile.open(QFile::ReadOnly)) { + stream << '\n' << osReleaseFile.readAll() << '\n'; + osReleaseFile.close(); + } else { + stream << "FAILED TO OPEN\n"; + } + + stream << "/etc/lsb-release:"; + auto lsbReleaseFile = QFile("/etc/lsb-release"); + if (lsbReleaseFile.open(QFile::ReadOnly)) { + stream << '\n' << lsbReleaseFile.readAll(); + lsbReleaseFile.close(); + } else { + stream << "FAILED TO OPEN\n"; + } + + return info; +} + +QString combinedInfo() { + QString info; + auto stream = QTextStream(&info); + + stream << "===== Version Information =====\n"; + stream << "Quickshell: " << qsVersion() << '\n'; + stream << "Qt: " << qtVersion() << '\n'; + + stream << "\n===== Build Information =====\n"; + stream << "Build Type: " << BUILD_TYPE << '\n'; + stream << "Compiler: " << COMPILER << '\n'; + stream << "Compile Flags: " << COMPILE_FLAGS << '\n'; + stream << "Configuration:\n" << BUILD_CONFIGURATION << '\n'; + + stream << "\n===== System Information =====\n"; + stream << systemInfo(); + + return info; +} + +} // namespace qs::debuginfo diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp new file mode 100644 index 0000000..7759d53 --- /dev/null +++ b/src/core/debuginfo.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace qs::debuginfo { + +QString qsVersion(); +QString qtVersion(); +QString systemInfo(); +QString combinedInfo(); + +} // namespace qs::debuginfo diff --git a/src/crash/main.cpp b/src/crash/main.cpp index c406ba6..05927f2 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -15,19 +14,18 @@ #include #include #include -#include #include #include #include #include +#include "../core/debuginfo.hpp" #include "../core/instanceinfo.hpp" #include "../core/logcat.hpp" #include "../core/logging.hpp" #include "../core/logging_p.hpp" #include "../core/paths.hpp" #include "../core/ringbuf.hpp" -#include "build.hpp" #include "interface.hpp" namespace { @@ -171,41 +169,15 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCCritical(logCrashReporter) << "Failed to open crash info file for writing."; } else { auto stream = QTextStream(&extraInfoFile); - stream << "===== Build Information =====\n"; - stream << "Git Revision: " << GIT_REVISION << '\n'; - stream << "Buildtime Qt Version: " << QT_VERSION_STR << "\n"; - stream << "Build Type: " << BUILD_TYPE << '\n'; - stream << "Compiler: " << COMPILER << '\n'; - stream << "Complie Flags: " << COMPILE_FLAGS << "\n\n"; - stream << "Build configuration:\n" << BUILD_CONFIGURATION << "\n"; + stream << qs::debuginfo::combinedInfo(); - stream << "\n===== Runtime Information =====\n"; - stream << "Runtime Qt Version: " << qVersion() << '\n'; + stream << "\n===== Instance Information =====\n"; stream << "Signal: " << strsignal(crashSignal) << " (" << crashSignal << ")\n"; // NOLINT stream << "Crashed process ID: " << crashProc << '\n'; stream << "Run ID: " << instance.instanceId << '\n'; stream << "Shell ID: " << instance.shellId << '\n'; stream << "Config Path: " << instance.configPath << '\n'; - stream << "\n===== System Information =====\n\n"; - stream << "/etc/os-release:"; - auto osReleaseFile = QFile("/etc/os-release"); - if (osReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << osReleaseFile.readAll() << '\n'; - osReleaseFile.close(); - } else { - stream << "FAILED TO OPEN\n"; - } - - stream << "/etc/lsb-release:"; - auto lsbReleaseFile = QFile("/etc/lsb-release"); - if (lsbReleaseFile.open(QFile::ReadOnly)) { - stream << '\n' << lsbReleaseFile.readAll(); - lsbReleaseFile.close(); - } else { - stream << "FAILED TO OPEN\n"; - } - stream << "\n===== Stacktrace =====\n"; if (stacktrace.empty()) { stream << "(no trace available)\n"; diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 151fc24..807eb24 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -25,12 +25,12 @@ #include #include +#include "../core/debuginfo.hpp" #include "../core/instanceinfo.hpp" #include "../core/logging.hpp" #include "../core/paths.hpp" #include "../io/ipccomm.hpp" #include "../ipc/ipc.hpp" -#include "build.hpp" #include "launch_p.hpp" namespace qs::launch { @@ -519,20 +519,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { } if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() << "quickshell " << QS_VERSION << ", revision " - << GIT_REVISION << ", distributed by: " << DISTRIBUTOR; - - if (state.log.verbosity > 1) { - qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; - qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); - qCInfo(logBare).noquote() << "Compiler:" << COMPILER; - qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; - } - - if (state.log.verbosity > 0) { - qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; - qCInfo(logBare).noquote() << "Build configuration:"; - qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + if (state.log.verbosity == 0) { + qCInfo(logBare).noquote() << "Quickshell" << qs::debuginfo::qsVersion(); + } else { + qCInfo(logBare).noquote() << qs::debuginfo::combinedInfo(); } } else if (*state.subcommand.log) { return readLogFile(state); From 1b2519d9f3d963e575b8a1ef08fab47c7af0d1b3 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 14 Mar 2026 02:31:47 -0700 Subject: [PATCH 185/226] core: log gpu information in debuginfo --- .clang-tidy | 1 + BUILD.md | 2 +- changelog/next.md | 1 + default.nix | 3 +- src/core/CMakeLists.txt | 3 +- src/core/debuginfo.cpp | 74 +++++++++++++++++++++++++++++++++++++++++ src/core/debuginfo.hpp | 1 + 7 files changed, 82 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index c83ed8f..da14682 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -20,6 +20,7 @@ Checks: > -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-pro-type-reinterpret-cast, -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-use-enum-class, google-global-names-in-headers, google-readability-casting, diff --git a/BUILD.md b/BUILD.md index 04421c0..d624a06 100644 --- a/BUILD.md +++ b/BUILD.md @@ -33,6 +33,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `cmake` - `qt6base` - `qt6declarative` +- `libdrm` - `qtshadertools` (build-time) - `spirv-tools` (build-time) - `pkg-config` (build-time) @@ -146,7 +147,6 @@ Enables streaming video from monitors and toplevel windows through various proto To disable: `-DSCREENCOPY=OFF` Dependencies: -- `libdrm` - `libgbm` - `vulkan-headers` (build-time) diff --git a/changelog/next.md b/changelog/next.md index 587e667..e9b297c 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -64,3 +64,4 @@ set shell id. - `vulkan-headers` has been added as a build-time dependency for screencopy (Vulkan backend support). - `breakpad` has been replaced by `cpptrace`, which is far easier to package, and the `CRASH_REPORTER` cmake variable has been replaced with `CRASH_HANDLER` to stop this from being easy to ignore. - `DISTRIBUTOR_DEBUGINFO_AVAILABLE` was removed as it is no longer important without breakpad. +- `libdrm` is now unconditionally required as a direct dependency. diff --git a/default.nix b/default.nix index 02b8659..749ef49 100644 --- a/default.nix +++ b/default.nix @@ -76,6 +76,7 @@ buildInputs = [ qt6.qtbase qt6.qtdeclarative + libdrm cli11 ] ++ lib.optional withQtSvg qt6.qtsvg @@ -88,7 +89,7 @@ ++ lib.optional withJemalloc jemalloc ++ 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 vulkan-headers ] + ++ lib.optionals (withWayland && libgbm != null) [ libgbm vulkan-headers ] ++ lib.optional withX11 libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 076ab90..4824965 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,3 +1,4 @@ +pkg_check_modules(libdrm REQUIRED IMPORTED_TARGET libdrm) qt_add_library(quickshell-core STATIC plugin.cpp shell.cpp @@ -54,7 +55,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets quickshell-build PkgConfig::libdrm) qs_module_pch(quickshell-core SET large) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp index f948d42..f26c72e 100644 --- a/src/core/debuginfo.cpp +++ b/src/core/debuginfo.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include "build.hpp" @@ -20,10 +22,82 @@ QString qsVersion() { QString qtVersion() { return qVersion() % QStringLiteral(" (built against " QT_VERSION_STR ")"); } +QString gpuInfo() { + auto deviceCount = drmGetDevices2(0, nullptr, 0); + if (deviceCount < 0) return "Failed to get DRM device count: " % QString::number(deviceCount); + auto* devices = new drmDevicePtr[deviceCount]; + auto devicesArrayGuard = qScopeGuard([&] { delete[] devices; }); + auto r = drmGetDevices2(0, devices, deviceCount); + if (deviceCount < 0) return "Failed to get DRM devices: " % QString::number(r); + auto devicesGuard = qScopeGuard([&] { + for (auto i = 0; i != deviceCount; ++i) drmFreeDevice(&devices[i]); // NOLINT + }); + + QString info; + auto stream = QTextStream(&info); + + for (auto i = 0; i != deviceCount; ++i) { + auto* device = devices[i]; // NOLINT + + int deviceNodeType = -1; + if (device->available_nodes & (1 << DRM_NODE_RENDER)) deviceNodeType = DRM_NODE_RENDER; + else if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) deviceNodeType = DRM_NODE_PRIMARY; + + if (deviceNodeType == -1) continue; + + auto* deviceNode = device->nodes[DRM_NODE_RENDER]; // NOLINT + + auto driver = [&]() -> QString { + auto fd = open(deviceNode, O_RDWR | O_CLOEXEC); + if (fd == -1) return ""; + auto fdGuard = qScopeGuard([&] { close(fd); }); + auto* ver = drmGetVersion(fd); + if (!ver) return ""; + auto verGuard = qScopeGuard([&] { drmFreeVersion(ver); }); + + // clang-format off + return QString(ver->name) + % ' ' % QString::number(ver->version_major) + % '.' % QString::number(ver->version_minor) + % '.' % QString::number(ver->version_patchlevel) + % " (" % ver->desc % ')'; + // clang-format on + }(); + + QString product = "unknown"; + QString address = "unknown"; + + auto hex = [](int num, int pad) { return QString::number(num, 16).rightJustified(pad, '0'); }; + + switch (device->bustype) { + case DRM_BUS_PCI: { + auto* b = device->businfo.pci; + auto* d = device->deviceinfo.pci; + address = "PCI " % hex(b->bus, 2) % ':' % hex(b->dev, 2) % '.' % hex(b->func, 1); + product = hex(d->vendor_id, 4) % ':' % hex(d->device_id, 4); + } break; + case DRM_BUS_USB: { + auto* b = device->businfo.usb; + auto* d = device->deviceinfo.usb; + address = "USB " % QString::number(b->bus) % ':' % QString::number(b->dev); + product = hex(d->vendor, 4) % ':' % hex(d->product, 4); + } break; + default: break; + } + + stream << "GPU " << deviceNode << "\n Driver: " << driver << "\n Model: " << product + << "\n Address: " << address << '\n'; + } + + return info; +} + QString systemInfo() { QString info; auto stream = QTextStream(&info); + stream << gpuInfo() << '\n'; + stream << "/etc/os-release:"; auto osReleaseFile = QFile("/etc/os-release"); if (osReleaseFile.open(QFile::ReadOnly)) { diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp index 7759d53..cc13f97 100644 --- a/src/core/debuginfo.hpp +++ b/src/core/debuginfo.hpp @@ -6,6 +6,7 @@ namespace qs::debuginfo { QString qsVersion(); QString qtVersion(); +QString gpuInfo(); QString systemInfo(); QString combinedInfo(); From 9e8eecf2b8bfa9dd3eed5712d5856d7b041ea909 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 15 Mar 2026 21:13:35 -0700 Subject: [PATCH 186/226] core: log qt related environment variables in debuginfo --- src/core/debuginfo.cpp | 33 +++++++++++++++++++++++++++++++++ src/core/debuginfo.hpp | 1 + 2 files changed, 34 insertions(+) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp index f26c72e..ae227f8 100644 --- a/src/core/debuginfo.cpp +++ b/src/core/debuginfo.cpp @@ -1,4 +1,7 @@ #include "debuginfo.hpp" +#include +#include +#include #include #include @@ -14,6 +17,8 @@ #include "build.hpp" +extern char** environ; // NOLINT + namespace qs::debuginfo { QString qsVersion() { @@ -119,6 +124,31 @@ QString systemInfo() { return info; } +QString envInfo() { + QString info; + auto stream = QTextStream(&info); + + for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT + auto prefixes = std::array { + "QS_", + "QT_", + "QML_", + "QML2_", + "QSG_", + }; + + for (const auto& prefix: prefixes) { + if (strncmp(prefix.data(), *envp, prefix.length()) == 0) goto print; + } + continue; + + print: + stream << *envp << '\n'; + } + + return info; +} + QString combinedInfo() { QString info; auto stream = QTextStream(&info); @@ -136,6 +166,9 @@ QString combinedInfo() { stream << "\n===== System Information =====\n"; stream << systemInfo(); + stream << "\n===== Environment (trimmed) =====\n"; + stream << envInfo(); + return info; } diff --git a/src/core/debuginfo.hpp b/src/core/debuginfo.hpp index cc13f97..fc766fc 100644 --- a/src/core/debuginfo.hpp +++ b/src/core/debuginfo.hpp @@ -8,6 +8,7 @@ QString qsVersion(); QString qtVersion(); QString gpuInfo(); QString systemInfo(); +QString envInfo(); QString combinedInfo(); } // namespace qs::debuginfo From 6705e2da778d216e81dbdc3764a3f50e89bfd87d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Jun 2025 12:57:15 -0700 Subject: [PATCH 187/226] wm: add WindowManager module with ext-workspace support --- changelog/next.md | 1 + src/CMakeLists.txt | 1 + src/wayland/CMakeLists.txt | 2 + src/wayland/windowmanager/CMakeLists.txt | 19 ++ src/wayland/windowmanager/ext_workspace.cpp | 176 ++++++++++++ src/wayland/windowmanager/ext_workspace.hpp | 117 ++++++++ src/wayland/windowmanager/init.cpp | 23 ++ src/wayland/windowmanager/windowmanager.cpp | 21 ++ src/wayland/windowmanager/windowmanager.hpp | 17 ++ src/wayland/windowmanager/windowset.cpp | 252 ++++++++++++++++++ src/wayland/windowmanager/windowset.hpp | 85 ++++++ src/windowmanager/CMakeLists.txt | 20 ++ src/windowmanager/module.md | 10 + src/windowmanager/screenprojection.cpp | 30 +++ src/windowmanager/screenprojection.hpp | 34 +++ .../test/manual/WorkspaceDelegate.qml | 86 ++++++ src/windowmanager/test/manual/screenproj.qml | 45 ++++ src/windowmanager/test/manual/workspaces.qml | 46 ++++ src/windowmanager/windowmanager.cpp | 41 +++ src/windowmanager/windowmanager.hpp | 91 +++++++ src/windowmanager/windowset.cpp | 45 ++++ src/windowmanager/windowset.hpp | 175 ++++++++++++ 22 files changed, 1337 insertions(+) create mode 100644 src/wayland/windowmanager/CMakeLists.txt create mode 100644 src/wayland/windowmanager/ext_workspace.cpp create mode 100644 src/wayland/windowmanager/ext_workspace.hpp create mode 100644 src/wayland/windowmanager/init.cpp create mode 100644 src/wayland/windowmanager/windowmanager.cpp create mode 100644 src/wayland/windowmanager/windowmanager.hpp create mode 100644 src/wayland/windowmanager/windowset.cpp create mode 100644 src/wayland/windowmanager/windowset.hpp create mode 100644 src/windowmanager/CMakeLists.txt create mode 100644 src/windowmanager/module.md create mode 100644 src/windowmanager/screenprojection.cpp create mode 100644 src/windowmanager/screenprojection.hpp create mode 100644 src/windowmanager/test/manual/WorkspaceDelegate.qml create mode 100644 src/windowmanager/test/manual/screenproj.qml create mode 100644 src/windowmanager/test/manual/workspaces.qml create mode 100644 src/windowmanager/windowmanager.cpp create mode 100644 src/windowmanager/windowmanager.hpp create mode 100644 src/windowmanager/windowset.cpp create mode 100644 src/windowmanager/windowset.hpp diff --git a/changelog/next.md b/changelog/next.md index e9b297c..cbfd51b 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -26,6 +26,7 @@ set shell id. - Added Quickshell version checking and version gated preprocessing. - Added a way to detect if an icon is from the system icon theme or not. - Added vulkan support to screencopy. +- Added generic WindowManager interface implementing ext-workspace. ## Other Changes diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b13d45..0c05419 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(window) add_subdirectory(io) add_subdirectory(widgets) add_subdirectory(ui) +add_subdirectory(windowmanager) if (CRASH_HANDLER) add_subdirectory(crash) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ca49c8f..db53f37 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -123,6 +123,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) add_subdirectory(shortcuts_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) +add_subdirectory(windowmanager) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/windowmanager/CMakeLists.txt b/src/wayland/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..76d1d89 --- /dev/null +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-wayland-windowsystem STATIC + windowmanager.cpp + windowset.cpp + ext_workspace.cpp +) + +add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp) +target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick) + +wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") + +target_link_libraries(quickshell-wayland-windowsystem PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-ext-workspace +) + +qs_pch(quickshell-wayland-windowsystem SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init) diff --git a/src/wayland/windowmanager/ext_workspace.cpp b/src/wayland/windowmanager/ext_workspace.cpp new file mode 100644 index 0000000..fcb9ffa --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -0,0 +1,176 @@ +#include "ext_workspace.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::workspace { + +QS_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace", QtWarningMsg); + +WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace_group( + ::ext_workspace_group_handle_v1* handle +) { + auto* group = new WorkspaceGroup(handle); + qCDebug(logWorkspace) << "Created group" << group; + this->mGroups.insert(handle, group); + emit this->groupCreated(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) { + auto* workspace = new Workspace(handle); + qCDebug(logWorkspace) << "Created workspace" << workspace; + this->mWorkspaces.insert(handle, workspace); + emit this->workspaceCreated(workspace); +}; + +void WorkspaceManager::destroyWorkspace(Workspace* workspace) { + this->mWorkspaces.remove(workspace->object()); + this->destroyedWorkspaces.append(workspace); + emit this->workspaceDestroyed(workspace); +} + +void WorkspaceManager::destroyGroup(WorkspaceGroup* group) { + this->mGroups.remove(group->object()); + this->destroyedGroups.append(group); + emit this->groupDestroyed(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_done() { + qCDebug(logWorkspace) << "Workspace changes done"; + emit this->serverCommit(); + + for (auto* workspace: this->destroyedWorkspaces) delete workspace; + for (auto* group: this->destroyedGroups) delete group; + this->destroyedWorkspaces.clear(); + this->destroyedGroups.clear(); +} + +void WorkspaceManager::ext_workspace_manager_v1_finished() { + qCWarning(logWorkspace) << "ext_workspace_manager_v1.finished() was received"; +} + +Workspace::~Workspace() { + if (this->isInitialized()) this->destroy(); +} + +void Workspace::ext_workspace_handle_v1_id(const QString& id) { + qCDebug(logWorkspace) << "Updated id for workspace" << this << "to" << id; + this->id = id; +} + +void Workspace::ext_workspace_handle_v1_name(const QString& name) { + qCDebug(logWorkspace) << "Updated name for workspace" << this << "to" << name; + this->name = name; +} + +void Workspace::ext_workspace_handle_v1_coordinates(wl_array* coordinates) { + this->coordinates.clear(); + + auto* data = static_cast(coordinates->data); + auto size = static_cast(coordinates->size / sizeof(qint32)); + + for (auto i = 0; i != size; ++i) { + this->coordinates.append(data[i]); // NOLINT + } + + qCDebug(logWorkspace) << "Updated coordinates for workspace" << this << "to" << this->coordinates; +} + +void Workspace::ext_workspace_handle_v1_state(quint32 state) { + this->active = state & ext_workspace_handle_v1::state_active; + this->urgent = state & ext_workspace_handle_v1::state_urgent; + this->hidden = state & ext_workspace_handle_v1::state_hidden; + + qCDebug(logWorkspace).nospace() << "Updated state for workspace " << this + << " to [active: " << this->active << ", urgent: " << this->urgent + << ", hidden: " << this->hidden << ']'; +} + +void Workspace::ext_workspace_handle_v1_capabilities(quint32 capabilities) { + this->canActivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_activate; + this->canDeactivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_deactivate; + this->canRemove = capabilities & ext_workspace_handle_v1::workspace_capabilities_remove; + this->canAssign = capabilities & ext_workspace_handle_v1::workspace_capabilities_assign; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for workspace " << this + << " to [activate: " << this->canActivate + << ", deactivate: " << this->canDeactivate + << ", remove: " << this->canRemove + << ", assign: " << this->canAssign << ']'; +} + +void Workspace::ext_workspace_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed workspace" << this; + WorkspaceManager::instance()->destroyWorkspace(this); + this->destroy(); +} + +void Workspace::enterGroup(WorkspaceGroup* group) { this->group = group; } + +void Workspace::leaveGroup(WorkspaceGroup* group) { + if (this->group == group) this->group = nullptr; +} + +WorkspaceGroup::~WorkspaceGroup() { + if (this->isInitialized()) this->destroy(); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) { + this->canCreateWorkspace = + capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for group " << this + << " to [create_workspace: " << this->canCreateWorkspace << ']'; +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_enter(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "added to group" << this; + this->screens.addOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "removed from group" << this; + this->screens.removeOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter( + ::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "added to group" << this; + + if (workspace) workspace->enterGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave( + ::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "removed from group" << this; + + if (workspace) workspace->leaveGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed group" << this; + WorkspaceManager::instance()->destroyGroup(this); + this->destroy(); +} + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/ext_workspace.hpp b/src/wayland/windowmanager/ext_workspace.hpp new file mode 100644 index 0000000..6aff209 --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../output_tracking.hpp" + +namespace qs::wayland::workspace { + +QS_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; +class Workspace; + +class WorkspaceManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_workspace_manager_v1 { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + [[nodiscard]] QList workspaces() { return this->mWorkspaces.values(); } + +signals: + void serverCommit(); + void workspaceCreated(Workspace* workspace); + void workspaceDestroyed(Workspace* workspace); + void groupCreated(WorkspaceGroup* group); + void groupDestroyed(WorkspaceGroup* group); + +protected: + void ext_workspace_manager_v1_workspace_group(::ext_workspace_group_handle_v1* handle) override; + void ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) override; + void ext_workspace_manager_v1_done() override; + void ext_workspace_manager_v1_finished() override; + +private: + WorkspaceManager(); + + void destroyGroup(WorkspaceGroup* group); + void destroyWorkspace(Workspace* workspace); + + QHash<::ext_workspace_handle_v1*, Workspace*> mWorkspaces; + QHash<::ext_workspace_group_handle_v1*, WorkspaceGroup*> mGroups; + QList destroyedGroups; + QList destroyedWorkspaces; + + friend class Workspace; + friend class WorkspaceGroup; +}; + +class Workspace: public QtWayland::ext_workspace_handle_v1 { +public: + Workspace(::ext_workspace_handle_v1* handle): QtWayland::ext_workspace_handle_v1(handle) {} + ~Workspace() override; + Q_DISABLE_COPY_MOVE(Workspace); + + QString id; + QString name; + QList coordinates; + WorkspaceGroup* group = nullptr; + + bool active : 1 = false; + bool urgent : 1 = false; + bool hidden : 1 = false; + + bool canActivate : 1 = false; + bool canDeactivate : 1 = false; + bool canRemove : 1 = false; + bool canAssign : 1 = false; + +protected: + void ext_workspace_handle_v1_id(const QString& id) override; + void ext_workspace_handle_v1_name(const QString& name) override; + void ext_workspace_handle_v1_coordinates(wl_array* coordinates) override; + void ext_workspace_handle_v1_state(quint32 state) override; + void ext_workspace_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_handle_v1_removed() override; + +private: + void enterGroup(WorkspaceGroup* group); + void leaveGroup(WorkspaceGroup* group); + + friend class WorkspaceGroup; +}; + +class WorkspaceGroup: public QtWayland::ext_workspace_group_handle_v1 { +public: + WorkspaceGroup(::ext_workspace_group_handle_v1* handle) + : QtWayland::ext_workspace_group_handle_v1(handle) {} + + ~WorkspaceGroup() override; + Q_DISABLE_COPY_MOVE(WorkspaceGroup); + + WlOutputTracker screens; + bool canCreateWorkspace : 1 = false; + +protected: + void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override; + void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override; + void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_removed() override; +}; + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/init.cpp b/src/wayland/windowmanager/init.cpp new file mode 100644 index 0000000..88be01a --- /dev/null +++ b/src/wayland/windowmanager/init.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +#include "../../core/plugin.hpp" + +namespace qs::wm::wayland { +void installWmProvider(); +} + +namespace { + +class WaylandWmPlugin: public QsEnginePlugin { + QList dependencies() override { return {"window"}; } + + bool applies() override { return QGuiApplication::platformName() == "wayland"; } + + void init() override { qs::wm::wayland::installWmProvider(); } +}; + +QS_REGISTER_PLUGIN(WaylandWmPlugin); + +} // namespace diff --git a/src/wayland/windowmanager/windowmanager.cpp b/src/wayland/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..16245d0 --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -0,0 +1,21 @@ +#include "windowmanager.hpp" + +#include "../../windowmanager/windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm::wayland { + +WaylandWindowManager* WaylandWindowManager::instance() { + static auto* instance = []() { + auto* wm = new WaylandWindowManager(); + WindowsetManager::instance(); + return wm; + }(); + return instance; +} + +void installWmProvider() { // NOLINT (misc-use-internal-linkage) + qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); }); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowmanager.hpp b/src/wayland/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..9d48efd --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm::wayland { + +class WaylandWindowManager: public WindowManager { + Q_OBJECT; + +public: + static WaylandWindowManager* instance(); +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.cpp b/src/wayland/windowmanager/windowset.cpp new file mode 100644 index 0000000..796cfe2 --- /dev/null +++ b/src/wayland/windowmanager/windowset.cpp @@ -0,0 +1,252 @@ +#include "windowset.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "../../windowmanager/windowset.hpp" +#include "../../windowmanager/screenprojection.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WindowsetManager::WindowsetManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WindowsetManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WindowsetManager::onWindowsetCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WindowsetManager::onWindowsetDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WindowsetManager::onProjectionCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WindowsetManager::onProjectionDestroyed + ); +} + +void WindowsetManager::scheduleCommit() { + if (this->commitScheduled) { + qCDebug(impl::logWorkspace) << "Workspace commit already scheduled."; + return; + } + + qCDebug(impl::logWorkspace) << "Scheduling workspace commit..."; + this->commitScheduled = true; + QMetaObject::invokeMethod(this, &WindowsetManager::doCommit, Qt::QueuedConnection); +} + +void WindowsetManager::doCommit() { // NOLINT + qCDebug(impl::logWorkspace) << "Committing workspaces..."; + impl::WorkspaceManager::instance()->commit(); + this->commitScheduled = false; +} + +void WindowsetManager::onServerCommit() { + // Projections are created/destroyed around windowsets to avoid any nulls making it + // to the qml engine. + + Qt::beginPropertyUpdateGroup(); + + auto* wm = WindowManager::instance(); + auto windowsets = wm->bWindowsets.value(); + auto projections = wm->bWindowsetProjections.value(); + + for (auto* projImpl: this->pendingProjectionCreations) { + auto* projection = new WlWindowsetProjection(this, projImpl); + this->projectionsByImpl.insert(projImpl, projection); + projections.append(projection); + } + + for (auto* wsImpl: this->pendingWindowsetCreations) { + auto* ws = new WlWindowset(this, wsImpl); + this->windowsetByImpl.insert(wsImpl, ws); + windowsets.append(ws); + } + + for (auto* wsImpl: this->pendingWindowsetDestructions) { + windowsets.removeOne(this->windowsetByImpl.value(wsImpl)); + this->windowsetByImpl.remove(wsImpl); + } + + for (auto* projImpl: this->pendingProjectionDestructions) { + projections.removeOne(this->projectionsByImpl.value(projImpl)); + this->projectionsByImpl.remove(projImpl); + } + + for (auto* ws: windowsets) { + static_cast(ws)->commitImpl(); // NOLINT + } + + for (auto* projection: projections) { + static_cast(projection)->commitImpl(); // NOLINT + } + + this->pendingWindowsetCreations.clear(); + this->pendingWindowsetDestructions.clear(); + this->pendingProjectionCreations.clear(); + this->pendingProjectionDestructions.clear(); + + wm->bWindowsets = windowsets; + wm->bWindowsetProjections = projections; + + Qt::endPropertyUpdateGroup(); +} + +void WindowsetManager::onWindowsetCreated(impl::Workspace* workspace) { + this->pendingWindowsetCreations.append(workspace); +} + +void WindowsetManager::onWindowsetDestroyed(impl::Workspace* workspace) { + if (!this->pendingWindowsetCreations.removeOne(workspace)) { + this->pendingWindowsetDestructions.append(workspace); + } +} + +void WindowsetManager::onProjectionCreated(impl::WorkspaceGroup* group) { + this->pendingProjectionCreations.append(group); +} + +void WindowsetManager::onProjectionDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingProjectionCreations.removeOne(group)) { + this->pendingProjectionDestructions.append(group); + } +} + +WindowsetManager* WindowsetManager::instance() { + static auto* instance = new WindowsetManager(); + return instance; +} + +WlWindowset::WlWindowset(WindowsetManager* manager, impl::Workspace* impl) + : Windowset(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWindowset::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + this->bCoordinates = this->impl->coordinates; + this->bActive = this->impl->active; + this->bShouldDisplay = !this->impl->hidden; + this->bUrgent = this->impl->urgent; + this->bCanActivate = this->impl->canActivate; + this->bCanDeactivate = this->impl->canDeactivate; + this->bCanSetProjection = this->impl->canAssign; + this->bProjection = this->manager()->projectionsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWindowset::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::setProjection(WindowsetProjection* projection) { + if (!this->bCanSetProjection) { + qCritical(logWorkspace) << this << "cannot be assigned to a projection"; + return; + } + + if (!projection) { + qCritical(logWorkspace) << "Cannot set a windowset's projection to null"; + return; + } + + WlWindowsetProjection* wlProjection = nullptr; + if (auto* p = dynamic_cast(projection)) { + wlProjection = p; + } else if (auto* p = dynamic_cast(projection)) { + // In the 99% case, there will only be a single windowset on a screen. + // In the 1% case, the oldest projection (first in list) is most likely the desired one. + auto* screen = p->screen(); + for (const auto& proj: WindowsetManager::instance()->projectionsByImpl.values()) { + if (proj->bQScreens.value().contains(screen)) { + wlProjection = proj; + break; + } + } + } + + if (!wlProjection) { + qCritical(logWorkspace) << "Cannot set a windowset's projection to" << projection + << "as no wayland projection could be derived."; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << projection; + this->impl->assign(wlProjection->impl->object()); + WindowsetManager::instance()->scheduleCommit(); +} + +WlWindowsetProjection::WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl) + : WindowsetProjection(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWindowsetProjection::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bQScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.hpp b/src/wayland/windowmanager/windowset.hpp new file mode 100644 index 0000000..52d1c63 --- /dev/null +++ b/src/wayland/windowmanager/windowset.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../windowmanager/windowset.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWindowset; +class WlWindowsetProjection; + +class WindowsetManager: public QObject { + Q_OBJECT; + +public: + static WindowsetManager* instance(); + + void scheduleCommit(); + +private slots: + void doCommit(); + void onServerCommit(); + void onWindowsetCreated(impl::Workspace* workspace); + void onWindowsetDestroyed(impl::Workspace* workspace); + void onProjectionCreated(impl::WorkspaceGroup* group); + void onProjectionDestroyed(impl::WorkspaceGroup* group); + +private: + WindowsetManager(); + + bool commitScheduled = false; + + QList pendingWindowsetCreations; + QList pendingWindowsetDestructions; + QHash windowsetByImpl; + + QList pendingProjectionCreations; + QList pendingProjectionDestructions; + QHash projectionsByImpl; + + friend class WlWindowset; +}; + +class WlWindowset: public Windowset { +public: + WlWindowset(WindowsetManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setProjection(WindowsetProjection* projection) override; + + [[nodiscard]] WindowsetManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWindowsetProjection: public WindowsetProjection { +public: + WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WindowsetManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWindowset; +}; + +} // namespace qs::wm::wayland diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..3c032f4 --- /dev/null +++ b/src/windowmanager/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_add_library(quickshell-windowmanager STATIC + screenprojection.cpp + windowmanager.cpp + windowset.cpp +) + +qt_add_qml_module(quickshell-windowmanager + URI Quickshell.WindowManager + VERSION 0.1 + DEPENDENCIES QtQuick +) + +qs_add_module_deps_light(quickshell-windowmanager Quickshell) + +install_qml_module(quickshell-windowmanager) + +qs_module_pch(quickshell-windowmanager SET large) + +target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-windowmanagerplugin) diff --git a/src/windowmanager/module.md b/src/windowmanager/module.md new file mode 100644 index 0000000..3480d60 --- /dev/null +++ b/src/windowmanager/module.md @@ -0,0 +1,10 @@ +name = "Quickshell.WindowManager" +description = "Window manager interface" +headers = [ + "windowmanager.hpp", + "windowset.hpp", + "screenprojection.hpp", +] +----- +Currently only supports the [ext-workspace-v1](https://wayland.app/protocols/ext-workspace-v1) wayland protocol. +Support will be expanded in future releases. diff --git a/src/windowmanager/screenprojection.cpp b/src/windowmanager/screenprojection.cpp new file mode 100644 index 0000000..c09e6f0 --- /dev/null +++ b/src/windowmanager/screenprojection.cpp @@ -0,0 +1,30 @@ +#include "screenprojection.hpp" + +#include +#include +#include + +#include "windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm { + +ScreenProjection::ScreenProjection(QScreen* screen, QObject* parent) + : WindowsetProjection(parent) + , mScreen(screen) { + this->bQScreens = {screen}; + this->bWindowsets.setBinding([this]() { + QList result; + for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) { + auto* proj = ws->bindableProjection().value(); + if (proj && proj->bindableQScreens().value().contains(this->mScreen)) { + result.append(ws); + } + } + return result; + }); +} + +QScreen* ScreenProjection::screen() const { return this->mScreen; } + +} // namespace qs::wm diff --git a/src/windowmanager/screenprojection.hpp b/src/windowmanager/screenprojection.hpp new file mode 100644 index 0000000..6b0f31e --- /dev/null +++ b/src/windowmanager/screenprojection.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +#include "windowset.hpp" + +namespace qs::wm { + +///! WindowsetProjection covering one specific screen. +/// A ScreenProjection is a special type of @@WindowsetProjection which aggregates +/// all windowsets across all projections covering a specific screen. +/// +/// When used with @@Windowset.setProjection(), an arbitrary projection on the screen +/// will be picked. Usually there is only one. +/// +/// Use @@WindowManager.screenProjection() to get a ScreenProjection for a given screen. +class ScreenProjection: public WindowsetProjection { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + ScreenProjection(QScreen* screen, QObject* parent); + + [[nodiscard]] QScreen* screen() const; + +private: + QScreen* mScreen; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/test/manual/WorkspaceDelegate.qml b/src/windowmanager/test/manual/WorkspaceDelegate.qml new file mode 100644 index 0000000..4ebd7f2 --- /dev/null +++ b/src/windowmanager/test/manual/WorkspaceDelegate.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +WrapperRectangle { + id: delegate + required property Windowset modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + Label { text: `Coordinates: ${delegate.modelData.coordinates.toString()}`} + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetProjection + model: [...WindowManager.windowsetProjections].map(w => w.toString()) + currentIndex: WindowManager.windowsetProjections.indexOf(delegate.modelData.projection) + onActivated: i => delegate.modelData.setProjection(WindowManager.windowsetProjections[i]) + } + } + + RowLayout { + Label { text: "Screen:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetProjection + model: [...Quickshell.screens].map(w => w.name) + currentIndex: Quickshell.screens.indexOf(delegate.modelData.projection.screens[0]) + onActivated: i => delegate.modelData.setProjection(WindowManager.screenProjection(Quickshell.screens[i])) + } + } + + + RowLayout { + DisplayCheckBox { + text: "Active" + checked: delegate.modelData.active + } + + DisplayCheckBox { + text: "Urgent" + checked: delegate.modelData.urgent + } + + DisplayCheckBox { + text: "Should Display" + checked: delegate.modelData.shouldDisplay + } + } + + RowLayout { + Button { + text: "Activate" + enabled: delegate.modelData.canActivate + onClicked: delegate.modelData.activate() + } + + Button { + text: "Deactivate" + enabled: delegate.modelData.canDeactivate + onClicked: delegate.modelData.deactivate() + } + + Button { + text: "Remove" + enabled: delegate.modelData.canRemove + onClicked: delegate.modelData.remove() + } + } + } + + component DisplayCheckBox: CheckBox { + enabled: false + palette.disabled.text: parent.palette.active.text + palette.disabled.windowText: parent.palette.active.windowText + } +} diff --git a/src/windowmanager/test/manual/screenproj.qml b/src/windowmanager/test/manual/screenproj.qml new file mode 100644 index 0000000..d06036c --- /dev/null +++ b/src/windowmanager/test/manual/screenproj.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: Quickshell.screens + + WrapperRectangle { + id: delegate + required property ShellScreen modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: `Screen: ${delegate.modelData.name}` } + + Repeater { + model: ScriptModel { + values: WindowManager.screenProjection(delegate.modelData).windowsets + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.windowsets.filter(w => w.projection == null) + } + + WorkspaceDelegate {} + } + } + } +} diff --git a/src/windowmanager/test/manual/workspaces.qml b/src/windowmanager/test/manual/workspaces.qml new file mode 100644 index 0000000..d6fdf05 --- /dev/null +++ b/src/windowmanager/test/manual/workspaces.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: WindowManager.windowsetProjections + + WrapperRectangle { + id: delegate + required property WindowsetProjection modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Screens: ${delegate.modelData.screens.map(s => s.name)}` } + + Repeater { + model: ScriptModel { + values: delegate.modelData.windowsets + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.windowsets.filter(w => w.projection == null) + } + + WorkspaceDelegate {} + } + } + } +} diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..6b51db1 --- /dev/null +++ b/src/windowmanager/windowmanager.cpp @@ -0,0 +1,41 @@ +#include "windowmanager.hpp" +#include +#include + +#include + +#include "../core/qmlscreen.hpp" +#include "screenprojection.hpp" + +namespace qs::wm { + +std::function WindowManager::provider; + +void WindowManager::setProvider(std::function provider) { + WindowManager::provider = std::move(provider); +} + +WindowManager* WindowManager::instance() { + static auto* instance = WindowManager::provider(); + return instance; +} + +ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) { + auto* qscreen = screen->screen; + auto it = this->mScreenProjections.find(qscreen); + if (it != this->mScreenProjections.end()) { + return *it; + } + + auto* projection = new ScreenProjection(qscreen, this); + this->mScreenProjections.insert(qscreen, projection); + + QObject::connect(qscreen, &QObject::destroyed, this, [this, projection, qscreen]() { + this->mScreenProjections.remove(qscreen); + delete projection; + }); + + return projection; +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowmanager.hpp b/src/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..054e485 --- /dev/null +++ b/src/windowmanager/windowmanager.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../core/qmlscreen.hpp" +#include "screenprojection.hpp" +#include "windowset.hpp" + +namespace qs::wm { + +class WindowManager: public QObject { + Q_OBJECT; + +public: + static void setProvider(std::function provider); + static WindowManager* instance(); + + Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen); + + [[nodiscard]] QBindable> bindableWindowsets() const { + return &this->bWindowsets; + } + + [[nodiscard]] QBindable> bindableWindowsetProjections() const { + return &this->bWindowsetProjections; + } + +signals: + void windowsetsChanged(); + void windowsetProjectionsChanged(); + +public: + Q_OBJECT_BINDABLE_PROPERTY( + WindowManager, + QList, + bWindowsets, + &WindowManager::windowsetsChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowManager, + QList, + bWindowsetProjections, + &WindowManager::windowsetProjectionsChanged + ); + +private: + static std::function provider; + QHash mScreenProjections; +}; + +///! Window management interfaces exposed by the window manager. +class WindowManagerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(WindowManager); + QML_SINGLETON; + // clang-format off + /// All windowsets tracked by the WM across all projections. + Q_PROPERTY(QList windowsets READ default BINDABLE bindableWindowsets); + /// All windowset projections tracked by the WM. Does not include + /// internal projections from @@screenProjection(). + Q_PROPERTY(QList windowsetProjections READ default BINDABLE bindableWindowsetProjections); + // clang-format on + +public: + /// Returns an internal WindowsetProjection that covers a single screen and contains all + /// windowsets on that screen, regardless of the WM-specified projection. Depending on + /// how the WM lays out its actual projections, multiple ScreenProjections may contain + /// the same Windowsets. + Q_INVOKABLE static ScreenProjection* screenProjection(QuickshellScreenInfo* screen) { + return WindowManager::instance()->screenProjection(screen); + } + + [[nodiscard]] static QBindable> bindableWindowsets() { + return WindowManager::instance()->bindableWindowsets(); + } + + [[nodiscard]] static QBindable> bindableWindowsetProjections() { + return WindowManager::instance()->bindableWindowsetProjections(); + } +}; + +} // namespace qs::wm diff --git a/src/windowmanager/windowset.cpp b/src/windowmanager/windowset.cpp new file mode 100644 index 0000000..6231c40 --- /dev/null +++ b/src/windowmanager/windowset.cpp @@ -0,0 +1,45 @@ +#include "windowset.hpp" + +#include +#include +#include +#include + +#include "../core/qmlglobal.hpp" +#include "windowmanager.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Windowset::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Windowset::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Windowset::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Windowset::setProjection(WindowsetProjection* /*projection*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a projection"; +} + +WindowsetProjection::WindowsetProjection(QObject* parent): QObject(parent) { + this->bWindowsets.setBinding([this] { + QList result; + for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) { + if (ws->bindableProjection().value() == this) { + result.append(ws); + } + } + return result; + }); + + this->bScreens.setBinding([this] { + QList screens; + + for (auto* screen: this->bQScreens.value()) { + screens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + return screens; + }); +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowset.hpp b/src/windowmanager/windowset.hpp new file mode 100644 index 0000000..51cbd9b --- /dev/null +++ b/src/windowmanager/windowset.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WindowsetProjection; + +///! A group of windows worked with by a user, usually known as a Workspace or Tag. +/// A Windowset is a generic type that encompasses both "Workspaces" and "Tags" in window managers. +/// Because the definition encompasses both you may not necessarily need all features. +class Windowset: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// A persistent internal identifier for the windowset. This property should be identical + /// across restarts and destruction/recreation of a windowset. + Q_PROPERTY(QString id READ default NOTIFY idChanged BINDABLE bindableId); + /// Human readable name of the windowset. + Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); + /// Coordinates of the workspace, represented as an N-dimensional array. Most WMs + /// will only expose one coordinate. If more than one is exposed, the first is + /// conventionally X, the second Y, and the third Z. + Q_PROPERTY(QList coordinates READ default NOTIFY coordinatesChanged BINDABLE bindableCoordinates); + /// True if the windowset is currently active. In a workspace based WM, this means the + /// represented workspace is current. In a tag based WM, this means the represented tag + /// is active. + Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive); + /// The projection this windowset is a member of. A projection is the set of screens covered by + /// a windowset. + Q_PROPERTY(WindowsetProjection* projection READ default NOTIFY projectionChanged BINDABLE bindableProjection); + /// If false, this windowset should generally be hidden from workspace pickers. + Q_PROPERTY(bool shouldDisplay READ default NOTIFY shouldDisplayChanged BINDABLE bindableShouldDisplay); + /// If true, a window in this windowset has been marked as urgent. + Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent); + /// If true, the windowset can be activated. In a workspace based WM, this will make the workspace + /// current, in a tag based wm, the tag will be activated. + Q_PROPERTY(bool canActivate READ default NOTIFY canActivateChanged BINDABLE bindableCanActivate); + /// If true, the windowset can be deactivated. In a workspace based WM, deactivation is usually implicit + /// and based on activation of another workspace. + Q_PROPERTY(bool canDeactivate READ default NOTIFY canDeactivateChanged BINDABLE bindableCanDeactivate); + /// If true, the windowset can be removed. This may be done implicitly by the WM as well. + Q_PROPERTY(bool canRemove READ default NOTIFY canRemoveChanged BINDABLE bindableCanRemove); + /// If true, the windowset can be moved to a different projection. + Q_PROPERTY(bool canSetProjection READ default NOTIFY canSetProjectionChanged BINDABLE bindableCanSetProjection); + // clang-format on + +public: + explicit Windowset(QObject* parent): QObject(parent) {} + + /// Activate the windowset, making it the current workspace on a workspace based WM, or activating + /// the tag on a tag based WM. Requires @@canActivate. + Q_INVOKABLE virtual void activate(); + /// Deactivate the windowset, hiding it. Requires @@canDeactivate. + Q_INVOKABLE virtual void deactivate(); + /// Remove or destroy the windowset. Requires @@canRemove. + Q_INVOKABLE virtual void remove(); + /// Move the windowset to a different projection. A projection represents the set of screens + /// a workspace spans. Requires @@canSetProjection. + Q_INVOKABLE virtual void setProjection(WindowsetProjection* projection); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable> bindableCoordinates() const { return &this->bCoordinates; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + + [[nodiscard]] QBindable bindableProjection() const { + return &this->bProjection; + } + + [[nodiscard]] QBindable bindableShouldDisplay() const { return &this->bShouldDisplay; } + [[nodiscard]] QBindable bindableUrgent() const { return &this->bUrgent; } + [[nodiscard]] QBindable bindableCanActivate() const { return &this->bCanActivate; } + [[nodiscard]] QBindable bindableCanDeactivate() const { return &this->bCanDeactivate; } + [[nodiscard]] QBindable bindableCanRemove() const { return &this->bCanRemove; } + + [[nodiscard]] QBindable bindableCanSetProjection() const { + return &this->bCanSetProjection; + } + +signals: + void idChanged(); + void nameChanged(); + void coordinatesChanged(); + void activeChanged(); + void projectionChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetProjectionChanged(); + +protected: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bId, &Windowset::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bName, &Windowset::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QList, bCoordinates); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bActive, &Windowset::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, WindowsetProjection*, bProjection, &Windowset::projectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bShouldDisplay, &Windowset::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bUrgent, &Windowset::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanActivate, &Windowset::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanDeactivate, &Windowset::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanRemove, &Windowset::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanSetProjection, &Windowset::canSetProjectionChanged); + // clang-format on +}; + +///! A space occupiable by a Windowset. +/// A WindowsetProjection represents a space that can be occupied by one or more @@Windowset$s. +/// The space is one or more screens. Multiple projections may occupy the same screens. +/// +/// @@WindowManager.screenProjection() can be used to get a projection representing all +/// @@Windowset$s on a given screen regardless of the WM's actual projection layout. +class WindowsetProjection: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// Screens the windowset projection spans, often a single screen or all screens. + Q_PROPERTY(QList screens READ default NOTIFY screensChanged BINDABLE bindableScreens); + /// Windowsets that are currently present on the projection. + Q_PROPERTY(QList windowsets READ default NOTIFY windowsetsChanged BINDABLE bindableWindowsets); + // clang-format on + +public: + explicit WindowsetProjection(QObject* parent); + + [[nodiscard]] QBindable> bindableScreens() const { + return &this->bScreens; + } + + [[nodiscard]] QBindable> bindableQScreens() const { return &this->bQScreens; } + + [[nodiscard]] QBindable> bindableWindowsets() const { + return &this->bWindowsets; + } + +signals: + void screensChanged(); + void windowsetsChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(WindowsetProjection, QList, bQScreens); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowsetProjection, + QList, + bScreens, + &WindowsetProjection::screensChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowsetProjection, + QList, + bWindowsets, + &WindowsetProjection::windowsetsChanged + ); +}; + +} // namespace qs::wm From 365bf16b1ebc221f6124e19a0fa5b6ef8dc1d517 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 16 Mar 2026 21:19:20 -0700 Subject: [PATCH 188/226] wayland: hook wl_proxy_get_listener avoiding QTBUG-145022 crash Co-authored-by: Lemmy --- changelog/next.md | 1 + src/wayland/CMakeLists.txt | 8 +++++ src/wayland/init.cpp | 2 ++ src/wayland/windowmanager/windowset.cpp | 2 +- src/wayland/wl_proxy_safe_deref.cpp | 42 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/wayland/wl_proxy_safe_deref.cpp diff --git a/changelog/next.md b/changelog/next.md index cbfd51b..3969d55 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -58,6 +58,7 @@ set shell id. - Fixed ToplevelManager not clearing activeToplevel on deactivation. - Desktop action order is now preserved. - Fixed partial socket reads in greetd and hyprland on slow machines. +- Worked around Qt bug causing crashes when plugging and unplugging monitors. ## Packaging Changes diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index db53f37..13e648a 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -73,6 +73,7 @@ endfunction() # ----- qt_add_library(quickshell-wayland STATIC + wl_proxy_safe_deref.cpp platformmenu.cpp popupanchor.cpp xdgshell.cpp @@ -80,6 +81,13 @@ qt_add_library(quickshell-wayland STATIC output_tracking.cpp ) +# required for wl_proxy_safe_deref +target_link_libraries(quickshell-wayland PRIVATE ${CMAKE_DL_LIBS}) +target_link_options(quickshell PRIVATE + "LINKER:--export-dynamic-symbol=wl_proxy_get_listener" + "LINKER:--require-defined=wl_proxy_get_listener" +) + # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index e56eee3..790cebb 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -10,6 +10,7 @@ #include "wlr_layershell/wlr_layershell.hpp" #endif +void installWlProxySafeDeref(); // NOLINT(misc-use-internal-linkage) void installPlatformMenuHook(); // NOLINT(misc-use-internal-linkage) void installPopupPositioner(); // NOLINT(misc-use-internal-linkage) @@ -33,6 +34,7 @@ class WaylandPlugin: public QsEnginePlugin { } void init() override { + installWlProxySafeDeref(); installPlatformMenuHook(); installPopupPositioner(); } diff --git a/src/wayland/windowmanager/windowset.cpp b/src/wayland/windowmanager/windowset.cpp index 796cfe2..74e273d 100644 --- a/src/wayland/windowmanager/windowset.cpp +++ b/src/wayland/windowmanager/windowset.cpp @@ -8,9 +8,9 @@ #include #include +#include "../../windowmanager/screenprojection.hpp" #include "../../windowmanager/windowmanager.hpp" #include "../../windowmanager/windowset.hpp" -#include "../../windowmanager/screenprojection.hpp" #include "ext_workspace.hpp" namespace qs::wm::wayland { diff --git a/src/wayland/wl_proxy_safe_deref.cpp b/src/wayland/wl_proxy_safe_deref.cpp new file mode 100644 index 0000000..0ebc258 --- /dev/null +++ b/src/wayland/wl_proxy_safe_deref.cpp @@ -0,0 +1,42 @@ + +#include +#include +#include +#include +#include + +#include "../core/logcat.hpp" + +namespace { +QS_LOGGING_CATEGORY(logDeref, "quickshell.wayland.safederef", QtWarningMsg); +using wl_proxy_get_listener_t = const void* (*) (wl_proxy*); +wl_proxy_get_listener_t original_wl_proxy_get_listener = nullptr; // NOLINT +} // namespace + +extern "C" { +WL_EXPORT const void* wl_proxy_get_listener(struct wl_proxy* proxy) { + // Avoid null derefs of protocol objects in qtbase. + // https://qt-project.atlassian.net/browse/QTBUG-145022 + if (!proxy) [[unlikely]] { + qCCritical(logDeref) << "wl_proxy_get_listener called with a null proxy!"; + return nullptr; + } + + return original_wl_proxy_get_listener(proxy); +} +} + +// NOLINTBEGIN (concurrency-mt-unsafe) +void installWlProxySafeDeref() { + dlerror(); // clear old errors + + original_wl_proxy_get_listener = + reinterpret_cast(dlsym(RTLD_NEXT, "wl_proxy_get_listener")); + + if (auto* error = dlerror()) { + qCCritical(logDeref) << "Failed to find wl_proxy_get_listener for hooking:" << error; + } else { + qCInfo(logDeref) << "Installed wl_proxy_get_listener hook."; + } +} +// NOLINTEND From 1bd5b083cb48c13f901f276fc5d94c1b0a1ef9a1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 16 Mar 2026 22:38:32 -0700 Subject: [PATCH 189/226] hyprland/ipc: add null checks and ws preinit to toplevel object init Previously HyprlandToplevel::updateFromObject did not call findWorkspaceByName with createIfMissing=true, leaving bWorkspace null for a later insertToplevel call from HyprlandIpc::refreshToplevels. --- changelog/next.md | 1 + src/wayland/hyprland/ipc/connection.cpp | 2 +- src/wayland/hyprland/ipc/hyprland_toplevel.cpp | 10 +++------- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 3969d55..cceb79e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -48,6 +48,7 @@ set shell id. - Fixed volumes not initializing if a pipewire device was already loaded before its node. - Fixed hyprland active toplevel not resetting after window closes. - Fixed hyprland ipc window names and titles being reversed. +- Fixed a hyprland ipc crash when refreshing toplevels before workspaces. - Fixed missing signals for system tray item title and description updates. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index d2d5105..d15701d 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -729,7 +729,7 @@ void HyprlandIpc::refreshToplevels() { } auto* workspace = toplevel->bindableWorkspace().value(); - workspace->insertToplevel(toplevel); + if (workspace) workspace->insertToplevel(toplevel); } }); } diff --git a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp index 7b07bc8..43b9838 100644 --- a/src/wayland/hyprland/ipc/hyprland_toplevel.cpp +++ b/src/wayland/hyprland/ipc/hyprland_toplevel.cpp @@ -72,20 +72,16 @@ void HyprlandToplevel::updateFromObject(const QVariantMap& object) { Qt::beginPropertyUpdateGroup(); bool ok = false; auto address = addressStr.toULongLong(&ok, 16); - if (!ok || !address) { - return; - } + if (ok && address) this->setAddress(address); - this->setAddress(address); this->bTitle = title; auto workspaceMap = object.value("workspace").toMap(); auto workspaceName = workspaceMap.value("name").toString(); - auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false); - if (!workspace) return; + auto* workspace = this->ipc->findWorkspaceByName(workspaceName, true); + if (workspace) this->setWorkspace(workspace); - this->setWorkspace(workspace); this->bLastIpcObject = object; Qt::endPropertyUpdateGroup(); } From 0a859d51f25e8fafccad8fa3bade7306e9f0da39 Mon Sep 17 00:00:00 2001 From: -k Date: Thu, 22 Jan 2026 21:40:56 -0500 Subject: [PATCH 190/226] service/pam: include `signal.h` on freebsd --- src/services/pam/conversation.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/pam/conversation.cpp b/src/services/pam/conversation.cpp index f8f5a09..1fb4c04 100644 --- a/src/services/pam/conversation.cpp +++ b/src/services/pam/conversation.cpp @@ -8,6 +8,9 @@ #include #include #include +#ifdef __FreeBSD__ +#include +#endif #include "../../core/logcat.hpp" #include "ipc.hpp" From 97b2688ad67d4af95c7378f2ca0cece8bd3f9952 Mon Sep 17 00:00:00 2001 From: -k Date: Tue, 3 Mar 2026 11:20:56 -0500 Subject: [PATCH 191/226] core/log: fix non-linux typo and import unistd on freebsd --- src/core/logging.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index d24225b..893c56e 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -31,6 +31,9 @@ #include #include #endif +#ifdef __FreeBSD__ +#include +#endif #include "instanceinfo.hpp" #include "logcat.hpp" @@ -67,7 +70,7 @@ bool copyFileData(int sourceFd, int destFd, qint64 size) { return true; #else std::array buffer = {}; - auto remaining = totalTarget; + auto remaining = usize; while (remaining > 0) { auto chunk = std::min(remaining, buffer.size()); From a51dcd0a015f72a9af5c2d188e056c58740948d2 Mon Sep 17 00:00:00 2001 From: -k Date: Mon, 16 Mar 2026 17:40:55 -0400 Subject: [PATCH 192/226] wayland: use patched surfaceRole accessor on FreeBSD --- src/wayland/wlr_layershell/surface.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 4a5015e..0b0e7d7 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "../../window/panelinterface.hpp" #include "shell_integration.hpp" @@ -247,9 +248,19 @@ void LayerSurface::commit() { } void LayerSurface::attachPopup(QtWaylandClient::QWaylandShellSurface* popup) { - std::any role = popup->surfaceRole(); - - if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) { // NOLINT +#ifdef __FreeBSD__ + // FreeBSD uses an alternate RTTI matching strategy by default which does + // not work across modules, preventing std::any from downcasting. On + // FreeBSD, Qt is built with a patch to expose the surface role through a + // pointer instead of an any, which does not have this problem. + // See https://bugs.kde.org/show_bug.cgi?id=479679 + if (auto* xdgPopup = static_cast<::xdg_popup*>(popup->nativeResource("xdg_popup"))) { + this->get_popup(xdgPopup); + return; + } +#endif + auto role = popup->surfaceRole(); // NOLINT + if (auto* popupRole = std::any_cast<::xdg_popup*>(&role)) { this->get_popup(*popupRole); } else { qWarning() << "Cannot attach popup" << popup << "to shell surface" << this From 3cf65af49f22843386ac421f3889762e6f43a425 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Mar 2026 03:49:20 -0700 Subject: [PATCH 193/226] docs: ask users not to submit v1 crash reports --- .github/ISSUE_TEMPLATE/crash.yml | 91 +++++------------------------ .github/ISSUE_TEMPLATE/crash2.yml | 8 +-- src/wayland/wl_proxy_safe_deref.cpp | 1 - 3 files changed, 17 insertions(+), 83 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index 80fa827..958c884 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,82 +1,17 @@ name: Crash Report (v1) -description: Quickshell has crashed -labels: ["bug", "crash"] +description: Quickshell has crashed (old) +labels: ["unactionable"] body: - - type: textarea - id: crashinfo + - type: markdown attributes: - label: General crash information - description: | - Paste the contents of the `info.txt` file in your crash folder here. - value: "
General information - - - ``` - - - - ``` - - -
" - validations: - required: true - - type: textarea - id: userinfo + value: | + Thank you for taking the time to click the report button. + At this point most of the worst issues in 0.2.1 and before have been fixed and we are + preparing for a new release. Please do not submit this report. + - type: checkboxes + id: donotcheck attributes: - label: What caused the crash - description: | - Any information likely to help debug the crash. What were you doing when the crash occurred, - what changes did you make, can you get it to happen again? - - type: textarea - id: dump - attributes: - label: Minidump - description: | - Attach `minidump.dmp.log` here. If it is too big to upload, compress it. - - You may skip this step if quickshell crashed while processing a password - or other sensitive information. If you skipped it write why instead. - validations: - required: true - - type: textarea - id: logs - attributes: - label: Log file - description: | - Attach `log.qslog.log` here. If it is too big to upload, compress it. - - You can preview the log if you'd like using `quickshell read-log `. - validations: - required: true - - type: textarea - id: config - attributes: - label: Configuration - description: | - Attach your configuration here, preferrably in full (not just one file). - Compress it into a zip, tar, etc. - - This will help us reproduce the crash ourselves. - - type: textarea - id: bt - attributes: - label: Backtrace - description: | - If you have gdb installed and use systemd, or otherwise know how to get a backtrace, - we would appreciate one. (You may have gdb installed without knowing it) - - 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" - in the crash reporter. - 2. Once it loads, type `bt -full` (then enter) - 3. Copy the output and attach it as a file or in a spoiler. - - type: textarea - id: exe - attributes: - label: Executable - description: | - If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field. - If it is too big to upload, compress it. - - Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on - filetypes. + label: Read the text above. Do not submit the report. + options: + - label: Yes I want this report to be deleted. + required: true diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml index 84beef8..86f490c 100644 --- a/.github/ISSUE_TEMPLATE/crash2.yml +++ b/.github/ISSUE_TEMPLATE/crash2.yml @@ -9,21 +9,21 @@ body: description: | Any information likely to help debug the crash. What were you doing when the crash occurred, what changes did you make, can you get it to happen again? - - type: textarea + - type: upload id: report attributes: label: Report file description: Attach `report.txt` here. validations: required: true - - type: textarea + - type: upload id: logs attributes: label: Log file description: | Attach `log.qslog.log` here. If it is too big to upload, compress it. - You can preview the log if you'd like using `quickshell read-log `. + You can preview the log if you'd like using `qs log -r '*=true'`. validations: required: true - type: textarea @@ -31,7 +31,7 @@ body: attributes: label: Configuration description: | - Attach your configuration here, preferrably in full (not just one file). + Attach or link your configuration here, preferrably in full (not just one file). Compress it into a zip, tar, etc. This will help us reproduce the crash ourselves. diff --git a/src/wayland/wl_proxy_safe_deref.cpp b/src/wayland/wl_proxy_safe_deref.cpp index 0ebc258..2664a99 100644 --- a/src/wayland/wl_proxy_safe_deref.cpp +++ b/src/wayland/wl_proxy_safe_deref.cpp @@ -1,4 +1,3 @@ - #include #include #include From 3520c85d77ccf6cbfc158057447f44657a0bc9d4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Mar 2026 19:42:47 -0700 Subject: [PATCH 194/226] wayland: remove --require-defined linker argument Not supported by lld --- src/wayland/CMakeLists.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 13e648a..4a67558 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -83,10 +83,7 @@ qt_add_library(quickshell-wayland STATIC # required for wl_proxy_safe_deref target_link_libraries(quickshell-wayland PRIVATE ${CMAKE_DL_LIBS}) -target_link_options(quickshell PRIVATE - "LINKER:--export-dynamic-symbol=wl_proxy_get_listener" - "LINKER:--require-defined=wl_proxy_get_listener" -) +target_link_options(quickshell PRIVATE "LINKER:--export-dynamic-symbol=wl_proxy_get_listener") # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) From 0cb62920a7ab0b199754c941046ae86e3a1c368d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 18 Mar 2026 02:34:06 -0700 Subject: [PATCH 195/226] hyprland/focus_grab: handle destruction of tracked windows --- changelog/next.md | 1 + src/core/platformmenu.cpp | 5 +- src/core/popupanchor.cpp | 5 +- src/wayland/hyprland/focus_grab/qml.cpp | 99 +++++++++++---------- src/wayland/hyprland/focus_grab/qml.hpp | 4 +- src/wayland/hyprland/surface/qml.cpp | 9 +- src/wayland/idle_inhibit/inhibitor.cpp | 19 +--- src/wayland/shortcuts_inhibit/inhibitor.cpp | 10 +-- src/wayland/toplevel_management/qml.cpp | 9 +- src/window/proxywindow.cpp | 6 ++ src/window/proxywindow.hpp | 2 + 11 files changed, 67 insertions(+), 102 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index cceb79e..a8981b9 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -60,6 +60,7 @@ set shell id. - Desktop action order is now preserved. - Fixed partial socket reads in greetd and hyprland on slow machines. - Worked around Qt bug causing crashes when plugging and unplugging monitors. +- Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. ## Packaging Changes diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 427dde0..d8901e2 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -18,7 +18,6 @@ #include #include "../window/proxywindow.hpp" -#include "../window/windowinterface.hpp" #include "iconprovider.hpp" #include "model.hpp" #include "platformmenu_p.hpp" @@ -91,10 +90,8 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati } else if (parentWindow == nullptr) { qCritical() << "Cannot display PlatformMenuEntry with null parent window."; return false; - } else if (auto* proxy = qobject_cast(parentWindow)) { + } else if (auto* proxy = ProxyWindowBase::forObject(parentWindow)) { window = proxy->backingWindow(); - } else if (auto* interface = qobject_cast(parentWindow)) { - window = interface->proxyWindow()->backingWindow(); } else { qCritical() << "PlatformMenuEntry.display() must be called with a window."; return false; diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 151dd5d..ca817c9 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -11,7 +11,6 @@ #include #include "../window/proxywindow.hpp" -#include "../window/windowinterface.hpp" #include "types.hpp" bool PopupAnchorState::operator==(const PopupAnchorState& other) const { @@ -40,10 +39,8 @@ void PopupAnchor::setWindowInternal(QObject* window) { } if (window) { - if (auto* proxy = qobject_cast(window)) { + if (auto* proxy = ProxyWindowBase::forObject(window)) { this->bProxyWindow = proxy; - } else if (auto* interface = qobject_cast(window)) { - this->bProxyWindow = interface->proxyWindow(); } else { qWarning() << "Tried to set popup anchor window to" << window << "which is not a quickshell window."; diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp index e26a75a..cf1ac24 100644 --- a/src/wayland/hyprland/focus_grab/qml.cpp +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -9,7 +9,6 @@ #include #include "../../../window/proxywindow.hpp" -#include "../../../window/windowinterface.hpp" #include "grab.hpp" #include "manager.hpp" @@ -38,8 +37,51 @@ QObjectList HyprlandFocusGrab::windows() const { return this->windowObjects; } void HyprlandFocusGrab::setWindows(QObjectList windows) { if (windows == this->windowObjects) return; + if (this->grab) this->grab->startTransaction(); + + for (auto* obj: this->windowObjects) { + if (windows.contains(obj)) continue; + QObject::disconnect(obj, nullptr, this, nullptr); + + auto* proxy = ProxyWindowBase::forObject(obj); + if (!proxy) continue; + + QObject::disconnect(proxy, nullptr, this, nullptr); + + if (this->grab && proxy->backingWindow()) { + this->grab->removeWindow(proxy->backingWindow()); + } + } + + for (auto it = windows.begin(); it != windows.end();) { + auto* proxy = ProxyWindowBase::forObject(*it); + if (!proxy) { + it = windows.erase(it); + continue; + } + + if (this->windowObjects.contains(*it)) { + ++it; + continue; + } + + QObject::connect(*it, &QObject::destroyed, this, &HyprlandFocusGrab::onObjectDestroyed); + QObject::connect( + proxy, + &ProxyWindowBase::windowConnected, + this, + &HyprlandFocusGrab::onProxyConnected + ); + + if (this->grab && proxy->backingWindow()) { + this->grab->addWindow(proxy->backingWindow()); + } + + ++it; + } + + if (this->grab) this->grab->completeTransaction(); this->windowObjects = std::move(windows); - this->syncWindows(); emit this->windowsChanged(); } @@ -75,59 +117,18 @@ void HyprlandFocusGrab::tryActivate() { QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared); this->grab->startTransaction(); - for (auto* proxy: this->trackedProxies) { - if (proxy->backingWindow() != nullptr) { + for (auto* obj: this->windowObjects) { + auto* proxy = ProxyWindowBase::forObject(obj); + if (proxy && proxy->backingWindow()) { this->grab->addWindow(proxy->backingWindow()); } } this->grab->completeTransaction(); } -void HyprlandFocusGrab::syncWindows() { - auto newProxy = QList(); - for (auto* windowObject: this->windowObjects) { - auto* proxyWindow = qobject_cast(windowObject); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(windowObject)) { - proxyWindow = iface->proxyWindow(); - } - } - - if (proxyWindow != nullptr) { - newProxy.push_back(proxyWindow); - } - } - - if (this->grab) this->grab->startTransaction(); - - for (auto* oldWindow: this->trackedProxies) { - if (!newProxy.contains(oldWindow)) { - QObject::disconnect(oldWindow, nullptr, this, nullptr); - - if (this->grab != nullptr && oldWindow->backingWindow() != nullptr) { - this->grab->removeWindow(oldWindow->backingWindow()); - } - } - } - - for (auto* newProxy: newProxy) { - if (!this->trackedProxies.contains(newProxy)) { - QObject::connect( - newProxy, - &ProxyWindowBase::windowConnected, - this, - &HyprlandFocusGrab::onProxyConnected - ); - - if (this->grab != nullptr && newProxy->backingWindow() != nullptr) { - this->grab->addWindow(newProxy->backingWindow()); - } - } - } - - this->trackedProxies = newProxy; - if (this->grab) this->grab->completeTransaction(); +void HyprlandFocusGrab::onObjectDestroyed(QObject* object) { + this->windowObjects.removeOne(object); + emit this->windowsChanged(); } } // namespace qs::hyprland diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp index 705b0d3..97a10de 100644 --- a/src/wayland/hyprland/focus_grab/qml.hpp +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -96,15 +96,13 @@ private slots: void onGrabActivated(); void onGrabCleared(); void onProxyConnected(); + void onObjectDestroyed(QObject* object); private: void tryActivate(); - void syncWindows(); bool targetActive = false; QObjectList windowObjects; - QList trackedProxies; - QList trackedWindows; focus_grab::FocusGrab* grab = nullptr; }; diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index c4f7d67..4575842 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -14,7 +14,6 @@ #include "../../../core/region.hpp" #include "../../../window/proxywindow.hpp" -#include "../../../window/windowinterface.hpp" #include "manager.hpp" #include "surface.hpp" @@ -23,13 +22,7 @@ using QtWaylandClient::QWaylandWindow; namespace qs::hyprland::surface { HyprlandWindow* HyprlandWindow::qmlAttachedProperties(QObject* object) { - auto* proxyWindow = qobject_cast(object); - - if (!proxyWindow) { - if (auto* iface = qobject_cast(object)) { - proxyWindow = iface->proxyWindow(); - } - } + auto* proxyWindow = ProxyWindowBase::forObject(object); if (!proxyWindow) return nullptr; return new HyprlandWindow(proxyWindow); diff --git a/src/wayland/idle_inhibit/inhibitor.cpp b/src/wayland/idle_inhibit/inhibitor.cpp index efeeae1..bfea7a0 100644 --- a/src/wayland/idle_inhibit/inhibitor.cpp +++ b/src/wayland/idle_inhibit/inhibitor.cpp @@ -6,7 +6,6 @@ #include #include "../../window/proxywindow.hpp" -#include "../../window/windowinterface.hpp" #include "proto.hpp" namespace qs::wayland::idle_inhibit { @@ -25,27 +24,13 @@ QObject* IdleInhibitor::window() const { return this->bWindowObject; } void IdleInhibitor::setWindow(QObject* window) { if (window == this->bWindowObject) return; - auto* proxyWindow = qobject_cast(window); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } - + auto* proxyWindow = ProxyWindowBase::forObject(window); this->bWindowObject = proxyWindow ? window : nullptr; } void IdleInhibitor::boundWindowChanged() { auto* window = this->bBoundWindow.value(); - auto* proxyWindow = qobject_cast(window); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } - + auto* proxyWindow = ProxyWindowBase::forObject(window); if (proxyWindow == this->proxyWindow) return; if (this->mWaylandWindow) { diff --git a/src/wayland/shortcuts_inhibit/inhibitor.cpp b/src/wayland/shortcuts_inhibit/inhibitor.cpp index 2fca9bc..a91d5e2 100644 --- a/src/wayland/shortcuts_inhibit/inhibitor.cpp +++ b/src/wayland/shortcuts_inhibit/inhibitor.cpp @@ -9,7 +9,6 @@ #include #include "../../window/proxywindow.hpp" -#include "../../window/windowinterface.hpp" #include "proto.hpp" namespace qs::wayland::shortcuts_inhibit { @@ -48,14 +47,7 @@ ShortcutInhibitor::~ShortcutInhibitor() { void ShortcutInhibitor::onBoundWindowChanged() { auto* window = this->bBoundWindow.value(); - auto* proxyWindow = qobject_cast(window); - - if (!proxyWindow) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } - + auto* proxyWindow = ProxyWindowBase::forObject(window); if (proxyWindow == this->proxyWindow) return; if (this->proxyWindow) { diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 6a1d96b..cb53381 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -9,7 +9,6 @@ #include "../../core/qmlscreen.hpp" #include "../../core/util.hpp" #include "../../window/proxywindow.hpp" -#include "../../window/windowinterface.hpp" #include "../output_tracking.hpp" #include "handle.hpp" #include "manager.hpp" @@ -73,13 +72,7 @@ void Toplevel::fullscreenOn(QuickshellScreenInfo* screen) { } void Toplevel::setRectangle(QObject* window, QRect rect) { - auto* proxyWindow = qobject_cast(window); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } + auto* proxyWindow = ProxyWindowBase::forObject(window); if (proxyWindow != this->rectWindow) { if (this->rectWindow != nullptr) { diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 62126bd..8a20dfa 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -57,6 +57,12 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } +ProxyWindowBase* ProxyWindowBase::forObject(QObject* obj) { + if (auto* proxy = qobject_cast(obj)) return proxy; + if (auto* iface = qobject_cast(obj)) return iface->proxyWindow(); + return nullptr; +} + void ProxyWindowBase::onReload(QObject* oldInstance) { if (this->mVisible) this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index aec821e..9ff66c4 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -66,6 +66,8 @@ public: explicit ProxyWindowBase(QObject* parent = nullptr); ~ProxyWindowBase() override; + static ProxyWindowBase* forObject(QObject* obj); + ProxyWindowBase(ProxyWindowBase&) = delete; ProxyWindowBase(ProxyWindowBase&&) = delete; void operator=(ProxyWindowBase&) = delete; From 7511545ee20664e3b8b8d3322c0ffe7567c56f7a Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Wed, 18 Mar 2026 20:37:17 +0100 Subject: [PATCH 196/226] build: add missing wayland-client CFLAGS Fixes #276 --- src/wayland/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 4a67558..196f1e0 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -68,6 +68,7 @@ function (wl_proto target name dir) target_include_directories(${target} INTERFACE ${PROTO_BUILD_PATH}) target_link_libraries(${target} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate) qs_pch(${target} SET wayland-protocol) + target_compile_options(wl-proto-${name}-wl PRIVATE ${wayland_CFLAGS}) endfunction() # ----- From eb6eaf59c79408f1778248a3360c7a6d8ff89a47 Mon Sep 17 00:00:00 2001 From: Dan Aloni Date: Fri, 3 Oct 2025 17:11:03 +0300 Subject: [PATCH 197/226] core/log: add a mutex to protect stdoutStream QTextStream is not thread safe. --- src/core/logging.cpp | 2 ++ src/core/logging.hpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 893c56e..415cf61 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -220,6 +221,7 @@ void LogManager::messageHandler( } if (display) { + auto locker = QMutexLocker(&self->stdoutMutex); LogMessage::formatMessage( self->stdoutStream, message, diff --git a/src/core/logging.hpp b/src/core/logging.hpp index bf81133..7b6a758 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -135,6 +136,7 @@ private: QHash allFilters; QTextStream stdoutStream; + QMutex stdoutMutex; LoggingThreadProxy threadProxy; friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel); From 77c04a9447918bf7d052b4203b01ac2947ab9b35 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Mar 2026 09:40:36 -0400 Subject: [PATCH 198/226] launch: add ability to override AppId via pragma or QS_APP_ID --- changelog/next.md | 1 + src/core/instanceinfo.cpp | 8 ++++---- src/core/instanceinfo.hpp | 1 + src/crash/main.cpp | 4 +++- src/launch/launch.cpp | 8 +++++++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index a8981b9..3f059b1 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -38,6 +38,7 @@ set shell id. - Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching. - Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. - Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link. +- Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID. ## Bug Fixes diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp index 1f71b8a..b9b7b44 100644 --- a/src/core/instanceinfo.cpp +++ b/src/core/instanceinfo.cpp @@ -3,14 +3,14 @@ #include QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid - << info.display; + stream << info.instanceId << info.configPath << info.shellId << info.appId << info.launchTime + << info.pid << info.display; return stream; } QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid - >> info.display; + stream >> info.instanceId >> info.configPath >> info.shellId >> info.appId >> info.launchTime + >> info.pid >> info.display; return stream; } diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index 977e4c2..a4a7e66 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -9,6 +9,7 @@ struct InstanceInfo { QString instanceId; QString configPath; QString shellId; + QString appId; QDateTime launchTime; pid_t pid = -1; QString display; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 05927f2..30cf94d 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -230,7 +230,9 @@ void qsCheckCrash(int argc, char** argv) { ); auto app = QApplication(argc, argv); - QApplication::setDesktopFileName("org.quickshell"); + auto desktopId = + info.instance.appId.isEmpty() ? QStringLiteral("org.quickshell") : info.instance.appId; + QApplication::setDesktopFileName(desktopId); auto crashDir = QsPaths::crashDir(info.instance.instanceId); diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 3a9a2a5..0f5b090 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -76,6 +76,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio bool useSystemStyle = false; QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QHash envOverrides; + QString appId = qEnvironmentVariable("QS_APP_ID"); QString dataDir; QString stateDir; QString cacheDir; @@ -104,6 +105,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio auto var = envPragma.sliced(0, splitIdx).trimmed(); auto val = envPragma.sliced(splitIdx + 1).trimmed(); pragmas.envOverrides.insert(var, val); + } else if (pragma.startsWith("AppId ")) { + pragmas.appId = pragma.sliced(6).trimmed(); } else if (pragma.startsWith("ShellId ")) { shellId = pragma.sliced(8).trimmed(); } else if (pragma.startsWith("DataDir ")) { @@ -128,10 +131,13 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + auto appId = pragmas.appId.isEmpty() ? QStringLiteral("org.quickshell") : pragmas.appId; + InstanceInfo::CURRENT = InstanceInfo { .instanceId = base36Encode(getpid()) + base36Encode(launchTime), .configPath = args.configPath, .shellId = shellId, + .appId = appId, .launchTime = qs::Common::LAUNCH_TIME, .pid = getpid(), .display = getDisplayConnection(), @@ -231,7 +237,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio app = new QGuiApplication(qArgC, argv); } - QGuiApplication::setDesktopFileName("org.quickshell"); + QGuiApplication::setDesktopFileName(appId); if (args.debugPort != -1) { QQmlDebuggingEnabler::enableDebugging(true); From d7451848238f7f7ade38f00fe7ef91da1cc719a6 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Mar 2026 23:39:21 -0700 Subject: [PATCH 199/226] wayland/background-effect: add ext-background-effect-v1 support --- changelog/next.md | 1 + src/wayland/CMakeLists.txt | 3 + src/wayland/background_effect/CMakeLists.txt | 24 ++ src/wayland/background_effect/manager.cpp | 38 +++ src/wayland/background_effect/manager.hpp | 37 +++ src/wayland/background_effect/qml.cpp | 246 ++++++++++++++++++ src/wayland/background_effect/qml.hpp | 80 ++++++ src/wayland/background_effect/surface.cpp | 37 +++ src/wayland/background_effect/surface.hpp | 18 ++ .../test/manual/background_effect.qml | 47 ++++ src/wayland/module.md | 1 + 11 files changed, 532 insertions(+) create mode 100644 src/wayland/background_effect/CMakeLists.txt create mode 100644 src/wayland/background_effect/manager.cpp create mode 100644 src/wayland/background_effect/manager.hpp create mode 100644 src/wayland/background_effect/qml.cpp create mode 100644 src/wayland/background_effect/qml.hpp create mode 100644 src/wayland/background_effect/surface.cpp create mode 100644 src/wayland/background_effect/surface.hpp create mode 100644 src/wayland/background_effect/test/manual/background_effect.qml diff --git a/changelog/next.md b/changelog/next.md index 3f059b1..c5d93e2 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -27,6 +27,7 @@ set shell id. - Added a way to detect if an icon is from the system icon theme or not. - Added vulkan support to screencopy. - Added generic WindowManager interface implementing ext-workspace. +- Added ext-background-effect window blur support. ## Other Changes diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 196f1e0..cf84713 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -120,6 +120,9 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +add_subdirectory(background_effect) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._BackgroundEffect) + add_subdirectory(idle_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) diff --git a/src/wayland/background_effect/CMakeLists.txt b/src/wayland/background_effect/CMakeLists.txt new file mode 100644 index 0000000..f45f94d --- /dev/null +++ b/src/wayland/background_effect/CMakeLists.txt @@ -0,0 +1,24 @@ +qt_add_library(quickshell-wayland-background-effect STATIC + manager.cpp + surface.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-wayland-background-effect + URI Quickshell.Wayland._BackgroundEffect + VERSION 0.1 + DEPENDENCIES QtQml +) + +install_qml_module(quickshell-wayland-background-effect) + +wl_proto(wlp-background-effect ext-background-effect-v1 "${WAYLAND_PROTOCOLS}/staging/ext-background-effect") + +target_link_libraries(quickshell-wayland-background-effect PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-background-effect +) + +qs_module_pch(quickshell-wayland-background-effect) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-background-effectplugin) diff --git a/src/wayland/background_effect/manager.cpp b/src/wayland/background_effect/manager.cpp new file mode 100644 index 0000000..4cb06f1 --- /dev/null +++ b/src/wayland/background_effect/manager.cpp @@ -0,0 +1,38 @@ +#include "manager.hpp" +#include + +#include +#include +#include +#include + +#include "surface.hpp" + +namespace qs::wayland::background_effect::impl { + +BackgroundEffectManager::BackgroundEffectManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +BackgroundEffectSurface* +BackgroundEffectManager::createEffectSurface(QtWaylandClient::QWaylandWindow* window) { + return new BackgroundEffectSurface(this->get_background_effect(window->surface())); +} + +bool BackgroundEffectManager::blurAvailable() const { + return this->isActive() && this->mBlurAvailable; +} + +void BackgroundEffectManager::ext_background_effect_manager_v1_capabilities(uint32_t flags) { + auto available = static_cast(flags & capability_blur); + if (available == this->mBlurAvailable) return; + this->mBlurAvailable = available; + emit this->blurAvailableChanged(); +} + +BackgroundEffectManager* BackgroundEffectManager::instance() { + static auto* instance = new BackgroundEffectManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/manager.hpp b/src/wayland/background_effect/manager.hpp new file mode 100644 index 0000000..6c2e981 --- /dev/null +++ b/src/wayland/background_effect/manager.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "surface.hpp" + +namespace qs::wayland::background_effect::impl { + +class BackgroundEffectManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_background_effect_manager_v1 { + Q_OBJECT; + +public: + explicit BackgroundEffectManager(); + + BackgroundEffectSurface* createEffectSurface(QtWaylandClient::QWaylandWindow* window); + + [[nodiscard]] bool blurAvailable() const; + + static BackgroundEffectManager* instance(); + +signals: + void blurAvailableChanged(); + +protected: + void ext_background_effect_manager_v1_capabilities(uint32_t flags) override; + +private: + bool mBlurAvailable = false; +}; + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/qml.cpp b/src/wayland/background_effect/qml.cpp new file mode 100644 index 0000000..b54a847 --- /dev/null +++ b/src/wayland/background_effect/qml.cpp @@ -0,0 +1,246 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/region.hpp" +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "manager.hpp" +#include "surface.hpp" + +using QtWaylandClient::QWaylandWindow; + +namespace qs::wayland::background_effect { + +BackgroundEffect* BackgroundEffect::qmlAttachedProperties(QObject* object) { + auto* proxyWindow = qobject_cast(object); + + if (!proxyWindow) { + if (auto* iface = qobject_cast(object)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (!proxyWindow) return nullptr; + return new BackgroundEffect(proxyWindow); +} + +BackgroundEffect::BackgroundEffect(ProxyWindowBase* window): QObject(nullptr), proxyWindow(window) { + QObject::connect( + window, + &ProxyWindowBase::windowConnected, + this, + &BackgroundEffect::onWindowConnected + ); + + QObject::connect(window, &ProxyWindowBase::polished, this, &BackgroundEffect::onWindowPolished); + + QObject::connect( + window, + &ProxyWindowBase::devicePixelRatioChanged, + this, + &BackgroundEffect::updateBlurRegion + ); + + QObject::connect(window, &QObject::destroyed, this, &BackgroundEffect::onProxyWindowDestroyed); + + if (window->backingWindow()) { + this->onWindowConnected(); + } +} + +PendingRegion* BackgroundEffect::blurRegion() const { return this->mBlurRegion; } + +void BackgroundEffect::setBlurRegion(PendingRegion* region) { + if (region == this->mBlurRegion) return; + + if (this->mBlurRegion) { + QObject::disconnect(this->mBlurRegion, nullptr, this, nullptr); + } + + this->mBlurRegion = region; + + if (region) { + QObject::connect(region, &QObject::destroyed, this, &BackgroundEffect::onBlurRegionDestroyed); + QObject::connect(region, &PendingRegion::changed, this, &BackgroundEffect::updateBlurRegion); + } + + this->updateBlurRegion(); + emit this->blurRegionChanged(); +} + +void BackgroundEffect::onBlurRegionDestroyed() { + this->mBlurRegion = nullptr; + this->updateBlurRegion(); + emit this->blurRegionChanged(); +} + +void BackgroundEffect::updateBlurRegion() { + if (!this->surface || !this->proxyWindow) return; + + this->pendingBlurRegion = true; + this->proxyWindow->schedulePolish(); +} + +void BackgroundEffect::onWindowPolished() { + if (!this->surface || !this->pendingBlurRegion) return; + if (!this->mWaylandWindow || !this->mWaylandWindow->surface()) { + this->pendingBlurRegion = false; + return; + } + + QRegion region; + if (this->mBlurRegion) { + region = + this->mBlurRegion->applyTo(QRect(0, 0, this->mWindow->width(), this->mWindow->height())); + + auto scale = QHighDpiScaling::factor(this->mWindow); + if (!qFuzzyCompare(scale, 1.0)) { + region = QHighDpi::scale(region, scale); + } + + auto margins = this->mWaylandWindow->clientSideMargins(); + region.translate(margins.left(), margins.top()); + } + + this->surface->setBlurRegion(region); + this->pendingBlurRegion = false; +} + +bool BackgroundEffect::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::PlatformSurface) { + auto* surfaceEvent = dynamic_cast(event); + if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { + this->surface = nullptr; + this->pendingBlurRegion = false; + } + } + + return this->QObject::eventFilter(object, event); +} + +void BackgroundEffect::onWindowConnected() { + this->mWindow = this->proxyWindow->backingWindow(); + this->mWindow->installEventFilter(this); + + QObject::connect( + this->mWindow, + &QWindow::visibleChanged, + this, + &BackgroundEffect::onWindowVisibleChanged + ); + + this->onWindowVisibleChanged(); +} + +void BackgroundEffect::onWindowVisibleChanged() { + if (this->mWindow->isVisible()) { + if (!this->mWindow->handle()) { + this->mWindow->create(); + } + } + + auto* window = dynamic_cast(this->mWindow->handle()); + if (window == this->mWaylandWindow) return; + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + } + + this->mWaylandWindow = window; + if (!window) return; + + QObject::connect( + this->mWaylandWindow, + &QObject::destroyed, + this, + &BackgroundEffect::onWaylandWindowDestroyed + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &BackgroundEffect::onWaylandSurfaceCreated + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &BackgroundEffect::onWaylandSurfaceDestroyed + ); + + if (this->mWaylandWindow->surface()) { + this->onWaylandSurfaceCreated(); + } +} + +void BackgroundEffect::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void BackgroundEffect::onWaylandSurfaceCreated() { + auto* manager = impl::BackgroundEffectManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable background effect as ext-background-effect-v1 is not supported " + "by the current compositor."; + return; + } + + // Steal protocol surface from previous BackgroundEffect to avoid duplicate-attachment on reload. + auto v = this->mWaylandWindow->property("qs_background_effect"); + if (v.canConvert()) { + auto* prev = v.value(); + if (prev != this && prev->surface) { + this->surface.swap(prev->surface); + } + } + + if (!this->surface) { + this->surface = std::unique_ptr( + manager->createEffectSurface(this->mWaylandWindow) + ); + } + + this->mWaylandWindow->setProperty("qs_background_effect", QVariant::fromValue(this)); + + this->pendingBlurRegion = this->mBlurRegion != nullptr; + if (this->pendingBlurRegion) { + this->proxyWindow->schedulePolish(); + } +} + +void BackgroundEffect::onWaylandSurfaceDestroyed() { + this->surface = nullptr; + this->pendingBlurRegion = false; + + if (!this->proxyWindow) { + this->deleteLater(); + } +} + +void BackgroundEffect::onProxyWindowDestroyed() { + // Don't delete the BackgroundEffect, and therefore the impl::BackgroundEffectSurface + // until the wl_surface is destroyed. Deleting it when the proxy window is deleted would + // cause a frame without blur between the destruction of the ext_background_effect_surface_v1 + // and wl_surface objects. + + this->proxyWindow = nullptr; + + if (this->surface == nullptr) { + this->deleteLater(); + } +} + +} // namespace qs::wayland::background_effect diff --git a/src/wayland/background_effect/qml.hpp b/src/wayland/background_effect/qml.hpp new file mode 100644 index 0000000..dd93aec --- /dev/null +++ b/src/wayland/background_effect/qml.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "../../core/region.hpp" +#include "../../window/proxywindow.hpp" +#include "surface.hpp" + +namespace qs::wayland::background_effect { + +///! Background blur effect for Wayland surfaces. +/// Applies background blur behind a @@Quickshell.QsWindow or subclass, +/// as an attached object, using the [ext-background-effect-v1] Wayland protocol. +/// +/// > [!NOTE] Using a background effect requires the compositor support the +/// > [ext-background-effect-v1] protocol. +/// +/// [ext-background-effect-v1]: https://wayland.app/protocols/ext-background-effect-v1 +/// +/// #### Example +/// ```qml +/// @@Quickshell.PanelWindow { +/// id: root +/// color: "#80000000" +/// +/// BackgroundEffect.blurRegion: Region { item: root.contentItem } +/// } +/// ``` +class BackgroundEffect: public QObject { + Q_OBJECT; + // clang-format off + /// Region to blur behind the surface. Set to null to remove blur. + Q_PROPERTY(PendingRegion* blurRegion READ blurRegion WRITE setBlurRegion NOTIFY blurRegionChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE("BackgroundEffect can only be used as an attached object."); + QML_ATTACHED(BackgroundEffect); + +public: + explicit BackgroundEffect(ProxyWindowBase* window); + + [[nodiscard]] PendingRegion* blurRegion() const; + void setBlurRegion(PendingRegion* region); + + static BackgroundEffect* qmlAttachedProperties(QObject* object); + + bool eventFilter(QObject* object, QEvent* event) override; + +signals: + void blurRegionChanged(); + +private slots: + void onWindowConnected(); + void onWindowVisibleChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + void onProxyWindowDestroyed(); + void onBlurRegionDestroyed(); + void onWindowPolished(); + void updateBlurRegion(); + +private: + ProxyWindowBase* proxyWindow = nullptr; + QWindow* mWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + bool pendingBlurRegion = false; + PendingRegion* mBlurRegion = nullptr; + std::unique_ptr surface; +}; + +} // namespace qs::wayland::background_effect diff --git a/src/wayland/background_effect/surface.cpp b/src/wayland/background_effect/surface.cpp new file mode 100644 index 0000000..648361d --- /dev/null +++ b/src/wayland/background_effect/surface.cpp @@ -0,0 +1,37 @@ +#include "surface.hpp" + +#include +#include +#include +#include +#include + +namespace qs::wayland::background_effect::impl { + +BackgroundEffectSurface::BackgroundEffectSurface( + ::ext_background_effect_surface_v1* surface // NOLINT(misc-include-cleaner) +) + : QtWayland::ext_background_effect_surface_v1(surface) {} + +BackgroundEffectSurface::~BackgroundEffectSurface() { + if (!this->isInitialized()) return; + this->destroy(); +} + +void BackgroundEffectSurface::setBlurRegion(const QRegion& region) { + if (!this->isInitialized()) return; + + if (region.isEmpty()) { + this->set_blur_region(nullptr); + return; + } + + static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance(); + auto* display = waylandIntegration->display(); + + auto* wlRegion = display->createRegion(region); + this->set_blur_region(wlRegion); + wl_region_destroy(wlRegion); // NOLINT(misc-include-cleaner) +} + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/surface.hpp b/src/wayland/background_effect/surface.hpp new file mode 100644 index 0000000..65b0bc8 --- /dev/null +++ b/src/wayland/background_effect/surface.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace qs::wayland::background_effect::impl { + +class BackgroundEffectSurface: public QtWayland::ext_background_effect_surface_v1 { +public: + explicit BackgroundEffectSurface(::ext_background_effect_surface_v1* surface); + ~BackgroundEffectSurface() override; + Q_DISABLE_COPY_MOVE(BackgroundEffectSurface); + + void setBlurRegion(const QRegion& region); +}; + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/test/manual/background_effect.qml b/src/wayland/background_effect/test/manual/background_effect.qml new file mode 100644 index 0000000..8cb4e12 --- /dev/null +++ b/src/wayland/background_effect/test/manual/background_effect.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + id: root + color: "transparent" + contentItem.palette.windowText: "white" + + ColumnLayout { + anchors.centerIn: parent + + CheckBox { + id: enableBox + checked: true + text: "Enable Blur" + } + + Button { + text: "Hide->Show" + onClicked: { + root.visible = false + showTimer.start() + } + } + + Timer { + id: showTimer + interval: 200 + onTriggered: root.visible = true + } + + Slider { + id: radiusSlider + from: 0 + to: 1000 + value: 100 + } + } + + BackgroundEffect.blurRegion: Region { + item: enableBox.checked ? root.contentItem : null + radius: radiusSlider.value == -1 ? undefined : radiusSlider.value + } +} diff --git a/src/wayland/module.md b/src/wayland/module.md index 9ad15ba..964fa76 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -8,5 +8,6 @@ headers = [ "idle_inhibit/inhibitor.hpp", "idle_notify/monitor.hpp", "shortcuts_inhibit/inhibitor.hpp", + "background_effect/qml.hpp", ] ----- From 6a244c3c560b45f3b860ed6c0fc54d0291ab6f57 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Mar 2026 23:42:29 -0700 Subject: [PATCH 200/226] core/region: add per-corner radius support --- changelog/next.md | 1 + src/core/region.cpp | 133 ++++++++++++++++++ src/core/region.hpp | 60 ++++++++ .../test/manual/background_effect.qml | 15 ++ 4 files changed, 209 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index c5d93e2..fc6d79e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -28,6 +28,7 @@ set shell id. - Added vulkan support to screencopy. - Added generic WindowManager interface implementing ext-workspace. - Added ext-background-effect window blur support. +- Added per-corner radius support to Region. ## Other Changes diff --git a/src/core/region.cpp b/src/core/region.cpp index 11892d6..82cc2e7 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -1,4 +1,5 @@ #include "region.hpp" +#include #include #include @@ -18,6 +19,11 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::radiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::topLeftRadiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::topRightRadiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::bottomLeftRadiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::bottomRightRadiusChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); } @@ -45,6 +51,79 @@ void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } +qint32 PendingRegion::radius() const { return this->mRadius; } + +void PendingRegion::setRadius(qint32 radius) { + if (radius == this->mRadius) return; + this->mRadius = radius; + emit this->radiusChanged(); + + if (!(this->mCornerOverrides & TopLeft)) emit this->topLeftRadiusChanged(); + if (!(this->mCornerOverrides & TopRight)) emit this->topRightRadiusChanged(); + if (!(this->mCornerOverrides & BottomLeft)) emit this->bottomLeftRadiusChanged(); + if (!(this->mCornerOverrides & BottomRight)) emit this->bottomRightRadiusChanged(); +} + +qint32 PendingRegion::topLeftRadius() const { + return (this->mCornerOverrides & TopLeft) ? this->mTopLeftRadius : this->mRadius; +} + +void PendingRegion::setTopLeftRadius(qint32 radius) { + this->mTopLeftRadius = radius; + this->mCornerOverrides |= TopLeft; + emit this->topLeftRadiusChanged(); +} + +void PendingRegion::resetTopLeftRadius() { + this->mCornerOverrides &= ~TopLeft; + emit this->topLeftRadiusChanged(); +} + +qint32 PendingRegion::topRightRadius() const { + return (this->mCornerOverrides & TopRight) ? this->mTopRightRadius : this->mRadius; +} + +void PendingRegion::setTopRightRadius(qint32 radius) { + this->mTopRightRadius = radius; + this->mCornerOverrides |= TopRight; + emit this->topRightRadiusChanged(); +} + +void PendingRegion::resetTopRightRadius() { + this->mCornerOverrides &= ~TopRight; + emit this->topRightRadiusChanged(); +} + +qint32 PendingRegion::bottomLeftRadius() const { + return (this->mCornerOverrides & BottomLeft) ? this->mBottomLeftRadius : this->mRadius; +} + +void PendingRegion::setBottomLeftRadius(qint32 radius) { + this->mBottomLeftRadius = radius; + this->mCornerOverrides |= BottomLeft; + emit this->bottomLeftRadiusChanged(); +} + +void PendingRegion::resetBottomLeftRadius() { + this->mCornerOverrides &= ~BottomLeft; + emit this->bottomLeftRadiusChanged(); +} + +qint32 PendingRegion::bottomRightRadius() const { + return (this->mCornerOverrides & BottomRight) ? this->mBottomRightRadius : this->mRadius; +} + +void PendingRegion::setBottomRightRadius(qint32 radius) { + this->mBottomRightRadius = radius; + this->mCornerOverrides |= BottomRight; + emit this->bottomRightRadiusChanged(); +} + +void PendingRegion::resetBottomRightRadius() { + this->mCornerOverrides &= ~BottomRight; + emit this->bottomRightRadiusChanged(); +} + QQmlListProperty PendingRegion::regions() { return QQmlListProperty( this, @@ -90,6 +169,60 @@ QRegion PendingRegion::build() const { region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type); } + if (this->mShape == RegionShape::Rect && !region.isEmpty()) { + auto tl = std::max(this->topLeftRadius(), 0); + auto tr = std::max(this->topRightRadius(), 0); + auto bl = std::max(this->bottomLeftRadius(), 0); + auto br = std::max(this->bottomRightRadius(), 0); + + if (tl > 0 || tr > 0 || bl > 0 || br > 0) { + auto rect = region.boundingRect(); + auto x = rect.x(); + auto y = rect.y(); + auto w = rect.width(); + auto h = rect.height(); + + // Normalize so adjacent corners don't exceed their shared edge. + // Each corner is scaled by the tightest constraint of its two edges. + auto topScale = tl + tr > w ? static_cast(w) / (tl + tr) : 1.0; + auto bottomScale = bl + br > w ? static_cast(w) / (bl + br) : 1.0; + auto leftScale = tl + bl > h ? static_cast(h) / (tl + bl) : 1.0; + auto rightScale = tr + br > h ? static_cast(h) / (tr + br) : 1.0; + + tl = static_cast(tl * std::min(topScale, leftScale)); + tr = static_cast(tr * std::min(topScale, rightScale)); + bl = static_cast(bl * std::min(bottomScale, leftScale)); + br = static_cast(br * std::min(bottomScale, rightScale)); + + // Unlock each corner: subtract (cornerBox - quarterEllipse) from the + // full rect. Each corner only modifies pixels inside its own box, + // so no diagonal overlap is possible. + if (tl > 0) { + auto box = QRegion(x, y, tl, tl); + auto ellipse = QRegion(x, y, tl * 2, tl * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + + if (tr > 0) { + auto box = QRegion(x + w - tr, y, tr, tr); + auto ellipse = QRegion(x + w - tr * 2, y, tr * 2, tr * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + + if (bl > 0) { + auto box = QRegion(x, y + h - bl, bl, bl); + auto ellipse = QRegion(x, y + h - bl * 2, bl * 2, bl * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + + if (br > 0) { + auto box = QRegion(x + w - br, y + h - br, br, br); + auto ellipse = QRegion(x + w - br * 2, y + h - br * 2, br * 2, br * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + } + } + for (const auto& childRegion: this->mRegions) { region = childRegion->applyTo(region); } diff --git a/src/core/region.hpp b/src/core/region.hpp index 6637d7b..dfd1566 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -66,6 +66,29 @@ class PendingRegion: public QObject { Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged); /// Defaults to 0. Does nothing if @@item is set. Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged); + // clang-format off + /// Corner radius for rounded rectangles. Only applies when @@shape is `Rect`. Defaults to 0. + /// + /// Acts as the default for @@topLeftRadius, @@topRightRadius, @@bottomLeftRadius, + /// and @@bottomRightRadius. + Q_PROPERTY(qint32 radius READ radius WRITE setRadius NOTIFY radiusChanged); + /// Top-left corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 topLeftRadius READ topLeftRadius WRITE setTopLeftRadius RESET resetTopLeftRadius NOTIFY topLeftRadiusChanged); + /// Top-right corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 topRightRadius READ topRightRadius WRITE setTopRightRadius RESET resetTopRightRadius NOTIFY topRightRadiusChanged); + /// Bottom-left corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius RESET resetBottomLeftRadius NOTIFY bottomLeftRadiusChanged); + /// Bottom-right corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius RESET resetBottomRightRadius NOTIFY bottomRightRadiusChanged); + // clang-format on /// Regions to apply on top of this region. /// @@ -91,6 +114,25 @@ public: void setItem(QQuickItem* item); + [[nodiscard]] qint32 radius() const; + void setRadius(qint32 radius); + + [[nodiscard]] qint32 topLeftRadius() const; + void setTopLeftRadius(qint32 radius); + void resetTopLeftRadius(); + + [[nodiscard]] qint32 topRightRadius() const; + void setTopRightRadius(qint32 radius); + void resetTopRightRadius(); + + [[nodiscard]] qint32 bottomLeftRadius() const; + void setBottomLeftRadius(qint32 radius); + void resetBottomLeftRadius(); + + [[nodiscard]] qint32 bottomRightRadius() const; + void setBottomRightRadius(qint32 radius); + void resetBottomRightRadius(); + QQmlListProperty regions(); [[nodiscard]] bool empty() const; @@ -109,6 +151,11 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); + void radiusChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); void childrenChanged(); /// Triggered when the region's geometry changes. @@ -130,12 +177,25 @@ private: static void regionsReplace(QQmlListProperty* prop, qsizetype i, PendingRegion* region); + enum CornerOverride : quint8 { + TopLeft = 0b1, + TopRight = 0b10, + BottomLeft = 0b100, + BottomRight = 0b1000, + }; + QQuickItem* mItem = nullptr; qint32 mX = 0; qint32 mY = 0; qint32 mWidth = 0; qint32 mHeight = 0; + qint32 mRadius = 0; + qint32 mTopLeftRadius = 0; + qint32 mTopRightRadius = 0; + qint32 mBottomLeftRadius = 0; + qint32 mBottomRightRadius = 0; + quint8 mCornerOverrides = 0; QList mRegions; }; diff --git a/src/wayland/background_effect/test/manual/background_effect.qml b/src/wayland/background_effect/test/manual/background_effect.qml index 8cb4e12..679cb01 100644 --- a/src/wayland/background_effect/test/manual/background_effect.qml +++ b/src/wayland/background_effect/test/manual/background_effect.qml @@ -38,10 +38,25 @@ FloatingWindow { to: 1000 value: 100 } + + component EdgeSlider: Slider { + from: -1 + to: 1000 + value: -1 + } + + EdgeSlider { id: topLeftSlider } + EdgeSlider { id: topRightSlider } + EdgeSlider { id: bottomLeftSlider } + EdgeSlider { id: bottomRightSlider } } BackgroundEffect.blurRegion: Region { item: enableBox.checked ? root.contentItem : null radius: radiusSlider.value == -1 ? undefined : radiusSlider.value + topLeftRadius: topLeftSlider.value == -1 ? undefined : topLeftSlider.value + topRightRadius: topRightSlider.value == -1 ? undefined : topRightSlider.value + bottomLeftRadius: bottomLeftSlider.value == -1 ? undefined : bottomLeftSlider.value + bottomRightRadius: bottomRightSlider.value == -1 ? undefined : bottomRightSlider.value } } From 08058326f04e9b5e55c903b3702405a8d3556ac6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 25 Mar 2026 00:16:36 -0700 Subject: [PATCH 201/226] core: reuse global pragma parsing js engine during scanning QJSEngine cleanup is not fast or clean and results in speed degradation over time if too many are destroyed. --- src/core/scan.cpp | 15 +++++++++++---- src/core/scan.hpp | 3 +++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 58da38c..3605c52 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -145,10 +145,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna QString overrideText; bool isOverridden = false; - auto pragmaEngine = QJSEngine(); - pragmaEngine.globalObject().setPrototype( - pragmaEngine.newQObject(new qs::scan::env::PreprocEnv()) - ); + auto& pragmaEngine = *QmlScanner::preprocEngine(); auto postError = [&, this](QString error) { this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum}); @@ -370,3 +367,13 @@ QPair QmlScanner::jsonToQml(const QJsonValue& value, int inden return qMakePair(QStringLiteral("var"), "null"); } } + +QJSEngine* QmlScanner::preprocEngine() { + static auto* engine = [] { + auto* engine = new QJSEngine(); + engine->globalObject().setPrototype(engine->newQObject(new qs::scan::env::PreprocEnv())); + return engine; + }(); + + return engine; +} diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 26034e1..7d807e1 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -42,4 +43,6 @@ private: bool scanQmlFile(const QString& path, bool& singleton, bool& internal); bool scanQmlJson(const QString& path); [[nodiscard]] static QPair jsonToQml(const QJsonValue& value, int indent = 0); + + static QJSEngine* preprocEngine(); }; From 308f1e249b178c394509341ba7ab49fc98b9c824 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 20:14:58 -0700 Subject: [PATCH 202/226] crash: unmask signals before reexec Signals were previously left masked before reexec, causing UB if a child were to crash again, instead of triggering the reporter. This might've been responsible for a number of unexplainable bugs. --- src/crash/handler.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 8f37085..045a148 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -58,6 +58,12 @@ void signalHandler( siginfo_t* /*info*/, // NOLINT (misc-include-cleaner) void* /*context*/ ) { + // NOLINTBEGIN (misc-include-cleaner) + sigset_t set; + sigfillset(&set); + sigprocmask(SIG_UNBLOCK, &set, nullptr); + // NOLINTEND + if (CrashInfo::INSTANCE.traceFd != -1) { auto traceBuffer = std::array(); auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1); @@ -79,13 +85,9 @@ void signalHandler( fail:; } + // TODO: coredump fork and crash reporter remain as zombies, fix auto coredumpPid = fork(); if (coredumpPid == 0) { - // NOLINTBEGIN (misc-include-cleaner) - sigset_t set; - sigfillset(&set); - sigprocmask(SIG_UNBLOCK, &set, nullptr); - // NOLINTEND raise(sig); _exit(-1); } @@ -131,7 +133,6 @@ void signalHandler( perror("Failed to fork and launch crash reporter.\n"); _exit(-1); } else if (pid == 0) { - // dup to remove CLOEXEC auto dumpFdStr = std::array(); auto logFdStr = std::array(); From 6ef86dd5aa3dec6fe7dbc8f51e08ad6d1b5c8cc0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 20:17:07 -0700 Subject: [PATCH 203/226] crash: run platform compat hooks in crash reporter init For some reason, QtWayland crashes we work around trigger in this path. This was previously masked when the crash reporter didn't unmask signals, as long as the original process crashed with SIGSEGV. --- src/core/plugin.cpp | 16 ++++++++++++++++ src/core/plugin.hpp | 2 ++ src/crash/main.cpp | 4 ++++ src/wayland/init.cpp | 3 ++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp index 0eb9a06..e6cd1bb 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -9,6 +9,18 @@ static QVector plugins; // NOLINT void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } +void QsEnginePlugin::preinitPluginsOnly() { + plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); + + std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) { + return b->dependencies().contains(a->name()); + }); + + for (QsEnginePlugin* plugin: plugins) { + plugin->preinit(); + } +} + void QsEnginePlugin::initPlugins() { plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); @@ -16,6 +28,10 @@ void QsEnginePlugin::initPlugins() { return b->dependencies().contains(a->name()); }); + for (QsEnginePlugin* plugin: plugins) { + plugin->preinit(); + } + for (QsEnginePlugin* plugin: plugins) { plugin->init(); } diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp index f0c14dc..f692e91 100644 --- a/src/core/plugin.hpp +++ b/src/core/plugin.hpp @@ -18,12 +18,14 @@ public: virtual QString name() { return QString(); } virtual QList dependencies() { return {}; } virtual bool applies() { return true; } + virtual void preinit() {} virtual void init() {} virtual void registerTypes() {} virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT virtual void onReload() {} static void registerPlugin(QsEnginePlugin& plugin); + static void preinitPluginsOnly(); static void initPlugins(); static void runConstructGeneration(EngineGeneration& generation); static void runOnReload(); diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 30cf94d..6533b43 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -25,6 +25,7 @@ #include "../core/logging.hpp" #include "../core/logging_p.hpp" #include "../core/paths.hpp" +#include "../core/plugin.hpp" #include "../core/ringbuf.hpp" #include "interface.hpp" @@ -238,6 +239,9 @@ void qsCheckCrash(int argc, char** argv) { qCInfo(logCrashReporter) << "Starting crash reporter..."; + // Required platform compatibility hooks + QsEnginePlugin::preinitPluginsOnly(); + recordCrashInfo(crashDir, info.instance); auto gui = CrashReporterGui(crashDir.path(), crashProc); diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 790cebb..579e42a 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -33,8 +33,9 @@ class WaylandPlugin: public QsEnginePlugin { return isWayland; } + void preinit() override { installWlProxySafeDeref(); } + void init() override { - installWlProxySafeDeref(); installPlatformMenuHook(); installPopupPositioner(); } From 313f4e47f6f3d7204586721b2fbd0a54d542a84c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 20:25:58 -0700 Subject: [PATCH 204/226] core: track XDG_CURRENT_DESKTOP in debuginfo --- src/core/debuginfo.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp index ae227f8..abc467d 100644 --- a/src/core/debuginfo.cpp +++ b/src/core/debuginfo.cpp @@ -129,12 +129,13 @@ QString envInfo() { auto stream = QTextStream(&info); for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT - auto prefixes = std::array { + auto prefixes = std::array { "QS_", "QT_", "QML_", "QML2_", "QSG_", + "XDG_CURRENT_DESKTOP=", }; for (const auto& prefix: prefixes) { From 9bf752ac33b2181356d33251c3b1b4dedde0bbc6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 23:07:37 -0700 Subject: [PATCH 205/226] crash: add std::terminate handler --- src/crash/handler.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 045a148..33506a6 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -156,6 +158,21 @@ void signalHandler( } } +void handleCppTerminate() { + if (auto ptr = std::current_exception()) { + try { + std::rethrow_exception(ptr); + } catch (std::exception& e) { + qFatal().nospace() << "Terminate called with C++ exception (" + << cpptrace::demangle(typeid(e).name()).data() << "): " << e.what(); + } catch (...) { + qFatal() << "Terminate called with non exception object"; + } + } + + qFatal() << "Terminate called without active C++ exception"; +} + } // namespace void CrashHandler::init() { @@ -204,6 +221,8 @@ void CrashHandler::init() { // NOLINTEND (misc-include-cleaner) + std::set_terminate(&handleCppTerminate); + qCInfo(logCrashHandler) << "Crash handler initialized."; } From ee1100eb98d5033d8d4b76bf9fb0e720fec4c191 Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Sun, 29 Mar 2026 09:31:28 +0200 Subject: [PATCH 206/226] wayland/buffer: drop unused GLESv3 include MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That one is often in a separate Mesa package and contrary to GLESv2 doesn’t come with a pkg-config file. Mildly annoying… --- src/wayland/buffer/dmabuf.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index ed9dbeb..47462fb 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include From b83c39c8afd58c86af1c49a7c0e081b30c86d823 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 30 Mar 2026 21:41:10 -0700 Subject: [PATCH 207/226] services/pipewire: add -fno-strict-overflow to fix PCH with pipewire Unclear how this should be handled long term. --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1226342..4ed8374 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,8 +87,9 @@ include(cmake/util.cmake) add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension) -# pipewire defines this, breaking PCH +# pipewire defines these, breaking PCH add_compile_definitions(_REENTRANT) +add_compile_options(-fno-strict-overflow) if (FRAME_POINTERS) add_compile_options(-fno-omit-frame-pointer) From d6122277409783377059be6399004eb090a6008e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:21:20 +1100 Subject: [PATCH 208/226] core/qmlglobal: add shellId, instanceId, appId and launchTime props --- src/core/qmlglobal.cpp | 17 +++++++++++++++++ src/core/qmlglobal.hpp | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 35504f6..6cac3aa 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -26,6 +26,7 @@ #include "../io/processcore.hpp" #include "generation.hpp" #include "iconimageprovider.hpp" +#include "instanceinfo.hpp" #include "paths.hpp" #include "qmlscreen.hpp" #include "rootwrapper.hpp" @@ -153,6 +154,22 @@ qint32 QuickshellGlobal::processId() const { // NOLINT return getpid(); } +QString QuickshellGlobal::instanceId() const { // NOLINT + return InstanceInfo::CURRENT.instanceId; +} + +QString QuickshellGlobal::shellId() const { // NOLINT + return InstanceInfo::CURRENT.shellId; +} + +QString QuickshellGlobal::appId() const { // NOLINT + return InstanceInfo::CURRENT.appId; +} + +QDateTime QuickshellGlobal::launchTime() const { // NOLINT + return InstanceInfo::CURRENT.launchTime; +} + qsizetype QuickshellGlobal::screensCount(QQmlListProperty* /*unused*/) { return QuickshellTracked::instance()->screens.size(); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 94b42f6..72055df 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -17,6 +17,7 @@ #include "../io/processcore.hpp" #include "doc.hpp" +#include "instanceinfo.hpp" #include "qmlscreen.hpp" ///! Accessor for some options under the Quickshell type. @@ -83,6 +84,21 @@ class QuickshellGlobal: public QObject { // clang-format off /// Quickshell's process id. Q_PROPERTY(qint32 processId READ processId CONSTANT); + /// A unique identifier for this Quickshell instance + Q_PROPERTY(QString instanceId READ instanceId CONSTANT) + /// The shell ID, used to differentiate between different shell configurations. + /// + /// Defaults to a stable value derived from the config path. + /// Can be overridden with `//@ pragma ShellId ` in the root qml file. + Q_PROPERTY(QString shellId READ shellId CONSTANT) + /// The desktop application ID. + /// + /// Defaults to `org.quickshell`. + /// Can be overridden with `//@ pragma AppId ` in the root qml file + /// or the `QS_APP_ID` environment variable. + Q_PROPERTY(QString appId READ appId CONSTANT) + /// The time at which this Quickshell instance was launched. + Q_PROPERTY(QDateTime launchTime READ launchTime CONSTANT) /// All currently connected screens. /// /// This property updates as connected screens change. @@ -149,6 +165,10 @@ class QuickshellGlobal: public QObject { public: [[nodiscard]] qint32 processId() const; + [[nodiscard]] QString instanceId() const; + [[nodiscard]] QString shellId() const; + [[nodiscard]] QString appId() const; + [[nodiscard]] QDateTime launchTime() const; QQmlListProperty screens(); From 92b336c80c563e04b215c532e512772abd6e17e7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 2 Apr 2026 03:21:25 -0700 Subject: [PATCH 209/226] tooling: ensure intercepts do not overwrite symlinks to cfg files Intercept-file writes could end up opening an existing vfs symlink back to the user's actual config instead of creating a new file in the vfs. --- src/core/toolsupport.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/toolsupport.cpp b/src/core/toolsupport.cpp index 8aa5ac9..585656e 100644 --- a/src/core/toolsupport.cpp +++ b/src/core/toolsupport.cpp @@ -177,6 +177,8 @@ void QmlToolingSupport::updateToolingFs( auto fileInfo = QFileInfo(path); if (!fileInfo.isFile()) continue; + if (scanner.fileIntercepts.contains(path)) continue; + auto spath = linkDir.filePath(name); auto sFileInfo = QFileInfo(spath); @@ -205,8 +207,10 @@ void QmlToolingSupport::updateToolingFs( } auto spath = linkDir.filePath(name); + QFile::remove(spath); + auto file = QFile(spath); - if (!file.open(QFile::ReadWrite | QFile::Text)) { + if (!file.open(QFile::ReadWrite | QFile::Text | QFile::NewOnly)) { qCCritical(logTooling) << "Failed to open injected file" << spath; continue; } From 20c691cdf1013899cd5bd790eb725594aceb9f49 Mon Sep 17 00:00:00 2001 From: Carson Powers Date: Sun, 1 Feb 2026 22:15:42 -0600 Subject: [PATCH 210/226] networking: add PSK, settings and connection status support --- CMakeLists.txt | 2 +- changelog/next.md | 2 +- src/network/CMakeLists.txt | 1 + src/network/device.cpp | 52 +- src/network/device.hpp | 109 ++-- src/network/enums.cpp | 86 ++++ src/network/enums.hpp | 154 ++++++ src/network/module.md | 2 + src/network/network.cpp | 90 +++- src/network/network.hpp | 213 +++++--- src/network/nm/CMakeLists.txt | 4 +- src/network/nm/accesspoint.hpp | 16 +- src/network/nm/active_connection.cpp | 68 +++ .../{connection.hpp => active_connection.hpp} | 54 +- src/network/nm/backend.cpp | 63 ++- src/network/nm/backend.hpp | 35 +- src/network/nm/connection.cpp | 151 ------ src/network/nm/dbus_types.cpp | 69 +++ src/network/nm/dbus_types.hpp | 37 +- src/network/nm/device.cpp | 58 ++- src/network/nm/device.hpp | 40 +- src/network/nm/enums.hpp | 158 ++++++ .../org.freedesktop.NetworkManager.Device.xml | 5 + ...top.NetworkManager.Settings.Connection.xml | 12 +- .../nm/org.freedesktop.NetworkManager.xml | 5 +- src/network/nm/settings.cpp | 227 ++++++++ src/network/nm/settings.hpp | 82 +++ src/network/nm/utils.cpp | 291 ++++++++++- src/network/nm/utils.hpp | 15 +- src/network/nm/wireless.cpp | 231 ++++++--- src/network/nm/wireless.hpp | 59 ++- src/network/test/manual/network.qml | 483 +++++++++++++----- src/network/wifi.cpp | 84 +-- src/network/wifi.hpp | 123 +---- 34 files changed, 2200 insertions(+), 881 deletions(-) create mode 100644 src/network/enums.cpp create mode 100644 src/network/enums.hpp create mode 100644 src/network/nm/active_connection.cpp rename src/network/nm/{connection.hpp => active_connection.hpp} (51%) delete mode 100644 src/network/nm/connection.cpp create mode 100644 src/network/nm/dbus_types.cpp create mode 100644 src/network/nm/settings.cpp create mode 100644 src/network/nm/settings.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ed8374..b02b3d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(quickshell VERSION "0.2.1" LANGUAGES CXX C) -set(UNRELEASED_FEATURES) +set(UNRELEASED_FEATURES "network.2") set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) diff --git a/changelog/next.md b/changelog/next.md index fc6d79e..024ab10 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -20,7 +20,7 @@ set shell id. - Added the ability to handle move and resize events to FloatingWindow. - Pipewire service now reconnects if pipewire dies or a protocol error occurs. - Added pipewire audio peak detection. -- Added initial support for network management. +- Added network management support. - Added support for grabbing focus from popup windows. - Added support for IPC signal listeners. - Added Quickshell version checking and version gated preprocessing. diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 6075040..03ef86a 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -4,6 +4,7 @@ qt_add_library(quickshell-network STATIC network.cpp device.cpp wifi.cpp + enums.cpp ) target_include_directories(quickshell-network PRIVATE diff --git a/src/network/device.cpp b/src/network/device.cpp index 22e3949..5679e8d 100644 --- a/src/network/device.cpp +++ b/src/network/device.cpp @@ -8,6 +8,7 @@ #include #include "../core/logcat.hpp" +#include "enums.hpp" namespace qs::network { @@ -15,49 +16,9 @@ namespace { QS_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); } // namespace -QString DeviceConnectionState::toString(DeviceConnectionState::Enum state) { - switch (state) { - case Unknown: return QStringLiteral("Unknown"); - case Connecting: return QStringLiteral("Connecting"); - case Connected: return QStringLiteral("Connected"); - case Disconnecting: return QStringLiteral("Disconnecting"); - case Disconnected: return QStringLiteral("Disconnected"); - default: return QStringLiteral("Unknown"); - } -} - -QString DeviceType::toString(DeviceType::Enum type) { - switch (type) { - case None: return QStringLiteral("None"); - case Wifi: return QStringLiteral("Wifi"); - default: return QStringLiteral("Unknown"); - } -} - -QString NMDeviceState::toString(NMDeviceState::Enum state) { - switch (state) { - case Unknown: return QStringLiteral("Unknown"); - case Unmanaged: return QStringLiteral("Not managed by NetworkManager"); - case Unavailable: return QStringLiteral("Unavailable"); - case Disconnected: return QStringLiteral("Disconnected"); - case Prepare: return QStringLiteral("Preparing to connect"); - case Config: return QStringLiteral("Connecting to a network"); - case NeedAuth: return QStringLiteral("Waiting for authentication"); - case IPConfig: return QStringLiteral("Requesting IPv4 and/or IPv6 addresses from the network"); - case IPCheck: - return QStringLiteral("Checking if further action is required for the requested connection"); - case Secondaries: - return QStringLiteral("Waiting for a required secondary connection to activate"); - case Activated: return QStringLiteral("Connected"); - case Deactivating: return QStringLiteral("Disconnecting"); - case Failed: return QStringLiteral("Failed to connect"); - default: return QStringLiteral("Unknown"); - }; -} - NetworkDevice::NetworkDevice(DeviceType::Enum type, QObject* parent): QObject(parent), mType(type) { this->bindableConnected().setBinding([this]() { - return this->bState == DeviceConnectionState::Connected; + return this->bState == ConnectionState::Connected; }); }; @@ -66,12 +27,17 @@ void NetworkDevice::setAutoconnect(bool autoconnect) { emit this->requestSetAutoconnect(autoconnect); } +void NetworkDevice::setNmManaged(bool managed) { + if (this->bNmManaged == managed) return; + emit this->requestSetNmManaged(managed); +} + void NetworkDevice::disconnect() { - if (this->bState == DeviceConnectionState::Disconnected) { + if (this->bState == ConnectionState::Disconnected) { qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; return; } - if (this->bState == DeviceConnectionState::Disconnecting) { + if (this->bState == ConnectionState::Disconnecting) { qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; return; } diff --git a/src/network/device.hpp b/src/network/device.hpp index f3807c2..8d914a1 100644 --- a/src/network/device.hpp +++ b/src/network/device.hpp @@ -6,76 +6,22 @@ #include #include +#include "../core/doc.hpp" +#include "enums.hpp" + namespace qs::network { -///! Connection state of a NetworkDevice. -class DeviceConnectionState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - Connecting = 1, - Connected = 2, - Disconnecting = 3, - Disconnected = 4, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(DeviceConnectionState::Enum state); -}; - -///! Type of network device. -class DeviceType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - None = 0, - Wifi = 1, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(DeviceType::Enum type); -}; - -///! NetworkManager-specific device state. -/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. -class NMDeviceState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - Unmanaged = 10, - Unavailable = 20, - Disconnected = 30, - Prepare = 40, - Config = 50, - NeedAuth = 60, - IPConfig = 70, - IPCheck = 80, - Secondaries = 90, - Activated = 100, - Deactivating = 110, - Failed = 120, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NMDeviceState::Enum state); -}; - ///! A network device. -/// When @@type is `Wifi`, the device is a @@WifiDevice, which can be used to scan for and connect to access points. +/// The @@type property may be used to determine if this device is a @@WifiDevice. class NetworkDevice: public QObject { Q_OBJECT; QML_ELEMENT; QML_UNCREATABLE("Devices can only be acquired through Network"); // clang-format off /// The device type. + /// + /// When the device type is `Wifi`, the device object is a @@WifiDevice which exposes wifi network + /// connection and scanning. Q_PROPERTY(DeviceType::Enum type READ type CONSTANT); /// The name of the device's control interface. Q_PROPERTY(QString name READ name NOTIFY nameChanged BINDABLE bindableName); @@ -84,10 +30,12 @@ class NetworkDevice: public QObject { /// True if the device is connected. Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); /// Connection state of the device. - Q_PROPERTY(qs::network::DeviceConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); - /// A more specific device state when the backend is NetworkManager. - Q_PROPERTY(qs::network::NMDeviceState::Enum nmState READ default NOTIFY nmStateChanged BINDABLE bindableNmState); - /// True if the device is allowed to autoconnect. + Q_PROPERTY(qs::network::ConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + /// True if the device is managed by NetworkManager. + /// + /// > [!WARNING] Only valid for the NetworkManager backend. + Q_PROPERTY(bool nmManaged READ nmManaged WRITE setNmManaged NOTIFY nmManagedChanged) + /// True if the device is allowed to autoconnect to a network. Q_PROPERTY(bool autoconnect READ autoconnect WRITE setAutoconnect NOTIFY autoconnectChanged); // clang-format on @@ -97,25 +45,28 @@ public: /// Disconnects the device and prevents it from automatically activating further connections. Q_INVOKABLE void disconnect(); - [[nodiscard]] DeviceType::Enum type() const { return this->mType; }; - QBindable bindableName() { return &this->bName; }; - [[nodiscard]] QString name() const { return this->bName; }; - QBindable bindableAddress() { return &this->bAddress; }; - QBindable bindableConnected() { return &this->bConnected; }; - QBindable bindableState() { return &this->bState; }; - QBindable bindableNmState() { return &this->bNmState; }; - [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; - QBindable bindableAutoconnect() { return &this->bAutoconnect; }; + [[nodiscard]] DeviceType::Enum type() const { return this->mType; } + QBindable bindableName() { return &this->bName; } + [[nodiscard]] QString name() const { return this->bName; } + QBindable bindableAddress() { return &this->bAddress; } + QBindable bindableConnected() { return &this->bConnected; } + QBindable bindableState() { return &this->bState; } + QBindable bindableNmManaged() { return &this->bNmManaged; } + [[nodiscard]] bool nmManaged() { return this->bNmManaged; } + void setNmManaged(bool managed); + QBindable bindableAutoconnect() { return &this->bAutoconnect; } + [[nodiscard]] bool autoconnect() { return this->bAutoconnect; } void setAutoconnect(bool autoconnect); signals: - void requestDisconnect(); - void requestSetAutoconnect(bool autoconnect); + QSDOC_HIDE void requestDisconnect(); + QSDOC_HIDE void requestSetAutoconnect(bool autoconnect); + QSDOC_HIDE void requestSetNmManaged(bool managed); void nameChanged(); void addressChanged(); void connectedChanged(); void stateChanged(); - void nmStateChanged(); + void nmManagedChanged(); void autoconnectChanged(); private: @@ -124,8 +75,8 @@ private: Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bName, &NetworkDevice::nameChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, QString, bAddress, &NetworkDevice::addressChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bConnected, &NetworkDevice::connectedChanged); - Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, DeviceConnectionState::Enum, bState, &NetworkDevice::stateChanged); - Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, NMDeviceState::Enum, bNmState, &NetworkDevice::nmStateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, ConnectionState::Enum, bState, &NetworkDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bNmManaged, &NetworkDevice::nmManagedChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkDevice, bool, bAutoconnect, &NetworkDevice::autoconnectChanged); // clang-format on }; diff --git a/src/network/enums.cpp b/src/network/enums.cpp new file mode 100644 index 0000000..2cf36c1 --- /dev/null +++ b/src/network/enums.cpp @@ -0,0 +1,86 @@ +#include "enums.hpp" + +#include + +namespace qs::network { + +QString NetworkConnectivity::toString(NetworkConnectivity::Enum conn) { + switch (conn) { + case Unknown: return QStringLiteral("Unknown"); + case None: return QStringLiteral("Not connected to a network"); + case Portal: return QStringLiteral("Connection intercepted by a captive portal"); + case Limited: return QStringLiteral("Partial internet connectivity"); + case Full: return QStringLiteral("Full internet connectivity"); + default: return QStringLiteral("Unknown"); + } +} + +QString NetworkBackendType::toString(NetworkBackendType::Enum type) { + switch (type) { + case NetworkBackendType::None: return "None"; + case NetworkBackendType::NetworkManager: return "NetworkManager"; + default: return "Unknown"; + } +} + +QString ConnectionState::toString(ConnectionState::Enum state) { + switch (state) { + case Unknown: return QStringLiteral("Unknown"); + case Connecting: return QStringLiteral("Connecting"); + case Connected: return QStringLiteral("Connected"); + case Disconnecting: return QStringLiteral("Disconnecting"); + case Disconnected: return QStringLiteral("Disconnected"); + default: return QStringLiteral("Unknown"); + } +} + +QString ConnectionFailReason::toString(ConnectionFailReason::Enum reason) { + switch (reason) { + case Unknown: return QStringLiteral("Unknown"); + case NoSecrets: return QStringLiteral("Secrets were required but not provided"); + case WifiClientDisconnected: return QStringLiteral("Wi-Fi supplicant diconnected"); + case WifiClientFailed: return QStringLiteral("Wi-Fi supplicant failed"); + case WifiAuthTimeout: return QStringLiteral("Wi-Fi connection took too long to authenticate"); + case WifiNetworkLost: return QStringLiteral("Wi-Fi network could not be found"); + default: return QStringLiteral("Unknown"); + } +} + +QString DeviceType::toString(DeviceType::Enum type) { + switch (type) { + case None: return QStringLiteral("None"); + case Wifi: return QStringLiteral("Wifi"); + default: return QStringLiteral("Unknown"); + } +} + +QString WifiSecurityType::toString(WifiSecurityType::Enum type) { + switch (type) { + case Unknown: return QStringLiteral("Unknown"); + case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); + case Sae: return QStringLiteral("WPA3"); + case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); + case Wpa2Psk: return QStringLiteral("WPA2"); + case WpaEap: return QStringLiteral("WPA Enterprise"); + case WpaPsk: return QStringLiteral("WPA"); + case StaticWep: return QStringLiteral("WEP"); + case DynamicWep: return QStringLiteral("Dynamic WEP"); + case Leap: return QStringLiteral("LEAP"); + case Owe: return QStringLiteral("OWE"); + case Open: return QStringLiteral("Open"); + default: return QStringLiteral("Unknown"); + } +} + +QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) { + switch (mode) { + case Unknown: return QStringLiteral("Unknown"); + case AdHoc: return QStringLiteral("Ad-Hoc"); + case Station: return QStringLiteral("Station"); + case AccessPoint: return QStringLiteral("Access Point"); + case Mesh: return QStringLiteral("Mesh"); + default: return QStringLiteral("Unknown"); + }; +} + +} // namespace qs::network diff --git a/src/network/enums.hpp b/src/network/enums.hpp new file mode 100644 index 0000000..49c28ce --- /dev/null +++ b/src/network/enums.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include +#include +#include + +namespace qs::network { + +///! The degree to which the host can reach the internet. +class NetworkConnectivity: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// Network connectivity is unknown. This means the connectivity checks are disabled or have not run yet. + Unknown = 0, + /// The host is not connected to any network. + None = 1, + /// The internet connection is hijacked by a captive portal gateway. + /// This indicates the shell should open a sandboxed web browser window for the purpose of authenticating to a gateway. + Portal = 2, + /// The host is connected to a network but does not appear to be able to reach the full internet. + Limited = 3, + /// The host is connected to a network and appears to be able to reach the full internet. + Full = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkConnectivity::Enum conn); +}; + +///! The backend supplying the Network service. +class NetworkBackendType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + NetworkManager = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkBackendType::Enum type); +}; + +///! The connection state of a device or network. +class ConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(ConnectionState::Enum state); +}; + +///! The reason a connection failed. +class ConnectionFailReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The connection failed for an unknown reason. + Unknown = 0, + /// Secrets were required, but not provided. + NoSecrets = 1, + /// The Wi-Fi supplicant disconnected. + WifiClientDisconnected = 2, + /// The Wi-Fi supplicant failed. + WifiClientFailed = 3, + /// The Wi-Fi connection took too long to authenticate. + WifiAuthTimeout = 4, + /// The Wi-Fi network could not be found. + WifiNetworkLost = 5, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(ConnectionFailReason::Enum reason); +}; + +///! Type of a @@NetworkDevice. +class DeviceType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + Wifi = 1, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(DeviceType::Enum type); +}; + +///! The security type of a @@WifiNetwork. +class WifiSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + Open = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiSecurityType::Enum type); +}; + +///! The 802.11 mode of a @@WifiDevice. +class WifiDeviceMode: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// The device is part of an Ad-Hoc network without a central access point. + AdHoc = 0, + /// The device is a station that can connect to networks. + Station = 1, + /// The device is a local hotspot/access point. + AccessPoint = 2, + /// The device is an 802.11s mesh point. + Mesh = 3, + /// The device mode is unknown. + Unknown = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode); +}; + +} // namespace qs::network diff --git a/src/network/module.md b/src/network/module.md index a0c8e64..91ff2f1 100644 --- a/src/network/module.md +++ b/src/network/module.md @@ -4,6 +4,8 @@ headers = [ "network.hpp", "device.hpp", "wifi.hpp", + "enums.hpp", + "nm/settings.hpp", ] ----- This module exposes Network management APIs provided by a supported network backend. diff --git a/src/network/network.cpp b/src/network/network.cpp index e325b05..e66ffa6 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -1,6 +1,7 @@ #include "network.hpp" #include +#include #include #include #include @@ -9,7 +10,9 @@ #include "../core/logcat.hpp" #include "device.hpp" +#include "enums.hpp" #include "nm/backend.hpp" +#include "nm/settings.hpp" namespace qs::network { @@ -17,25 +20,22 @@ namespace { QS_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); } // namespace -QString NetworkState::toString(NetworkState::Enum state) { - switch (state) { - case NetworkState::Connecting: return QStringLiteral("Connecting"); - case NetworkState::Connected: return QStringLiteral("Connected"); - case NetworkState::Disconnecting: return QStringLiteral("Disconnecting"); - case NetworkState::Disconnected: return QStringLiteral("Disconnected"); - default: return QStringLiteral("Unknown"); - } -} - Networking::Networking(QObject* parent): QObject(parent) { // Try to create the NetworkManager backend and bind to it. auto* nm = new NetworkManager(this); if (nm->isAvailable()) { + // clang-format off QObject::connect(nm, &NetworkManager::deviceAdded, this, &Networking::deviceAdded); QObject::connect(nm, &NetworkManager::deviceRemoved, this, &Networking::deviceRemoved); QObject::connect(this, &Networking::requestSetWifiEnabled, nm, &NetworkManager::setWifiEnabled); + QObject::connect(this, &Networking::requestSetConnectivityCheckEnabled, nm, &NetworkManager::setConnectivityCheckEnabled); + QObject::connect(this, &Networking::requestCheckConnectivity, nm, &NetworkManager::checkConnectivity); this->bindableWifiEnabled().setBinding([nm]() { return nm->wifiEnabled(); }); this->bindableWifiHardwareEnabled().setBinding([nm]() { return nm->wifiHardwareEnabled(); }); + this->bindableCanCheckConnectivity().setBinding([nm]() { return nm->connectivityCheckAvailable(); }); + this->bindableConnectivityCheckEnabled().setBinding([nm]() { return nm->connectivityCheckEnabled(); }); + this->bindableConnectivity().setBinding([nm]() { return static_cast(nm->connectivity()); }); + // clang-format on this->mBackend = nm; this->mBackendType = NetworkBackendType::NetworkManager; @@ -43,23 +43,89 @@ Networking::Networking(QObject* parent): QObject(parent) { } else { delete nm; } - qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; } +Networking* Networking::instance() { + static Networking* instance = new Networking(); // NOLINT + return instance; +} + void Networking::deviceAdded(NetworkDevice* dev) { this->mDevices.insertObject(dev); } void Networking::deviceRemoved(NetworkDevice* dev) { this->mDevices.removeObject(dev); } +void Networking::checkConnectivity() { + if (!this->bConnectivityCheckEnabled || !this->bCanCheckConnectivity) return; + emit this->requestCheckConnectivity(); +} + void Networking::setWifiEnabled(bool enabled) { if (this->bWifiEnabled == enabled) return; emit this->requestSetWifiEnabled(enabled); } +void Networking::setConnectivityCheckEnabled(bool enabled) { + if (this->bConnectivityCheckEnabled == enabled) return; + emit this->requestSetConnectivityCheckEnabled(enabled); +} + +NetworkingQml::NetworkingQml(QObject* parent): QObject(parent) { + // clang-format off + QObject::connect(Networking::instance(), &Networking::wifiEnabledChanged, this, &NetworkingQml::wifiEnabledChanged); + QObject::connect(Networking::instance(), &Networking::wifiHardwareEnabledChanged, this, &NetworkingQml::wifiHardwareEnabledChanged); + QObject::connect(Networking::instance(), &Networking::canCheckConnectivityChanged, this, &NetworkingQml::canCheckConnectivityChanged); + QObject::connect(Networking::instance(), &Networking::connectivityCheckEnabledChanged, this, &NetworkingQml::connectivityCheckEnabledChanged); + QObject::connect(Networking::instance(), &Networking::connectivityChanged, this, &NetworkingQml::connectivityChanged); + // clang-format on +} + +void NetworkingQml::checkConnectivity() { Networking::instance()->checkConnectivity(); } + Network::Network(QString name, QObject* parent): QObject(parent), mName(std::move(name)) { this->bStateChanging.setBinding([this] { auto state = this->bState.value(); - return state == NetworkState::Connecting || state == NetworkState::Disconnecting; + return state == ConnectionState::Connecting || state == ConnectionState::Disconnecting; }); }; +void Network::connect() { + if (this->bConnected) { + qCCritical(logNetwork) << this << "is already connected."; + return; + } + this->requestConnect(); +} + +void Network::connectWithSettings(NMSettings* settings) { + if (this->bConnected) { + qCCritical(logNetwork) << this << "is already connected."; + return; + } + if (this->bNmSettings.value().indexOf(settings) == -1) return; + this->requestConnectWithSettings(settings); +} + +void Network::disconnect() { + if (!this->bConnected) { + qCCritical(logNetwork) << this << "is not currently connected"; + return; + } + this->requestDisconnect(); +} + +void Network::forget() { this->requestForget(); } + +void Network::settingsAdded(NMSettings* settings) { + auto list = this->bNmSettings.value(); + if (list.contains(settings)) return; + list.append(settings); + this->bNmSettings = list; +} + +void Network::settingsRemoved(NMSettings* settings) { + auto list = this->bNmSettings.value(); + list.removeOne(settings); + this->bNmSettings = list; +} + } // namespace qs::network diff --git a/src/network/network.hpp b/src/network/network.hpp index 8af7c9d..f7734a2 100644 --- a/src/network/network.hpp +++ b/src/network/network.hpp @@ -6,43 +6,14 @@ #include #include +#include "../core/doc.hpp" #include "../core/model.hpp" #include "device.hpp" +#include "enums.hpp" +#include "nm/settings.hpp" namespace qs::network { -///! The connection state of a Network. -class NetworkState: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - Connecting = 1, - Connected = 2, - Disconnecting = 3, - Disconnected = 4, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NetworkState::Enum state); -}; - -///! The backend supplying the Network service. -class NetworkBackendType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - None = 0, - NetworkManager = 1, - }; - Q_ENUM(Enum); -}; - class NetworkBackend: public QObject { Q_OBJECT; @@ -53,15 +24,65 @@ protected: explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; }; +class Networking: public QObject { + Q_OBJECT; + +public: + static Networking* instance(); + + void checkConnectivity(); + + [[nodiscard]] ObjectModel* devices() { return &this->mDevices; } + [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; } + QBindable bindableWifiEnabled() { return &this->bWifiEnabled; } + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; } + void setWifiEnabled(bool enabled); + QBindable bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; } + QBindable bindableCanCheckConnectivity() { return &this->bCanCheckConnectivity; } + QBindable bindableConnectivityCheckEnabled() { return &this->bConnectivityCheckEnabled; } + [[nodiscard]] bool connectivityCheckEnabled() const { return this->bConnectivityCheckEnabled; } + void setConnectivityCheckEnabled(bool enabled); + QBindable bindableConnectivity() { return &this->bConnectivity; } + +signals: + void requestSetWifiEnabled(bool enabled); + void requestSetConnectivityCheckEnabled(bool enabled); + void requestCheckConnectivity(); + + void wifiEnabledChanged(); + void wifiHardwareEnabledChanged(); + void canCheckConnectivityChanged(); + void connectivityCheckEnabledChanged(); + void connectivityChanged(); + +private slots: + void deviceAdded(NetworkDevice* dev); + void deviceRemoved(NetworkDevice* dev); + +private: + explicit Networking(QObject* parent = nullptr); + + ObjectModel mDevices {this}; + NetworkBackend* mBackend = nullptr; + NetworkBackendType::Enum mBackendType = NetworkBackendType::None; + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bCanCheckConnectivity, &Networking::canCheckConnectivityChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bConnectivityCheckEnabled, &Networking::connectivityCheckEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(Networking, NetworkConnectivity::Enum, bConnectivity, &Networking::connectivityChanged); + // clang-format on +}; + ///! The Network service. /// An interface to a network backend (currently only NetworkManager), /// which can be used to view, configure, and connect to various networks. -class Networking: public QObject { +class NetworkingQml: public QObject { Q_OBJECT; + QML_NAMED_ELEMENT(Networking); QML_SINGLETON; - QML_ELEMENT; // clang-format off - /// A list of all network devices. + /// A list of all network devices. Networks are exposed through their respective devices. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); /// The backend being used to power the Network service. @@ -70,73 +91,143 @@ class Networking: public QObject { Q_PROPERTY(bool wifiEnabled READ wifiEnabled WRITE setWifiEnabled NOTIFY wifiEnabledChanged); /// State of the rfkill hardware block of all wireless devices. Q_PROPERTY(bool wifiHardwareEnabled READ default NOTIFY wifiHardwareEnabledChanged BINDABLE bindableWifiHardwareEnabled); + /// True if the @@backend supports connectivity checks. + Q_PROPERTY(bool canCheckConnectivity READ default NOTIFY canCheckConnectivityChanged BINDABLE bindableCanCheckConnectivity); + /// True if connectivity checking is enabled. + Q_PROPERTY(bool connectivityCheckEnabled READ connectivityCheckEnabled WRITE setConnectivityCheckEnabled NOTIFY connectivityCheckEnabledChanged); + /// The result of the last connectivity check. + /// + /// Connectivity checks may require additional configuration depending on your distro. + /// + /// > [!NOTE] This property can be used to determine if network access is restricted + /// > or gated behind a captive portal. + /// > + /// > If checking for captive portals, @@checkConnectivity() should be called after + /// > the portal is dismissed to update this property. + Q_PROPERTY(qs::network::NetworkConnectivity::Enum connectivity READ default NOTIFY connectivityChanged BINDABLE bindableConnectivity); // clang-format on public: - explicit Networking(QObject* parent = nullptr); + explicit NetworkingQml(QObject* parent = nullptr); - [[nodiscard]] ObjectModel* devices() { return &this->mDevices; }; - [[nodiscard]] NetworkBackendType::Enum backend() const { return this->mBackendType; }; - QBindable bindableWifiEnabled() { return &this->bWifiEnabled; }; - [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; - void setWifiEnabled(bool enabled); - QBindable bindableWifiHardwareEnabled() { return &this->bWifiHardwareEnabled; }; + /// Re-check the network connectivity state immediately. + /// > [!NOTE] This should be invoked after a user dismisses a web browser that was opened to authenticate via a captive portal. + Q_INVOKABLE static void checkConnectivity(); + + [[nodiscard]] static ObjectModel* devices() { + return Networking::instance()->devices(); + } + [[nodiscard]] static NetworkBackendType::Enum backend() { + return Networking::instance()->backend(); + } + [[nodiscard]] static bool wifiEnabled() { return Networking::instance()->wifiEnabled(); } + static void setWifiEnabled(bool enabled) { Networking::instance()->setWifiEnabled(enabled); } + [[nodiscard]] static QBindable bindableWifiHardwareEnabled() { + return Networking::instance()->bindableWifiHardwareEnabled(); + } + [[nodiscard]] static QBindable bindableWifiEnabled() { + return Networking::instance()->bindableWifiEnabled(); + } + [[nodiscard]] static QBindable bindableCanCheckConnectivity() { + return Networking::instance()->bindableCanCheckConnectivity(); + } + [[nodiscard]] static bool connectivityCheckEnabled() { + return Networking::instance()->connectivityCheckEnabled(); + } + static void setConnectivityCheckEnabled(bool enabled) { + Networking::instance()->setConnectivityCheckEnabled(enabled); + } + [[nodiscard]] static QBindable bindableConnectivity() { + return Networking::instance()->bindableConnectivity(); + } signals: - void requestSetWifiEnabled(bool enabled); void wifiEnabledChanged(); void wifiHardwareEnabledChanged(); - -private slots: - void deviceAdded(NetworkDevice* dev); - void deviceRemoved(NetworkDevice* dev); - -private: - ObjectModel mDevices {this}; - NetworkBackend* mBackend = nullptr; - NetworkBackendType::Enum mBackendType = NetworkBackendType::None; - // clang-format off - Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiEnabled, &Networking::wifiEnabledChanged); - Q_OBJECT_BINDABLE_PROPERTY(Networking, bool, bWifiHardwareEnabled, &Networking::wifiHardwareEnabledChanged); - // clang-format on + void canCheckConnectivityChanged(); + void connectivityCheckEnabledChanged(); + void connectivityChanged(); }; ///! A network. +/// A network. Networks derived from a @@WifiDevice are @@WifiNetwork instances. class Network: public QObject { Q_OBJECT; QML_ELEMENT; - QML_UNCREATABLE("BaseNetwork can only be aqcuired through network devices"); + QML_UNCREATABLE("Network can only be aqcuired through networking devices"); // clang-format off /// The name of the network. Q_PROPERTY(QString name READ name CONSTANT); + /// A list of NetworkManager connnection settings profiles for this network. + /// + /// > [!WARNING] Only valid for the NetworkManager backend. + Q_PROPERTY(QList nmSettings READ nmSettings NOTIFY nmSettingsChanged BINDABLE bindableNmSettings); /// True if the network is connected. Q_PROPERTY(bool connected READ default NOTIFY connectedChanged BINDABLE bindableConnected); + /// True if the wifi network has known connection settings saved. + Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); /// The connectivity state of the network. - Q_PROPERTY(NetworkState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); + Q_PROPERTY(ConnectionState::Enum state READ default NOTIFY stateChanged BINDABLE bindableState); /// If the network is currently connecting or disconnecting. Shorthand for checking @@state. Q_PROPERTY(bool stateChanging READ default NOTIFY stateChangingChanged BINDABLE bindableStateChanging); // clang-format on public: explicit Network(QString name, QObject* parent = nullptr); + /// Attempt to connect to the network. + /// + /// > [!NOTE] If the network is a @@WifiNetwork and requires secrets, a @@connectionFailed(s) + /// > signal will be emitted with `NoSecrets`. + /// > @@WifiNetwork.connectWithPsk() can be used to provide secrets. + Q_INVOKABLE void connect(); + /// Attempt to connect to the network with a specific @@nmSettings entry. + /// + /// > [!WARNING] Only valid for the NetworkManager backend. + Q_INVOKABLE void connectWithSettings(NMSettings* settings); + /// Disconnect from the network. + Q_INVOKABLE void disconnect(); + /// Forget all connection settings for this network. + Q_INVOKABLE void forget(); - [[nodiscard]] QString name() const { return this->mName; }; + void settingsAdded(NMSettings* settings); + void settingsRemoved(NMSettings* settings); + + // clang-format off + [[nodiscard]] QString name() const { return this->mName; } + [[nodiscard]] const QList& nmSettings() const { return this->bNmSettings; } + QBindable> bindableNmSettings() const { return &this->bNmSettings; } QBindable bindableConnected() { return &this->bConnected; } - QBindable bindableState() { return &this->bState; } + QBindable bindableKnown() { return &this->bKnown; } + [[nodiscard]] ConnectionState::Enum state() const { return this->bState; } + QBindable bindableState() { return &this->bState; } QBindable bindableStateChanging() { return &this->bStateChanging; } + // clang-format on signals: + /// Signals that a connection to the network has failed because of the given @@ConnectionFailReason. + void connectionFailed(ConnectionFailReason::Enum reason); + void connectedChanged(); + void knownChanged(); void stateChanged(); void stateChangingChanged(); + void nmSettingsChanged(); + QSDOC_HIDE void requestConnect(); + QSDOC_HIDE void requestConnectWithSettings(NMSettings* settings); + QSDOC_HIDE void requestDisconnect(); + QSDOC_HIDE void requestForget(); protected: QString mName; + // clang-format off Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bConnected, &Network::connectedChanged); - Q_OBJECT_BINDABLE_PROPERTY(Network, NetworkState::Enum, bState, &Network::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bKnown, &Network::knownChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, ConnectionState::Enum, bState, &Network::stateChanged); Q_OBJECT_BINDABLE_PROPERTY(Network, bool, bStateChanging, &Network::stateChangingChanged); + Q_OBJECT_BINDABLE_PROPERTY(Network, QList, bNmSettings, &Network::nmSettingsChanged); + // clang-format on }; } // namespace qs::network diff --git a/src/network/nm/CMakeLists.txt b/src/network/nm/CMakeLists.txt index bb8635e..61f7e66 100644 --- a/src/network/nm/CMakeLists.txt +++ b/src/network/nm/CMakeLists.txt @@ -63,10 +63,12 @@ qt_add_dbus_interface(NM_DBUS_INTERFACES qt_add_library(quickshell-network-nm STATIC backend.cpp device.cpp - connection.cpp + active_connection.cpp + settings.cpp accesspoint.cpp wireless.cpp utils.cpp + dbus_types.cpp enums.hpp ${NM_DBUS_INTERFACES} ) diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp index 8409089..63e35ee 100644 --- a/src/network/nm/accesspoint.hpp +++ b/src/network/nm/accesspoint.hpp @@ -48,14 +48,14 @@ public: [[nodiscard]] bool isValid() const; [[nodiscard]] QString path() const; [[nodiscard]] QString address() const; - [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; - [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; - [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; - [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; - [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; - [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; - [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; - [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; } + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; } + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; } + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; } + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; } + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; } + [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; } + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; } signals: void loaded(); diff --git a/src/network/nm/active_connection.cpp b/src/network/nm/active_connection.cpp new file mode 100644 index 0000000..cab0e52 --- /dev/null +++ b/src/network/nm/active_connection.cpp @@ -0,0 +1,68 @@ +#include "active_connection.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_nm_active_connection.h" +#include "enums.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::loaded, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { + auto enumReason = static_cast(reason); + if (this->bStateReason == enumReason) return; + this->bStateReason = enumReason; +} + +bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnection::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/connection.hpp b/src/network/nm/active_connection.hpp similarity index 51% rename from src/network/nm/connection.hpp rename to src/network/nm/active_connection.hpp index 4f126c8..33426a1 100644 --- a/src/network/nm/connection.hpp +++ b/src/network/nm/active_connection.hpp @@ -1,18 +1,16 @@ #pragma once +#include #include #include -#include #include #include -#include +#include +#include #include #include "../../dbus/properties.hpp" -#include "../wifi.hpp" #include "dbus_nm_active_connection.h" -#include "dbus_nm_connection_settings.h" -#include "dbus_types.hpp" #include "enums.hpp" namespace qs::dbus { @@ -28,40 +26,6 @@ struct DBusDataTransform { namespace qs::network { -// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. -class NMConnectionSettings: public QObject { - Q_OBJECT; - -public: - explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr); - - void forget(); - - [[nodiscard]] bool isValid() const; - [[nodiscard]] QString path() const; - [[nodiscard]] QString address() const; - [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; - [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; - [[nodiscard]] QBindable bindableSecurity() { return &this->bSecurity; }; - -signals: - void loaded(); - void settingsChanged(ConnectionSettingsMap settings); - void securityChanged(WifiSecurityType::Enum security); - void ssidChanged(QString ssid); - -private: - bool mLoaded = false; - void updateSettings(); - // clang-format off - Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged); - Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, WifiSecurityType::Enum, bSecurity, &NMConnectionSettings::securityChanged); - QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties); - // clang-format on - - DBusNMConnectionSettingsProxy* proxy = nullptr; -}; - // Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object. class NMActiveConnection: public QObject { Q_OBJECT; @@ -72,31 +36,27 @@ public: [[nodiscard]] bool isValid() const; [[nodiscard]] QString path() const; [[nodiscard]] QString address() const; - [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; - [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; - [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; } + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; } + [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->bStateReason; } signals: void loaded(); void connectionChanged(QDBusObjectPath path); void stateChanged(NMConnectionState::Enum state); void stateReasonChanged(NMConnectionStateReason::Enum reason); - void uuidChanged(const QString& uuid); private slots: void onStateChanged(quint32 state, quint32 reason); private: - NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown; - // clang-format off Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged); - Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged); Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionStateReason::Enum, bStateReason, &NMActiveConnection::stateReasonChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties); QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection"); - QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid"); QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State"); // clang-format on DBusNMActiveConnectionProxy* proxy = nullptr; diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp index 4b61e33..a46ccb2 100644 --- a/src/network/nm/backend.cpp +++ b/src/network/nm/backend.cpp @@ -1,5 +1,6 @@ #include "backend.hpp" +#include #include #include #include @@ -15,6 +16,7 @@ #include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" #include "../device.hpp" +#include "../enums.hpp" #include "../network.hpp" #include "../wifi.hpp" #include "dbus_nm_backend.h" @@ -31,7 +33,8 @@ QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWa } NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { - qDBusRegisterMetaType(); + qCDebug(logNetworkManager) << "Connecting to NetworkManager"; + qDBusRegisterMetaType(); auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { @@ -69,6 +72,23 @@ void NetworkManager::init() { this->registerDevices(); } +void NetworkManager::checkConnectivity() { + auto pending = this->proxy->CheckConnectivity(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCInfo(logNetworkManager) << "Failed to check connectivity: " << reply.error().message(); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + void NetworkManager::registerDevices() { auto pending = this->proxy->GetAllDevices(); auto* call = new QDBusPendingCallWatcher(pending, this); @@ -117,23 +137,21 @@ void NetworkManager::registerDevice(const QString& path) { } if (dev) { + qCDebug(logNetworkManager) << "Device added:" << path; if (!dev->isValid()) { qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; delete dev; } else { this->mDevices[path] = dev; - // Only register a frontend device while it's managed by NM. - auto onManagedChanged = [this, dev, type](bool managed) { - managed ? this->registerFrontendDevice(type, dev) : this->removeFrontendDevice(dev); - }; // clang-format off QObject::connect(dev, &NMDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection); QObject::connect(dev, &NMDevice::activateConnection, this, &NetworkManager::activateConnection); - QObject::connect(dev, &NMDevice::managedChanged, this, onManagedChanged); // clang-format on - if (dev->managed()) this->registerFrontendDevice(type, dev); + this->registerFrontendDevice(type, dev); } + } else { + qCDebug(logNetworkManager) << "Ignoring registration of unsupported device:" << path; } temp->deleteLater(); } @@ -173,21 +191,22 @@ void NetworkManager::registerFrontendDevice(NMDeviceType::Enum type, NMDevice* d // Bind generic NetworkDevice properties auto translateState = [dev]() { switch (dev->state()) { - case 0 ... 20: return DeviceConnectionState::Unknown; - case 30: return DeviceConnectionState::Disconnected; - case 40 ... 90: return DeviceConnectionState::Connecting; - case 100: return DeviceConnectionState::Connected; - case 110 ... 120: return DeviceConnectionState::Disconnecting; + case 0 ... 20: return ConnectionState::Unknown; + case 30: return ConnectionState::Disconnected; + case 40 ... 90: return ConnectionState::Connecting; + case 100: return ConnectionState::Connected; + case 110 ... 120: return ConnectionState::Disconnecting; } }; // clang-format off frontendDev->bindableName().setBinding([dev]() { return dev->interface(); }); frontendDev->bindableAddress().setBinding([dev]() { return dev->hwAddress(); }); - frontendDev->bindableNmState().setBinding([dev]() { return dev->state(); }); frontendDev->bindableState().setBinding(translateState); frontendDev->bindableAutoconnect().setBinding([dev]() { return dev->autoconnect(); }); + frontendDev->bindableNmManaged().setBinding([dev]() { return dev->managed(); }); QObject::connect(frontendDev, &WifiDevice::requestDisconnect, dev, &NMDevice::disconnect); QObject::connect(frontendDev, &NetworkDevice::requestSetAutoconnect, dev, &NMDevice::setAutoconnect); + QObject::connect(frontendDev, &NetworkDevice::requestSetNmManaged, dev, &NMDevice::setManaged); // clang-format on this->mFrontendDevices.insert(dev->path(), frontendDev); @@ -215,6 +234,7 @@ void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) { auto* dev = iter.value(); this->mDevices.erase(iter); if (dev) { + qCDebug(logNetworkManager) << "Device removed:" << path.path(); this->removeFrontendDevice(dev); delete dev; } @@ -240,7 +260,7 @@ void NetworkManager::activateConnection( } void NetworkManager::addAndActivateConnection( - const ConnectionSettingsMap& settings, + const NMSettingsMap& settings, const QDBusObjectPath& devPath, const QDBusObjectPath& specificObjectPath ) { @@ -259,6 +279,12 @@ void NetworkManager::addAndActivateConnection( QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } +void NetworkManager::setConnectivityCheckEnabled(bool enabled) { + if (enabled == this->bConnectivityCheckEnabled) return; + this->bConnectivityCheckEnabled = enabled; + this->pConnectivityCheckEnabled.write(); +} + void NetworkManager::setWifiEnabled(bool enabled) { if (enabled == this->bWifiEnabled) return; this->bWifiEnabled = enabled; @@ -268,3 +294,12 @@ void NetworkManager::setWifiEnabled(bool enabled) { bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; } // namespace qs::network + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp index 471f57a..2825a17 100644 --- a/src/network/nm/backend.hpp +++ b/src/network/nm/backend.hpp @@ -10,7 +10,20 @@ #include "../../dbus/properties.hpp" #include "../network.hpp" #include "dbus_nm_backend.h" +#include "dbus_types.hpp" #include "device.hpp" +#include "enums.hpp" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMConnectivityState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus namespace qs::network { @@ -21,24 +34,34 @@ public: explicit NetworkManager(QObject* parent = nullptr); [[nodiscard]] bool isAvailable() const override; - [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; }; - [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; }; + [[nodiscard]] bool wifiEnabled() const { return this->bWifiEnabled; } + [[nodiscard]] bool wifiHardwareEnabled() const { return this->bWifiHardwareEnabled; } + [[nodiscard]] bool connectivityCheckAvailable() const { + return this->bConnectivityCheckAvailable; + }; + [[nodiscard]] bool connectivityCheckEnabled() const { return this->bConnectivityCheckEnabled; } + [[nodiscard]] NMConnectivityState::Enum connectivity() const { return this->bConnectivity; } signals: void deviceAdded(NetworkDevice* device); void deviceRemoved(NetworkDevice* device); void wifiEnabledChanged(bool enabled); void wifiHardwareEnabledChanged(bool enabled); + void connectivityStateChanged(NMConnectivityState::Enum state); + void connectivityCheckAvailableChanged(bool available); + void connectivityCheckEnabledChanged(bool enabled); public slots: void setWifiEnabled(bool enabled); + void setConnectivityCheckEnabled(bool enabled); + void checkConnectivity(); private slots: void onDevicePathAdded(const QDBusObjectPath& path); void onDevicePathRemoved(const QDBusObjectPath& path); void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); void addAndActivateConnection( - const ConnectionSettingsMap& settings, + const NMSettingsMap& settings, const QDBusObjectPath& devPath, const QDBusObjectPath& specificObjectPath ); @@ -56,10 +79,16 @@ private: // clang-format off Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged); Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, NMConnectivityState::Enum, bConnectivity, &NetworkManager::connectivityStateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bConnectivityCheckAvailable, &NetworkManager::connectivityCheckAvailableChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bConnectivityCheckEnabled, &NetworkManager::connectivityCheckEnabledChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled"); QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pConnectivity, bConnectivity, dbusProperties, "Connectivity"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pConnectivityCheckAvailable, bConnectivityCheckAvailable, dbusProperties, "ConnectivityCheckAvailable"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pConnectivityCheckEnabled, bConnectivityCheckEnabled, dbusProperties, "ConnectivityCheckEnabled"); // clang-format on DBusNetworkManagerProxy* proxy = nullptr; }; diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp deleted file mode 100644 index 39b6f66..0000000 --- a/src/network/nm/connection.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "connection.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../core/logcat.hpp" -#include "../../dbus/properties.hpp" -#include "../wifi.hpp" -#include "dbus_nm_active_connection.h" -#include "dbus_nm_connection_settings.h" -#include "dbus_types.hpp" -#include "enums.hpp" -#include "utils.hpp" - -namespace qs::network { -using namespace qs::dbus; - -namespace { -QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); -} - -NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) { - qDBusRegisterMetaType(); - - this->proxy = new DBusNMConnectionSettingsProxy( - "org.freedesktop.NetworkManager", - path, - QDBusConnection::systemBus(), - this - ); - - if (!this->proxy->isValid()) { - qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; - return; - } - - QObject::connect( - this->proxy, - &DBusNMConnectionSettingsProxy::Updated, - this, - &NMConnectionSettings::updateSettings - ); - this->bSecurity.setBinding([this]() { return securityFromConnectionSettings(this->bSettings); }); - - this->connectionSettingsProperties.setInterface(this->proxy); - this->connectionSettingsProperties.updateAllViaGetAll(); - - this->updateSettings(); -} - -void NMConnectionSettings::updateSettings() { - auto pending = this->proxy->GetSettings(); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logNetworkManager) - << "Failed to get" << this->path() << "settings:" << reply.error().message(); - } else { - this->bSettings = reply.value(); - } - - if (!this->mLoaded) { - emit this->loaded(); - this->mLoaded = true; - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -void NMConnectionSettings::forget() { - auto pending = this->proxy->Delete(); - auto* call = new QDBusPendingCallWatcher(pending, this); - - auto responseCallback = [this](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logNetworkManager) - << "Failed to forget" << this->path() << ":" << reply.error().message(); - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); -} - -bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); } -QString NMConnectionSettings::address() const { - return this->proxy ? this->proxy->service() : QString(); -} -QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } - -NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { - this->proxy = new DBusNMActiveConnectionProxy( - "org.freedesktop.NetworkManager", - path, - QDBusConnection::systemBus(), - this - ); - - if (!this->proxy->isValid()) { - qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; - return; - } - - // clang-format off - QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, &NMActiveConnection::loaded, Qt::SingleShotConnection); - QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); - // clang-format on - - this->activeConnectionProperties.setInterface(this->proxy); - this->activeConnectionProperties.updateAllViaGetAll(); -} - -void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { - auto enumReason = static_cast(reason); - if (this->mStateReason == enumReason) return; - this->mStateReason = enumReason; - emit this->stateReasonChanged(enumReason); -} - -bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } -QString NMActiveConnection::address() const { - return this->proxy ? this->proxy->service() : QString(); -} -QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } - -} // namespace qs::network - -namespace qs::dbus { - -DBusResult -DBusDataTransform::fromWire(quint32 wire) { - return DBusResult(static_cast(wire)); -} - -} // namespace qs::dbus diff --git a/src/network/nm/dbus_types.cpp b/src/network/nm/dbus_types.cpp new file mode 100644 index 0000000..e161f11 --- /dev/null +++ b/src/network/nm/dbus_types.cpp @@ -0,0 +1,69 @@ +#include "dbus_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qs::network { + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMSettingsMap& map) { + argument.beginMap(); + while (!argument.atEnd()) { + argument.beginMapEntry(); + QString groupName; + argument >> groupName; + + QVariantMap group; + argument >> group; + + map.insert(groupName, group); + argument.endMapEntry(); + } + argument.endMap(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const NMSettingsMap& map) { + argument.beginMap(qMetaTypeId(), qMetaTypeId()); + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + argument.beginMapEntry(); + argument << it.key(); + argument << it.value(); + argument.endMapEntry(); + } + argument.endMap(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMIPv6Address& addr) { + argument.beginStructure(); + argument >> addr.address >> addr.prefix >> addr.gateway; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const NMIPv6Address& addr) { + argument.beginStructure(); + argument << addr.address << addr.prefix << addr.gateway; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMIPv6Route& route) { + argument.beginStructure(); + argument >> route.destination >> route.prefix >> route.nexthop >> route.metric; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const NMIPv6Route& route) { + argument.beginStructure(); + argument << route.destination << route.prefix << route.nexthop << route.metric; + argument.endStructure(); + return argument; +} + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp index dadbcf3..bf428e5 100644 --- a/src/network/nm/dbus_types.hpp +++ b/src/network/nm/dbus_types.hpp @@ -1,9 +1,40 @@ #pragma once -#include +#include +#include #include #include +#include #include -using ConnectionSettingsMap = QMap; -Q_DECLARE_METATYPE(ConnectionSettingsMap); +namespace qs::network { + +using NMSettingsMap = QMap; + +const QDBusArgument& operator>>(const QDBusArgument& argument, NMSettingsMap& map); +const QDBusArgument& operator<<(QDBusArgument& argument, const NMSettingsMap& map); + +struct NMIPv6Address { + QByteArray address; + quint32 prefix = 0; + QByteArray gateway; +}; + +const QDBusArgument& operator>>(const QDBusArgument& argument, qs::network::NMIPv6Address& addr); +const QDBusArgument& operator<<(QDBusArgument& argument, const qs::network::NMIPv6Address& addr); + +struct NMIPv6Route { + QByteArray destination; + quint32 prefix = 0; + QByteArray nexthop; + quint32 metric = 0; +}; + +const QDBusArgument& operator>>(const QDBusArgument& argument, qs::network::NMIPv6Route& route); +const QDBusArgument& operator<<(QDBusArgument& argument, const qs::network::NMIPv6Route& route); + +} // namespace qs::network + +Q_DECLARE_METATYPE(qs::network::NMSettingsMap); +Q_DECLARE_METATYPE(qs::network::NMIPv6Address); +Q_DECLARE_METATYPE(qs::network::NMIPv6Route); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp index aad565d..1f229c8 100644 --- a/src/network/nm/device.cpp +++ b/src/network/nm/device.cpp @@ -14,9 +14,10 @@ #include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" -#include "../device.hpp" -#include "connection.hpp" +#include "active_connection.hpp" #include "dbus_nm_device.h" +#include "enums.hpp" +#include "settings.hpp" namespace qs::network { using namespace qs::dbus; @@ -39,19 +40,29 @@ NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) { } // clang-format off - QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged); + QObject::connect(this, &NMDevice::availableSettingsPathsChanged, this, &NMDevice::onAvailableSettingsPathsChanged); QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged); + QObject::connect(this->deviceProxy, &DBusNMDeviceProxy::StateChanged, this, &NMDevice::onStateChanged); // clang-format on this->deviceProperties.setInterface(this->deviceProxy); this->deviceProperties.updateAllViaGetAll(); } +void NMDevice::onStateChanged(quint32 newState, quint32 /*oldState*/, quint32 reason) { + auto enumReason = static_cast(reason); + auto enumNewState = static_cast(newState); + if (enumNewState == NMDeviceState::Failed) this->bLastFailReason = enumReason; + if (this->bStateReason == enumReason) return; + this->bStateReason = enumReason; +} + void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { const QString stringPath = path.path(); // Remove old active connection if (this->mActiveConnection) { + qCDebug(logNetworkManager) << "Active connection removed:" << this->mActiveConnection->path(); QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); delete this->mActiveConnection; this->mActiveConnection = nullptr; @@ -64,6 +75,7 @@ void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { qCWarning(logNetworkManager) << "Ignoring invalid registration of" << stringPath; delete active; } else { + qCDebug(logNetworkManager) << "Active connection added:" << stringPath; this->mActiveConnection = active; QObject::connect( active, @@ -76,42 +88,44 @@ void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { } } -void NMDevice::onAvailableConnectionPathsChanged(const QList& paths) { +void NMDevice::onAvailableSettingsPathsChanged(const QList& paths) { QSet newPathSet; for (const QDBusObjectPath& path: paths) { newPathSet.insert(path.path()); } - const auto existingPaths = this->mConnections.keys(); + const auto existingPaths = this->mSettings.keys(); const QSet existingPathSet(existingPaths.begin(), existingPaths.end()); - const auto addedConnections = newPathSet - existingPathSet; - const auto removedConnections = existingPathSet - newPathSet; + const auto addedSettings = newPathSet - existingPathSet; + const auto removedSettings = existingPathSet - newPathSet; - for (const QString& path: addedConnections) { - this->registerConnection(path); + for (const QString& path: addedSettings) { + this->registerSettings(path); } - for (const QString& path: removedConnections) { - auto* connection = this->mConnections.take(path); + for (const QString& path: removedSettings) { + auto* connection = this->mSettings.take(path); if (!connection) { qCDebug(logNetworkManager) << "Sent removal signal for" << path << "which is not registered."; } else { + qCDebug(logNetworkManager) << "Connection settings removed:" << path; delete connection; } }; } -void NMDevice::registerConnection(const QString& path) { - auto* connection = new NMConnectionSettings(path, this); - if (!connection->isValid()) { +void NMDevice::registerSettings(const QString& path) { + auto* settings = new NMSettings(path, this); + if (!settings->isValid()) { qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; - delete connection; + delete settings; } else { - this->mConnections.insert(path, connection); + qCDebug(logNetworkManager) << "Connection settings added:" << path; + this->mSettings.insert(path, settings); QObject::connect( - connection, - &NMConnectionSettings::loaded, + settings, + &NMSettings::loaded, this, - [this, connection]() { emit this->connectionLoaded(connection); }, + [this, settings]() { emit this->settingsLoaded(settings); }, Qt::SingleShotConnection ); } @@ -125,6 +139,12 @@ void NMDevice::setAutoconnect(bool autoconnect) { this->pAutoconnect.write(); } +void NMDevice::setManaged(bool managed) { + if (managed == this->bManaged) return; + this->bManaged = managed; + this->pManaged.write(); +} + bool NMDevice::isValid() const { return this->deviceProxy && this->deviceProxy->isValid(); } QString NMDevice::address() const { return this->deviceProxy ? this->deviceProxy->service() : QString(); diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp index e3ff4b9..963f574 100644 --- a/src/network/nm/device.hpp +++ b/src/network/nm/device.hpp @@ -8,8 +8,10 @@ #include #include "../../dbus/properties.hpp" -#include "connection.hpp" +#include "../enums.hpp" +#include "active_connection.hpp" #include "dbus_nm_device.h" +#include "settings.hpp" namespace qs::dbus { @@ -36,43 +38,49 @@ public: [[nodiscard]] virtual bool isValid() const; [[nodiscard]] QString path() const; [[nodiscard]] QString address() const; - [[nodiscard]] QString interface() const { return this->bInterface; }; - [[nodiscard]] QString hwAddress() const { return this->bHwAddress; }; - [[nodiscard]] bool managed() const { return this->bManaged; }; - [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; }; - [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; }; - [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; }; + [[nodiscard]] QString interface() const { return this->bInterface; } + [[nodiscard]] QString hwAddress() const { return this->bHwAddress; } + [[nodiscard]] bool managed() const { return this->bManaged; } + [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; } + [[nodiscard]] NMDeviceStateReason::Enum stateReason() const { return this->bStateReason; } + [[nodiscard]] NMDeviceStateReason::Enum lastFailReason() const { return this->bLastFailReason; } + [[nodiscard]] bool autoconnect() const { return this->bAutoconnect; } + [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; } signals: void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); void addAndActivateConnection( - const ConnectionSettingsMap& settings, + const NMSettingsMap& settings, const QDBusObjectPath& devPath, const QDBusObjectPath& apPath ); - void connectionLoaded(NMConnectionSettings* connection); - void connectionRemoved(NMConnectionSettings* connection); - void availableConnectionPathsChanged(QList paths); + void settingsLoaded(NMSettings* settings); + void settingsRemoved(NMSettings* settings); + void availableSettingsPathsChanged(QList paths); void activeConnectionPathChanged(const QDBusObjectPath& connection); void activeConnectionLoaded(NMActiveConnection* active); void interfaceChanged(const QString& interface); void hwAddressChanged(const QString& hwAddress); void managedChanged(bool managed); void stateChanged(NMDeviceState::Enum state); + void stateReasonChanged(NMDeviceStateReason::Enum reason); + void lastFailReasonChanged(NMDeviceStateReason::Enum reason); void autoconnectChanged(bool autoconnect); public slots: void disconnect(); void setAutoconnect(bool autoconnect); + void setManaged(bool managed); private slots: - void onAvailableConnectionPathsChanged(const QList& paths); + void onStateChanged(quint32 newState, quint32 oldState, quint32 reason); + void onAvailableSettingsPathsChanged(const QList& paths); void onActiveConnectionPathChanged(const QDBusObjectPath& path); private: - void registerConnection(const QString& path); + void registerSettings(const QString& path); - QHash mConnections; + QHash mSettings; NMActiveConnection* mActiveConnection = nullptr; // clang-format off @@ -80,8 +88,10 @@ private: Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bManaged, &NMDevice::managedChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceStateReason::Enum, bStateReason, &NMDevice::stateReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceStateReason::Enum, bLastFailReason, &NMDevice::lastFailReasonChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, bool, bAutoconnect, &NMDevice::autoconnectChanged); - Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableConnectionPathsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableSettingsPathsChanged); Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp index 34e5b65..18b1b8b 100644 --- a/src/network/nm/enums.hpp +++ b/src/network/nm/enums.hpp @@ -7,6 +7,20 @@ namespace qs::network { +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMConnectivityState +class NMConnectivityState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + Portal = 2, + Limited = 3, + Full = 4, + }; +}; + // Indicates the type of hardware represented by a device object. class NMDeviceType: public QObject { Q_OBJECT; @@ -52,6 +66,123 @@ public: Q_ENUM(Enum); }; +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); +}; + +// Device state change reason codes. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceStateReason. +class NMDeviceStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + None = 0, + Unknown = 1, + NowManaged = 2, + NowUnmanaged = 3, + ConfigFailed = 4, + IpConfigUnavailable = 5, + IpConfigExpired = 6, + NoSecrets = 7, + SupplicantDisconnect = 8, + SupplicantConfigFailed = 9, + SupplicantFailed = 10, + SupplicantTimeout = 11, + PppStartFailed = 12, + PppDisconnect = 13, + PppFailed = 14, + DhcpStartFailed = 15, + DhcpError = 16, + DhcpFailed = 17, + SharedStartFailed = 18, + SharedFailed = 19, + AutoIpStartFailed = 20, + AutoIpError = 21, + AutoIpFailed = 22, + ModemBusy = 23, + ModemNoDialTone = 24, + ModemNoCarrier = 25, + ModemDialTimeout = 26, + ModemDialFailed = 27, + ModemInitFailed = 28, + GsmApnFailed = 29, + GsmRegistrationNotSearching = 30, + GsmRegistrationDenied = 31, + GsmRegistrationTimeout = 32, + GsmRegistrationFailed = 33, + GsmPinCheckFailed = 34, + FirmwareMissing = 35, + Removed = 36, + Sleeping = 37, + ConnectionRemoved = 38, + UserRequested = 39, + Carrier = 40, + ConnectionAssumed = 41, + SupplicantAvailable = 42, + ModemNotFound = 43, + BtFailed = 44, + GsmSimNotInserted = 45, + GsmSimPinRequired = 46, + GsmSimPukRequired = 47, + GsmSimWrong = 48, + InfinibandMode = 49, + DependencyFailed = 50, + Br2684Failed = 51, + ModemManagerUnavailable = 52, + SsidNotFound = 53, + SecondaryConnectionFailed = 54, + DcbFcoeFailed = 55, + TeamdControlFailed = 56, + ModemFailed = 57, + ModemAvailable = 58, + SimPinIncorrect = 59, + NewActivation = 60, + ParentChanged = 61, + ParentManagedChanged = 62, + OvsdbFailed = 63, + IpAddressDuplicate = 64, + IpMethodUnsupported = 65, + SriovConfigurationFailed = 66, + PeerNotFound = 67, + DeviceHandlerFailed = 68, + UnmanagedByDefault = 69, + UnmanagedExternalDown = 70, + UnmanagedLinkNotInit = 71, + UnmanagedQuitting = 72, + UnmanagedManagerDisabled = 73, + UnmanagedUserConf = 74, + UnmanagedUserExplicit = 75, + UnmanagedUserSettings = 76, + UnmanagedUserUdev = 77, + NetworkingOff = 78, + }; + Q_ENUM(Enum); +}; + // 802.11 specific device encryption and authentication capabilities. // In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. class NMWirelessCapabilities: public QObject { @@ -153,4 +284,31 @@ public: Q_ENUM(Enum); }; +/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMConnectionStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); +}; + } // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml index 322635f..414d24f 100644 --- a/src/network/nm/org.freedesktop.NetworkManager.Device.xml +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -1,5 +1,10 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml index 0283847..81419b9 100644 --- a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -2,8 +2,18 @@ - + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml index d4470ea..75c314a 100644 --- a/src/network/nm/org.freedesktop.NetworkManager.xml +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -1,5 +1,8 @@ + + + @@ -11,7 +14,7 @@ - + diff --git a/src/network/nm/settings.cpp b/src/network/nm/settings.cpp new file mode 100644 index 0000000..af36dae --- /dev/null +++ b/src/network/nm/settings.cpp @@ -0,0 +1,227 @@ +#include "settings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" +#include "utils.hpp" + +namespace qs::network { +using namespace qs::dbus; + +namespace { +QS_LOGGING_CATEGORY(logNetworkManager, "quickshell.network", QtWarningMsg); +QS_LOGGING_CATEGORY(logNMSettings, "quickshell.network.nm_settings", QtWarningMsg); +} // namespace + +NMSettings::NMSettings(const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>>(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection settings at" + << path; + return; + } + + QObject::connect( + this->proxy, + &DBusNMConnectionSettingsProxy::Updated, + this, + &NMSettings::getSettings + ); + + this->bId.setBinding([this]() { return this->bSettings.value()["connection"]["id"].toString(); }); + this->bUuid.setBinding([this]() { + return this->bSettings.value()["connection"]["uuid"].toString(); + }); + + this->settingsProperties.setInterface(this->proxy); + this->settingsProperties.updateAllViaGetAll(); + + this->getSettings(); +} + +void NMSettings::getSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get settings for" << this->path() << ":" << reply.error().message(); + } else { + auto settings = reply.value(); + manualSettingDemarshall(settings); + this->bSettings = settings; + qCDebug(logNetworkManager) << "Settings map updated for" << this->path(); + + if (!this->mLoaded) { + emit this->loaded(); + this->mLoaded = true; + } + }; + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +QDBusPendingCallWatcher* NMSettings::updateSettings( + const NMSettingsMap& settingsToChange, + const NMSettingsMap& settingsToRemove +) { + auto settings = removeSettingsInMap(this->bSettings, settingsToRemove); + settings = mergeSettingsMaps(settings, settingsToChange); + auto pending = this->proxy->Update(settings); + auto* call = new QDBusPendingCallWatcher(pending, this); + + return call; +} + +void NMSettings::clearSecrets() { + auto pending = this->proxy->ClearSecrets(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to clear secrets for" << this->path() << ":" << reply.error().message(); + } else { + qCDebug(logNetworkManager) << "Cleared secrets for" << this->path(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMSettings::forget() { + auto pending = this->proxy->Delete(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to forget" << this->path() << ":" << reply.error().message(); + } else { + qCDebug(logNetworkManager) << "Successfully deletion of" << this->path(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +QVariantMap NMSettings::read() { + QVariantMap result; + const auto& settings = this->bSettings.value(); + for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) { + QVariantMap group; + for (auto jt = it.value().constBegin(); jt != it.value().constEnd(); ++jt) { + group.insert(jt.key(), settingTypeToQml(jt.value())); + } + result.insert(it.key(), group); + } + return result; +} + +void NMSettings::write(const QVariantMap& settings) { + NMSettingsMap changedSettings; + NMSettingsMap removedSettings; + QStringList failedSettings; + + for (auto it = settings.constBegin(); it != settings.constEnd(); ++it) { + if (!it.value().canConvert()) continue; + + auto group = it.value().toMap(); + QVariantMap toChange; + QVariantMap toRemove; + for (auto jt = group.constBegin(); jt != group.constEnd(); ++jt) { + if (jt.value().isNull()) { + toRemove.insert(jt.key(), QVariant()); + } else { + auto converted = settingTypeFromQml(it.key(), jt.key(), jt.value()); + if (!converted.isValid()) failedSettings.append(it.key() + "." + jt.key()); + else toChange.insert(jt.key(), converted); + } + } + if (!toChange.isEmpty()) changedSettings.insert(it.key(), toChange); + if (!toRemove.isEmpty()) removedSettings.insert(it.key(), toRemove); + } + + if (!failedSettings.isEmpty()) { + qCWarning(logNMSettings) << "A write to" << this + << "has received bad types for the following settings:" + << failedSettings.join(", "); + } + + auto* call = this->updateSettings(changedSettings, removedSettings); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to update settings for" << this->path() << ":" << reply.error().message(); + } else { + qCDebug(logNMSettings) << "Successful write to" << this; + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMSettings::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMSettings::address() const { return this->proxy ? this->proxy->service() : QString(); } +QString NMSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::NMSettings* settings) { + auto saver = QDebugStateSaver(debug); + + if (settings) { + debug.nospace() << "NMSettings(" << static_cast(settings) + << ", uuid=" << settings->uuid() << ")"; + } else { + debug << "WifiNetwork(nullptr)"; + } + + return debug; +} diff --git a/src/network/nm/settings.hpp b/src/network/nm/settings.hpp new file mode 100644 index 0000000..3a76c61 --- /dev/null +++ b/src/network/nm/settings.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_nm_connection_settings.h" +#include "dbus_types.hpp" + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. +///! A NetworkManager connection settings profile. +class NMSettings: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + + /// The human-readable unique identifier for the connection. + Q_PROPERTY(QString id READ default NOTIFY idChanged BINDABLE bindableId); + /// A universally unique identifier for the connection. + Q_PROPERTY(QString uuid READ uuid NOTIFY uuidChanged BINDABLE bindableUuid); + +public: + explicit NMSettings(const QString& path, QObject* parent = nullptr); + + /// Clear all of the secrets belonging to the settings. + Q_INVOKABLE void clearSecrets(); + /// Delete the settings. + Q_INVOKABLE void forget(); + /// Update the connection with new settings and save the connection to disk. + /// Only changed fields need to be included. + /// Writing a setting to `null` will remove the setting or reset it to its default. + /// + /// > [!NOTE] Secrets may be part of the update request, + /// > and will be either stored in persistent storage or sent to a Secret Agent for storage, + /// > depending on the flags associated with each secret. + Q_INVOKABLE void write(const QVariantMap& settings); + /// Get the settings map describing this network configuration. + /// + /// > [!NOTE] This will never include any secrets required for connection to the network, as those are often protected. + Q_INVOKABLE QVariantMap read(); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] NMSettingsMap map() { return this->bSettings; } + QDBusPendingCallWatcher* + updateSettings(const NMSettingsMap& settingsToChange, const NMSettingsMap& settingsToRemove = {}); + QBindable bindableId() { return &this->bId; } + [[nodiscard]] QString uuid() const { return this->bUuid; } + QBindable bindableUuid() { return &this->bUuid; } + +signals: + void loaded(); + void settingsChanged(NMSettingsMap settings); + void idChanged(QString id); + void uuidChanged(QString uuid); + +private: + bool mLoaded = false; + + void getSettings(); + + Q_OBJECT_BINDABLE_PROPERTY(NMSettings, NMSettingsMap, bSettings, &NMSettings::settingsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMSettings, QString, bId, &NMSettings::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMSettings, QString, bUuid, &NMSettings::uuidChanged); + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMSettings, settingsProperties); + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +} // namespace qs::network + +QDebug operator<<(QDebug debug, const qs::network::NMSettings* settings); diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp index 0be29e5..afdc796 100644 --- a/src/network/nm/utils.cpp +++ b/src/network/nm/utils.cpp @@ -1,27 +1,28 @@ #include "utils.hpp" - +#include // We depend on non-std Linux extensions that ctime doesn't put in the global namespace // NOLINTNEXTLINE(modernize-deprecated-headers) #include #include #include +#include #include #include #include #include -#include "../wifi.hpp" +#include "../enums.hpp" #include "dbus_types.hpp" #include "enums.hpp" namespace qs::network { -WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { - const QVariantMap& security = settings.value("802-11-wireless-security"); - if (security.isEmpty()) { - return WifiSecurityType::Open; - }; +WifiSecurityType::Enum securityFromSettingsMap(const NMSettingsMap& settings) { + const QString mapName = "802-11-wireless-security"; + if (!settings.contains(mapName)) return WifiSecurityType::Unknown; + const QVariantMap& security = settings.value(mapName); + if (security.isEmpty()) return WifiSecurityType::Open; const QString keyMgmt = security["key-mgmt"].toString(); const QString authAlg = security["auth-alg"].toString(); @@ -45,6 +46,8 @@ WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMa return WifiSecurityType::Sae; } else if (keyMgmt == "wpa-eap-suite-b-192") { return WifiSecurityType::Wpa3SuiteB192; + } else if (keyMgmt == "owe") { + return WifiSecurityType::Owe; } return WifiSecurityType::Open; } @@ -224,6 +227,280 @@ WifiSecurityType::Enum findBestWirelessSecurity( return WifiSecurityType::Unknown; } +NMSettingsMap mergeSettingsMaps(const NMSettingsMap& target, const NMSettingsMap& source) { + NMSettingsMap result = target; + for (auto iter = source.constBegin(); iter != source.constEnd(); ++iter) { + result[iter.key()].insert(iter.value()); + } + return result; +} + +NMSettingsMap removeSettingsInMap(const NMSettingsMap& target, const NMSettingsMap& toRemove) { + NMSettingsMap result = target; + for (auto iter = toRemove.constBegin(); iter != toRemove.constEnd(); ++iter) { + const QString& group = iter.key(); + const QVariantMap& keysToRemove = iter.value(); + + if (!result.contains(group)) continue; + + for (auto jt = keysToRemove.constBegin(); jt != keysToRemove.constEnd(); ++jt) { + result[group].remove(jt.key()); + } + + // Remove the group entirely if it's now empty + if (result[group].isEmpty()) { + result.remove(group); + } + } + return result; +} + +// Some NMSettingsMap settings remain QDBusArguments after autodemarshalling. +// Manually demarshall these for any complex signature we have registered. +void manualSettingDemarshall(NMSettingsMap& map) { + auto demarshallValue = [](const QVariant& value) -> QVariant { + if (value.userType() != qMetaTypeId()) { + return value; + } + + auto arg = value.value(); + auto signature = arg.currentSignature(); + + if (signature == "ay") return QVariant::fromValue(qdbus_cast(arg)); + if (signature == "aay") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "au") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "aau") return QVariant::fromValue(qdbus_cast>>(arg)); + if (signature == "aa{sv}") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "a(ayuay)") return QVariant::fromValue(qdbus_cast>(arg)); + if (signature == "a(ayuayu)") return QVariant::fromValue(qdbus_cast>(arg)); + + return value; + }; + + for (auto it = map.begin(); it != map.end(); ++it) + for (auto jt = it.value().begin(); jt != it.value().end(); ++jt) + jt.value() = demarshallValue(jt.value()); +} + +// Some NMSettingsMap setting types can't be expressed in QML. +// Convert these settings to their correct type or return an invalid QVariant. +QVariant settingTypeFromQml(const QString& group, const QString& key, const QVariant& value) { + auto s = group + "." + key; + + // QString -> QByteArray + if (s == "802-1x.ca-cert" || s == "802-1x.client-cert" || s == "802-1x.private-key" + || s == "802-1x.password-raw" || s == "802-1x.phase2-ca-cert" + || s == "802-1x.phase2-client-cert" || s == "802-1x.phase2-private-key" + || s == "802-11-wireless.ssid") + { + if (value.typeId() == QMetaType::QString) { + return value.toString().toUtf8(); + } + if (value.typeId() == QMetaType::QByteArray) { + return value; + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv6.dns") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (v.typeId() == QMetaType::QString) { + r.append(v.toString().toUtf8()); + } else { + r.append(v.toByteArray()); + } + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv4.dns") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + r.append(v.value()); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList> + if (s == "ipv4.addresses" || s == "ipv4.routes") { + if (value.typeId() == QMetaType::QVariantList) { + QList> r; + for (const auto& v: value.toList()) { + if (v.typeId() != QMetaType::QVariantList) { + continue; + } + QList inner; + for (const auto& u: v.toList()) { + inner.append(u.value()); + } + r.append(inner); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv4.address-data" || s == "ipv4.route-data" || s == "ipv4.routing-rules" + || s == "ipv6.address-data" || s == "ipv6.route-data" || s == "ipv6.routing-rules") + { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (!v.canConvert()) { + continue; + } + r.append(v.toMap()); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv6.addresses") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (v.typeId() != QMetaType::QVariantList) { + continue; + } + auto fields = v.toList(); + if (fields.size() != 3) { + continue; + } + const QByteArray address = fields[0].typeId() == QMetaType::QString + ? fields[0].toString().toUtf8() + : fields[0].toByteArray(); + const QByteArray gateway = fields[2].typeId() == QMetaType::QString + ? fields[2].toString().toUtf8() + : fields[2].toByteArray(); + r.append({.address = address, .prefix = fields[1].value(), .gateway = gateway}); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QList + if (s == "ipv6.routes") { + if (value.typeId() == QMetaType::QVariantList) { + QList r; + for (const auto& v: value.toList()) { + if (v.typeId() != QMetaType::QVariantList) { + continue; + } + auto fields = v.toList(); + if (fields.size() != 4) { + continue; + } + const QByteArray destination = fields[0].typeId() == QMetaType::QString + ? fields[0].toString().toUtf8() + : fields[0].toByteArray(); + const QByteArray nexthop = fields[2].typeId() == QMetaType::QString + ? fields[2].toString().toUtf8() + : fields[2].toByteArray(); + r.append( + {.destination = destination, + .prefix = fields[1].value(), + .nexthop = nexthop, + .metric = fields[3].value()} + ); + } + return QVariant::fromValue(r); + } + return QVariant(); + } + + // QVariantList -> QStringList + if (s == "connection.permissions" || s == "ipv4.dns-search" || s == "ipv6.dns-search" + || s == "802-11-wireless.seen-bssids") + { + if (value.typeId() == QMetaType::QVariantList) { + QStringList stringList; + for (const auto& item: value.toList()) { + stringList.append(item.toString()); + } + return stringList; + } + return QVariant(); + } + + // double (whole number) -> qint32 + if (value.typeId() == QMetaType::Double) { + auto num = value.toDouble(); + if (std::isfinite(num) && num == std::trunc(num)) { + return QVariant::fromValue(static_cast(num)); + } + } + + return value; +} + +// Some NMSettingsMap setting types must be converted to a type that is supported by QML. +// Although QByteArrays can be represented in QML, we convert them to strings for convenience. +QVariant settingTypeToQml(const QVariant& value) { + // QByteArray -> QString + if (value.typeId() == QMetaType::QByteArray) { + return QString::fromUtf8(value.toByteArray()); + } + + // QList -> QVariantList + if (value.userType() == qMetaTypeId>()) { + QVariantList out; + for (const auto& ba: value.value>()) { + out.append(QString::fromUtf8(ba)); + } + return out; + } + + // QList -> QVariantList + if (value.userType() == qMetaTypeId>()) { + QVariantList out; + for (const auto& addr: value.value>()) { + out.append( + QVariant::fromValue( + QVariantList { + QString::fromUtf8(addr.address), + addr.prefix, + QString::fromUtf8(addr.gateway), + } + ) + ); + } + return out; + } + + // QList -> QVariantList + if (value.userType() == qMetaTypeId>()) { + QVariantList out; + for (const auto& route: value.value>()) { + out.append( + QVariant::fromValue( + QVariantList { + QString::fromUtf8(route.destination), + route.prefix, + QString::fromUtf8(route.nexthop), + route.metric, + } + ) + ); + } + return out; + } + + return value; +} + // NOLINTBEGIN QDateTime clockBootTimeToDateTime(qint64 clockBootTime) { clockid_t clkId = CLOCK_BOOTTIME; diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp index ce8b784..8c51423 100644 --- a/src/network/nm/utils.hpp +++ b/src/network/nm/utils.hpp @@ -3,15 +3,14 @@ #include #include #include -#include -#include "../wifi.hpp" +#include "../enums.hpp" #include "dbus_types.hpp" #include "enums.hpp" namespace qs::network { -WifiSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); +WifiSecurityType::Enum securityFromSettingsMap(const NMSettingsMap& settings); bool deviceSupportsApCiphers( NMWirelessCapabilities::Enum caps, @@ -40,6 +39,16 @@ WifiSecurityType::Enum findBestWirelessSecurity( NM80211ApSecurityFlags::Enum apRsn ); +NMSettingsMap mergeSettingsMaps(const NMSettingsMap& target, const NMSettingsMap& source); + +NMSettingsMap removeSettingsInMap(const NMSettingsMap& target, const NMSettingsMap& toRemove); + +void manualSettingDemarshall(NMSettingsMap& map); + +QVariant settingTypeFromQml(const QString& group, const QString& key, const QVariant& value); + +QVariant settingTypeToQml(const QVariant& value); + QDateTime clockBootTimeToDateTime(qint64 clockBootTime); } // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp index 9dff14b..5f55bed 100644 --- a/src/network/nm/wireless.cpp +++ b/src/network/nm/wireless.cpp @@ -1,6 +1,7 @@ #include "wireless.hpp" #include +#include #include #include #include @@ -11,20 +12,23 @@ #include #include #include +#include #include #include #include +#include #include "../../core/logcat.hpp" #include "../../dbus/properties.hpp" -#include "../network.hpp" +#include "../enums.hpp" #include "../wifi.hpp" #include "accesspoint.hpp" -#include "connection.hpp" +#include "active_connection.hpp" #include "dbus_nm_wireless.h" #include "dbus_types.hpp" #include "device.hpp" #include "enums.hpp" +#include "settings.hpp" #include "utils.hpp" namespace qs::network { @@ -42,38 +46,43 @@ NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent) , bReason(NMConnectionStateReason::None) , bState(NMConnectionState::Deactivated) {} -void NMWirelessNetwork::updateReferenceConnection() { +void NMWirelessNetwork::updateReferenceSettings() { // If the network has no connections, the reference is nullptr. - if (this->mConnections.isEmpty()) { - this->mReferenceConn = nullptr; + if (this->mSettings.isEmpty()) { + this->mReferenceSettings = nullptr; this->bSecurity = WifiSecurityType::Unknown; - // Set security back to reference AP. if (this->mReferenceAp) { this->bSecurity.setBinding([this]() { return this->mReferenceAp->security(); }); } return; }; - // If the network has an active connection, use it as the reference. + // If the network has an active connection, use its settings as the reference. if (this->mActiveConnection) { - auto* conn = this->mConnections.value(this->mActiveConnection->connection().path()); - if (conn && conn != this->mReferenceConn) { - this->mReferenceConn = conn; - this->bSecurity.setBinding([conn]() { return conn->security(); }); + auto* settings = this->mSettings.value(this->mActiveConnection->connection().path()); + if (settings && settings != this->mReferenceSettings) { + this->mReferenceSettings = settings; + this->bSecurity.setBinding([settings]() { return securityFromSettingsMap(settings->map()); }); } return; } - // Otherwise, choose the connection with the strongest security settings. - NMConnectionSettings* selectedConn = nullptr; - for (auto* conn: this->mConnections.values()) { - if (!selectedConn || conn->security() > selectedConn->security()) { - selectedConn = conn; + // Otherwise, choose the settings responsible for the last successful connection. + NMSettings* selectedSettings = nullptr; + quint64 selectedTimestamp = 0; + for (auto* settings: this->mSettings.values()) { + const quint64 timestamp = settings->map()["connection"]["timestamp"].toULongLong(); + if (!selectedSettings || timestamp > selectedTimestamp) { + selectedSettings = settings; + selectedTimestamp = timestamp; } } - if (this->mReferenceConn != selectedConn) { - this->mReferenceConn = selectedConn; - this->bSecurity.setBinding([selectedConn]() { return selectedConn->security(); }); + + if (this->mReferenceSettings != selectedSettings) { + this->mReferenceSettings = selectedSettings; + this->bSecurity.setBinding([selectedSettings]() { + return securityFromSettingsMap(selectedSettings->map()); + }); } } @@ -101,7 +110,7 @@ void NMWirelessNetwork::updateReferenceAp() { this->mReferenceAp = selectedAp; this->bSignalStrength.setBinding([selectedAp]() { return selectedAp->signalStrength(); }); // Reference AP is used for security when there's no connection settings. - if (!this->mReferenceConn) { + if (!this->mReferenceSettings) { this->bSecurity.setBinding([selectedAp]() { return selectedAp->security(); }); } } @@ -113,7 +122,7 @@ void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { auto onDestroyed = [this, ap]() { if (this->mAccessPoints.take(ap->path())) { this->updateReferenceAp(); - if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); + if (this->mAccessPoints.isEmpty() && this->mSettings.isEmpty()) emit this->disappeared(); } }; // clang-format off @@ -123,44 +132,45 @@ void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { this->updateReferenceAp(); }; -void NMWirelessNetwork::addConnection(NMConnectionSettings* conn) { - if (this->mConnections.contains(conn->path())) return; - this->mConnections.insert(conn->path(), conn); - auto onDestroyed = [this, conn]() { - if (this->mConnections.take(conn->path())) { - this->updateReferenceConnection(); - if (this->mConnections.isEmpty()) this->bKnown = false; - if (this->mAccessPoints.isEmpty() && this->mConnections.isEmpty()) emit this->disappeared(); +void NMWirelessNetwork::addSettings(NMSettings* settings) { + if (this->mSettings.contains(settings->path())) return; + this->mSettings.insert(settings->path(), settings); + + auto onDestroyed = [this, settings]() { + if (this->mSettings.take(settings->path())) { + emit this->settingsRemoved(settings); + this->updateReferenceSettings(); + if (this->mSettings.isEmpty()) this->bKnown = false; + if (this->mAccessPoints.isEmpty() && this->mSettings.isEmpty()) emit this->disappeared(); } }; - // clang-format off - QObject::connect(conn, &NMConnectionSettings::securityChanged, this, &NMWirelessNetwork::updateReferenceConnection); - QObject::connect(conn, &NMConnectionSettings::destroyed, this, onDestroyed); - // clang-format on + QObject::connect(settings, &NMSettings::destroyed, this, onDestroyed); this->bKnown = true; - this->updateReferenceConnection(); + this->updateReferenceSettings(); + emit this->settingsAdded(settings); }; void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) { if (this->mActiveConnection) return; this->mActiveConnection = active; + this->bState.setBinding([active]() { return active->state(); }); this->bReason.setBinding([active]() { return active->stateReason(); }); auto onDestroyed = [this, active]() { if (this->mActiveConnection && this->mActiveConnection == active) { this->mActiveConnection = nullptr; - this->updateReferenceConnection(); + this->updateReferenceSettings(); this->bState = NMConnectionState::Deactivated; this->bReason = NMConnectionStateReason::None; } }; QObject::connect(active, &NMActiveConnection::destroyed, this, onDestroyed); - this->updateReferenceConnection(); + this->updateReferenceSettings(); }; void NMWirelessNetwork::forget() { - if (this->mConnections.isEmpty()) return; - for (auto* conn: this->mConnections.values()) { + if (this->mSettings.isEmpty()) return; + for (auto* conn: this->mSettings.values()) { conn->forget(); } } @@ -200,7 +210,7 @@ void NMWirelessDevice::initWireless() { QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointAdded); QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointRemoved); QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded); - QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::settingsLoaded, this, &NMWirelessDevice::onSettingsLoaded); QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded); QObject::connect(this, &NMWirelessDevice::scanningChanged, this, &NMWirelessDevice::onScanningChanged); // clang-format on @@ -218,6 +228,7 @@ void NMWirelessDevice::onAccessPointRemoved(const QDBusObjectPath& path) { << "which is not registered."; return; } + qCDebug(logNetworkManager) << "Access point removed:" << path.path(); delete ap; } @@ -233,28 +244,26 @@ void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) { } } -void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) { - const ConnectionSettingsMap& settings = conn->settings(); +void NMWirelessDevice::onSettingsLoaded(NMSettings* settings) { + const NMSettingsMap& map = settings->map(); // Filter connections that aren't wireless or have missing settings - if (settings["connection"]["id"].toString().isEmpty() - || settings["connection"]["uuid"].toString().isEmpty() - || !settings.contains("802-11-wireless") - || settings["802-11-wireless"]["ssid"].toString().isEmpty()) + if (map["connection"]["id"].toString().isEmpty() || map["connection"]["uuid"].toString().isEmpty() + || !map.contains("802-11-wireless") || map["802-11-wireless"]["ssid"].toString().isEmpty()) { return; } - const auto ssid = settings["802-11-wireless"]["ssid"].toString(); - const auto mode = settings["802-11-wireless"]["mode"].toString(); + const auto ssid = map["802-11-wireless"]["ssid"].toString(); + const auto mode = map["802-11-wireless"]["mode"].toString(); if (mode == "infrastructure") { auto* net = this->mNetworks.value(ssid); if (!net) net = this->registerNetwork(ssid); - net->addConnection(conn); + net->addSettings(settings); // Check for active connections that loaded before their respective connection settings auto* active = this->activeConnection(); - if (active && conn->path() == active->connection().path()) { + if (active && settings->path() == active->connection().path()) { net->addActiveConnection(active); } } @@ -265,8 +274,8 @@ void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) { // Find an exisiting network with connection settings that matches the active const QString activeConnPath = active->connection().path(); for (const auto& net: this->mNetworks.values()) { - for (auto* conn: net->connections()) { - if (activeConnPath == conn->path()) { + for (auto* settings: net->settings()) { + if (activeConnPath == settings->path()) { net->addActiveConnection(active); return; } @@ -334,6 +343,7 @@ void NMWirelessDevice::registerAccessPoint(const QString& path) { return; } + qCDebug(logNetworkManager) << "Access point added:" << path; this->mAccessPoints.insert(path, ap); QObject::connect( ap, @@ -356,22 +366,18 @@ void NMWirelessDevice::registerAccessPoint(const QString& path) { NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) { auto* net = new NMWirelessNetwork(ssid, this); - // To avoid exposing outdated state to the frontend, filter the backend networks to only show - // the known or currently connected networks when the scanner is off. auto visible = [this, net]() { return this->bScanning || net->state() == NMConnectionState::Activated || net->known(); }; - auto onVisibilityChanged = [this, net](bool visible) { - visible ? this->registerFrontendNetwork(net) : this->removeFrontendNetwork(net); - }; net->bindableVisible().setBinding(visible); net->bindableActiveApPath().setBinding([this]() { return this->activeApPath().path(); }); + net->bindableDeviceFailReason().setBinding([this]() { return this->lastFailReason(); }); QObject::connect(net, &NMWirelessNetwork::disappeared, this, &NMWirelessDevice::removeNetwork); - QObject::connect(net, &NMWirelessNetwork::visibilityChanged, this, onVisibilityChanged); + qCDebug(logNetworkManager) << "Registered network for SSID" << ssid; this->mNetworks.insert(ssid, net); - if (net->visible()) this->registerFrontendNetwork(net); + this->registerFrontendNetwork(net); return net; } @@ -385,46 +391,137 @@ void NMWirelessDevice::registerFrontendNetwork(NMWirelessNetwork* net) { frontendNet->bindableSignalStrength().setBinding(translateSignal); frontendNet->bindableConnected().setBinding(translateState); frontendNet->bindableKnown().setBinding([net]() { return net->known(); }); - frontendNet->bindableNmReason().setBinding([net]() { return net->reason(); }); frontendNet->bindableSecurity().setBinding([net]() { return net->security(); }); frontendNet->bindableState().setBinding([net]() { - return static_cast(net->state()); + return static_cast(net->state()); + }); + + QObject::connect(net, &NMWirelessNetwork::reasonChanged, this, [net, frontendNet]() { + if (net->reason() == NMConnectionStateReason::DeviceDisconnected) { + auto deviceReason = net->deviceFailReason(); + if (deviceReason == NMDeviceStateReason::NoSecrets) + emit frontendNet->connectionFailed(ConnectionFailReason::NoSecrets); + if (deviceReason == NMDeviceStateReason::SupplicantDisconnect) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiClientDisconnected); + if (deviceReason == NMDeviceStateReason::SupplicantFailed) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiClientFailed); + if (deviceReason == NMDeviceStateReason::SupplicantTimeout) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiAuthTimeout); + if (deviceReason == NMDeviceStateReason::SsidNotFound) + emit frontendNet->connectionFailed(ConnectionFailReason::WifiNetworkLost); + } }); QObject::connect(frontendNet, &WifiNetwork::requestConnect, this, [this, net]() { - if (net->referenceConnection()) { + if (net->referenceSettings()) { emit this->activateConnection( - QDBusObjectPath(net->referenceConnection()->path()), + QDBusObjectPath(net->referenceSettings()->path()), QDBusObjectPath(this->path()) ); return; } if (net->referenceAp()) { emit this->addAndActivateConnection( - ConnectionSettingsMap(), + NMSettingsMap(), QDBusObjectPath(this->path()), QDBusObjectPath(net->referenceAp()->path()) ); + return; } + qCInfo(logNetworkManager) << "Failed to connect to" + << this->path() + ": The network disappeared."; }); QObject::connect( frontendNet, - &WifiNetwork::requestDisconnect, + &WifiNetwork::requestConnectWithPsk, this, - &NMWirelessDevice::disconnect + [this, net](const QString& psk) { + NMSettingsMap settings; + settings["802-11-wireless-security"]["psk"] = psk; + if (const QPointer ref = net->referenceSettings()) { + auto* call = ref->updateSettings(settings); + QObject::connect( + call, + &QDBusPendingCallWatcher::finished, + this, + [this, ref](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCInfo(logNetworkManager) + << "Failed to write PSK for" << this->path() + ":" << reply.error().message(); + } else { + if (!ref) { + qCInfo(logNetworkManager) << "Failed to connectWithPsk to" + << this->path() + ": The settings disappeared."; + } else { + emit this->activateConnection( + QDBusObjectPath(ref->path()), + QDBusObjectPath(this->path()) + ); + } + } + delete call; + } + ); + return; + } + if (net->referenceAp()) { + emit this->addAndActivateConnection( + settings, + QDBusObjectPath(this->path()), + QDBusObjectPath(net->referenceAp()->path()) + ); + return; + } + qCInfo(logNetworkManager) << "Failed to connectWithPsk to" + << this->path() + ": The network disappeared."; + } ); + QObject::connect( + frontendNet, + &WifiNetwork::requestConnectWithSettings, + this, + [this](NMSettings* settings) { + if (settings) { + emit this->activateConnection( + QDBusObjectPath(settings->path()), + QDBusObjectPath(this->path()) + ); + return; + } + qCInfo(logNetworkManager) << "Failed to connectWithSettings to" + << this->path() + ": The provided settings no longer exist."; + } + ); + + QObject::connect( + net, + &NMWirelessNetwork::visibilityChanged, + this, + [this, frontendNet](bool visible) { + if (visible) this->networkAdded(frontendNet); + else this->networkRemoved(frontendNet); + } + ); + + // clang-format off + QObject::connect(frontendNet, &WifiNetwork::requestDisconnect, this, &NMWirelessDevice::disconnect); QObject::connect(frontendNet, &WifiNetwork::requestForget, net, &NMWirelessNetwork::forget); + QObject::connect(net, &NMWirelessNetwork::settingsAdded, frontendNet, &WifiNetwork::settingsAdded); + QObject::connect(net, &NMWirelessNetwork::settingsRemoved, frontendNet, &WifiNetwork::settingsRemoved); + // clang-format on this->mFrontendNetworks.insert(ssid, frontendNet); - emit this->networkAdded(frontendNet); + if (net->visible()) emit this->networkAdded(frontendNet); } void NMWirelessDevice::removeFrontendNetwork(NMWirelessNetwork* net) { auto* frontendNet = this->mFrontendNetworks.take(net->ssid()); if (frontendNet) { - emit this->networkRemoved(frontendNet); + if (net->visible()) emit this->networkRemoved(frontendNet); frontendNet->deleteLater(); } } diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp index fe4010e..94ce754 100644 --- a/src/network/nm/wireless.hpp +++ b/src/network/nm/wireless.hpp @@ -9,10 +9,11 @@ #include "../wifi.hpp" #include "accesspoint.hpp" -#include "connection.hpp" +#include "active_connection.hpp" #include "dbus_nm_wireless.h" #include "device.hpp" #include "enums.hpp" +#include "settings.hpp" namespace qs::dbus { template <> @@ -32,7 +33,7 @@ struct DBusDataTransform { } // namespace qs::dbus namespace qs::network { -// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMConnectionSetting objects. +// NMWirelessNetwork aggregates all related NMActiveConnection, NMAccessPoint, and NMSettings objects. class NMWirelessNetwork: public QObject { Q_OBJECT; @@ -40,46 +41,51 @@ public: explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr); void addAccessPoint(NMAccessPoint* ap); - void addConnection(NMConnectionSettings* conn); + void addSettings(NMSettings* settings); void addActiveConnection(NMActiveConnection* active); void forget(); - [[nodiscard]] QString ssid() const { return this->mSsid; }; - [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; - [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; }; - [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; - [[nodiscard]] bool known() const { return this->bKnown; }; - [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; }; - [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; }; - [[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; }; - [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); }; - [[nodiscard]] QList connections() const { - return this->mConnections.values(); - } - [[nodiscard]] QBindable bindableActiveApPath() { return &this->bActiveApPath; }; - [[nodiscard]] QBindable bindableVisible() { return &this->bVisible; }; - [[nodiscard]] bool visible() const { return this->bVisible; }; + // clang-format off + [[nodiscard]] QString ssid() const { return this->mSsid; } + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; } + [[nodiscard]] WifiSecurityType::Enum security() const { return this->bSecurity; } + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; } + [[nodiscard]] bool known() const { return this->bKnown; } + [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->bReason; } + QBindable bindableDeviceFailReason() { return &this->bDeviceFailReason; } + [[nodiscard]] NMDeviceStateReason::Enum deviceFailReason() const { return this->bDeviceFailReason; } + [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; } + [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); } + [[nodiscard]] QList settings() const { return this->mSettings.values(); } + [[nodiscard]] NMSettings* referenceSettings() const { return this->mReferenceSettings; } + QBindable bindableActiveApPath() { return &this->bActiveApPath; } + QBindable bindableVisible() { return &this->bVisible; } + bool visible() const { return this->bVisible; } + // clang-format on signals: void disappeared(); + void settingsAdded(NMSettings* settings); + void settingsRemoved(NMSettings* settings); void visibilityChanged(bool visible); void signalStrengthChanged(quint8 signal); void stateChanged(NMConnectionState::Enum state); void knownChanged(bool known); void securityChanged(WifiSecurityType::Enum security); void reasonChanged(NMConnectionStateReason::Enum reason); + void deviceFailReasonChanged(NMDeviceStateReason::Enum reason); void capabilitiesChanged(NMWirelessCapabilities::Enum caps); void activeApPathChanged(QString path); private: void updateReferenceAp(); - void updateReferenceConnection(); + void updateReferenceSettings(); QString mSsid; QHash mAccessPoints; - QHash mConnections; + QHash mSettings; NMAccessPoint* mReferenceAp = nullptr; - NMConnectionSettings* mReferenceConn = nullptr; + NMSettings* mReferenceSettings = nullptr; NMActiveConnection* mActiveConnection = nullptr; // clang-format off @@ -88,6 +94,7 @@ private: Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, WifiSecurityType::Enum, bSecurity, &NMWirelessNetwork::securityChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionStateReason::Enum, bReason, &NMWirelessNetwork::reasonChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMConnectionState::Enum, bState, &NMWirelessNetwork::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, NMDeviceStateReason::Enum, bDeviceFailReason, &NMWirelessNetwork::deviceFailReasonChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, quint8, bSignalStrength, &NMWirelessNetwork::signalStrengthChanged); Q_OBJECT_BINDABLE_PROPERTY(NMWirelessNetwork, QString, bActiveApPath, &NMWirelessNetwork::activeApPathChanged); // clang-format on @@ -103,10 +110,10 @@ public: explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr); [[nodiscard]] bool isValid() const override; - [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; - [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; }; - [[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; }; - [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; }; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; } + [[nodiscard]] const QDBusObjectPath& activeApPath() { return this->bActiveAccessPoint; } + [[nodiscard]] NM80211Mode::Enum mode() { return this->bMode; } + [[nodiscard]] QBindable bindableScanning() { return &this->bScanning; } signals: void accessPointLoaded(NMAccessPoint* ap); @@ -123,7 +130,7 @@ private slots: void onAccessPointAdded(const QDBusObjectPath& path); void onAccessPointRemoved(const QDBusObjectPath& path); void onAccessPointLoaded(NMAccessPoint* ap); - void onConnectionLoaded(NMConnectionSettings* conn); + void onSettingsLoaded(NMSettings* settings); void onActiveConnectionLoaded(NMActiveConnection* active); void onScanTimeout(); void onScanningChanged(bool scanning); diff --git a/src/network/test/manual/network.qml b/src/network/test/manual/network.qml index 0fd0f72..eadc159 100644 --- a/src/network/test/manual/network.qml +++ b/src/network/test/manual/network.qml @@ -5,151 +5,356 @@ import Quickshell import Quickshell.Widgets import Quickshell.Networking -FloatingWindow { - color: contentItem.palette.window +Scope { + Component { + id: editorComponent + FloatingWindow { + id: editorWindow + required property var nmSettings + color: contentItem.palette.window - ColumnLayout { - anchors.fill: parent - anchors.margins: 5 + Component.onCompleted: editorArea.text = JSON.stringify(nmSettings.read(), null, 2) - Column { - Layout.fillWidth: true - RowLayout { - Label { - text: "WiFi" - font.bold: true - font.pointSize: 12 - } - CheckBox { - text: "Software" - checked: Networking.wifiEnabled - onClicked: Networking.wifiEnabled = !Networking.wifiEnabled - } - CheckBox { - enabled: false - text: "Hardware" - checked: Networking.wifiHardwareEnabled - } - } - } + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 - ListView { - clip: true - Layout.fillWidth: true - Layout.fillHeight: true - model: Networking.devices + Label { + text: "Editing " + nmSettings?.id + " (" + nmSettings?.uuid + ")" + font.bold: true + font.pointSize: 12 + } - delegate: WrapperRectangle { - width: parent.width - color: "transparent" - border.color: palette.button - border.width: 1 - margin: 5 + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + TextArea { + id: editorArea + wrapMode: TextEdit.Wrap + selectByMouse: true + } + } - ColumnLayout { - RowLayout { - Label { text: modelData.name; font.bold: true } - Label { text: modelData.address } - Label { text: `(Type: ${DeviceType.toString(modelData.type)})` } - } - RowLayout { - Label { - text: DeviceConnectionState.toString(modelData.state) - color: modelData.connected ? palette.link : palette.placeholderText - } - Label { - visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.state == DeviceConnectionState.Connecting || modelData.state == DeviceConnectionState.Disconnecting) - text: `(${NMDeviceState.toString(modelData.nmState)})` - } - Button { - visible: modelData.state == DeviceConnectionState.Connected - text: "Disconnect" - onClicked: modelData.disconnect() - } - CheckBox { - text: "Autoconnect" - checked: modelData.autoconnect - onClicked: modelData.autoconnect = !modelData.autoconnect - } - Label { - text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}` - visible: modelData.type == DeviceType.Wifi - } - CheckBox { - text: "Scanner" - checked: modelData.scannerEnabled - onClicked: modelData.scannerEnabled = !modelData.scannerEnabled - visible: modelData.type === DeviceType.Wifi - } - } + RowLayout { + Layout.fillWidth: true + Label { + id: statusLabel + Layout.fillWidth: true + color: palette.placeholderText + } + Button { + text: "Reload" + onClicked: { + editorArea.text = JSON.stringify(editorWindow.nmSettings.read(), null, 2); + statusLabel.text = "Reloaded"; + } + } + Button { + text: "Save" + onClicked: { + try { + const parsed = JSON.parse(editorArea.text); + nmSettings.write(parsed); + statusLabel.text = "Saved"; + } catch (e) { + statusLabel.text = "Parse error: " + e.message; + } + } + } + Button { + text: "Close" + onClicked: { + editorArea.focus = false; + editorWindow.destroy(); + } + } + } + } + } + } - Repeater { - Layout.fillWidth: true - model: { - if (modelData.type !== DeviceType.Wifi) return [] - return [...modelData.networks.values].sort((a, b) => { - if (a.connected !== b.connected) { - return b.connected - a.connected - } - return b.signalStrength - a.signalStrength - }) - } + FloatingWindow { + color: contentItem.palette.window - WrapperRectangle { - Layout.fillWidth: true - color: modelData.connected ? palette.highlight : palette.button - border.color: palette.mid - border.width: 1 - margin: 5 + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 - RowLayout { - ColumnLayout { - Layout.fillWidth: true - RowLayout { - Label { text: modelData.name; font.bold: true } - Label { - text: modelData.known ? "Known" : "" - color: palette.placeholderText - } - } - RowLayout { - Label { - text: `Security: ${WifiSecurityType.toString(modelData.security)}` - color: palette.placeholderText - } - Label { - text: `| Signal strength: ${Math.round(modelData.signalStrength*100)}%` - color: palette.placeholderText - } - } - Label { - visible: Networking.backend == NetworkBackendType.NetworkManager && (modelData.nmReason != NMConnectionStateReason.Unknown && modelData.nmReason != NMConnectionStateReason.None) - text: `Connection change reason: ${NMConnectionStateReason.toString(modelData.nmReason)}` - } - } - RowLayout { - Layout.alignment: Qt.AlignRight - Button { - text: "Connect" - onClicked: modelData.connect() - visible: !modelData.connected - } - Button { - text: "Disconnect" - onClicked: modelData.disconnect() - visible: modelData.connected - } - Button { - text: "Forget" - onClicked: modelData.forget() - visible: modelData.known - } - } - } - } - } - } - } - } - } + ColumnLayout { + Label { + text: `Networking (${NetworkBackendType.toString(Networking.backend)} backend)` + font.bold: true + font.pointSize: 12 + } + RowLayout { + Label { + text: `Connectivity` + font.bold: true + } + Label { + text: `${NetworkConnectivity.toString(Networking.connectivity)}` + visible: Networking.canCheckConnectivity + } + Button { + text: "Re-check" + visible: Networking.canCheckConnectivity && Networking.connectivityCheckEnabled + onClicked: Networking.checkConnectivity() + } + CheckBox { + text: "Checking enabled" + checked: Networking.connectivityCheckEnabled + onClicked: Networking.connectivityCheckEnabled = !Networking.connectivityCheckEnabled + visible: Networking.canCheckConnectivity + } + CheckBox { + enabled: false + text: "Supported" + checked: Networking.canCheckConnectivity + } + } + } + + Column { + Layout.fillWidth: true + RowLayout { + Label { + text: "WiFi" + font.bold: true + } + CheckBox { + text: "Software" + checked: Networking.wifiEnabled + onClicked: Networking.wifiEnabled = !Networking.wifiEnabled + } + CheckBox { + enabled: false + text: "Hardware" + checked: Networking.wifiHardwareEnabled + } + } + } + + ListView { + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + model: Networking.devices + + delegate: WrapperRectangle { + width: parent.width + color: "transparent" + border.color: palette.button + border.width: 1 + margin: 5 + + ColumnLayout { + RowLayout { + Label { + text: modelData.name + font.bold: true + } + Label { + text: modelData.address + } + Label { + text: `(Type: ${DeviceType.toString(modelData.type)})` + } + CheckBox { + text: `Managed` + checked: modelData.nmManaged + onClicked: modelData.nmManaged = !modelData.nmManaged + } + } + RowLayout { + Label { + text: ConnectionState.toString(modelData.state) + color: modelData.connected ? palette.link : palette.placeholderText + } + Button { + visible: modelData.state == ConnectionState.Connected + text: "Disconnect" + onClicked: modelData.disconnect() + } + CheckBox { + text: "Autoconnect" + checked: modelData.autoconnect + onClicked: modelData.autoconnect = !modelData.autoconnect + } + Label { + text: `Mode: ${WifiDeviceMode.toString(modelData.mode)}` + visible: modelData.type == DeviceType.Wifi + } + CheckBox { + text: "Scanner" + checked: modelData.scannerEnabled + onClicked: modelData.scannerEnabled = !modelData.scannerEnabled + visible: modelData.type === DeviceType.Wifi + } + } + + Repeater { + Layout.fillWidth: true + model: ScriptModel { + values: [...modelData.networks.values].sort((a, b) => { + if (a.connected !== b.connected) { + return b.connected - a.connected; + } + return b.signalStrength - a.signalStrength; + }) + } + + WrapperRectangle { + property var chosenSettings: { + const settings = modelData.nmSettings; + if (!settings || settings.length === 0) { + return null; + } + if (settings.length === 1) { + return settings[0]; + } + return settings[settingsComboBox.currentIndex]; + } + + Connections { + target: modelData + function onConnectionFailed(reason) { + failLoader.sourceComponent = failComponent; + failLoader.item.failReason = reason; + } + function onStateChanged() { + if (modelData.state == ConnectionState.Connecting) { + failLoader.sourceComponent = null; + } + } + } + + Component { + id: failComponent + RowLayout { + property var failReason + Label { + text: ConnectionFailReason.toString(failReason) + } + RowLayout { + TextField { + id: pskField + placeholderText: "PSK" + } + Button { + text: "Set" + visible: pskField.visible + onClicked: { + modelData.connectWithPsk(pskField.text); + failLoader.sourceComponent = null; + } + } + visible: modelData.security === WifiSecurityType.WpaPsk || modelData.security === WifiSecurityType.Wpa2Psk || modelData.security === WifiSecurityType.Sae + } + Button { + text: "Close" + onClicked: failLoader.sourceComponent = null + } + } + } + + Layout.fillWidth: true + color: modelData.connected ? palette.highlight : palette.button + border.color: palette.mid + border.width: 1 + margin: 5 + + RowLayout { + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Label { + text: modelData.name + font.bold: true + } + Label { + text: modelData.known ? "Known" : "" + color: palette.placeholderText + } + } + RowLayout { + Label { + text: `Security: ${WifiSecurityType.toString(modelData.security)}` + color: palette.placeholderText + } + Label { + text: `| Signal strength: ${Math.round(modelData.signalStrength * 100)}%` + color: palette.placeholderText + } + } + } + ColumnLayout { + Layout.alignment: Qt.AlignRight + RowLayout { + Layout.alignment: Qt.AlignRight + BusyIndicator { + implicitHeight: 30 + implicitWidth: 30 + running: modelData.stateChanging + visible: modelData.stateChanging + } + Label { + text: ConnectionState.toString(modelData.state) + color: modelData.connected ? palette.link : palette.placeholderText + } + RowLayout { + Label { + text: "Choose settings:" + } + ComboBox { + id: settingsComboBox + model: modelData.nmSettings.map(s => s?.read()?.connection?.id) + currentIndex: 0 + } + visible: modelData.nmSettings.length > 1 + } + Button { + text: "Connect" + onClicked: { + if (chosenSettings) + modelData.connectWithSettings(chosenSettings); + else + modelData.connect(); + } + visible: !modelData.connected + } + Button { + text: "Disconnect" + onClicked: modelData.disconnect() + visible: modelData.connected + } + Button { + text: "Forget" + onClicked: modelData.forget() + visible: modelData.known + } + Button { + text: "Edit" + visible: modelData.known + onClicked: { + if (chosenSettings) + editorComponent.createObject(null, { + nmSettings: chosenSettings + }); + } + } + } + Loader { + id: failLoader + Layout.alignment: Qt.AlignRight + visible: sourceComponent !== null + } + } + } + } + } + } + } + } + } + } } diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp index 57fb8ea..e9939c2 100644 --- a/src/network/wifi.cpp +++ b/src/network/wifi.cpp @@ -6,99 +6,35 @@ #include #include #include +#include #include "../core/logcat.hpp" #include "device.hpp" +#include "enums.hpp" #include "network.hpp" namespace qs::network { namespace { -QS_LOGGING_CATEGORY(logWifi, "quickshell.network.wifi", QtWarningMsg); -} // namespace - -QString WifiSecurityType::toString(WifiSecurityType::Enum type) { - switch (type) { - case Unknown: return QStringLiteral("Unknown"); - case Wpa3SuiteB192: return QStringLiteral("WPA3 Suite B 192-bit"); - case Sae: return QStringLiteral("WPA3"); - case Wpa2Eap: return QStringLiteral("WPA2 Enterprise"); - case Wpa2Psk: return QStringLiteral("WPA2"); - case WpaEap: return QStringLiteral("WPA Enterprise"); - case WpaPsk: return QStringLiteral("WPA"); - case StaticWep: return QStringLiteral("WEP"); - case DynamicWep: return QStringLiteral("Dynamic WEP"); - case Leap: return QStringLiteral("LEAP"); - case Owe: return QStringLiteral("OWE"); - case Open: return QStringLiteral("Open"); - default: return QStringLiteral("Unknown"); - } +QS_LOGGING_CATEGORY(logWifiNetwork, "quickshell.wifinetwork", QtWarningMsg); } -QString WifiDeviceMode::toString(WifiDeviceMode::Enum mode) { - switch (mode) { - case Unknown: return QStringLiteral("Unknown"); - case AdHoc: return QStringLiteral("Ad-Hoc"); - case Station: return QStringLiteral("Station"); - case AccessPoint: return QStringLiteral("Access Point"); - case Mesh: return QStringLiteral("Mesh"); - default: return QStringLiteral("Unknown"); - }; -} - -QString NMConnectionStateReason::toString(NMConnectionStateReason::Enum reason) { - switch (reason) { - case Unknown: return QStringLiteral("Unknown"); - case None: return QStringLiteral("No reason"); - case UserDisconnected: return QStringLiteral("User disconnection"); - case DeviceDisconnected: - return QStringLiteral("The device the connection was using was disconnected."); - case ServiceStopped: - return QStringLiteral("The service providing the VPN connection was stopped."); - case IpConfigInvalid: - return QStringLiteral("The IP config of the active connection was invalid."); - case ConnectTimeout: - return QStringLiteral("The connection attempt to the VPN service timed out."); - case ServiceStartTimeout: - return QStringLiteral( - "A timeout occurred while starting the service providing the VPN connection." - ); - case ServiceStartFailed: - return QStringLiteral("Starting the service providing the VPN connection failed."); - case NoSecrets: return QStringLiteral("Necessary secrets for the connection were not provided."); - case LoginFailed: return QStringLiteral("Authentication to the server failed."); - case ConnectionRemoved: - return QStringLiteral("Necessary secrets for the connection were not provided."); - case DependencyFailed: - return QStringLiteral("Master connection of this connection failed to activate."); - case DeviceRealizeFailed: return QStringLiteral("Could not create the software device link."); - case DeviceRemoved: return QStringLiteral("The device this connection depended on disappeared."); - default: return QStringLiteral("Unknown"); - }; -}; - WifiNetwork::WifiNetwork(QString ssid, QObject* parent): Network(std::move(ssid), parent) {}; -void WifiNetwork::connect() { +void WifiNetwork::connectWithPsk(const QString& psk) { if (this->bConnected) { - qCCritical(logWifi) << this << "is already connected."; + qCCritical(logWifiNetwork) << this << "is already connected."; return; } - - this->requestConnect(); -} - -void WifiNetwork::disconnect() { - if (!this->bConnected) { - qCCritical(logWifi) << this << "is not currently connected"; + if (this->bSecurity != WifiSecurityType::WpaPsk && this->bSecurity != WifiSecurityType::Wpa2Psk + && this->bSecurity != WifiSecurityType::Sae) + { + qCCritical(logWifiNetwork) << this << "has the wrong security type for a PSK."; return; } - - this->requestDisconnect(); + emit this->requestConnectWithPsk(psk); } -void WifiNetwork::forget() { this->requestForget(); } - WifiDevice::WifiDevice(QObject* parent): NetworkDevice(DeviceType::Wifi, parent) {}; void WifiDevice::setScannerEnabled(bool enabled) { diff --git a/src/network/wifi.hpp b/src/network/wifi.hpp index 15b093d..c0091f7 100644 --- a/src/network/wifi.hpp +++ b/src/network/wifi.hpp @@ -6,90 +6,15 @@ #include #include +#include "../core/doc.hpp" #include "../core/model.hpp" #include "device.hpp" +#include "enums.hpp" #include "network.hpp" namespace qs::network { -///! The security type of a wifi network. -class WifiSecurityType: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Wpa3SuiteB192 = 0, - Sae = 1, - Wpa2Eap = 2, - Wpa2Psk = 3, - WpaEap = 4, - WpaPsk = 5, - StaticWep = 6, - DynamicWep = 7, - Leap = 8, - Owe = 9, - Open = 10, - Unknown = 11, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(WifiSecurityType::Enum type); -}; - -///! The 802.11 mode of a wifi device. -class WifiDeviceMode: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - /// The device is part of an Ad-Hoc network without a central access point. - AdHoc = 0, - /// The device is a station that can connect to networks. - Station = 1, - /// The device is a local hotspot/access point. - AccessPoint = 2, - /// The device is an 802.11s mesh point. - Mesh = 3, - /// The device mode is unknown. - Unknown = 4, - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(WifiDeviceMode::Enum mode); -}; - -///! NetworkManager-specific reason for a WifiNetworks connection state. -/// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. -class NMConnectionStateReason: public QObject { - Q_OBJECT; - QML_ELEMENT; - QML_SINGLETON; - -public: - enum Enum : quint8 { - Unknown = 0, - None = 1, - UserDisconnected = 2, - DeviceDisconnected = 3, - ServiceStopped = 4, - IpConfigInvalid = 5, - ConnectTimeout = 6, - ServiceStartTimeout = 7, - ServiceStartFailed = 8, - NoSecrets = 9, - LoginFailed = 10, - ConnectionRemoved = 11, - DependencyFailed = 12, - DeviceRealizeFailed = 13, - DeviceRemoved = 14 - }; - Q_ENUM(Enum); - Q_INVOKABLE static QString toString(NMConnectionStateReason::Enum reason); -}; - -///! An available wifi network. +///! WiFi subtype of @@Network. class WifiNetwork: public Network { Q_OBJECT; QML_ELEMENT; @@ -97,58 +22,46 @@ class WifiNetwork: public Network { // clang-format off /// The current signal strength of the network, from 0.0 to 1.0. Q_PROPERTY(qreal signalStrength READ default NOTIFY signalStrengthChanged BINDABLE bindableSignalStrength); - /// True if the wifi network has known connection settings saved. - Q_PROPERTY(bool known READ default NOTIFY knownChanged BINDABLE bindableKnown); /// The security type of the wifi network. Q_PROPERTY(WifiSecurityType::Enum security READ default NOTIFY securityChanged BINDABLE bindableSecurity); - /// A specific reason for the connection state when the backend is NetworkManager. - Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ default NOTIFY nmReasonChanged BINDABLE bindableNmReason); // clang-format on public: explicit WifiNetwork(QString ssid, QObject* parent = nullptr); - - /// Attempt to connect to the wifi network. + /// Attempt to connect to the network with the given PSK. If the PSK is wrong, + /// a @@Network.connectionFailed(s) signal will be emitted with `NoSecrets`. /// - /// > [!WARNING] Quickshell does not yet provide a NetworkManager authentication agent, - /// > meaning another agent will need to be active to enter passwords for unsaved networks. - Q_INVOKABLE void connect(); - /// Disconnect from the wifi network. - Q_INVOKABLE void disconnect(); - /// Forget all connection settings for this wifi network. - Q_INVOKABLE void forget(); + /// The networking backend may store the PSK for future use with @@Network.connect(). + /// As such, calling that function first is recommended to avoid having to show a + /// prompt if not required. + /// + /// > [!NOTE] PSKs should only be provided when the @@security is one of + /// > `WpaPsk`, `Wpa2Psk`, or `Sae`. + Q_INVOKABLE void connectWithPsk(const QString& psk); QBindable bindableSignalStrength() { return &this->bSignalStrength; } - QBindable bindableKnown() { return &this->bKnown; } - QBindable bindableNmReason() { return &this->bNmReason; } QBindable bindableSecurity() { return &this->bSecurity; } signals: - void requestConnect(); - void requestDisconnect(); - void requestForget(); + QSDOC_HIDE void requestConnectWithPsk(QString psk); void signalStrengthChanged(); - void knownChanged(); void securityChanged(); - void nmReasonChanged(); private: // clang-format off Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, qreal, bSignalStrength, &WifiNetwork::signalStrengthChanged); - Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, bool, bKnown, &WifiNetwork::knownChanged); - Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, NMConnectionStateReason::Enum, bNmReason, &WifiNetwork::nmReasonChanged); Q_OBJECT_BINDABLE_PROPERTY(WifiNetwork, WifiSecurityType::Enum, bSecurity, &WifiNetwork::securityChanged); // clang-format on }; -///! Wireless variant of a NetworkDevice. +///! WiFi variant of a @@NetworkDevice. class WifiDevice: public NetworkDevice { Q_OBJECT; QML_ELEMENT; QML_UNCREATABLE(""); // clang-format off - /// A list of this available and connected wifi networks. + /// A list of this available or connected wifi networks. QSDOC_TYPE_OVERRIDE(ObjectModel*); Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); /// True when currently scanning for networks. @@ -164,9 +77,9 @@ public: void networkAdded(WifiNetwork* net); void networkRemoved(WifiNetwork* net); - [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; }; - QBindable bindableScannerEnabled() { return &this->bScannerEnabled; }; - [[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; }; + [[nodiscard]] ObjectModel* networks() { return &this->mNetworks; } + QBindable bindableScannerEnabled() { return &this->bScannerEnabled; } + [[nodiscard]] bool scannerEnabled() const { return this->bScannerEnabled; } void setScannerEnabled(bool enabled); QBindable bindableMode() { return &this->bMode; } From 4b751ccb0d431fc8fcdeba81ee09ddaf98ff848d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 2 Apr 2026 21:56:52 -0700 Subject: [PATCH 211/226] wayland/screencopy: use linear texture filtering over nearest Fixes pixelated views at scaled resolutions. --- changelog/next.md | 1 + src/wayland/buffer/manager.cpp | 4 ++++ src/wayland/buffer/qsg.hpp | 1 + src/wayland/screencopy/view.cpp | 1 + 4 files changed, 7 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index 024ab10..17b8cd4 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -64,6 +64,7 @@ set shell id. - Fixed partial socket reads in greetd and hyprland on slow machines. - Worked around Qt bug causing crashes when plugging and unplugging monitors. - Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. +- Fixed ScreencopyView pixelation when scaled. ## Packaging Changes diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp index 713752a..9cf77fd 100644 --- a/src/wayland/buffer/manager.cpp +++ b/src/wayland/buffer/manager.cpp @@ -123,6 +123,10 @@ void WlBufferQSGDisplayNode::setRect(const QRectF& rect) { this->setMatrix(matrix); } +void WlBufferQSGDisplayNode::setFiltering(QSGTexture::Filtering filtering) { + this->imageNode->setFiltering(filtering); +} + void WlBufferQSGDisplayNode::syncSwapchain(const WlBufferSwapchain& swapchain) { auto* buffer = swapchain.frontbuffer(); auto& texture = swapchain.presentSecondBuffer ? this->buffer2 : this->buffer1; diff --git a/src/wayland/buffer/qsg.hpp b/src/wayland/buffer/qsg.hpp index c230cfe..bb05954 100644 --- a/src/wayland/buffer/qsg.hpp +++ b/src/wayland/buffer/qsg.hpp @@ -33,6 +33,7 @@ public: void syncSwapchain(const WlBufferSwapchain& swapchain); void setRect(const QRectF& rect); + void setFiltering(QSGTexture::Filtering filtering); private: QQuickWindow* window; diff --git a/src/wayland/screencopy/view.cpp b/src/wayland/screencopy/view.cpp index 7828c98..7d10dc2 100644 --- a/src/wayland/screencopy/view.cpp +++ b/src/wayland/screencopy/view.cpp @@ -167,6 +167,7 @@ QSGNode* ScreencopyView::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* auto& swapchain = this->context->swapchain(); node->syncSwapchain(swapchain); node->setRect(this->boundingRect()); + node->setFiltering(QSGTexture::Linear); // NOLINT (misc-include-cleaner) if (this->mLive) this->context->captureFrame(); return node; From 50cdf9886803c0279aafa43d0b590abdc34f5766 Mon Sep 17 00:00:00 2001 From: HigherOrderLogic <73709188+HigherOrderLogic@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:50:13 +0000 Subject: [PATCH 212/226] core/colorquant: add imageRect option for cropping image --- CMakeLists.txt | 5 ++++- changelog/next.md | 1 + src/core/colorquantizer.cpp | 38 ++++++++++++++++++++++++++++++++----- src/core/colorquantizer.hpp | 17 ++++++++++++++++- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b02b3d8..966e8c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,10 @@ cmake_minimum_required(VERSION 3.20) project(quickshell VERSION "0.2.1" LANGUAGES CXX C) -set(UNRELEASED_FEATURES "network.2") +set(UNRELEASED_FEATURES + "network.2" + "colorquant-imagerect" +) set(QT_MIN_VERSION "6.6.0") set(CMAKE_CXX_STANDARD 20) diff --git a/changelog/next.md b/changelog/next.md index 17b8cd4..8b22d07 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -29,6 +29,7 @@ set shell id. - Added generic WindowManager interface implementing ext-workspace. - Added ext-background-effect window blur support. - Added per-corner radius support to Region. +- Added ColorQuantizer region selection. ## Other Changes diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp index 4ac850b..d983f76 100644 --- a/src/core/colorquantizer.cpp +++ b/src/core/colorquantizer.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -24,9 +25,15 @@ namespace { QS_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg); } -ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize) +ColorQuantizerOperation::ColorQuantizerOperation( + QUrl* source, + qreal depth, + QRect imageRect, + qreal rescaleSize +) : source(source) , maxDepth(depth) + , imageRect(imageRect) , rescaleSize(rescaleSize) { this->setAutoDelete(false); } @@ -37,6 +44,11 @@ void ColorQuantizerOperation::quantizeImage(const QAtomicInteger& shouldCa this->colors.clear(); auto image = QImage(this->source->toLocalFile()); + + if (this->imageRect.isValid()) { + image = image.copy(this->imageRect); + } + if ((image.width() > this->rescaleSize || image.height() > this->rescaleSize) && this->rescaleSize > 0) { @@ -198,16 +210,27 @@ void ColorQuantizer::setDepth(qreal depth) { this->mDepth = depth; emit this->depthChanged(); - if (this->componentCompleted) this->quantizeAsync(); + if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); } } +void ColorQuantizer::setImageRect(QRect imageRect) { + if (this->mImageRect != imageRect) { + this->mImageRect = imageRect; + emit this->imageRectChanged(); + + if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); + } +} + +void ColorQuantizer::resetImageRect() { this->setImageRect(QRect()); } + void ColorQuantizer::setRescaleSize(int rescaleSize) { if (this->mRescaleSize != rescaleSize) { this->mRescaleSize = rescaleSize; emit this->rescaleSizeChanged(); - if (this->componentCompleted) this->quantizeAsync(); + if (this->componentCompleted && !this->mSource.isEmpty()) this->quantizeAsync(); } } @@ -221,8 +244,13 @@ void ColorQuantizer::quantizeAsync() { if (this->liveOperation) this->cancelAsync(); qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; - this->liveOperation = - new ColorQuantizerOperation(&this->mSource, this->mDepth, this->mRescaleSize); + + this->liveOperation = new ColorQuantizerOperation( + &this->mSource, + this->mDepth, + this->mImageRect, + this->mRescaleSize + ); QObject::connect( this->liveOperation, diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp index f6e158d..0159181 100644 --- a/src/core/colorquantizer.hpp +++ b/src/core/colorquantizer.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,7 +17,7 @@ class ColorQuantizerOperation Q_OBJECT; public: - explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); + explicit ColorQuantizerOperation(QUrl* source, qreal depth, QRect imageRect, qreal rescaleSize); void run() override; void tryCancel(); @@ -44,6 +45,7 @@ private: QList colors; QUrl* source; qreal maxDepth; + QRect imageRect; qreal rescaleSize; }; @@ -78,6 +80,13 @@ class ColorQuantizer /// binary split of the color space Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged); + // clang-format off + /// Rectangle that the source image is cropped to. + /// + /// Can be set to `undefined` to reset. + Q_PROPERTY(QRect imageRect READ imageRect WRITE setImageRect RESET resetImageRect NOTIFY imageRectChanged); + // clang-format on + /// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done. /// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's /// > reccommended to rescale, otherwise the quantization process will take much longer. @@ -97,6 +106,10 @@ public: [[nodiscard]] qreal depth() const { return this->mDepth; } void setDepth(qreal depth); + [[nodiscard]] QRect imageRect() const { return this->mImageRect; } + void setImageRect(QRect imageRect); + void resetImageRect(); + [[nodiscard]] qreal rescaleSize() const { return this->mRescaleSize; } void setRescaleSize(int rescaleSize); @@ -104,6 +117,7 @@ signals: void colorsChanged(); void sourceChanged(); void depthChanged(); + void imageRectChanged(); void rescaleSizeChanged(); public slots: @@ -117,6 +131,7 @@ private: ColorQuantizerOperation* liveOperation = nullptr; QUrl mSource; qreal mDepth = 0; + QRect mImageRect; qreal mRescaleSize = 0; Q_OBJECT_BINDABLE_PROPERTY( From aaff22f4b0239529f738770fa3c4388438f219fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Apr 2026 21:28:09 -0700 Subject: [PATCH 213/226] io/fileview: write values into correct JsonObjects in deserialize Property writes were being done on the JsonAdapter and not the child JsonObject, resulting in the data of children being set on the adapter's props, and occasional crashes. --- changelog/next.md | 1 + src/io/jsonadapter.cpp | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 8b22d07..3059cc9 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -66,6 +66,7 @@ set shell id. - Worked around Qt bug causing crashes when plugging and unplugging monitors. - Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. - Fixed ScreencopyView pixelation when scaled. +- Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. ## Packaging Changes diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index e80c6f2..68e85ab 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -155,13 +155,13 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM if (prop.metaType() == QMetaType::fromType()) { auto variant = jval.toVariant(); - auto oldValue = prop.read(this).value(); + auto oldValue = prop.read(obj).value(); // Calling prop.write with a new QJSValue will cause a property update // even if content is identical. if (jval.toVariant() != oldValue.toVariant()) { auto jsValue = qmlEngine(this)->fromVariant(jval.toVariant()); - prop.write(this, QVariant::fromValue(jsValue)); + prop.write(obj, QVariant::fromValue(jsValue)); } } else if (QMetaType::canView(prop.metaType(), QMetaType::fromType())) { // FIXME: This doesn't support creating descendants of JsonObject, as QMetaType.metaObject() @@ -196,7 +196,7 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM QMetaType::fromType>() )) { - auto pval = prop.read(this); + auto pval = prop.read(obj); if (pval.canConvert>()) { auto lp = pval.value>(); From ceac3c6cfacfd7afc806de230ec5a239c24a4199 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Apr 2026 21:30:05 -0700 Subject: [PATCH 214/226] io/fileview: use QVariant when QJSValue cast fails in adapter prop read A QVariant(QVariantMap) does not convert implicitly to a QVaraint(QJSValue), causing extra signals to be emitted if the old value was not updated by js (replaced by a QJSValue) before deserializing again. --- changelog/next.md | 1 + src/io/jsonadapter.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 3059cc9..88c7484 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -67,6 +67,7 @@ set shell id. - Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. - Fixed ScreencopyView pixelation when scaled. - Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. +- Fixed JsonAdapter sending unnecessary property changes for primitive values. ## Packaging Changes diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 68e85ab..9ca7060 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -154,13 +154,15 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM auto jval = json.value(prop.name()); if (prop.metaType() == QMetaType::fromType()) { - auto variant = jval.toVariant(); - auto oldValue = prop.read(obj).value(); + auto newVariant = jval.toVariant(); + auto oldValue = prop.read(obj); + auto oldVariant = + oldValue.canConvert() ? oldValue.value().toVariant() : oldValue; // Calling prop.write with a new QJSValue will cause a property update // even if content is identical. - if (jval.toVariant() != oldValue.toVariant()) { - auto jsValue = qmlEngine(this)->fromVariant(jval.toVariant()); + if (newVariant != oldVariant) { + auto jsValue = qmlEngine(this)->fromVariant(newVariant); prop.write(obj, QVariant::fromValue(jsValue)); } } else if (QMetaType::canView(prop.metaType(), QMetaType::fromType())) { From b4e71cb2c0afe640e62ceb7b7fb44c5fdb3546cf Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 26 Nov 2025 10:29:51 -0500 Subject: [PATCH 215/226] core/window: add parentWindow property to FloatingWindow --- CMakeLists.txt | 1 + changelog/next.md | 1 + src/window/floatingwindow.cpp | 85 ++++++++++++++++++++++++- src/window/floatingwindow.hpp | 34 +++++++++- src/window/test/manual/parentwindow.qml | 42 ++++++++++++ 5 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/window/test/manual/parentwindow.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 966e8c3..8293f23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(quickshell VERSION "0.2.1" LANGUAGES CXX C) set(UNRELEASED_FEATURES "network.2" "colorquant-imagerect" + "window-parent" ) set(QT_MIN_VERSION "6.6.0") diff --git a/changelog/next.md b/changelog/next.md index 88c7484..1137e9a 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -30,6 +30,7 @@ set shell id. - Added ext-background-effect window blur support. - Added per-corner radius support to Region. - Added ColorQuantizer region selection. +- Added dialog window support to FloatingWindow. ## Other Changes diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index a0c9fdd..7a46bbf 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include @@ -11,6 +11,27 @@ #include "proxywindow.hpp" #include "windowinterface.hpp" +ProxyFloatingWindow::ProxyFloatingWindow(QObject* parent): ProxyWindowBase(parent) { + this->bTargetVisible.setBinding([this] { + if (!this->bWantsVisible) return false; + auto* parent = this->bParentProxyWindow.value(); + if (!parent) return true; + return parent->bindableBackerVisibility().value(); + }); +} + +void ProxyFloatingWindow::targetVisibleChanged() { + if (this->window && this->bParentProxyWindow) { + auto* bw = this->bParentProxyWindow.value()->backingWindow(); + + if (bw != this->window->transientParent()) { + this->window->setTransientParent(bw); + } + } + + this->ProxyWindowBase::setVisible(this->bTargetVisible); +} + void ProxyFloatingWindow::connectWindow() { this->ProxyWindowBase::connectWindow(); @@ -19,6 +40,25 @@ void ProxyFloatingWindow::connectWindow() { this->window->setMaximumSize(this->bMaximumSize); } +void ProxyFloatingWindow::completeWindow() { + this->ProxyWindowBase::completeWindow(); + + auto* parent = this->bParentProxyWindow.value(); + this->window->setTransientParent(parent ? parent->backingWindow() : nullptr); +} + +void ProxyFloatingWindow::postCompleteWindow() { + this->ProxyWindowBase::setVisible(this->bTargetVisible); +} + +void ProxyFloatingWindow::onParentDestroyed() { + this->mParentWindow = nullptr; + this->bParentProxyWindow = nullptr; + emit this->parentWindowChanged(); +} + +void ProxyFloatingWindow::setVisible(bool visible) { this->bWantsVisible = visible; } + void ProxyFloatingWindow::trySetWidth(qint32 implicitWidth) { if (!this->window->isVisible()) { this->ProxyWindowBase::trySetWidth(implicitWidth); @@ -46,6 +86,42 @@ void ProxyFloatingWindow::onMaximumSizeChanged() { emit this->maximumSizeChanged(); } +QObject* ProxyFloatingWindow::parentWindow() const { return this->mParentWindow; } + +void ProxyFloatingWindow::setParentWindow(QObject* window) { + if (window == this->mParentWindow) return; + + if (this->window && this->window->isVisible()) { + qmlWarning(this) << "parentWindow cannot be changed after the window is visible."; + return; + } + + if (this->bParentProxyWindow) { + QObject::disconnect(this->bParentProxyWindow, nullptr, this, nullptr); + } + + if (this->mParentWindow) { + QObject::disconnect(this->mParentWindow, nullptr, this, nullptr); + } + + this->mParentWindow = nullptr; + this->bParentProxyWindow = nullptr; + + if (auto* proxy = ProxyWindowBase::forObject(window)) { + this->mParentWindow = window; + this->bParentProxyWindow = proxy; + + QObject::connect( + this->mParentWindow, + &QObject::destroyed, + this, + &ProxyFloatingWindow::onParentDestroyed + ); + } + + emit this->parentWindowChanged(); +} + // FloatingWindowInterface FloatingWindowInterface::FloatingWindowInterface(QObject* parent) @@ -57,6 +133,7 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) QObject::connect(this->window, &ProxyFloatingWindow::titleChanged, this, &FloatingWindowInterface::titleChanged); QObject::connect(this->window, &ProxyFloatingWindow::minimumSizeChanged, this, &FloatingWindowInterface::minimumSizeChanged); QObject::connect(this->window, &ProxyFloatingWindow::maximumSizeChanged, this, &FloatingWindowInterface::maximumSizeChanged); + QObject::connect(this->window, &ProxyFloatingWindow::parentWindowChanged, this, &FloatingWindowInterface::parentWindowChanged); QObject::connect(this->window, &ProxyWindowBase::windowConnected, this, &FloatingWindowInterface::onWindowConnected); // clang-format on } @@ -169,3 +246,9 @@ bool FloatingWindowInterface::startSystemResize(Qt::Edges edges) const { if (!qw) return false; return qw->startSystemResize(edges); } + +QObject* FloatingWindowInterface::parentWindow() const { return this->window->parentWindow(); } + +void FloatingWindowInterface::setParentWindow(QObject* window) { + this->window->setParentWindow(window); +} diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index 06b5b9e..e9e536a 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -16,9 +16,15 @@ class ProxyFloatingWindow: public ProxyWindowBase { Q_OBJECT; public: - explicit ProxyFloatingWindow(QObject* parent = nullptr): ProxyWindowBase(parent) {} + explicit ProxyFloatingWindow(QObject* parent = nullptr); void connectWindow() override; + void completeWindow() override; + void postCompleteWindow() override; + void setVisible(bool visible) override; + + [[nodiscard]] QObject* parentWindow() const; + void setParentWindow(QObject* window); // Setting geometry while the window is visible makes the content item shrink but not the window // which is awful so we disable it for floating windows. @@ -29,11 +35,28 @@ signals: void minimumSizeChanged(); void maximumSizeChanged(); void titleChanged(); + void parentWindowChanged(); + +private slots: + void onParentDestroyed(); private: void onMinimumSizeChanged(); void onMaximumSizeChanged(); void onTitleChanged(); + void targetVisibleChanged(); + + QObject* mParentWindow = nullptr; + + Q_OBJECT_BINDABLE_PROPERTY(ProxyFloatingWindow, ProxyWindowBase*, bParentProxyWindow); + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(ProxyFloatingWindow, bool, bWantsVisible, true); + + Q_OBJECT_BINDABLE_PROPERTY( + ProxyFloatingWindow, + bool, + bTargetVisible, + &ProxyFloatingWindow::targetVisibleChanged + ); public: Q_OBJECT_BINDABLE_PROPERTY( @@ -75,6 +98,11 @@ class FloatingWindowInterface: public WindowInterface { Q_PROPERTY(bool maximized READ isMaximized WRITE setMaximized NOTIFY maximizedChanged); /// Whether the window is currently fullscreen. Q_PROPERTY(bool fullscreen READ isFullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + /// The parent window of this window. Setting this makes the window a child of the parent, + /// which affects window stacking behavior. + /// + /// > [!NOTE] This property cannot be changed after the window is visible. + Q_PROPERTY(QObject* parentWindow READ parentWindow WRITE setParentWindow NOTIFY parentWindowChanged); // clang-format on QML_NAMED_ELEMENT(FloatingWindow); @@ -101,6 +129,9 @@ public: /// Start a system resize operation. Must be called during a pointer press/drag. Q_INVOKABLE [[nodiscard]] bool startSystemResize(Qt::Edges edges) const; + [[nodiscard]] QObject* parentWindow() const; + void setParentWindow(QObject* window); + signals: void minimumSizeChanged(); void maximumSizeChanged(); @@ -108,6 +139,7 @@ signals: void minimizedChanged(); void maximizedChanged(); void fullscreenChanged(); + void parentWindowChanged(); private slots: void onWindowConnected(); diff --git a/src/window/test/manual/parentwindow.qml b/src/window/test/manual/parentwindow.qml new file mode 100644 index 0000000..214ee25 --- /dev/null +++ b/src/window/test/manual/parentwindow.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls.Fusion +import Quickshell + +Scope { + FloatingWindow { + id: control + color: contentItem.palette.window + ColumnLayout { + CheckBox { + id: parentCb + text: "Show parent" + } + + CheckBox { + id: dialogCb + text: "Show dialog" + } + } + } + + FloatingWindow { + id: parentw + Text { + text: "parent" + } + visible: parentCb.checked + color: contentItem.palette.window + + FloatingWindow { + id: dialog + parentWindow: parentw + visible: dialogCb.checked + color: contentItem.palette.window + + Text { + text: "dialog" + } + } + } +} From 854088c48c4020f35019851137197b1112a9b9ee Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Apr 2026 22:57:58 -0700 Subject: [PATCH 216/226] io/fileview: convert containers to QVariantList/Map before serialize QJsonValue::fromVariant doesn't do this automatically for some reason. --- changelog/next.md | 1 + src/io/jsonadapter.cpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 1137e9a..86687eb 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -69,6 +69,7 @@ set shell id. - Fixed ScreencopyView pixelation when scaled. - Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. - Fixed JsonAdapter sending unnecessary property changes for primitive values. +- Fixed JsonAdapter serialization for lists. ## Packaging Changes diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 9ca7060..df65120 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -1,5 +1,6 @@ #include "jsonadapter.hpp" +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -131,13 +133,16 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas } json.insert(prop.name(), array); - } else if (val.canConvert()) { - auto variant = val.value().toVariant(); - auto jv = QJsonValue::fromVariant(variant); - json.insert(prop.name(), jv); } else { - auto jv = QJsonValue::fromVariant(val); - json.insert(prop.name(), jv); + if (val.canConvert()) val = val.value().toVariant(); + + if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } else if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } + + json.insert(prop.name(), QJsonValue::fromVariant(val)); } } } From 9b98d101786da92be63ce0f4072a98152f07afcc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Apr 2026 12:28:40 -0700 Subject: [PATCH 217/226] io/fileview: try to convert values to json before handling sequences The previous code was interpreting a string as a list of characters and therefore a sequence. --- src/io/jsonadapter.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index df65120..6d1f080 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -136,13 +136,19 @@ QJsonObject JsonAdapter::serializeRec(const QObject* obj, const QMetaObject* bas } else { if (val.canConvert()) val = val.value().toVariant(); - if (val.canConvert()) { - val.convert(QMetaType::fromType()); - } else if (val.canConvert()) { - val.convert(QMetaType::fromType()); + auto jsonVal = QJsonValue::fromVariant(val); + + if (jsonVal.isNull() && !val.isNull() && val.isValid()) { + if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } else if (val.canConvert()) { + val.convert(QMetaType::fromType()); + } + + jsonVal = QJsonValue::fromVariant(val); } - json.insert(prop.name(), QJsonValue::fromVariant(val)); + json.insert(prop.name(), jsonVal); } } } From 49d4f46cf1b2d40e4095791d56e3b88eb8f0d0df Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Apr 2026 13:05:33 -0700 Subject: [PATCH 218/226] io/fileview: handle deserialization to list properties --- src/io/jsonadapter.cpp | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/io/jsonadapter.cpp b/src/io/jsonadapter.cpp index 6d1f080..369ccbe 100644 --- a/src/io/jsonadapter.cpp +++ b/src/io/jsonadapter.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -260,12 +261,35 @@ void JsonAdapter::deserializeRec(const QJsonObject& json, QObject* obj, const QM } } else { auto variant = jval.toVariant(); + auto convVariant = variant; - if (variant.convert(prop.metaType())) { - prop.write(obj, variant); + if (convVariant.convert(prop.metaType())) { + prop.write(obj, convVariant); } else { - qmlWarning(this) << "Failed to deserialize property " << prop.name() << ": expected " - << prop.metaType().name() << " but got " << jval.toVariant().typeName(); + auto pval = prop.read(obj); + if (variant.canConvert() && pval.canView()) { + auto targetv = QVariant(pval.metaType()); + auto target = targetv.view().metaContainer(); + auto valueType = target.valueMetaType(); + auto i = 0; + + for (QVariant item: variant.value()) { + if (item.convert(valueType)) { + target.addValueAtEnd(targetv.data(), item.constData()); + } else { + qmlWarning(this) << "Failed to deserialize list member " << i << " of property " + << prop.name() << ": expected " << valueType.name() << " but got " + << item.typeName(); + } + + ++i; + } + prop.write(obj, targetv); + } else { + qmlWarning(this) << "Failed to deserialize property " << prop.name() << ": expected " + << prop.metaType().name() << " but got " + << jval.toVariant().typeName(); + } } } } From ad5fd9116e25bc502468f4dfa884ee027887c51c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Apr 2026 13:51:32 -0700 Subject: [PATCH 219/226] wm: add nullptr guard to WindowManager::screenProjection --- src/windowmanager/windowmanager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp index 6b51db1..511e8ec 100644 --- a/src/windowmanager/windowmanager.cpp +++ b/src/windowmanager/windowmanager.cpp @@ -21,6 +21,8 @@ WindowManager* WindowManager::instance() { } ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) { + if (!screen) return nullptr; + auto* qscreen = screen->screen; auto it = this->mScreenProjections.find(qscreen); if (it != this->mScreenProjections.end()) { From 13fe9b0d98028361344b7422b1ebe238d1d29d02 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Apr 2026 00:35:48 -0700 Subject: [PATCH 220/226] services/pipewire: avoid blanket disconnect for default nodes The same nodes can be both default and default configured nodes. When the default and default configured node are not changed in unison, a blanket disconnect will also disconnect the other's destroy handler, causing a crash if the other is accessed after the node is destroyed. --- changelog/next.md | 1 + src/services/pipewire/defaults.cpp | 50 +++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 86687eb..b430a27 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -70,6 +70,7 @@ set shell id. - Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. - Fixed JsonAdapter sending unnecessary property changes for primitive values. - Fixed JsonAdapter serialization for lists. +- Fixed pipewire crashes after hotplugging devices and changing default outputs. ## Packaging Changes diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 7a24a65..b9c8e35 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -214,13 +214,24 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) { qCInfo(logDefaults) << "Default sink changed to" << node; if (this->mDefaultSink != nullptr) { - QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default configured sink. + QObject::disconnect( + this->mDefaultSink, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSinkDestroyed + ); } this->mDefaultSink = node; if (node != nullptr) { - QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSinkDestroyed); + QObject::connect( + node, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSinkDestroyed + ); } emit this->defaultSinkChanged(); @@ -244,13 +255,24 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) { qCInfo(logDefaults) << "Default source changed to" << node; if (this->mDefaultSource != nullptr) { - QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default configured source. + QObject::disconnect( + this->mDefaultSource, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSourceDestroyed + ); } this->mDefaultSource = node; if (node != nullptr) { - QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSourceDestroyed); + QObject::connect( + node, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSourceDestroyed + ); } emit this->defaultSourceChanged(); @@ -274,7 +296,13 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { qCInfo(logDefaults) << "Default configured sink changed to" << node; if (this->mDefaultConfiguredSink != nullptr) { - QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default sink. + QObject::disconnect( + this->mDefaultConfiguredSink, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultConfiguredSinkDestroyed + ); } this->mDefaultConfiguredSink = node; @@ -282,7 +310,7 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { if (node != nullptr) { QObject::connect( node, - &QObject::destroyed, + &PwBindableObject::destroying, this, &PwDefaultTracker::onDefaultConfiguredSinkDestroyed ); @@ -309,7 +337,13 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { qCInfo(logDefaults) << "Default configured source changed to" << node; if (this->mDefaultConfiguredSource != nullptr) { - QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default source. + QObject::disconnect( + this->mDefaultConfiguredSource, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultConfiguredSourceDestroyed + ); } this->mDefaultConfiguredSource = node; @@ -317,7 +351,7 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { if (node != nullptr) { QObject::connect( node, - &QObject::destroyed, + &PwBindableObject::destroying, this, &PwDefaultTracker::onDefaultConfiguredSourceDestroyed ); From 5bf6a412b0b03ecc77aac08ad09bd3f52967f017 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Apr 2026 00:43:02 -0700 Subject: [PATCH 221/226] core: correctly construct runtime path when XDG_RUNTIME_DIR missing Fixes a typo of % as $, which broke string substitution. --- src/core/paths.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 6555e54..d361e3d 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -64,7 +64,7 @@ QDir* QsPaths::baseRunDir() { if (this->baseRunState == DirState::Unknown) { auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); if (runtimeDir.isEmpty()) { - runtimeDir = QString("/run/user/$1").arg(getuid()); + runtimeDir = QString("/run/user/%1").arg(getuid()); qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir; } From 7c5a6c4bd4be1f258aa47626cf5cde02215adad2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Apr 2026 00:45:26 -0700 Subject: [PATCH 222/226] core/log: crash if Quickshell's log filter is installed twice Crashes from recursion inside filterCategories through the old filter have been observed. Presumably this means the log filter is getting installed twice somehow. This should catch it. --- src/core/logging.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 415cf61..1b19fab 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -310,10 +310,15 @@ void LogManager::init( instance->rules->append(parser.rules()); } - qInstallMessageHandler(&LogManager::messageHandler); - instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory); + if (instance->lastCategoryFilter == &LogManager::filterCategory) { + qCFatal(logLogging) << "Quickshell's log filter has been installed twice. This is a bug."; + instance->lastCategoryFilter = nullptr; + } + + qInstallMessageHandler(&LogManager::messageHandler); + qCDebug(logLogging) << "Creating offthread logger..."; auto* thread = new QThread(); instance->threadProxy.moveToThread(thread); From f0d0216b3d293f2813112cd74d74d4e7de57931e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 8 Apr 2026 00:39:52 -0700 Subject: [PATCH 223/226] core: add DropExpensiveFonts pragma disabling woff and woff2 fonts --- changelog/next.md | 2 ++ src/launch/launch.cpp | 45 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index b430a27..95de5dc 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -43,6 +43,8 @@ set shell id. - Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. - Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link. - Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID. +- Added `DropExpensiveFonts` pragma which avoids loading fonts which may cause lag and excessive memory usage if many variants are used. +- Unrecognized pragmas are no longer a hard error for future backward compatibility. ## Bug Fixes diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 0f5b090..de85956 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -77,6 +77,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QHash envOverrides; QString appId = qEnvironmentVariable("QS_APP_ID"); + bool dropExpensiveFonts = false; QString dataDir; QString stateDir; QString cacheDir; @@ -92,6 +93,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio else if (pragma == "NativeTextRendering") pragmas.nativeTextRendering = true; else if (pragma == "IgnoreSystemSettings") pragmas.desktopSettingsAware = false; else if (pragma == "RespectSystemStyle") pragmas.useSystemStyle = true; + else if (pragma == "DropExpensiveFonts") pragmas.dropExpensiveFonts = true; else if (pragma.startsWith("IconTheme ")) pragmas.iconTheme = pragma.sliced(10); else if (pragma.startsWith("Env ")) { auto envPragma = pragma.sliced(4); @@ -116,8 +118,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio } else if (pragma.startsWith("CacheDir ")) { pragmas.cacheDir = pragma.sliced(9).trimmed(); } else { - qCritical() << "Unrecognized pragma" << pragma; - return -1; + qWarning() << "Unrecognized pragma" << pragma; } } else if (line.startsWith("import")) break; } @@ -168,6 +169,46 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment(); + if (pragmas.dropExpensiveFonts) { + if (auto* runDir = QsPaths::instance()->instanceRunDir()) { + auto baseConfigPath = qEnvironmentVariable("FONTCONFIG_FILE"); + if (baseConfigPath.isEmpty()) baseConfigPath = "/etc/fonts/fonts.conf"; + + auto filterPath = runDir->filePath("fonts-override.conf"); + auto filterFile = QFile(filterPath); + if (filterFile.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { + auto filterTemplate = QStringLiteral(R"( + + + %1 + + + + + woff + + + + + woff2 + + + + + +)"); + + QTextStream(&filterFile) << filterTemplate.arg(baseConfigPath); + filterFile.close(); + qputenv("FONTCONFIG_FILE", filterPath.toUtf8()); + } else { + qCritical() << "Could not write fontconfig filter to" << filterPath; + } + } else { + qCritical() << "Could not create fontconfig filter: instance run directory unavailable"; + } + } + if (!pragmas.useSystemStyle) { qunsetenv("QT_STYLE_OVERRIDE"); qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion"); From 7208f68bb7f4bf7e476b828decde1321ae544f5d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 8 Apr 2026 01:35:15 -0700 Subject: [PATCH 224/226] core: add QS_DROP_EXPENSIVE_FONTS env var --- changelog/next.md | 2 +- src/launch/launch.cpp | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 95de5dc..d6dc60e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -43,7 +43,7 @@ set shell id. - Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. - Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link. - Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID. -- Added `DropExpensiveFonts` pragma which avoids loading fonts which may cause lag and excessive memory usage if many variants are used. +- Added `DropExpensiveFonts` pragma and `QS_DROP_EXPENSIVE_FONTS` environment variable which avoids loading fonts which may cause lag and excessive memory usage if many variants are used. - Unrecognized pragmas are no longer a hard error for future backward compatibility. ## Bug Fixes diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index de85956..dcdefa7 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -169,6 +169,17 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio Common::INITIAL_ENVIRONMENT = QProcessEnvironment::systemEnvironment(); + if (!pragmas.useSystemStyle) { + qunsetenv("QT_STYLE_OVERRIDE"); + qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion"); + } + + for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { + qputenv(var.toUtf8(), val.toUtf8()); + } + + pragmas.dropExpensiveFonts |= qEnvironmentVariableIntValue("QS_DROP_EXPENSIVE_FONTS") == 1; + if (pragmas.dropExpensiveFonts) { if (auto* runDir = QsPaths::instance()->instanceRunDir()) { auto baseConfigPath = qEnvironmentVariable("FONTCONFIG_FILE"); @@ -209,15 +220,6 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio } } - if (!pragmas.useSystemStyle) { - qunsetenv("QT_STYLE_OVERRIDE"); - qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion"); - } - - for (auto [var, val]: pragmas.envOverrides.asKeyValueRange()) { - qputenv(var.toUtf8(), val.toUtf8()); - } - // The qml engine currently refuses to cache non file (qsintercept) paths. // if (auto* cacheDir = QsPaths::instance()->cacheDir()) { From 7f7ab6bc8aac6148ef0aa25a7435ee11a78dba5f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 9 Apr 2026 00:10:49 -0700 Subject: [PATCH 225/226] launch: use dup2 to reset daemon stdio over close+open --- changelog/next.md | 1 + src/launch/main.cpp | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index d6dc60e..761161d 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -73,6 +73,7 @@ set shell id. - Fixed JsonAdapter sending unnecessary property changes for primitive values. - Fixed JsonAdapter serialization for lists. - Fixed pipewire crashes after hotplugging devices and changing default outputs. +- Fixed launches failing for `--daemonize` on some systems. ## Packaging Changes diff --git a/src/launch/main.cpp b/src/launch/main.cpp index a324e09..efd6628 100644 --- a/src/launch/main.cpp +++ b/src/launch/main.cpp @@ -84,21 +84,29 @@ void exitDaemon(int code) { close(DAEMON_PIPE); - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - - if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdin"; + auto fd = open("/dev/null", O_RDWR); + if (fd == -1) { + qCritical().nospace() << "Failed to open /dev/null for daemon stdio" << errno << ": " + << qt_error_string(); + return; } - if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdout"; + if (dup2(fd, STDIN_FILENO) != STDIN_FILENO) { // NOLINT + qCritical().nospace() << "Failed to set daemon stdin to /dev/null" << errno << ": " + << qt_error_string(); } - if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stderr"; + if (dup2(fd, STDOUT_FILENO) != STDOUT_FILENO) { // NOLINT + qCritical().nospace() << "Failed to set daemon stdout to /dev/null" << errno << ": " + << qt_error_string(); } + + if (dup2(fd, STDERR_FILENO) != STDERR_FILENO) { // NOLINT + qCritical().nospace() << "Failed to set daemon stderr to /dev/null" << errno << ": " + << qt_error_string(); + } + + close(fd); } int main(int argc, char** argv) { From d4c92973b53d9fa34cc110d3b974eb6bde5b3027 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 9 Apr 2026 00:34:57 -0700 Subject: [PATCH 226/226] i3/ipc: ensure monitor/workspace pointers are nulled on destroy --- src/x11/i3/ipc/controller.cpp | 8 +------- src/x11/i3/ipc/monitor.cpp | 28 ++++++++++++++++++++++------ src/x11/i3/ipc/monitor.hpp | 7 +++++-- src/x11/i3/ipc/workspace.cpp | 26 +++++++++++++++++++++++--- src/x11/i3/ipc/workspace.hpp | 7 ++++++- 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/src/x11/i3/ipc/controller.cpp b/src/x11/i3/ipc/controller.cpp index 1a08c63..a83afd4 100644 --- a/src/x11/i3/ipc/controller.cpp +++ b/src/x11/i3/ipc/controller.cpp @@ -276,7 +276,7 @@ void I3IpcController::handleWorkspaceEvent(I3IpcEvent* event) { if (newWorkspace->bindableMonitor().value()) { auto* monitor = newWorkspace->bindableMonitor().value(); - monitor->setFocusedWorkspace(newWorkspace); + monitor->setActiveWorkspace(newWorkspace); this->bFocusedMonitor = monitor; } } else if (change == "empty") { @@ -286,13 +286,7 @@ void I3IpcController::handleWorkspaceEvent(I3IpcEvent* event) { if (oldWorkspace != nullptr) { qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name; - - if (this->bFocusedWorkspace == oldWorkspace) { - this->bFocusedMonitor->setFocusedWorkspace(nullptr); - } - this->workspaces()->removeObject(oldWorkspace); - delete oldWorkspace; } else { qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; diff --git a/src/x11/i3/ipc/monitor.cpp b/src/x11/i3/ipc/monitor.cpp index fb0ec86..7afb68e 100644 --- a/src/x11/i3/ipc/monitor.cpp +++ b/src/x11/i3/ipc/monitor.cpp @@ -40,21 +40,37 @@ void I3Monitor::updateFromObject(const QVariantMap& obj) { this->bHeight = rect.value("height").value(); this->bScale = obj.value("scale").value(); - if (!this->bActiveWorkspace - || activeWorkspaceName != this->bActiveWorkspace->bindableName().value()) - { + auto* activeWorkspace = this->bActiveWorkspace.value(); + if (!activeWorkspace || activeWorkspaceName != activeWorkspace->bindableName().value()) { if (activeWorkspaceName.isEmpty()) { - this->bActiveWorkspace = nullptr; + activeWorkspace = nullptr; } else { - this->bActiveWorkspace = this->ipc->findWorkspaceByName(activeWorkspaceName); + activeWorkspace = this->ipc->findWorkspaceByName(activeWorkspaceName); } }; + this->setActiveWorkspace(activeWorkspace); + Qt::endPropertyUpdateGroup(); } void I3Monitor::updateInitial(const QString& name) { this->bName = name; } -void I3Monitor::setFocusedWorkspace(I3Workspace* workspace) { this->bActiveWorkspace = workspace; }; +void I3Monitor::setActiveWorkspace(I3Workspace* workspace) { + auto* oldWorkspace = this->bActiveWorkspace.value(); + if (oldWorkspace == workspace) return; + + if (oldWorkspace) { + QObject::disconnect(oldWorkspace, nullptr, this, nullptr); + } + + if (workspace) { + QObject::connect(workspace, &QObject::destroyed, this, &I3Monitor::onActiveWorkspaceDestroyed); + } + + this->bActiveWorkspace = workspace; +} + +void I3Monitor::onActiveWorkspaceDestroyed() { this->bActiveWorkspace = nullptr; } } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/monitor.hpp b/src/x11/i3/ipc/monitor.hpp index cd348b1..c328d8b 100644 --- a/src/x11/i3/ipc/monitor.hpp +++ b/src/x11/i3/ipc/monitor.hpp @@ -55,7 +55,7 @@ public: [[nodiscard]] QBindable bindableScale() { return &this->bScale; } [[nodiscard]] QBindable bindableFocused() { return &this->bFocused; } - [[nodiscard]] QBindable bindableActiveWorkspace() { + [[nodiscard]] QBindable bindableActiveWorkspace() const { return &this->bActiveWorkspace; } @@ -64,7 +64,7 @@ public: void updateFromObject(const QVariantMap& obj); void updateInitial(const QString& name); - void setFocusedWorkspace(I3Workspace* workspace); + void setActiveWorkspace(I3Workspace* workspace); signals: void idChanged(); @@ -79,6 +79,9 @@ signals: void lastIpcObjectChanged(); void focusedChanged(); +private slots: + void onActiveWorkspaceDestroyed(); + private: I3IpcController* ipc; diff --git a/src/x11/i3/ipc/workspace.cpp b/src/x11/i3/ipc/workspace.cpp index 03fadc2..530f0a2 100644 --- a/src/x11/i3/ipc/workspace.cpp +++ b/src/x11/i3/ipc/workspace.cpp @@ -43,14 +43,17 @@ void I3Workspace::updateFromObject(const QVariantMap& obj) { auto monitorName = obj.value("output").value(); - if (!this->bMonitor || monitorName != this->bMonitor->bindableName().value()) { + auto* monitor = this->bMonitor.value(); + if (!monitor || monitorName != monitor->bindableName().value()) { if (monitorName.isEmpty()) { - this->bMonitor = nullptr; + monitor = nullptr; } else { - this->bMonitor = this->ipc->findMonitorByName(monitorName, true); + monitor = this->ipc->findMonitorByName(monitorName, true); } } + this->setMonitor(monitor); + Qt::endPropertyUpdateGroup(); } @@ -58,4 +61,21 @@ void I3Workspace::activate() { this->ipc->dispatch(QString("workspace number %1").arg(this->bNumber.value())); } +void I3Workspace::setMonitor(I3Monitor* monitor) { + auto* oldMonitor = this->bMonitor.value(); + if (oldMonitor == monitor) return; + + if (oldMonitor) { + QObject::disconnect(oldMonitor, nullptr, this, nullptr); + } + + if (monitor) { + QObject::connect(monitor, &QObject::destroyed, this, &I3Workspace::onMonitorDestroyed); + } + + this->bMonitor = monitor; +} + +void I3Workspace::onMonitorDestroyed() { this->bMonitor = nullptr; } + } // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/workspace.hpp b/src/x11/i3/ipc/workspace.hpp index f540545..c08e926 100644 --- a/src/x11/i3/ipc/workspace.hpp +++ b/src/x11/i3/ipc/workspace.hpp @@ -57,11 +57,13 @@ public: [[nodiscard]] QBindable bindableActive() { return &this->bActive; } [[nodiscard]] QBindable bindableFocused() { return &this->bFocused; } [[nodiscard]] QBindable bindableUrgent() { return &this->bUrgent; } - [[nodiscard]] QBindable bindableMonitor() { return &this->bMonitor; } + [[nodiscard]] QBindable bindableMonitor() const { return &this->bMonitor; } [[nodiscard]] QVariantMap lastIpcObject() const; void updateFromObject(const QVariantMap& obj); + void setMonitor(I3Monitor* monitor); + signals: void idChanged(); void nameChanged(); @@ -72,6 +74,9 @@ signals: void monitorChanged(); void lastIpcObjectChanged(); +private slots: + void onMonitorDestroyed(); + private: I3IpcController* ipc;