From 5ac9096c1c63f6940c6b95f1118b540dfe029278 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 14 Jul 2025 02:54:46 -0700 Subject: [PATCH 001/120] 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 002/120] 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 003/120] 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 004/120] 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 005/120] 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 006/120] 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 007/120] 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 008/120] 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 009/120] 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 010/120] 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 011/120] 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 012/120] 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 013/120] 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 014/120] 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 015/120] 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 016/120] 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 017/120] 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 018/120] 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 019/120] 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 020/120] 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 021/120] 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 022/120] 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 023/120] 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 024/120] 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 025/120] 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 026/120] 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 027/120] 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 028/120] 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 029/120] 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 030/120] 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 031/120] 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 032/120] 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 033/120] 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 034/120] 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 035/120] 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 036/120] 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 037/120] 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 038/120] 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 039/120] 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 040/120] 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 041/120] 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 042/120] 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 043/120] 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 044/120] 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 045/120] 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 046/120] 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 047/120] 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 048/120] 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 049/120] 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 050/120] 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 051/120] 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 052/120] 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 053/120] 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 054/120] 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 055/120] 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 056/120] 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 057/120] 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 058/120] 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 059/120] 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 060/120] 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 061/120] 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 062/120] 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 063/120] 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 064/120] 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 065/120] 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 066/120] 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 067/120] 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 068/120] 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 069/120] 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 070/120] 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 071/120] 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 072/120] 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 073/120] 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 074/120] 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 075/120] 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 076/120] 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 077/120] 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 078/120] 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 079/120] 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 080/120] 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 081/120] 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 082/120] 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 083/120] 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 084/120] 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 085/120] 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 086/120] 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 087/120] 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 088/120] 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 089/120] 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 090/120] 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 091/120] 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 092/120] 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 093/120] 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 094/120] 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 095/120] 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 096/120] 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 097/120] 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 098/120] 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 099/120] 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 100/120] 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 101/120] 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 102/120] 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 103/120] 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 104/120] 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 105/120] 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 106/120] 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 107/120] 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 108/120] 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 109/120] 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 110/120] 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 111/120] 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 112/120] 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 113/120] 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 114/120] 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 115/120] 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 116/120] 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 117/120] 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 118/120] 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 119/120] 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 e2b0f8705ef240c9fbda6678f98a0ad40efae2a2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Jun 2025 12:57:15 -0700 Subject: [PATCH 120/120] wip ext-ws --- src/CMakeLists.txt | 1 + src/wayland/CMakeLists.txt | 2 + src/wayland/windowmanager/CMakeLists.txt | 20 ++ src/wayland/windowmanager/ext_workspace.cpp | 169 ++++++++++++++++ src/wayland/windowmanager/ext_workspace.hpp | 117 +++++++++++ src/wayland/windowmanager/init.cpp | 21 ++ src/wayland/windowmanager/windowmanager.cpp | 14 ++ src/wayland/windowmanager/windowmanager.hpp | 25 +++ src/wayland/windowmanager/workspace.cpp | 198 +++++++++++++++++++ src/wayland/windowmanager/workspace.hpp | 85 ++++++++ src/windowmanager/CMakeLists.txt | 18 ++ src/windowmanager/test/manual/workspaces.qml | 112 +++++++++++ src/windowmanager/windowmanager.cpp | 18 ++ src/windowmanager/windowmanager.hpp | 50 +++++ src/windowmanager/workspace.cpp | 31 +++ src/windowmanager/workspace.hpp | 119 +++++++++++ src/windowmanager/workspacemodel.cpp | 1 + src/windowmanager/workspacemodel.hpp | 39 ++++ 18 files changed, 1040 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/workspace.cpp create mode 100644 src/wayland/windowmanager/workspace.hpp create mode 100644 src/windowmanager/CMakeLists.txt 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/workspace.cpp create mode 100644 src/windowmanager/workspace.hpp create mode 100644 src/windowmanager/workspacemodel.cpp create mode 100644 src/windowmanager/workspacemodel.hpp 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..9e03b14 --- /dev/null +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_add_library(quickshell-wayland-windowsystem STATIC + windowmanager.cpp + workspace.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-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list") +wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") + +target_link_libraries(quickshell-wayland-windowsystem PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch? potentially, check w/ gcc + + wlp-ext-foreign-toplevel wlp-ext-workspace +) + +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..3e4c099 --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -0,0 +1,169 @@ +#include "ext_workspace.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qs::wayland::workspace { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace"); + +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(uint32_t 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..9dfac1b --- /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 + +#include "../output_tracking.hpp" + +namespace qs::wayland::workspace { + +Q_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(uint32_t state) override; + void ext_workspace_handle_v1_capabilities(uint32_t 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(uint32_t 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..fa336d7 --- /dev/null +++ b/src/wayland/windowmanager/init.cpp @@ -0,0 +1,21 @@ +#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..5f4a450 --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -0,0 +1,14 @@ +#include "windowmanager.hpp" + +namespace qs::wm::wayland { + +WaylandWindowManager* WaylandWindowManager::instance() { + static auto* instance = new WaylandWindowManager(); + return instance; +} + +void installWmProvider() { + 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..c732d6a --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "workspace.hpp" + +namespace qs::wm::wayland { + +class WaylandWindowManager: public WindowManager { + Q_OBJECT; + +public: + static WaylandWindowManager* instance(); + + [[nodiscard]] UntypedObjectModel* workspaces() const override { + return &WorkspaceManager::instance()->mWorkspaces; + } + + [[nodiscard]] UntypedObjectModel* workspaceGroups() const override { + return &WorkspaceManager::instance()->mWorkspaceGroups; + } +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.cpp b/src/wayland/windowmanager/workspace.cpp new file mode 100644 index 0000000..07bf3da --- /dev/null +++ b/src/wayland/windowmanager/workspace.cpp @@ -0,0 +1,198 @@ +#include "workspace.hpp" + +#include +#include +#include +#include +#include + +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WorkspaceManager::WorkspaceManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WorkspaceManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WorkspaceManager::onWorkspaceCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WorkspaceManager::onWorkspaceDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WorkspaceManager::onGroupCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WorkspaceManager::onGroupDestroyed + ); +} + +void WorkspaceManager::commit() { + qCDebug(impl::logWorkspace) << "Committing workspaces"; + impl::WorkspaceManager::instance()->commit(); +} + +void WorkspaceManager::onServerCommit() { + // Groups are created/destroyed around workspaces to avoid any nulls making it + // to the qml engine. + + for (auto* groupImpl: this->pendingGroupCreations) { + auto* group = new WlWorkspaceGroup(this, groupImpl); + this->groupsByImpl.insert(groupImpl, group); + this->mWorkspaceGroups.insertObject(group); + } + + for (auto* wsImpl: this->pendingWorkspaceCreations) { + auto* ws = new WlWorkspace(this, wsImpl); + this->workspaceByImpl.insert(wsImpl, ws); + this->mWorkspaces.insertObject(ws); + } + + for (auto* wsImpl: this->pendingWorkspaceDestructions) { + this->mWorkspaces.removeObject(this->workspaceByImpl.value(wsImpl)); + this->workspaceByImpl.remove(wsImpl); + } + + for (auto* groupImpl: this->pendingGroupDestructions) { + this->mWorkspaceGroups.removeObject(this->groupsByImpl.value(groupImpl)); + this->groupsByImpl.remove(groupImpl); + } + + for (auto* ws: this->mWorkspaces.valueList()) ws->commitImpl(); + for (auto* group: this->mWorkspaceGroups.valueList()) group->commitImpl(); + + this->pendingWorkspaceCreations.clear(); + this->pendingWorkspaceDestructions.clear(); + this->pendingGroupCreations.clear(); + this->pendingGroupDestructions.clear(); +} + +void WorkspaceManager::onWorkspaceCreated(impl::Workspace* workspace) { + this->pendingWorkspaceCreations.append(workspace); +} + +void WorkspaceManager::onWorkspaceDestroyed(impl::Workspace* workspace) { + if (!this->pendingWorkspaceCreations.removeOne(workspace)) { + this->pendingWorkspaceDestructions.append(workspace); + } +} + +void WorkspaceManager::onGroupCreated(impl::WorkspaceGroup* group) { + this->pendingGroupCreations.append(group); +} + +void WorkspaceManager::onGroupDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingGroupCreations.removeOne(group)) { + this->pendingGroupDestructions.append(group); + } +} + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +WlWorkspace::WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl) + : Workspace(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspace::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + 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->bCanSetGroup = this->impl->canAssign; + this->bGroup = this->manager()->groupsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWorkspace::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WorkspaceManager::commit(); +} + +void WlWorkspace::setGroup(WorkspaceGroup* group) { + if (!this->bCanSetGroup) { + qCritical(logWorkspace) << this << "cannot be assigned to a group"; + return; + } + + if (!group) { + qCritical(logWorkspace) << "Cannot set a workspace's group to null"; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << group; + // NOLINTNEXTLINE: A WorkspaceGroup will always be a WlWorkspaceGroup under wayland. + this->impl->assign(static_cast(group)->impl->object()); + WorkspaceManager::commit(); +} + +WlWorkspaceGroup::WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl) + : WorkspaceGroup(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspaceGroup::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.hpp b/src/wayland/windowmanager/workspace.hpp new file mode 100644 index 0000000..82742a9 --- /dev/null +++ b/src/wayland/windowmanager/workspace.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../windowmanager/workspace.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWorkspace; +class WlWorkspaceGroup; + +class WorkspaceManager: public QObject { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + ObjectModel mWorkspaces {this}; + ObjectModel mWorkspaceGroups {this}; + + static void commit(); + +private slots: + void onServerCommit(); + void onWorkspaceCreated(impl::Workspace* workspace); + void onWorkspaceDestroyed(impl::Workspace* workspace); + void onGroupCreated(impl::WorkspaceGroup* group); + void onGroupDestroyed(impl::WorkspaceGroup* group); + +private: + WorkspaceManager(); + + QList pendingWorkspaceCreations; + QList pendingWorkspaceDestructions; + QHash workspaceByImpl; + + QList pendingGroupCreations; + QList pendingGroupDestructions; + QHash groupsByImpl; + + friend class WlWorkspace; +}; + +class WlWorkspace: public Workspace { +public: + WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setGroup(WorkspaceGroup* group) override; + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWorkspaceGroup: public WorkspaceGroup { +public: + WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWorkspace; +}; + +} // namespace qs::wm::wayland diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..365c43a --- /dev/null +++ b/src/windowmanager/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-windowmanager STATIC + windowmanager.cpp + workspace.cpp + workspacemodel.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) + +target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-windowmanager) diff --git a/src/windowmanager/test/manual/workspaces.qml b/src/windowmanager/test/manual/workspaces.qml new file mode 100644 index 0000000..b379b47 --- /dev/null +++ b/src/windowmanager/test/manual/workspaces.qml @@ -0,0 +1,112 @@ +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.workspaceGroups + + WrapperRectangle { + id: delegate + required property WorkspaceGroup 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: [...WindowManager.workspaces.values].filter(w => w.group == delegate.modelData) + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.workspaces.values.filter(w => w.group == null) + } + + WorkspaceDelegate {} + } + } + } + + component WorkspaceDelegate: WrapperRectangle { + id: delegate + required property Workspace modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetGroup + model: [...WindowManager.workspaceGroups.values].map(w => w.toString()) + currentIndex: WindowManager.workspaceGroups.values.indexOf(delegate.modelData.group) + onActivated: i => delegate.modelData.setGroup(WindowManager.workspaceGroups.values[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/windowmanager.cpp b/src/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..7c6a6cc --- /dev/null +++ b/src/windowmanager/windowmanager.cpp @@ -0,0 +1,18 @@ +#include "windowmanager.hpp" +#include +#include + +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; +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowmanager.hpp b/src/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..03ebfc1 --- /dev/null +++ b/src/windowmanager/windowmanager.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +#include "../core/model.hpp" +#include "workspace.hpp" + +namespace qs::wm { + +class WindowManager: public QObject { + Q_OBJECT; + +public: + static void setProvider(std::function provider); + static WindowManager* instance(); + + [[nodiscard]] virtual UntypedObjectModel* workspaces() const { + return UntypedObjectModel::emptyInstance(); + } + + [[nodiscard]] virtual UntypedObjectModel* workspaceGroups() const { + return UntypedObjectModel::emptyInstance(); + } + +private: + static std::function provider; +}; + +class WindowManagerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(WindowManager); + QML_SINGLETON; + Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); + Q_PROPERTY(UntypedObjectModel* workspaceGroups READ workspaceGroups CONSTANT); + +public: + [[nodiscard]] static UntypedObjectModel* workspaces() { + return WindowManager::instance()->workspaces(); + } + + [[nodiscard]] static UntypedObjectModel* workspaceGroups() { + return WindowManager::instance()->workspaceGroups(); + } +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.cpp b/src/windowmanager/workspace.cpp new file mode 100644 index 0000000..472234e --- /dev/null +++ b/src/windowmanager/workspace.cpp @@ -0,0 +1,31 @@ +#include "workspace.hpp" + +#include +#include +#include + +#include "../core/qmlglobal.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Workspace::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Workspace::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Workspace::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Workspace::setGroup(WorkspaceGroup* /*group*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a group"; +} + +void WorkspaceGroup::onScreensChanged() { + mCachedScreens.clear(); + + for (auto* screen: this->bScreens.value()) { + mCachedScreens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + emit this->screensChanged(); +} + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.hpp b/src/windowmanager/workspace.hpp new file mode 100644 index 0000000..aa148a0 --- /dev/null +++ b/src/windowmanager/workspace.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; + +class Workspace: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + // persistent id + Q_PROPERTY(QString id READ default BINDABLE bindableId NOTIFY idChanged); + Q_PROPERTY(QString name READ default BINDABLE bindableName NOTIFY nameChanged); + // currently visible + Q_PROPERTY(bool active READ default BINDABLE bindableActive NOTIFY activeChanged); + Q_PROPERTY(WorkspaceGroup* group READ default BINDABLE bindableGroup NOTIFY groupChanged); + // in workspace pickers + Q_PROPERTY(bool shouldDisplay READ default BINDABLE bindableShouldDisplay NOTIFY shouldDisplayChanged); + Q_PROPERTY(bool urgent READ default BINDABLE bindableUrgent NOTIFY urgentChanged); + Q_PROPERTY(bool canActivate READ default BINDABLE bindableCanActivate NOTIFY canActivateChanged); + Q_PROPERTY(bool canDeactivate READ default BINDABLE bindableCanDeactivate NOTIFY canDeactivateChanged); + Q_PROPERTY(bool canRemove READ default BINDABLE bindableCanRemove NOTIFY canRemoveChanged); + Q_PROPERTY(bool canSetGroup READ default BINDABLE bindableCanSetGroup NOTIFY canSetGroupChanged); + // clang-format on + +public: + explicit Workspace(QObject* parent): QObject(parent) {} + + Q_INVOKABLE virtual void activate(); + Q_INVOKABLE virtual void deactivate(); + Q_INVOKABLE virtual void remove(); + Q_INVOKABLE virtual void setGroup(WorkspaceGroup* group); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] QBindable bindableGroup() const { return &this->bGroup; } + [[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 bindableCanSetGroup() const { return &this->bCanSetGroup; } + +signals: + void idChanged(); + void nameChanged(); + void activeChanged(); + void groupChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetGroupChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bId, &Workspace::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bName, &Workspace::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bActive, &Workspace::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, WorkspaceGroup*, bGroup, &Workspace::groupChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bShouldDisplay, &Workspace::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bUrgent, &Workspace::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanActivate, &Workspace::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanDeactivate, &Workspace::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanRemove, &Workspace::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanSetGroup, &Workspace::canSetGroupChanged); + //Q_OBJECT_BINDABLE_PROPERTY(Workspace, qint32, bIndex); +}; + +class WorkspaceGroup: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + /// Screens the workspace group is present on. + /// + /// > [!WARNING] This is not a model. Use @@Quickshell.ScriptModel if you need it to + /// > behave like one. + Q_PROPERTY(QList screens READ screens NOTIFY screensChanged); + +public: + explicit WorkspaceGroup(QObject* parent): QObject(parent) {} + + [[nodiscard]] const QList& screens() const { return this->mCachedScreens; } + +signals: + void screensChanged(); + +private slots: + void onScreensChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceGroup, + QList, + bScreens, + &WorkspaceGroup::onScreensChanged + ); + +private: + QList mCachedScreens; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspacemodel.cpp b/src/windowmanager/workspacemodel.cpp new file mode 100644 index 0000000..f6941fb --- /dev/null +++ b/src/windowmanager/workspacemodel.cpp @@ -0,0 +1 @@ +#include "workspacemodel.hpp" diff --git a/src/windowmanager/workspacemodel.hpp b/src/windowmanager/workspacemodel.hpp new file mode 100644 index 0000000..aeb855f --- /dev/null +++ b/src/windowmanager/workspacemodel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::windowsystem { + +class WorkspaceModel: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + enum ConflictStrategy : quint8 { + KeepFirst = 0, + ShowDuplicates, + }; + Q_ENUM(ConflictStrategy); + +signals: + void fromChanged(); + void toChanged(); + void screensChanged(); + void conflictStrategyChanged(); + +private: + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bFrom, &WorkspaceModel::fromChanged); + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bTo, &WorkspaceModel::toChanged); + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceModel, + ConflictStrategy, + bConflictStrategy, + &WorkspaceModel::conflictStrategyChanged + ); +}; + +} // namespace qs::windowsystem